feature/bitacora-correccion-por-tenant
This commit is contained in:
@@ -8,6 +8,7 @@ from sqlalchemy import event, inspect
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from api.v1.modules.a76.general_catalogs.company.models import Company
|
from api.v1.modules.a76.general_catalogs.company.models import Company
|
||||||
|
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||||
from core.database import rls_company_var, rls_tenant_var
|
from core.database import rls_company_var, rls_tenant_var
|
||||||
|
|
||||||
from .services.service import AuditService
|
from .services.service import AuditService
|
||||||
@@ -48,15 +49,39 @@ def _resolve_audit_company_tenant(session: Session, target) -> tuple:
|
|||||||
company_id / tenant_id desde la fila ORM, ContextVars RLS (petición HTTP),
|
company_id / tenant_id desde la fila ORM, ContextVars RLS (petición HTTP),
|
||||||
o ``Company.tenant_id`` por ``company_id``.
|
o ``Company.tenant_id`` por ``company_id``.
|
||||||
"""
|
"""
|
||||||
|
resolution_source = "target"
|
||||||
company_id = getattr(target, "company_id", None)
|
company_id = getattr(target, "company_id", None)
|
||||||
if company_id is None and getattr(target, "__tablename__", None) == "company":
|
if company_id is None and getattr(target, "__tablename__", None) == "company":
|
||||||
company_id = getattr(target, "id", None)
|
company_id = getattr(target, "id", None)
|
||||||
|
if company_id is not None:
|
||||||
|
resolution_source = "company_self_id"
|
||||||
if company_id is None:
|
if company_id is None:
|
||||||
company_id = rls_company_var.get()
|
company_id = rls_company_var.get()
|
||||||
|
if company_id is not None:
|
||||||
|
resolution_source = "rls_context"
|
||||||
|
|
||||||
tenant_id = getattr(target, "tenant_id", None)
|
tenant_id = getattr(target, "tenant_id", None)
|
||||||
if tenant_id is None:
|
if tenant_id is None:
|
||||||
tenant_id = rls_tenant_var.get()
|
tenant_id = rls_tenant_var.get()
|
||||||
|
if tenant_id is not None and resolution_source == "target":
|
||||||
|
resolution_source = "rls_context"
|
||||||
|
|
||||||
|
# Common fallback for invoice child tables where invoice_id points to header scope.
|
||||||
|
if (company_id is None or tenant_id is None) and hasattr(target, "invoice_id"):
|
||||||
|
invoice_id = getattr(target, "invoice_id", None)
|
||||||
|
if invoice_id is not None:
|
||||||
|
invoice_scope = (
|
||||||
|
session.query(InvoiceHeader.company_id, InvoiceHeader.tenant_id)
|
||||||
|
.filter(InvoiceHeader.id == invoice_id, InvoiceHeader.deleted_at.is_(None))
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if invoice_scope:
|
||||||
|
if company_id is None:
|
||||||
|
company_id = invoice_scope[0]
|
||||||
|
if tenant_id is None:
|
||||||
|
tenant_id = invoice_scope[1]
|
||||||
|
resolution_source = "invoice_header_lookup"
|
||||||
|
|
||||||
if tenant_id is None and company_id is not None:
|
if tenant_id is None and company_id is not None:
|
||||||
row = (
|
row = (
|
||||||
session.query(Company.tenant_id)
|
session.query(Company.tenant_id)
|
||||||
@@ -65,8 +90,9 @@ def _resolve_audit_company_tenant(session: Session, target) -> tuple:
|
|||||||
)
|
)
|
||||||
if row:
|
if row:
|
||||||
tenant_id = int(row[0])
|
tenant_id = int(row[0])
|
||||||
|
resolution_source = "company_lookup"
|
||||||
|
|
||||||
return company_id, tenant_id
|
return company_id, tenant_id, resolution_source
|
||||||
|
|
||||||
|
|
||||||
def after_insert_listener(mapper, connection, target):
|
def after_insert_listener(mapper, connection, target):
|
||||||
@@ -79,13 +105,17 @@ def after_insert_listener(mapper, connection, target):
|
|||||||
|
|
||||||
session = Session(bind=connection)
|
session = Session(bind=connection)
|
||||||
try:
|
try:
|
||||||
company_id, tenant_id = _resolve_audit_company_tenant(session, target)
|
company_id, tenant_id, resolution_source = _resolve_audit_company_tenant(
|
||||||
|
session, target
|
||||||
|
)
|
||||||
if company_id is None or tenant_id is None:
|
if company_id is None or tenant_id is None:
|
||||||
logger.debug(
|
logger.warning(
|
||||||
"Audit skip INSERT %s: missing company_id=%s tenant_id=%s",
|
"Audit skip INSERT table=%s record_id=%s company_id=%s tenant_id=%s source=%s",
|
||||||
table_name,
|
table_name,
|
||||||
|
getattr(target, "id", None),
|
||||||
company_id,
|
company_id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
|
resolution_source,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -131,13 +161,17 @@ def after_update_listener(mapper, connection, target):
|
|||||||
|
|
||||||
session = Session(bind=connection)
|
session = Session(bind=connection)
|
||||||
try:
|
try:
|
||||||
company_id, tenant_id = _resolve_audit_company_tenant(session, target)
|
company_id, tenant_id, resolution_source = _resolve_audit_company_tenant(
|
||||||
|
session, target
|
||||||
|
)
|
||||||
if company_id is None or tenant_id is None:
|
if company_id is None or tenant_id is None:
|
||||||
logger.debug(
|
logger.warning(
|
||||||
"Audit skip UPDATE %s: missing company_id=%s tenant_id=%s",
|
"Audit skip UPDATE table=%s record_id=%s company_id=%s tenant_id=%s source=%s",
|
||||||
table_name,
|
table_name,
|
||||||
|
getattr(target, "id", None),
|
||||||
company_id,
|
company_id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
|
resolution_source,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -169,13 +203,17 @@ def after_delete_listener(mapper, connection, target):
|
|||||||
|
|
||||||
session = Session(bind=connection)
|
session = Session(bind=connection)
|
||||||
try:
|
try:
|
||||||
company_id, tenant_id = _resolve_audit_company_tenant(session, target)
|
company_id, tenant_id, resolution_source = _resolve_audit_company_tenant(
|
||||||
|
session, target
|
||||||
|
)
|
||||||
if company_id is None or tenant_id is None:
|
if company_id is None or tenant_id is None:
|
||||||
logger.debug(
|
logger.warning(
|
||||||
"Audit skip DELETE %s: missing company_id=%s tenant_id=%s",
|
"Audit skip DELETE table=%s record_id=%s company_id=%s tenant_id=%s source=%s",
|
||||||
table_name,
|
table_name,
|
||||||
|
getattr(target, "id", None),
|
||||||
company_id,
|
company_id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
|
resolution_source,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,22 @@
|
|||||||
# Importar modelos para Audit Log
|
# Importar modelos para Audit Log
|
||||||
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
|
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
|
||||||
from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceSalesDetails
|
from api.v1.modules.a76.invoices.models import (
|
||||||
|
InvoiceHeader,
|
||||||
|
InvoiceSalesDetails,
|
||||||
|
InvoiceComplianceMx,
|
||||||
|
InvoiceFinancials,
|
||||||
|
InvoiceLogistics,
|
||||||
|
InvoiceCollections,
|
||||||
|
)
|
||||||
from api.v1.modules.a76.audit_log.events import register_audit_listeners
|
from api.v1.modules.a76.audit_log.events import register_audit_listeners
|
||||||
|
|
||||||
# Core Modules
|
# Core Modules
|
||||||
from api.v1.modules.a76.clients_and_providers.models import ClientProvider
|
from api.v1.modules.a76.clients_and_providers.models import ClientProvider
|
||||||
from api.v1.modules.a76.customs_brokers.models import CustomsBroker
|
from api.v1.modules.a76.customs_brokers.models import (
|
||||||
|
CustomsBroker,
|
||||||
|
CustomsBrokerVU,
|
||||||
|
CustomsBrokerPersonnel,
|
||||||
|
)
|
||||||
from api.v1.modules.a76.parts.models import Part
|
from api.v1.modules.a76.parts.models import Part
|
||||||
from api.v1.modules.a76.items.models import LineItem
|
from api.v1.modules.a76.items.models import LineItem
|
||||||
from api.v1.modules.a76.items.series.models import Serie
|
from api.v1.modules.a76.items.series.models import Serie
|
||||||
@@ -92,11 +103,17 @@ def register_audit():
|
|||||||
Pedimentos,
|
Pedimentos,
|
||||||
InvoiceHeader,
|
InvoiceHeader,
|
||||||
InvoiceSalesDetails,
|
InvoiceSalesDetails,
|
||||||
|
InvoiceComplianceMx,
|
||||||
|
InvoiceFinancials,
|
||||||
|
InvoiceLogistics,
|
||||||
|
InvoiceCollections,
|
||||||
LineItem,
|
LineItem,
|
||||||
Serie,
|
Serie,
|
||||||
# Sidebar Core Modules
|
# Sidebar Core Modules
|
||||||
ClientProvider,
|
ClientProvider,
|
||||||
CustomsBroker,
|
CustomsBroker,
|
||||||
|
CustomsBrokerVU,
|
||||||
|
CustomsBrokerPersonnel,
|
||||||
Part,
|
Part,
|
||||||
Company,
|
Company,
|
||||||
# Transportation Modules
|
# Transportation Modules
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from fastapi.responses import StreamingResponse
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy import or_, desc
|
from sqlalchemy import or_, desc
|
||||||
|
|
||||||
from core.database import get_core_db
|
from core.database import get_core_db, set_rls_context
|
||||||
from core.security import get_current_user, validate_access_to_resource
|
from core.security import get_current_user, validate_access_to_resource
|
||||||
from core.storage_s3 import get_object_bytes, list_objects_tree, should_ensure_s3_bucket
|
from core.storage_s3 import get_object_bytes, list_objects_tree, should_ensure_s3_bucket
|
||||||
from .models import AuditLog
|
from .models import AuditLog
|
||||||
@@ -209,6 +209,7 @@ async def get_bitacora(
|
|||||||
required_permissions=["audit_logs.view"],
|
required_permissions=["audit_logs.view"],
|
||||||
)
|
)
|
||||||
scope_tenant_id = _audit_scope_tenant_id(db, company_id)
|
scope_tenant_id = _audit_scope_tenant_id(db, company_id)
|
||||||
|
set_rls_context(db, tenant_id=scope_tenant_id, company_id=company_id)
|
||||||
|
|
||||||
query = db.query(AuditLog).filter(
|
query = db.query(AuditLog).filter(
|
||||||
AuditLog.company_id == company_id,
|
AuditLog.company_id == company_id,
|
||||||
@@ -271,6 +272,7 @@ async def get_procedures(
|
|||||||
required_permissions=["audit_logs.view"],
|
required_permissions=["audit_logs.view"],
|
||||||
)
|
)
|
||||||
scope_tenant_id = _audit_scope_tenant_id(db, company_id)
|
scope_tenant_id = _audit_scope_tenant_id(db, company_id)
|
||||||
|
set_rls_context(db, tenant_id=scope_tenant_id, company_id=company_id)
|
||||||
|
|
||||||
results = (
|
results = (
|
||||||
db.query(AuditLog.procedure)
|
db.query(AuditLog.procedure)
|
||||||
@@ -301,6 +303,7 @@ async def get_audit_detail(
|
|||||||
required_permissions=["audit_logs.view"],
|
required_permissions=["audit_logs.view"],
|
||||||
)
|
)
|
||||||
scope_tenant_id = _audit_scope_tenant_id(db, company_id)
|
scope_tenant_id = _audit_scope_tenant_id(db, company_id)
|
||||||
|
set_rls_context(db, tenant_id=scope_tenant_id, company_id=company_id)
|
||||||
|
|
||||||
log = (
|
log = (
|
||||||
db.query(AuditLog)
|
db.query(AuditLog)
|
||||||
@@ -339,6 +342,7 @@ async def list_tenant_files(
|
|||||||
required_permissions=["audit_logs.view"],
|
required_permissions=["audit_logs.view"],
|
||||||
)
|
)
|
||||||
scope_tenant_id = _audit_scope_tenant_id(db, company_id)
|
scope_tenant_id = _audit_scope_tenant_id(db, company_id)
|
||||||
|
set_rls_context(db, tenant_id=scope_tenant_id, company_id=company_id)
|
||||||
tenant_prefix = _tenant_prefix(scope_tenant_id)
|
tenant_prefix = _tenant_prefix(scope_tenant_id)
|
||||||
rel_path = _normalize_relative_path(path)
|
rel_path = _normalize_relative_path(path)
|
||||||
list_prefix = f"{tenant_prefix}{rel_path}/" if rel_path else tenant_prefix
|
list_prefix = f"{tenant_prefix}{rel_path}/" if rel_path else tenant_prefix
|
||||||
@@ -434,6 +438,7 @@ async def download_tenant_file(
|
|||||||
required_permissions=["audit_logs.view"],
|
required_permissions=["audit_logs.view"],
|
||||||
)
|
)
|
||||||
scope_tenant_id = _audit_scope_tenant_id(db, company_id)
|
scope_tenant_id = _audit_scope_tenant_id(db, company_id)
|
||||||
|
set_rls_context(db, tenant_id=scope_tenant_id, company_id=company_id)
|
||||||
tenant_prefix = _tenant_prefix(scope_tenant_id)
|
tenant_prefix = _tenant_prefix(scope_tenant_id)
|
||||||
rel_path = _normalize_relative_path(path)
|
rel_path = _normalize_relative_path(path)
|
||||||
if not rel_path or rel_path.endswith("/"):
|
if not rel_path or rel_path.endswith("/"):
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ class AuditMapper:
|
|||||||
# Invoices
|
# Invoices
|
||||||
"invoice_header": "IMPORT INVOICE BROWSE", # BROWSEOFACIMP
|
"invoice_header": "IMPORT INVOICE BROWSE", # BROWSEOFACIMP
|
||||||
"invoice_sales_details": "IMPORT INVOICE UPDATE", # UPDATEOFACIMP
|
"invoice_sales_details": "IMPORT INVOICE UPDATE", # UPDATEOFACIMP
|
||||||
|
"invoice_compliance_mx": "IMPORT INVOICE UPDATE",
|
||||||
|
"invoice_financials": "IMPORT INVOICE UPDATE",
|
||||||
|
"invoice_logistics": "IMPORT INVOICE UPDATE",
|
||||||
|
"invoice_collections": "IMPORT INVOICE UPDATE",
|
||||||
|
|
||||||
# Exports would be similar but we start with general
|
# Exports would be similar but we start with general
|
||||||
|
|
||||||
@@ -88,9 +92,11 @@ class AuditMapper:
|
|||||||
|
|
||||||
# General fallbacks
|
# General fallbacks
|
||||||
"clients_and_providers": "CATALOGS",
|
"clients_and_providers": "CATALOGS",
|
||||||
"clients_and_providers": "CATALOGS",
|
|
||||||
"clients_and_providers": "CATALOGS",
|
|
||||||
"items": "CATALOGS",
|
"items": "CATALOGS",
|
||||||
|
"parts": "CATALOGS",
|
||||||
|
"customs_brokers": "CATALOGS",
|
||||||
|
"customs_brokers_vu": "CATALOGS",
|
||||||
|
"customs_brokers_personnel": "CATALOGS",
|
||||||
"classes": "CATALOGS",
|
"classes": "CATALOGS",
|
||||||
"classification_concepts": "CATALOGS",
|
"classification_concepts": "CATALOGS",
|
||||||
"concepts": "CATALOGS",
|
"concepts": "CATALOGS",
|
||||||
@@ -107,6 +113,7 @@ class AuditMapper:
|
|||||||
"ports": "CATALOGS",
|
"ports": "CATALOGS",
|
||||||
"prevalidators": "CATALOGS",
|
"prevalidators": "CATALOGS",
|
||||||
"seal": "CATALOGS",
|
"seal": "CATALOGS",
|
||||||
|
"seals": "CATALOGS",
|
||||||
"signatures": "CATALOGS",
|
"signatures": "CATALOGS",
|
||||||
"tariff_fractions": "CATALOGS",
|
"tariff_fractions": "CATALOGS",
|
||||||
"unit_conversions": "CATALOGS",
|
"unit_conversions": "CATALOGS",
|
||||||
@@ -127,6 +134,18 @@ class AuditMapper:
|
|||||||
("invoice_sales_details", "CREATE"): "ADD IMPORT_INVOICE_ITEM",
|
("invoice_sales_details", "CREATE"): "ADD IMPORT_INVOICE_ITEM",
|
||||||
("invoice_sales_details", "UPDATE"): "EDIT IMPORT_INVOICE_ITEM",
|
("invoice_sales_details", "UPDATE"): "EDIT IMPORT_INVOICE_ITEM",
|
||||||
("invoice_sales_details", "DELETE"): "DELETE IMPORT_INVOICE_ITEM",
|
("invoice_sales_details", "DELETE"): "DELETE IMPORT_INVOICE_ITEM",
|
||||||
|
("invoice_compliance_mx", "CREATE"): "ADD IMPORT_INVOICE",
|
||||||
|
("invoice_compliance_mx", "UPDATE"): "EDIT IMPORT_INVOICE",
|
||||||
|
("invoice_compliance_mx", "DELETE"): "DELETE IMPORT_INVOICE",
|
||||||
|
("invoice_financials", "CREATE"): "ADD IMPORT_INVOICE",
|
||||||
|
("invoice_financials", "UPDATE"): "EDIT IMPORT_INVOICE",
|
||||||
|
("invoice_financials", "DELETE"): "DELETE IMPORT_INVOICE",
|
||||||
|
("invoice_logistics", "CREATE"): "ADD IMPORT_INVOICE",
|
||||||
|
("invoice_logistics", "UPDATE"): "EDIT IMPORT_INVOICE",
|
||||||
|
("invoice_logistics", "DELETE"): "DELETE IMPORT_INVOICE",
|
||||||
|
("invoice_collections", "CREATE"): "ADD IMPORT_INVOICE",
|
||||||
|
("invoice_collections", "UPDATE"): "EDIT IMPORT_INVOICE",
|
||||||
|
("invoice_collections", "DELETE"): "DELETE IMPORT_INVOICE",
|
||||||
|
|
||||||
("pedimentos", "CREATE"): "ADD PEDIMENTO",
|
("pedimentos", "CREATE"): "ADD PEDIMENTO",
|
||||||
("pedimentos", "UPDATE"): "EDIT PEDIMENTO",
|
("pedimentos", "UPDATE"): "EDIT PEDIMENTO",
|
||||||
@@ -146,6 +165,9 @@ class AuditMapper:
|
|||||||
("company", "CREATE"): "ADD COMPANY",
|
("company", "CREATE"): "ADD COMPANY",
|
||||||
("company", "UPDATE"): "EDIT COMPANY",
|
("company", "UPDATE"): "EDIT COMPANY",
|
||||||
("company", "DELETE"): "DELETE COMPANY",
|
("company", "DELETE"): "DELETE COMPANY",
|
||||||
|
("companies", "CREATE"): "ADD COMPANY",
|
||||||
|
("companies", "UPDATE"): "EDIT COMPANY",
|
||||||
|
("companies", "DELETE"): "DELETE COMPANY",
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ class AuditService:
|
|||||||
elif table_name == "parts":
|
elif table_name == "parts":
|
||||||
reference = record_data.get("part_number") or reference
|
reference = record_data.get("part_number") or reference
|
||||||
|
|
||||||
elif table_name == "companies":
|
elif table_name in {"companies", "company"}:
|
||||||
reference = record_data.get("rfc") or reference
|
reference = record_data.get("rfc") or reference
|
||||||
|
|
||||||
elif table_name == "classes":
|
elif table_name == "classes":
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ from sqlalchemy.orm import Session
|
|||||||
from sqlalchemy import and_, func, or_
|
from sqlalchemy import and_, func, or_
|
||||||
from core.exceptions import ErrorCollector, DuplicateResourceException
|
from core.exceptions import ErrorCollector, DuplicateResourceException
|
||||||
from core.context import get_user_context
|
from core.context import get_user_context
|
||||||
|
from api.v1.modules.a76.audit_log.models import AuditLog
|
||||||
|
from api.v1.modules.a76.audit_log.services.service import AuditService
|
||||||
from .common.mappers import clean_dict
|
from .common.mappers import clean_dict
|
||||||
from .imports.validators.create import validate_create as validate_create_import
|
from .imports.validators.create import validate_create as validate_create_import
|
||||||
from .imports.validators.update import validate_update as validate_update_import
|
from .imports.validators.update import validate_update as validate_update_import
|
||||||
@@ -221,6 +223,47 @@ def _autofill_remesa_if_needed(db: Session, invoice_data, tenant_id: int, compan
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_create_audit_log(
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
table_name: str,
|
||||||
|
record_id: str,
|
||||||
|
record_data: dict,
|
||||||
|
username: str,
|
||||||
|
tenant_id: int,
|
||||||
|
company_id: int,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Backup audit writer: writes CREATE only when missing.
|
||||||
|
Returns True when a backup log row was added.
|
||||||
|
"""
|
||||||
|
exists = (
|
||||||
|
db.query(AuditLog.spec_id)
|
||||||
|
.filter(
|
||||||
|
AuditLog.table_name == table_name,
|
||||||
|
AuditLog.operation_type == "CREATE",
|
||||||
|
AuditLog.record_id == record_id,
|
||||||
|
AuditLog.tenant_id == tenant_id,
|
||||||
|
AuditLog.company_id == company_id,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if exists:
|
||||||
|
return False
|
||||||
|
|
||||||
|
AuditService.log_crud_operation(
|
||||||
|
db=db,
|
||||||
|
table_name=table_name,
|
||||||
|
operation_type="CREATE",
|
||||||
|
record_data=record_data,
|
||||||
|
username=username,
|
||||||
|
record_id=record_id,
|
||||||
|
company_id=company_id,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class InvoiceService:
|
class InvoiceService:
|
||||||
"""Service for Invoice Header operations"""
|
"""Service for Invoice Header operations"""
|
||||||
|
|
||||||
@@ -521,6 +564,36 @@ class InvoiceService:
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(new_invoice)
|
db.refresh(new_invoice)
|
||||||
|
backup_written = _ensure_create_audit_log(
|
||||||
|
db,
|
||||||
|
table_name=new_invoice.__tablename__,
|
||||||
|
record_id=str(new_invoice.id),
|
||||||
|
record_data={
|
||||||
|
c.name: getattr(new_invoice, c.name)
|
||||||
|
for c in models.InvoiceHeader.__table__.columns
|
||||||
|
},
|
||||||
|
username=username,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
company_id=company_id,
|
||||||
|
)
|
||||||
|
for detail in new_invoice.details:
|
||||||
|
backup_written = (
|
||||||
|
_ensure_create_audit_log(
|
||||||
|
db,
|
||||||
|
table_name=detail.__tablename__,
|
||||||
|
record_id=str(detail.id),
|
||||||
|
record_data={
|
||||||
|
c.name: getattr(detail, c.name)
|
||||||
|
for c in models.InvoiceSalesDetails.__table__.columns
|
||||||
|
},
|
||||||
|
username=username,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
company_id=company_id,
|
||||||
|
)
|
||||||
|
or backup_written
|
||||||
|
)
|
||||||
|
if backup_written:
|
||||||
|
db.commit()
|
||||||
return new_invoice
|
return new_invoice
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ from sqlalchemy.orm import selectinload
|
|||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from api.v1.modules.a76.audit_log.models import AuditLog
|
||||||
|
from api.v1.modules.a76.audit_log.services.service import AuditService
|
||||||
|
from core.context import get_user_context
|
||||||
from ..dtos.pedimentos import PedimentosCreate, PedimentosUpdate
|
from ..dtos.pedimentos import PedimentosCreate, PedimentosUpdate
|
||||||
|
|
||||||
from .pedimento_config_additional import PedimentoConfigAdditionalService
|
from .pedimento_config_additional import PedimentoConfigAdditionalService
|
||||||
@@ -59,6 +62,54 @@ from ..models.pedimento_contributions import PedimentoContributions
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_create_audit_log(
|
||||||
|
db: Session, *, table_name: str, record_id: str, record_data: Dict[str, Any], username: str, tenant_id: int, company_id: int
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Backup audit writer: inserts CREATE log only when listener did not.
|
||||||
|
"""
|
||||||
|
exists = (
|
||||||
|
db.query(AuditLog.spec_id)
|
||||||
|
.filter(
|
||||||
|
AuditLog.table_name == table_name,
|
||||||
|
AuditLog.operation_type == "CREATE",
|
||||||
|
AuditLog.record_id == record_id,
|
||||||
|
AuditLog.tenant_id == tenant_id,
|
||||||
|
AuditLog.company_id == company_id,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if exists:
|
||||||
|
return
|
||||||
|
|
||||||
|
AuditService.log_crud_operation(
|
||||||
|
db=db,
|
||||||
|
table_name=table_name,
|
||||||
|
operation_type="CREATE",
|
||||||
|
record_data=record_data,
|
||||||
|
username=username,
|
||||||
|
record_id=record_id,
|
||||||
|
company_id=company_id,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_current_username() -> str:
|
||||||
|
try:
|
||||||
|
context = get_user_context()
|
||||||
|
if context:
|
||||||
|
return (
|
||||||
|
context.get("preferred_username")
|
||||||
|
or context.get("email")
|
||||||
|
or context.get("sub")
|
||||||
|
or "System"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return "System"
|
||||||
|
|
||||||
|
|
||||||
class PedimentosService:
|
class PedimentosService:
|
||||||
"""Service class for Pedimentos business logic"""
|
"""Service class for Pedimentos business logic"""
|
||||||
|
|
||||||
@@ -371,6 +422,15 @@ class PedimentosService:
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(pedimento)
|
db.refresh(pedimento)
|
||||||
|
_ensure_create_audit_log(
|
||||||
|
db,
|
||||||
|
table_name=pedimento.__tablename__,
|
||||||
|
record_id=str(pedimento.id),
|
||||||
|
record_data={c.name: getattr(pedimento, c.name) for c in Pedimentos.__table__.columns},
|
||||||
|
username=_get_current_username(),
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
company_id=company_id,
|
||||||
|
)
|
||||||
return pedimento
|
return pedimento
|
||||||
|
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
|
|||||||
@@ -14,22 +14,22 @@
|
|||||||
import { RefreshCw, Search } from 'lucide-svelte';
|
import { RefreshCw, Search } from 'lucide-svelte';
|
||||||
import { companyStore } from '$lib/stores/company.svelte';
|
import { companyStore } from '$lib/stores/company.svelte';
|
||||||
|
|
||||||
let logs: AuditLog[] = [];
|
let logs = $state<AuditLog[]>([]);
|
||||||
let total = 0;
|
let total = $state(0);
|
||||||
let page = 1;
|
let page = $state(1);
|
||||||
const pageSize = 50;
|
const pageSize = 50;
|
||||||
let loading = false;
|
let loading = $state(false);
|
||||||
let error: string | null = null;
|
let error = $state<string | null>(null);
|
||||||
|
|
||||||
let search = '';
|
let search = $state('');
|
||||||
let usernameFilter = '';
|
let usernameFilter = $state('');
|
||||||
let procedureFilter = '';
|
let procedureFilter = $state('');
|
||||||
let dateFrom = '';
|
let dateFrom = $state('');
|
||||||
let dateTo = '';
|
let dateTo = $state('');
|
||||||
|
|
||||||
let procedures: string[] = [];
|
let procedures = $state<string[]>([]);
|
||||||
|
|
||||||
let hasMore = true;
|
let hasMore = $state(true);
|
||||||
let sentinel: HTMLElement;
|
let sentinel: HTMLElement;
|
||||||
|
|
||||||
function isSpanish() {
|
function isSpanish() {
|
||||||
@@ -171,8 +171,11 @@
|
|||||||
hasMore = true;
|
hasMore = true;
|
||||||
logs = [];
|
logs = [];
|
||||||
loading = false;
|
loading = false;
|
||||||
void loadProcedures();
|
// Run async loaders outside effect tracking to avoid reactive loops.
|
||||||
void loadLogs();
|
queueMicrotask(() => {
|
||||||
|
void loadProcedures();
|
||||||
|
void loadLogs();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user