This commit is contained in:
hreyes
2026-03-10 12:43:34 -06:00
parent 31cd249fd4
commit e6184cb757
7 changed files with 923 additions and 22 deletions

View File

@@ -62,7 +62,7 @@ async def upload_import_file(
"user_id": current_user.get("id"),
"footer_config": footer_config,
"operation_type": operation_type or "exp",
"template_id": template_id or ("exp_def_header" if model_target == "invoice_header" else "exp_def_details"),
"template_id": template_id or ("exp_def_header" if model_target == "invoice_header" else "exp_def_partidas"),
}
try:

View File

@@ -38,7 +38,7 @@ def _norm_row(row: Dict[str, Any], template_id: str) -> Dict[str, Any]:
def scan_file(self, job_id: str, model_target: str, config: str = None):
"""
Para invoice_header: delega en facturas._do_scan_file con storage "exp" (validaciones FK, encabezados_expo).
Para invoice_details: scan sin validaciones (stub).
Para invoice_details: delega en facturas._do_scan_file con template exp_def_partidas (validaciones partidas expo).
"""
logger.info("Exportación import: starting scan for job %s target %s", job_id, model_target)
@@ -46,45 +46,37 @@ def scan_file(self, job_id: str, model_target: str, config: str = None):
from api.v1.modules.a76.layouts_csv.facturas.tasks import _do_scan_file
return _do_scan_file(job_id, "invoice_header", config, job_type_override="exp")
# invoice_details: stub sin validaciones
if model_target == "invoice_details":
from api.v1.modules.a76.layouts_csv.facturas.tasks import _do_scan_file
return _do_scan_file(job_id, "invoice_details", config, job_type_override="exp")
# Fallback (e.g. unknown model_target)
file_path = _ensure_file(job_id)
if not file_path:
return {"status": "failed", "error": "Archivo no encontrado (expirado o no subido). Sube de nuevo."}
if os.path.getsize(file_path) == 0:
return {"status": "failed", "error": "El archivo está vacío."}
_ensure_meta(job_id, file_path)
try:
common_meta.require_tenant_context(file_path)
except ValueError as e:
return {"status": "failed", "error": str(e)}
meta = common_meta.load_meta(file_path) or {}
template_id = meta.get("template_id") or (
"exp_def_header" if model_target == "invoice_header" else "exp_def_details"
)
total_rows = 0
processed_rows = 0
template_id = meta.get("template_id") or "exp_def_details"
try:
total_rows = common_csv_reader.count_csv_rows(file_path, has_header=True)
except Exception as e:
return {"status": "failed", "error": str(e)}
def on_progress(current: int, total: int) -> None:
self.update_state(state="PROGRESS", meta={"current": current, "total": total})
processed_rows = 0
try:
for i, row in common_csv_reader.iter_csv_rows(file_path, fieldnames=None):
if i % 500 == 0:
on_progress(i, total_rows)
self.update_state(state="PROGRESS", meta={"current": i, "total": total_rows})
_norm_row(row, template_id)
processed_rows += 1
except Exception as e:
logger.error("Exportación import scan failed: %s", e)
return {"status": "failed", "error": str(e)}
return common_responses.scan_result(job_id, processed_rows, 0, [])
@@ -92,7 +84,7 @@ def scan_file(self, job_id: str, model_target: str, config: str = None):
def insert_valid_rows(self, job_id: str, model_target: str):
"""
Commit: para invoice_header delega en facturas (inserción real en BD con storage "exp").
Para invoice_details mantiene stub (sin inserción).
Para invoice_details delega en facturas (inserción partidas expo cuando esté implementada; mientras tanto mismo flujo).
"""
logger.info("Exportación import: starting commit for job %s target %s", job_id, model_target)
@@ -100,7 +92,11 @@ def insert_valid_rows(self, job_id: str, model_target: str):
from api.v1.modules.a76.layouts_csv.facturas.tasks import _do_insert_valid_rows
return _do_insert_valid_rows(job_id, "invoice_header", job_type_override="exp")
# invoice_details: stub (sin inserción en BD)
if model_target == "invoice_details":
from api.v1.modules.a76.layouts_csv.facturas.tasks import _do_insert_valid_rows
return _do_insert_valid_rows(job_id, "invoice_details", job_type_override="exp")
# Fallback: stub sin inserción
file_path = _ensure_file(job_id)
if not file_path:
alt_path = common_storage.file_path_for_job(JOB_TYPE, job_id)
@@ -121,7 +117,7 @@ def insert_valid_rows(self, job_id: str, model_target: str):
error_lines = common_storage.get_error_lines(JOB_TYPE, job_id, error_path)
template_id = meta.get("template_id") or (
"exp_def_header" if model_target == "invoice_header" else "exp_def_details"
"exp_def_header" if model_target == "invoice_header" else "exp_def_partidas"
)
try:

View File

@@ -57,6 +57,36 @@ TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
{"canonical": "FRACCION"},
{"canonical": "ORDEN DE COMPRA", "aliases": ["ORDENCOMPRA"]},
],
# Partidas exportación definitiva (Clarion EstructuraParExpoCamReg: AV)
"exp_def_partidas": [
{"canonical": "NUMERO FACTURA EXPO", "aliases": ["NUMERO FACTURA EXPO.", "NUM FACTURA EXPO", "FACTURA EXPO"]},
{"canonical": "LINEA EXPO", "aliases": ["LINEA EXPO.", "RENGLON EXPO"]},
{"canonical": "TIPO DE IMPO", "aliases": ["TIPO DE IMPO.", "TIPO IMPO", "PROCEDENCIA"]},
{"canonical": "FACTURA IMPO", "aliases": ["FACTURA IMPO.", "FACTURA IMPORTACION"]},
{"canonical": "LINEA IMPO", "aliases": ["LINEA IMPO.", "LINEA IMPORTACION"]},
{"canonical": "GENERA DESCARGA", "aliases": ["GENERA DESCARGA?", "DESCARGA"]},
{"canonical": "CANTIDAD EXPORTADA/DESCARGAR", "aliases": ["CANTIDAD EXPORTADA", "CANTIDAD EXPORTADA/DESCARGAR", "CANTIDAD"]},
{"canonical": "UNIDAD DE MEDIDA", "aliases": ["U.M.", "UNIDAD MEDIDA"]},
{"canonical": "COSTO UNITARIO", "aliases": ["COSTOUNITARIO"]},
{"canonical": "PESO NETO", "aliases": ["PESONETO"]},
{"canonical": "PESO BRUTO", "aliases": ["PESOBRUTO"]},
{"canonical": "SE PAGO IMPUESTO", "aliases": ["SE PAGO IMPUESTO? (SI o NO)", "SEPAGOIMPUESTO"]},
{"canonical": "FORMA DE PAGO", "aliases": ["FORMADEPAGO", "FORMA PAGO"]},
{"canonical": "DESCRIPCION EXTRA", "aliases": ["DESCRIPCION EXTRA", "DESCRIPCIONEXTRA"]},
{"canonical": "INFORMACION ADICIONAL", "aliases": ["INFORMACION ADICIONAL", "INFORMACIONADICIONAL"]},
{"canonical": "AGREGAR(A)/SUSTITUIR(S)", "aliases": ["AGREGAR(A)/SUSTITUIR(S)", "AGREGAR/SUSTITUIR", "SUSTITUIR"]},
{"canonical": "LOTE"},
{"canonical": "NUMERO ENTRADA", "aliases": ["NUMERO ENTRADA", "NUM ENTRADA"]},
{"canonical": "ES PARTIDA/SUBPARTIDA", "aliases": ["ES PARTIDA/SUBPARTIDA", "ESSUBPARTIDA", "ES PARTIDA O SUBPARTIDA"]},
{"canonical": "LINEA PRINCIPAL", "aliases": ["LINEAPRINCIPAL", "PARTIDA PRINCIPAL"]},
{"canonical": "FRACCION AMERICANA", "aliases": ["FRACCION AMERICANA", "FRACCIONAMERICANA"]},
{"canonical": "FRACCION ARANCELARIA", "aliases": ["FRACCION ARANCELARIA", "FRACCIONARANCELARIA"]},
{"canonical": "CANTIDAD BULTOS", "aliases": ["CANTIDADBULTOS"]},
{"canonical": "CLAVE BULTOS", "aliases": ["CLAVEBULTOS"]},
{"canonical": "PAIS ORIGEN", "aliases": ["PAISORIGEN", "PAIS"]},
{"canonical": "NUM. PARTE", "aliases": ["NUMPARTE", "NUMERO PARTE", "NUM PARTE"]},
{"canonical": "ORDEN DE COMPRA", "aliases": ["ORDENCOMPRA", "ORDEN DE VENTA"]},
],
}

View File

@@ -222,9 +222,11 @@ def _do_scan_file(job_id: str, model_target: str, config: Optional[str] = None,
"imp_temp_header" if model_target == "invoice_header" else
"imp_temp_details" if model_target == "invoice_details" else "imp_temp_series"
)
# Cuando el scan viene de Exportación (job_type_override "exp"), forzar exp_def_header para que corran las validaciones FK en el escaneo
# Cuando el scan viene de Exportación (job_type_override "exp"), forzar exp_def_header o exp_def_partidas
if job_type_override == "exp" and model_target == "invoice_header":
template_id = "exp_def_header"
if job_type_override == "exp" and model_target == "invoice_details":
template_id = "exp_def_partidas"
inv_type_value = normalize_public_code(footer_config.get("invoice_type") or meta.get("invoice_type") or "TEM")
if not inv_type_value:
inv_type_value = "TEM"
@@ -1202,6 +1204,269 @@ def _do_scan_file(job_id: str, model_target: str, config: Optional[str] = None,
logger.exception("Partidas importación definitiva scan failed: %s", e)
return {"status": "failed", "error": str(e)}
# --- Partidas Exportación Definitiva: Clarion VALIDA_TODA_PAR_EXPO / VALIDA_PARCIAL_PAR_EXPO / VALIDACIONES_PAR_EXPO ---
if model_target == "invoice_details" and template_id == "exp_def_partidas":
try:
from api.v1.modules.a76.invoices.models import InvoiceHeader
from api.v1.modules.a76.items.models import LineItem
from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure
from api.v1.modules.a76.general_catalogs.packages.models import Package
from api.v1.modules.public.reference_data.payment_methods.models import PaymentMethod
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import USTariffFraction
from api.v1.modules.a76.general_catalogs.company.models import Company
from api.v1.modules.a76.parts.models import Part
from .validators.partidas_expo import validate_row_partidas_expo
_fc = parse_footer_config(meta.get("footer_config"))
autonumerar = meta.get("autonumerar", True)
actualizar = meta.get("actualizar", False)
levantar_subpartidas = meta.get("levantar_subpartidas", False)
validar_decimales_pza = meta.get("validar_decimales_pza", False)
if _fc:
if "autonumerar" in _fc:
autonumerar = bool(_fc["autonumerar"])
elif _fc.get("autonumber_partidas", "true") is not None:
autonumerar = str(_fc.get("autonumber_partidas", "true")).lower() in ("true", "1", "si", "", "yes")
if "actualizar" in _fc:
actualizar = bool(_fc["actualizar"])
if "levantar_subpartidas" in _fc:
levantar_subpartidas = bool(_fc["levantar_subpartidas"])
if "validar_decimales_pza" in _fc:
validar_decimales_pza = bool(_fc["validar_decimales_pza"])
RFC_EXCEPTION_UPDATED = set()
RFC_EXCEPTION_EGM = {"EGM0303257J1"}
with CoreSessionLocal() as session:
company = session.query(Company).filter(Company.id == company_id).first()
company_rfc = (company.rfc or "").strip().upper() if company else ""
if company_rfc in RFC_EXCEPTION_EGM:
rfc_exception_egm = True
else:
rfc_exception_egm = False
q_inv_expo = (
session.query(InvoiceHeader.invoice_number, InvoiceHeader.id, InvoiceHeader.is_updated)
.filter(
InvoiceHeader.tenant_id == tenant_id,
InvoiceHeader.company_id == company_id,
InvoiceHeader.operation_type == "exp",
)
)
invoice_id_by_number: Dict[str, int] = {}
invoice_updated_by_number: Dict[str, bool] = {}
for num, iid, is_upd in q_inv_expo.all():
if num:
invoice_id_by_number[str(num).strip()] = iid
invoice_updated_by_number[str(num).strip()] = bool(is_upd)
existing_line_keys_by_invoice: Dict[str, Set[str]] = {}
q_li_expo = (
session.query(InvoiceHeader.invoice_number, LineItem.line_number)
.join(LineItem, LineItem.invoice_id == InvoiceHeader.id)
.filter(
InvoiceHeader.tenant_id == tenant_id,
InvoiceHeader.company_id == company_id,
InvoiceHeader.operation_type == "exp",
)
)
for num, ln in q_li_expo.all():
if num is not None:
key = str(num).strip()
if key not in existing_line_keys_by_invoice:
existing_line_keys_by_invoice[key] = set()
existing_line_keys_by_invoice[key].add(str(ln).strip())
partidas_principales_bd: Set[Tuple[str, str]] = set()
try:
from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem
q_pp = (
session.query(InvoiceHeader.invoice_number, LineItem.line_number)
.join(LineItem, LineItem.invoice_id == InvoiceHeader.id)
.join(FaLineItem, FaLineItem.id == LineItem.id)
.filter(
InvoiceHeader.tenant_id == tenant_id,
InvoiceHeader.company_id == company_id,
InvoiceHeader.operation_type == "exp",
FaLineItem.is_subitem == False,
FaLineItem.contains_subitems == True,
)
)
for num, ln in q_pp.all():
if num is not None:
partidas_principales_bd.add((str(num).strip(), str(ln).strip()))
except Exception:
pass
factura_impo_tem_by_number: Dict[str, int] = {}
q_tem = session.query(InvoiceHeader.invoice_number, InvoiceHeader.id).filter(
InvoiceHeader.tenant_id == tenant_id,
InvoiceHeader.company_id == company_id,
InvoiceHeader.operation_type == "imp",
InvoiceHeader.invoice_type == "TEM",
)
for num, iid in q_tem.all():
if num:
factura_impo_tem_by_number[str(num).strip()] = iid
factura_impo_def_by_number: Dict[str, int] = {}
DEF_INVOICE_TYPES = ("DEF", "MATDE", "EXDEF")
q_def = session.query(InvoiceHeader.invoice_number, InvoiceHeader.id).filter(
InvoiceHeader.tenant_id == tenant_id,
InvoiceHeader.company_id == company_id,
InvoiceHeader.operation_type == "imp",
InvoiceHeader.invoice_type.in_(DEF_INVOICE_TYPES),
)
for num, iid in q_def.all():
if num:
factura_impo_def_by_number[str(num).strip()] = iid
line_exists_tem: Set[Tuple[int, str]] = set()
q_li_tem = (
session.query(LineItem.invoice_id, LineItem.line_number)
.join(InvoiceHeader, InvoiceHeader.id == LineItem.invoice_id)
.filter(
InvoiceHeader.tenant_id == tenant_id,
InvoiceHeader.company_id == company_id,
InvoiceHeader.operation_type == "imp",
InvoiceHeader.invoice_type == "TEM",
)
)
for inv_id, ln in q_li_tem.all():
if inv_id is not None and ln is not None:
line_exists_tem.add((inv_id, str(ln).strip()))
line_exists_def: Set[Tuple[int, str]] = set()
q_li_def = (
session.query(LineItem.invoice_id, LineItem.line_number)
.join(InvoiceHeader, InvoiceHeader.id == LineItem.invoice_id)
.filter(
InvoiceHeader.tenant_id == tenant_id,
InvoiceHeader.company_id == company_id,
InvoiceHeader.operation_type == "imp",
InvoiceHeader.invoice_type.in_(DEF_INVOICE_TYPES),
)
)
for inv_id, ln in q_li_def.all():
if inv_id is not None and ln is not None:
line_exists_def.add((inv_id, str(ln).strip()))
valid_uom_codes: Set[str] = set()
for u in session.query(UnitOfMeasure.code).filter(UnitOfMeasure.tenant_id == tenant_id, UnitOfMeasure.company_id == company_id).all():
if u[0]:
valid_uom_codes.add((u[0] or "").strip().upper())
valid_bulks_codes: Set[str] = set()
for p in session.query(Package.key).filter(Package.tenant_id == tenant_id, Package.company_id == company_id).all():
if p[0]:
valid_bulks_codes.add((p[0] or "").strip())
valid_payment_methods: Set[str] = set()
for row in session.query(PaymentMethod.key).all():
if row[0] is not None:
valid_payment_methods.add(str(row[0]).strip())
valid_fraction_ame: Set[str] = set()
for row in session.query(USTariffFraction.code).filter(USTariffFraction.tenant_id == tenant_id, USTariffFraction.company_id == company_id).all():
if row[0]:
valid_fraction_ame.add((row[0] or "").strip())
valid_part_numbers: Set[str] = set()
for row in session.query(Part.part_number).filter(Part.tenant_id == tenant_id, Part.company_id == company_id).all():
if row[0]:
valid_part_numbers.add((row[0] or "").strip().upper())
invoice_numbers_from_csv = set()
with open(file_path, "r", encoding="utf-8-sig") as f_in:
sample = f_in.read(2048)
f_in.seek(0)
try:
dialect = csv.Sniffer().sniff(sample, delimiters=",;\t")
except Exception:
dialect = "excel"
reader = csv.DictReader(f_in, dialect=dialect)
rows_list = list(reader)
for row in rows_list:
row_norm = row_from_template(row, "exp_def_partidas", normalize_header)
inv = (row_norm.get("NUMERO FACTURA EXPO") or row_norm.get("NUMERO FACTURA EXPO.") or row_norm.get("FACTURA EXPO") or "").strip()
if inv:
invoice_numbers_from_csv.add(inv)
if company_rfc in RFC_EXCEPTION_EGM:
rfc_exception_updated = invoice_numbers_from_csv
else:
rfc_exception_updated = set()
line_counts_csv: Dict[Tuple[str, str], int] = {}
partidas_principales_csv: Set[Tuple[str, str]] = set()
def _get_row_expo(row_norm: Dict[str, Any], *keys: str) -> str:
for k in keys:
v = row_norm.get(k)
if v is not None and str(v).strip():
return str(v).strip()
return ""
for row in rows_list:
row_norm = row_from_template(row, "exp_def_partidas", normalize_header)
inv = _get_row_expo(row_norm, "NUMERO FACTURA EXPO", "NUMERO FACTURA EXPO.", "FACTURA EXPO")
linea = _get_row_expo(row_norm, "LINEA EXPO", "LINEA EXPO.", "RENGLON EXPO")
if inv and linea:
key = (inv, linea)
line_counts_csv[key] = line_counts_csv.get(key, 0) + 1
u = _get_row_expo(row_norm, "ES PARTIDA/SUBPARTIDA", "ESSUBPARTIDA", "ES PARTIDA O SUBPARTIDA").upper()
if u == "P" and inv and linea:
partidas_principales_csv.add((inv, linea))
error_count = 0
processed_rows = 0
error_lines_list = []
errors_detail = []
with open(error_path, "w", encoding="utf-8") as f_err:
for i, row in enumerate(rows_list, start=1):
if i % 1000 == 0:
self.update_state(state="PROGRESS", meta={"current": i, "total": total_rows, "errors": error_count})
row_norm = row_from_template(row, "exp_def_partidas", normalize_header)
err = validate_row_partidas_expo(
row_norm,
i,
autonumerar=autonumerar,
actualizar=actualizar,
levantar_subpartidas=levantar_subpartidas,
invoice_id_by_number=invoice_id_by_number,
invoice_updated_by_number=invoice_updated_by_number,
rfc_exception_updated=rfc_exception_updated,
existing_line_keys_by_invoice=existing_line_keys_by_invoice,
line_counts_csv=line_counts_csv,
partidas_principales_csv=partidas_principales_csv,
partidas_principales_bd=partidas_principales_bd,
valid_uom_codes=valid_uom_codes,
valid_bulks_codes=valid_bulks_codes,
valid_payment_methods=valid_payment_methods,
valid_fraction_ame=valid_fraction_ame,
valid_part_numbers=valid_part_numbers,
factura_impo_tem_by_number=factura_impo_tem_by_number,
factura_impo_def_by_number=factura_impo_def_by_number,
line_exists_tem=line_exists_tem,
line_exists_def=line_exists_def,
rfc_exception_egm=rfc_exception_egm,
validar_decimales_pza=validar_decimales_pza,
)
if err:
error_count += 1
error_lines_list.append(err["line"])
f_err.write(json.dumps({"line": err["line"], "col": err.get("col", ""), "msg": err.get("msg", "")}) + "\n")
if len(errors_detail) < 500:
errors_detail.append({"line": err["line"], "col": err.get("col", ""), "msg": err.get("msg", "")})
processed_rows += 1
common_storage.store_error_lines(effective_job_type, job_id, error_lines_list)
return common_responses.scan_result(job_id, processed_rows, error_count, errors_detail)
except Exception as e:
logger.exception("Partidas exportación definitiva scan failed: %s", e)
return {"status": "failed", "error": str(e)}
# --- Partidas Compras Mexicanas: misma lógica que Impo Def, facturas MEX ---
if model_target == "invoice_details" and template_id == "cmex_details":
try:
@@ -3308,6 +3573,24 @@ def _do_insert_valid_rows(job_id: str, model_target: str, job_type_override: Opt
error_path = common_storage.error_path_for_job(effective_job_type, job_id)
error_lines = common_storage.get_error_lines(effective_job_type, job_id, error_path)
# Partidas exportación definitiva: inserción pendiente (Phase 2); solo cleanup y respuesta
if model_target == "invoice_details" and meta.get("template_id") == "exp_def_partidas":
common_storage.cleanup_import_job(
effective_job_type, job_id,
file_path=file_path,
error_path=error_path,
meta_path=meta_path,
)
return {
"status": "finished",
"inserted": 0,
"skipped_invalid": len(error_lines),
"skipped_missing_fk": 0,
"skipped_duplicate": 0,
"skipped_details": [],
"message": "Validación de partidas de exportación completada. Inserción en BD pendiente de implementación (Phase 2).",
}
# Si el upload fue de series (template_id imp_temp_series o imp_def_series), usar flujo series aunque model_target venga mal
use_series_flow = (
model_target == "invoice_series"

View File

@@ -188,6 +188,36 @@ TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
# --- Partidas: Impo Def y Expo - misma estructura ---
"imp_def_details": None,
"exp_def_details": None,
# --- Partidas exportación definitiva (Clarion EstructuraParExpoCamReg AV) ---
"exp_def_partidas": [
{"canonical": "NUMERO FACTURA EXPO", "aliases": ["NUMERO FACTURA EXPO.", "NUM FACTURA EXPO", "FACTURA EXPO"]},
{"canonical": "LINEA EXPO", "aliases": ["LINEA EXPO.", "RENGLON EXPO"]},
{"canonical": "TIPO DE IMPO", "aliases": ["TIPO DE IMPO.", "TIPO IMPO", "PROCEDENCIA"]},
{"canonical": "FACTURA IMPO", "aliases": ["FACTURA IMPO.", "FACTURA IMPORTACION"]},
{"canonical": "LINEA IMPO", "aliases": ["LINEA IMPO.", "LINEA IMPORTACION"]},
{"canonical": "GENERA DESCARGA", "aliases": ["GENERA DESCARGA?", "DESCARGA"]},
{"canonical": "CANTIDAD EXPORTADA/DESCARGAR", "aliases": ["CANTIDAD EXPORTADA", "CANTIDAD EXPORTADA/DESCARGAR", "CANTIDAD"]},
{"canonical": "UNIDAD DE MEDIDA", "aliases": ["U.M.", "UNIDAD MEDIDA"]},
{"canonical": "COSTO UNITARIO", "aliases": ["COSTOUNITARIO"]},
{"canonical": "PESO NETO", "aliases": ["PESONETO"]},
{"canonical": "PESO BRUTO", "aliases": ["PESOBRUTO"]},
{"canonical": "SE PAGO IMPUESTO", "aliases": ["SE PAGO IMPUESTO? (SI o NO)", "SEPAGOIMPUESTO"]},
{"canonical": "FORMA DE PAGO", "aliases": ["FORMADEPAGO", "FORMA PAGO"]},
{"canonical": "DESCRIPCION EXTRA", "aliases": ["DESCRIPCION EXTRA", "DESCRIPCIONEXTRA"]},
{"canonical": "INFORMACION ADICIONAL", "aliases": ["INFORMACION ADICIONAL", "INFORMACIONADICIONAL"]},
{"canonical": "AGREGAR(A)/SUSTITUIR(S)", "aliases": ["AGREGAR(A)/SUSTITUIR(S)", "AGREGAR/SUSTITUIR", "SUSTITUIR"]},
{"canonical": "LOTE"},
{"canonical": "NUMERO ENTRADA", "aliases": ["NUMERO ENTRADA", "NUM ENTRADA"]},
{"canonical": "ES PARTIDA/SUBPARTIDA", "aliases": ["ES PARTIDA/SUBPARTIDA", "ESSUBPARTIDA", "ES PARTIDA O SUBPARTIDA"]},
{"canonical": "LINEA PRINCIPAL", "aliases": ["LINEAPRINCIPAL", "PARTIDA PRINCIPAL"]},
{"canonical": "FRACCION AMERICANA", "aliases": ["FRACCION AMERICANA", "FRACCIONAMERICANA"]},
{"canonical": "FRACCION ARANCELARIA", "aliases": ["FRACCION ARANCELARIA", "FRACCIONARANCELARIA"]},
{"canonical": "CANTIDAD BULTOS", "aliases": ["CANTIDADBULTOS"]},
{"canonical": "CLAVE BULTOS", "aliases": ["CLAVEBULTOS"]},
{"canonical": "PAIS ORIGEN", "aliases": ["PAISORIGEN", "PAIS"]},
{"canonical": "NUM. PARTE", "aliases": ["NUMPARTE", "NUMERO PARTE", "NUM PARTE"]},
{"canonical": "ORDEN DE COMPRA", "aliases": ["ORDENCOMPRA", "ORDEN DE VENTA"]},
],
# --- Series de Importación Temporal (EstructuraSeriesFacImpoTemp.xls) ---
# Clarion: NUMERO FACTURA, LINEA FACTURA, LINEA SERIE, SERIE, MODELO, NUM PARTE, SUB MODELO, NUMERO ID
"imp_temp_series": [
@@ -224,6 +254,8 @@ def _resolve_template_columns(template_id: str) -> Optional[List[Dict[str, Any]]
return TEMPLATE_COLUMNS.get("imp_temp_header")
if template_id in ("imp_def_details", "exp_def_details"):
return TEMPLATE_COLUMNS.get("imp_temp_details")
if template_id == "exp_def_partidas":
return TEMPLATE_COLUMNS.get("exp_def_partidas")
if template_id == "cmex_details":
return TEMPLATE_COLUMNS.get("imp_temp_details")
if template_id == "cmex_series":

View File

@@ -11,6 +11,7 @@ from .encabezados_impo_def import (
)
from .encabezados_cmex import validate_row_encabezados_cmex
from .encabezados_expo import validate_row_encabezados_expo
from .partidas_expo import validate_row_partidas_expo
from .partidas_impo_def import validate_row_partidas_impo_def
from .series_impo_def import (
validate_row_series_impo_def,
@@ -22,6 +23,7 @@ __all__ = [
"validate_row_encabezados_impo_def",
"validate_row_encabezados_cmex",
"validate_row_encabezados_expo",
"validate_row_partidas_expo",
"validate_row_partidas_impo_def",
"validate_row_series_impo_def",
"row_to_series_normalized_def",

View File

@@ -0,0 +1,558 @@
"""
Validaciones CSV para Partidas de Exportación Definitiva (y Cambio de Régimen).
Paridad Clarion: VALIDA_TODA_PAR_EXPO, VALIDA_PARCIAL_PAR_EXPO, VALIDACIONES_PAR_EXPO.
Estructura: NUMERO FACTURA EXPO, LINEA EXPO, TIPO DE IMPO, FACTURA IMPO, LINEA IMPO, GENERA DESCARGA, ...
Variante RFC EGM0303257J1: columnas ES PARTIDA/SUBPARTIDA y LINEA PRINCIPAL en T y U.
"""
from decimal import Decimal, InvalidOperation
from typing import Dict, Any, Optional, Set, Tuple, List
from .partidas_impo_temp import _clip, _get
# Longitudes Clarion partidas expo
MAX_LEN_FACTURA_EXPO = 15
MAX_LEN_LINEA_EXPO = 5
MAX_LEN_TIPO_IMPO = 3
MAX_LEN_ORDEN_COMPRA = 20
MAX_LEN_NUM_PARTE = 30
TIPO_IMPO_VALIDOS = frozenset({"TEM", "DEF"})
GENERA_DESCARGA_VALIDOS = frozenset({"SI", "NO"})
SE_PAGO_IMPUESTO_VALIDOS = frozenset({"SI", "NO"})
def _err(
line_num: int,
col: str,
msg: str,
identifier: str = "ARCHIVO CSV",
) -> Dict[str, Any]:
return {"line": line_num, "col": col, "msg": msg}
def _parse_decimal(val: Any) -> Optional[Decimal]:
if val is None:
return None
s = _clip(val)
if not s:
return None
s = str(s).replace(",", "")
try:
return Decimal(s)
except (InvalidOperation, ValueError):
return None
def _check_factura_expo_vacia(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
val = _get(row, "NUMERO FACTURA EXPO", "NUMERO FACTURA EXPO.", "NUM FACTURA EXPO", "FACTURA EXPO")
if not val:
return _err(
line_num,
"NUMERO FACTURA EXPO",
f"Error: (Celda A{line_num}) La Factura de Exportación está vacía y no se pueden hacer las validaciones. "
"Capturar en la Celda A un número de Factura existente al cual desee agregar o actualizar partidas.",
)
return None
def _check_factura_expo_existe(
invoice_number: str,
line_num: int,
invoice_id_by_number: Dict[str, int],
) -> Optional[Dict[str, Any]]:
if invoice_number not in invoice_id_by_number:
return _err(
line_num,
"NUMERO FACTURA EXPO",
f"Error: (Celda A{line_num}) La Factura de Exportación {invoice_number} no existe en SCAII y no se pueden hacer las validaciones. "
"Capturar en la Celda A un número de Factura existente al cual desee agregar o actualizar partidas.",
)
return None
def _check_factura_expo_no_actualizada(
invoice_number: str,
line_num: int,
invoice_updated_by_number: Dict[str, bool],
rfc_exception_updated: Set[str],
) -> Optional[Dict[str, Any]]:
if invoice_number in rfc_exception_updated:
return None
if invoice_updated_by_number.get(invoice_number, False):
return _err(
line_num,
"NUMERO FACTURA EXPO",
f"Error: (Celda A{line_num}) La Factura de Exportación: {invoice_number} ya existe y está Actualizada, no se puede hacer cambios a las facturas actualizadas. "
"Capturar otro número de Factura de Exportación o Desactualizar la factura.",
)
return None
def _check_linea_expo_si_no_autonumerar(
row: Dict[str, Any], line_num: int, autonumerar: bool
) -> Optional[Dict[str, Any]]:
if autonumerar:
return None
val = _get(row, "LINEA EXPO", "LINEA EXPO.", "RENGLON EXPO")
if not val:
return _err(
line_num,
"LINEA EXPO",
f"Error: (Celda B{line_num}) El campo de la línea de la partida está vacío y no se pueden hacer las validaciones, ya que se tiene la opción de Autonumerar como NO. "
"Capturar en la Celda B la línea de la partida al cual desee agregar o actualizar información.",
)
return None
def _check_levantar_subpartidas_expo(
row: Dict[str, Any],
line_num: int,
levantar_subpartidas: bool,
rfc_exception_egm: bool,
) -> Optional[Dict[str, Any]]:
"""Clarion: si LevantarSubpartidas=S, obligatorios ES PARTIDA/SUBPARTIDA y LINEA PRINCIPAL (col S/T o T/U para EGM0303257J1)."""
if not levantar_subpartidas:
return None
es_sub = _get(row, "ES PARTIDA/SUBPARTIDA", "ESSUBPARTIDA", "ES PARTIDA O SUBPARTIDA")
if not es_sub:
col = "T" if rfc_exception_egm else "S"
return _err(
line_num,
"ES PARTIDA/SUBPARTIDA",
f"Error: (Celda {col}{line_num}) El campo del tipo de la partida (partida o subpartida) está vacío. "
"Capturar el tipo de la partida. [P] = Partida o [S] = Subpartida.",
)
linea_principal = _get(row, "LINEA PRINCIPAL", "LINEAPRINCIPAL", "PARTIDA PRINCIPAL")
if not linea_principal:
col = "U" if rfc_exception_egm else "T"
return _err(
line_num,
"LINEA PRINCIPAL",
f"Error: (Celda {col}{line_num}) El campo de la partida principal está vacío. "
f"Capturar en la Celda {col} la partida principal.",
)
return None
def _valida_toda_obligatorios_expo(
row: Dict[str, Any],
line_num: int,
levantar_subpartidas: bool,
rfc_exception_egm: bool,
) -> Optional[Dict[str, Any]]:
"""VALIDA_TODA_PAR_EXPO obligatorios: C (tipo impo), F (descarga), G (cantidad); si descarga=SI: D, E; si subpartidas: S/T o T/U."""
obligatorios: List[str] = []
tipo_impo = _get(row, "TIPO DE IMPO", "TIPO DE IMPO.", "TIPO IMPO", "PROCEDENCIA")
if not tipo_impo:
obligatorios.append("(Col.C) Procedencia de la Importación.")
descarga = _get(row, "GENERA DESCARGA", "GENERA DESCARGA?", "DESCARGA")
if descarga == "SI":
if not _get(row, "FACTURA IMPO", "FACTURA IMPO.", "FACTURA IMPORTACION"):
obligatorios.append("(Col.D) Factura de Impo.")
if not _get(row, "LINEA IMPO", "LINEA IMPO.", "LINEA IMPORTACION"):
obligatorios.append("(Col.E) Línea de Impo.")
if not descarga:
obligatorios.append("(Col.F) Descarga? SI o NO")
if not _get(row, "CANTIDAD EXPORTADA/DESCARGAR", "CANTIDAD EXPORTADA", "CANTIDAD"):
obligatorios.append("(Col.G) Cantidad Expo.")
if levantar_subpartidas:
if rfc_exception_egm:
if not _get(row, "ES PARTIDA/SUBPARTIDA", "ESSUBPARTIDA", "ES PARTIDA O SUBPARTIDA"):
obligatorios.append("(Col.T) EsSubpartida?.")
if not _get(row, "LINEA PRINCIPAL", "LINEAPRINCIPAL", "PARTIDA PRINCIPAL"):
obligatorios.append("(Col.U) Partida Principal.")
else:
if not _get(row, "ES PARTIDA/SUBPARTIDA", "ESSUBPARTIDA", "ES PARTIDA O SUBPARTIDA"):
obligatorios.append("(Col.S) EsSubpartida?.")
if not _get(row, "LINEA PRINCIPAL", "LINEAPRINCIPAL", "PARTIDA PRINCIPAL"):
obligatorios.append("(Col.T) Partida Principal.")
if obligatorios:
return _err(
line_num,
"TIPO DE IMPO",
f"Existen campos vacíos que son obligatorios, es la {', '.join(obligatorios)}. "
"Revisar la línea del archivo y capturar los campos con la información correcta.",
)
return None
def _valida_subpartidas_duplicados_expo(
factura_expo: str,
linea_expo: str,
line_num: int,
line_counts_csv: Dict[Tuple[str, str], int],
) -> Optional[Dict[str, Any]]:
key = (factura_expo.strip(), _clip(linea_expo))
if line_counts_csv.get(key, 0) > 1:
return _err(
line_num,
"LINEA EXPO",
f"Error: (Celda B{line_num}) El campo de la partida está duplicado entre las partidas. "
f"Capturar en la Celda B{line_num} otro número de partida.",
)
return None
def _valida_subpartida_tiene_principal_expo(
row: Dict[str, Any],
line_num: int,
factura_expo: str,
partidas_principales_csv: Set[Tuple[str, str]],
partidas_principales_bd: Set[Tuple[str, str]],
rfc_exception_egm: bool,
) -> Optional[Dict[str, Any]]:
es_sub = _get(row, "ES PARTIDA/SUBPARTIDA", "ESSUBPARTIDA", "ES PARTIDA O SUBPARTIDA").upper()
linea_principal = _get(row, "LINEA PRINCIPAL", "LINEAPRINCIPAL", "PARTIDA PRINCIPAL")
if es_sub != "S" or not linea_principal or linea_principal == "0":
return None
key_principal = (factura_expo.strip(), _clip(linea_principal))
if key_principal in partidas_principales_csv or key_principal in partidas_principales_bd:
return None
col = "U" if rfc_exception_egm else "T"
return _err(
line_num,
"LINEA PRINCIPAL",
f"Error: (Celda {col}{line_num}) La partida principal {linea_principal} no existe. "
f"Capturar en la Celda {col} la partida principal y/o verificar que si permita contener subpartidas.",
)
def _valida_subpartida_linea_principal_no_cero_expo(
row: Dict[str, Any], line_num: int, rfc_exception_egm: bool
) -> Optional[Dict[str, Any]]:
es_sub = _get(row, "ES PARTIDA/SUBPARTIDA", "ESSUBPARTIDA", "ES PARTIDA O SUBPARTIDA").upper()
linea_principal = _get(row, "LINEA PRINCIPAL", "LINEAPRINCIPAL", "PARTIDA PRINCIPAL")
if es_sub == "S" and linea_principal == "0":
col = "U" if rfc_exception_egm else "T"
return _err(
line_num,
"LINEA PRINCIPAL",
f"Error: (Celda {col}{line_num}) La SubPartida no tiene asignada una partida principal. "
f"Capturar en la Celda {col} una partida principal.",
)
return None
def _validaciones_par_expo(
row: Dict[str, Any],
line_num: int,
factura_expo: str,
valid_uom_codes: Set[str],
valid_bulks_codes: Set[str],
valid_payment_methods: Set[str],
valid_fraction_ame: Set[str],
valid_part_numbers: Optional[Set[str]],
factura_impo_tem_by_number: Dict[str, int],
factura_impo_def_by_number: Dict[str, int],
line_exists_tem: Set[Tuple[int, str]],
line_exists_def: Set[Tuple[int, str]],
validar_decimales_pza: bool = False,
) -> Optional[Dict[str, Any]]:
"""VALIDACIONES_PAR_EXPO: longitudes, TEM/DEF, FK factura impo + línea, descarga SI/NO, cantidad, U.M., bultos, forma pago, impuesto, fracción ame, num parte, decimales PZA."""
tipo_impo = _get(row, "TIPO DE IMPO", "TIPO DE IMPO.", "TIPO IMPO", "PROCEDENCIA").strip().upper()
if not tipo_impo:
tipo_impo = "TEM"
factura_impo = _get(row, "FACTURA IMPO", "FACTURA IMPO.", "FACTURA IMPORTACION")
linea_impo = _get(row, "LINEA IMPO", "LINEA IMPO.", "LINEA IMPORTACION")
descarga = _get(row, "GENERA DESCARGA", "GENERA DESCARGA?", "DESCARGA").upper()
# Longitud A
if factura_expo and len(factura_expo) > MAX_LEN_FACTURA_EXPO:
return _err(
line_num,
"NUMERO FACTURA EXPO",
f"Error: (Celda A{line_num}) La Factura de Exportación: {factura_expo} supera la longitud de caracteres. "
"Capturar en la Celda A el campo Factura de Exportación con formato ###############.",
)
# Longitud B
linea_expo = _get(row, "LINEA EXPO", "LINEA EXPO.", "RENGLON EXPO")
if linea_expo and len(linea_expo) > MAX_LEN_LINEA_EXPO:
return _err(
line_num,
"LINEA EXPO",
f"Error: (Celda B{line_num}) La Línea de Exportación: {linea_expo} supera la longitud de caracteres. "
"Capturar en la Celda B una Línea de Exportación con formato #####.",
)
# Longitud C
if tipo_impo and len(tipo_impo) > MAX_LEN_TIPO_IMPO:
return _err(
line_num,
"TIPO DE IMPO",
f"Error: (Celda C{line_num}) La Procedencia debe ser especificada como TEM o DEF. "
"Capturar en la Celda C una procedencia no mayor de 3 caracteres.",
)
# C: TEM o DEF
if tipo_impo and tipo_impo not in TIPO_IMPO_VALIDOS:
return _err(
line_num,
"TIPO DE IMPO",
f"Error: (Celda C{line_num}) El Tipo de Descargo: {tipo_impo} no es valido. Capturar uno valido como TEM o DEF.",
)
# D, E: factura impo + línea existen en catálogo TEM o DEF
if factura_impo and tipo_impo:
consec_tem = factura_impo_tem_by_number.get(factura_impo.strip())
consec_def = factura_impo_def_by_number.get(factura_impo.strip())
if tipo_impo == "TEM":
if consec_tem is None:
return _err(
line_num,
"FACTURA IMPO",
f"Error: (Celda D{line_num}) La Factura: {factura_impo} de Importación Temporal no existe. "
"Capturar un Número de Factura que exista en el Catálogo de Importaciones Temporales.",
)
key_line = (consec_tem, _clip(linea_impo))
if linea_impo and key_line not in line_exists_tem:
return _err(
line_num,
"LINEA IMPO",
f"Error: (Celda D{line_num}, E{line_num}) La Factura: {factura_impo} con línea: {linea_impo} de Importación Temporal no existe. "
"Capturar un Número de Factura con diferente línea que este en el Catálogo de Importaciones Temporales.",
)
elif tipo_impo == "DEF":
if consec_def is None:
return _err(
line_num,
"FACTURA IMPO",
f"Error: (Celda D{line_num}) La Factura: {factura_impo} de Importación Definitiva no existe. "
"Capturar un Número de Factura que exista en el Catálogo de Importaciones Definitivas.",
)
key_line = (consec_def, _clip(linea_impo))
if linea_impo and key_line not in line_exists_def:
return _err(
line_num,
"LINEA IMPO",
f"Error: (Celda D{line_num}, E{line_num}) La Factura: {factura_impo} con línea: {linea_impo} de Importación Definitiva no existe. "
"Capturar un Número de Factura con diferente línea que este en el Catálogo de Importaciones Definitivas.",
)
# F: SI o NO
if descarga and descarga not in GENERA_DESCARGA_VALIDOS:
return _err(
line_num,
"GENERA DESCARGA",
f"Error: (Celda F{line_num}) La captura: {descarga} no es valido para la opción de que la partida genere descarga. "
"Capturar un valor valido como SI o NO o dejar vacio y lo tomará como un SI.",
)
# G: cantidad no cero
cant_str = _get(row, "CANTIDAD EXPORTADA/DESCARGAR", "CANTIDAD EXPORTADA", "CANTIDAD")
if cant_str:
cant = _parse_decimal(cant_str)
if cant is not None and cant == 0:
return _err(
line_num,
"CANTIDAD EXPORTADA/DESCARGAR",
f"Error: (Celda G{line_num}) La Cantidad a Exportar: {cant_str} no puede ser cero. Capturar una cantidad a exportar valida.",
)
# H: U.M. en catálogo (si no se toma de impo)
um = _get(row, "UNIDAD DE MEDIDA", "U.M.", "UNIDAD MEDIDA")
if um and valid_uom_codes and um.upper() not in valid_uom_codes:
return _err(
line_num,
"UNIDAD DE MEDIDA",
f"Error: (Celda H{line_num}) La U.M.: {um} no existe en catálogo de Unidades de Medida. "
"Capturar una Clave de Unidad de Medida que exista en el Catálogo.",
)
# Bultos: cantidad + clave (Clarion Col L, M)
clave_bultos = _get(row, "CLAVE BULTOS", "CLAVEBULTOS")
cant_bultos = row.get("CANTIDAD BULTOS") or row.get("CANTIDADBULTOS")
if clave_bultos:
if valid_bulks_codes and clave_bultos not in valid_bulks_codes:
return _err(
line_num,
"CLAVE BULTOS",
f"Error: (Celda M{line_num}) La Clave de Bulto: {clave_bultos} no existe en el Catálogo de Claves de Bultos. "
"Darlo de alta en el Catálogo de Claves de Bultos o capturar uno ya existente.",
)
cant_bultos_val = _parse_decimal(cant_bultos)
if cant_bultos_val is None:
return _err(
line_num,
"CANTIDAD BULTOS",
f"Error: (Celda L{line_num}) La Cantidad de Bultos está vacía y en la Celda M{line_num} se tienen la Clave de Bulto: {clave_bultos}.",
)
if cant_bultos_val == 0:
return _err(
line_num,
"CANTIDAD BULTOS",
f"Error: (Celda L{line_num}) La Cantidad de Bultos es cero y en la Celda M{line_num} se tienen la Clave de Bulto: {clave_bultos}.",
)
else:
cant_bultos_val = _parse_decimal(cant_bultos)
if cant_bultos_val is not None and cant_bultos_val > 0:
return _err(
line_num,
"CANTIDAD BULTOS",
f"Error: (Celda L{line_num}) La Cantidad de Bultos es {cant_bultos} y en la Celda M{line_num} no se tienen la Clave de Bulto.",
)
# L: Se pagó impuesto SI/NO
se_pago = _get(row, "SE PAGO IMPUESTO", "SE PAGO IMPUESTO? (SI o NO)", "SEPAGOIMPUESTO")
if se_pago and se_pago.upper() not in SE_PAGO_IMPUESTO_VALIDOS:
return _err(
line_num,
"SE PAGO IMPUESTO",
f"Error: (Celda L{line_num}) El Valor Capturado para Se Pago Impuesto no es Válido. Capturar en la Celda L{line_num} SI o NO.",
)
# M: Forma de pago en catálogo
forma_pago = _get(row, "FORMA DE PAGO", "FORMADEPAGO", "FORMA PAGO")
if forma_pago and valid_payment_methods and forma_pago not in valid_payment_methods:
return _err(
line_num,
"FORMA DE PAGO",
f"Error: (Celda M{line_num}) La Forma de Pago Capturado no es Válido. "
"Capturar en la Celda M una Forma de Pago dentro del Catálogo General de Formas de Pago.",
)
# Fracción americana (Clarion Col R)
frac_ame = _get(row, "FRACCION AMERICANA", "FRACCIONAMERICANA")
if frac_ame and valid_fraction_ame and frac_ame not in valid_fraction_ame:
return _err(
line_num,
"FRACCION AMERICANA",
f"Error: (Celda R{line_num}) La Fracción Americana: {frac_ame} no está en el Catálogo de Fracciones Americanas. "
"Capturar en la Celda R una fracción que se encuentre en el catálogo o dar la de alta.",
)
# Orden de compra / orden venta (Clarion Col S) máx 20
orden = _get(row, "ORDEN DE COMPRA", "ORDENCOMPRA", "ORDEN DE VENTA")
if orden and len(orden) > MAX_LEN_ORDEN_COMPRA:
return _err(
line_num,
"ORDEN DE COMPRA",
f"Error: (Celda S{line_num}) La Orden de Venta: {orden} supera la cantidad de caracteres permitidos. "
"Capturar en la Celda S una orden de compra no mayor de 20 caracteres.",
)
# Número de parte (Clarion Col T): longitud y catálogo
num_parte = _get(row, "NUM. PARTE", "NUMPARTE", "NUMERO PARTE", "NUM PARTE")
if num_parte:
if len(num_parte) > MAX_LEN_NUM_PARTE:
return _err(
line_num,
"NUM. PARTE",
f"Error: (Celda T{line_num}) El Número de Parte: {num_parte} supera la longitud de caracteres. "
"Capturar en la Celda T el Número de Parte no mayor de 30 caracteres.",
)
if valid_part_numbers is not None and num_parte.upper() not in valid_part_numbers:
return _err(
line_num,
"NUM. PARTE",
f"Error: (Celda T{line_num}) El Número de Parte: {num_parte} no existe en el Catálogo de Partes. Darlo de alta en el Catálogo de Partes.",
)
# Decimales PZA
if validar_decimales_pza and um and um.upper() == "PZA" and cant_str:
d = _parse_decimal(cant_str)
if d is not None and d != int(d):
return _err(
line_num,
"CANTIDAD EXPORTADA/DESCARGAR",
f"Error: (Celda G{line_num}) La Unidad de Medida es PZA, Por lo Tanto no es Válida la Captura de Decimales. Asignar una Cantidad sin decimales.",
)
return None
def validate_row_partidas_expo(
row: Dict[str, Any],
line_num: int,
autonumerar: bool,
actualizar: bool,
levantar_subpartidas: bool,
invoice_id_by_number: Dict[str, int],
invoice_updated_by_number: Dict[str, bool],
rfc_exception_updated: Set[str],
existing_line_keys_by_invoice: Dict[str, Set[str]],
line_counts_csv: Dict[Tuple[str, str], int],
partidas_principales_csv: Set[Tuple[str, str]],
partidas_principales_bd: Set[Tuple[str, str]],
valid_uom_codes: Set[str],
valid_bulks_codes: Set[str],
valid_payment_methods: Set[str],
valid_fraction_ame: Set[str],
valid_part_numbers: Optional[Set[str]],
factura_impo_tem_by_number: Dict[str, int],
factura_impo_def_by_number: Dict[str, int],
line_exists_tem: Set[Tuple[int, str]],
line_exists_def: Set[Tuple[int, str]],
rfc_exception_egm: bool = False,
validar_decimales_pza: bool = False,
) -> Optional[Dict[str, Any]]:
"""
Valida una fila de CSV de Partidas de Exportación Definitiva (o Cambio de Régimen).
Clarion: VALIDA_TODA_PAR_EXPO vs VALIDA_PARCIAL_PAR_EXPO según autonumerar, actualizar y si la partida existe.
"""
err = _check_factura_expo_vacia(row, line_num)
if err:
return err
factura_expo = _get(row, "NUMERO FACTURA EXPO", "NUMERO FACTURA EXPO.", "NUM FACTURA EXPO", "FACTURA EXPO")
if not factura_expo:
return _err(line_num, "NUMERO FACTURA EXPO", "Requerido")
err = _check_factura_expo_existe(factura_expo, line_num, invoice_id_by_number)
if err:
return err
err = _check_factura_expo_no_actualizada(
factura_expo, line_num, invoice_updated_by_number, rfc_exception_updated
)
if err:
return err
err = _check_linea_expo_si_no_autonumerar(row, line_num, autonumerar)
if err:
return err
err = _check_levantar_subpartidas_expo(row, line_num, levantar_subpartidas, rfc_exception_egm)
if err:
return err
linea_expo = _get(row, "LINEA EXPO", "LINEA EXPO.", "RENGLON EXPO")
existing_lines = existing_line_keys_by_invoice.get(factura_expo.strip(), set())
partida_existe = bool(linea_expo and linea_expo in existing_lines)
use_partial = actualizar and not autonumerar and partida_existe
if use_partial:
return _validaciones_par_expo(
row,
line_num,
factura_expo,
valid_uom_codes=valid_uom_codes,
valid_bulks_codes=valid_bulks_codes,
valid_payment_methods=valid_payment_methods,
valid_fraction_ame=valid_fraction_ame,
valid_part_numbers=valid_part_numbers,
factura_impo_tem_by_number=factura_impo_tem_by_number,
factura_impo_def_by_number=factura_impo_def_by_number,
line_exists_tem=line_exists_tem,
line_exists_def=line_exists_def,
validar_decimales_pza=validar_decimales_pza,
)
else:
err = _valida_toda_obligatorios_expo(row, line_num, levantar_subpartidas, rfc_exception_egm)
if err:
return err
if levantar_subpartidas:
err = _valida_subpartidas_duplicados_expo(
factura_expo, linea_expo or "", line_num, line_counts_csv
)
if err:
return err
err = _valida_subpartida_tiene_principal_expo(
row, line_num, factura_expo,
partidas_principales_csv, partidas_principales_bd, rfc_exception_egm
)
if err:
return err
err = _valida_subpartida_linea_principal_no_cero_expo(row, line_num, rfc_exception_egm)
if err:
return err
return _validaciones_par_expo(
row,
line_num,
factura_expo,
valid_uom_codes=valid_uom_codes,
valid_bulks_codes=valid_bulks_codes,
valid_payment_methods=valid_payment_methods,
valid_fraction_ame=valid_fraction_ame,
valid_part_numbers=valid_part_numbers,
factura_impo_tem_by_number=factura_impo_tem_by_number,
factura_impo_def_by_number=factura_impo_def_by_number,
line_exists_tem=line_exists_tem,
line_exists_def=line_exists_def,
validar_decimales_pza=validar_decimales_pza,
)