fix(pos): resolve integration test failures for CFDI + accounting

- Fix sat_accounts.sql: split multi-row INSERT into individual statements
  so parent_id subqueries resolve correctly (was producing all NULLs)
- Add tenant_config table to v1.0 schema (required by CFDI invoicing)
- Seed tenant_config with RFC/regimen during tenant provisioning
- Fix cancel_sale to pass complete sale data for accounting reversal
- Fix CFDI XML builder: use `or` instead of dict.get() defaults to
  handle explicit None values from DB (clave_prod_serv, clave_unidad)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 04:54:50 +00:00
parent c56709d45e
commit bc950efc26
6 changed files with 88 additions and 43 deletions

View File

@@ -167,12 +167,12 @@ def build_ingreso_xml(sale, tenant_config, customer=None):
base = (importe - discount_amount).quantize(TWO, ROUND_HALF_UP)
concepto = _make_element(conceptos, 'Concepto')
concepto.set('ClaveProdServ', item.get('clave_prod_serv', '25174800')) # Default: autopartes
concepto.set('NoIdentificacion', item.get('part_number', ''))
concepto.set('ClaveProdServ', item.get('clave_prod_serv') or '25174800')
concepto.set('NoIdentificacion', item.get('part_number') or '')
concepto.set('Cantidad', str(qty))
concepto.set('ClaveUnidad', item.get('clave_unidad', 'H87')) # H87 = Pieza
concepto.set('ClaveUnidad', item.get('clave_unidad') or 'H87')
concepto.set('Unidad', 'PZA')
concepto.set('Descripcion', item.get('name', 'Autoparte'))
concepto.set('Descripcion', item.get('name') or 'Autoparte')
concepto.set('ValorUnitario', _format_amount(unit_price))
concepto.set('Importe', _format_amount(importe))
concepto.set('ObjetoImp', '02') # Si objeto de impuesto
@@ -286,12 +286,12 @@ def build_egreso_xml(sale, tenant_config, customer, original_uuid):
base = (importe - discount_amount).quantize(TWO, ROUND_HALF_UP)
concepto = _make_element(conceptos, 'Concepto')
concepto.set('ClaveProdServ', item.get('clave_prod_serv', '25174800'))
concepto.set('NoIdentificacion', item.get('part_number', ''))
concepto.set('ClaveProdServ', item.get('clave_prod_serv') or '25174800')
concepto.set('NoIdentificacion', item.get('part_number') or '')
concepto.set('Cantidad', str(qty))
concepto.set('ClaveUnidad', item.get('clave_unidad', 'H87'))
concepto.set('ClaveUnidad', item.get('clave_unidad') or 'H87')
concepto.set('Unidad', 'PZA')
concepto.set('Descripcion', item.get('name', 'Autoparte'))
concepto.set('Descripcion', item.get('name') or 'Autoparte')
concepto.set('ValorUnitario', _format_amount(unit_price))
concepto.set('Importe', _format_amount(importe))
concepto.set('ObjetoImp', '02')

View File

@@ -481,14 +481,19 @@ def cancel_sale(conn, sale_id, reason):
# Reverse accounting entry (non-blocking)
try:
# Fetch tax_total for the reversal entry
cur.execute("SELECT tax_total FROM sales WHERE id = %s", (sale_id,))
_tax_row = cur.fetchone()
record_cancellation_entry(conn, {
'id': sale_id,
'total': float(s_total),
'tax_total': float(_tax_row[0]) if _tax_row else 0.0,
})
# Fetch full sale data for the reversal entry
cur.execute("""SELECT subtotal, tax_total, total, sale_type, payment_method
FROM sales WHERE id = %s""", (sale_id,))
_sale_row = cur.fetchone()
if _sale_row:
record_cancellation_entry(conn, {
'id': sale_id,
'subtotal': float(_sale_row[0]) if _sale_row[0] else 0.0,
'tax_total': float(_sale_row[1]) if _sale_row[1] else 0.0,
'total': float(_sale_row[2]) if _sale_row[2] else 0.0,
'sale_type': _sale_row[3] or 'cash',
'payment_method': _sale_row[4] or 'efectivo',
})
except Exception:
pass # Accounting errors never block cancellations

View File

@@ -165,6 +165,18 @@ def provision_tenant(name, rfc=None, owner_name="Admin", owner_email=None, owner
(owner_id, perm)
)
# Seed tenant_config with RFC and defaults
if rfc:
tenant_cur.execute("""
INSERT INTO tenant_config (key, value) VALUES
('tenant_rfc', %s),
('tenant_razon_social', %s),
('tenant_cp', '00000'),
('cfdi_regimen_fiscal', '601'),
('cfdi_serie', 'A')
ON CONFLICT (key) DO NOTHING
""", (rfc, name))
tenant_conn.commit()
tenant_cur.close()
tenant_conn.close()