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:
@@ -85,8 +85,8 @@ def _get_sale_with_items(cur, sale_id):
|
||||
'tax_rate': float(r[9]) if r[9] else 0.16,
|
||||
'tax_amount': float(r[10]) if r[10] else 0,
|
||||
'subtotal': float(r[11]) if r[11] else 0,
|
||||
'clave_prod_serv': r[12],
|
||||
'clave_unidad': r[13],
|
||||
'clave_prod_serv': r[12] or '25174800',
|
||||
'clave_unidad': r[13] or 'H87',
|
||||
})
|
||||
|
||||
return sale
|
||||
|
||||
@@ -375,5 +375,14 @@ CREATE TABLE IF NOT EXISTS physical_count_lines (
|
||||
difference INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- =====================
|
||||
-- TENANT CONFIGURATION (key-value store for CFDI, Horux, etc.)
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS tenant_config (
|
||||
key VARCHAR(100) PRIMARY KEY,
|
||||
value TEXT,
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Barcode sequence
|
||||
CREATE SEQUENCE IF NOT EXISTS barcode_seq START 1;
|
||||
|
||||
@@ -1,50 +1,69 @@
|
||||
-- SAT Chart of Accounts (Catálogo de Cuentas SAT)
|
||||
-- SAT Chart of Accounts (Catalogo de Cuentas SAT)
|
||||
-- All accounts are system accounts (is_system = true)
|
||||
-- Parent IDs resolved via subqueries to avoid hardcoded integer dependencies
|
||||
|
||||
INSERT INTO accounts (code, name, parent_id, type, sat_code, is_system, is_active) VALUES
|
||||
-- Individual INSERTs so parent_id subqueries can resolve correctly.
|
||||
|
||||
-- ============================================================
|
||||
-- ACTIVO (100)
|
||||
-- ============================================================
|
||||
('100', 'Activo', NULL, 'activo', '100', true, true),
|
||||
('110', 'Caja', (SELECT id FROM accounts WHERE code = '100'), 'activo', '110', true, true),
|
||||
('111', 'Bancos', (SELECT id FROM accounts WHERE code = '100'), 'activo', '111', true, true),
|
||||
('120', 'Clientes', (SELECT id FROM accounts WHERE code = '100'), 'activo', '120', true, true),
|
||||
('130', 'Inventarios', (SELECT id FROM accounts WHERE code = '100'), 'activo', '130', true, true),
|
||||
('140', 'IVA Acreditable', (SELECT id FROM accounts WHERE code = '100'), 'activo', '140', true, true),
|
||||
INSERT INTO accounts (code, name, parent_id, type, sat_code, is_system, is_active)
|
||||
VALUES ('100', 'Activo', NULL, 'activo', '100', true, true);
|
||||
INSERT INTO accounts (code, name, parent_id, type, sat_code, is_system, is_active)
|
||||
VALUES ('110', 'Caja', (SELECT id FROM accounts WHERE code = '100'), 'activo', '110', true, true);
|
||||
INSERT INTO accounts (code, name, parent_id, type, sat_code, is_system, is_active)
|
||||
VALUES ('111', 'Bancos', (SELECT id FROM accounts WHERE code = '100'), 'activo', '111', true, true);
|
||||
INSERT INTO accounts (code, name, parent_id, type, sat_code, is_system, is_active)
|
||||
VALUES ('120', 'Clientes', (SELECT id FROM accounts WHERE code = '100'), 'activo', '120', true, true);
|
||||
INSERT INTO accounts (code, name, parent_id, type, sat_code, is_system, is_active)
|
||||
VALUES ('130', 'Inventarios', (SELECT id FROM accounts WHERE code = '100'), 'activo', '130', true, true);
|
||||
INSERT INTO accounts (code, name, parent_id, type, sat_code, is_system, is_active)
|
||||
VALUES ('140', 'IVA Acreditable', (SELECT id FROM accounts WHERE code = '100'), 'activo', '140', true, true);
|
||||
|
||||
-- ============================================================
|
||||
-- PASIVO (200)
|
||||
-- ============================================================
|
||||
('200', 'Pasivo', NULL, 'pasivo', '200', true, true),
|
||||
('210', 'Proveedores', (SELECT id FROM accounts WHERE code = '200'), 'pasivo', '210', true, true),
|
||||
('220', 'IVA Trasladado', (SELECT id FROM accounts WHERE code = '200'), 'pasivo', '220', true, true),
|
||||
('230', 'ISR por Pagar', (SELECT id FROM accounts WHERE code = '200'), 'pasivo', '230', true, true),
|
||||
INSERT INTO accounts (code, name, parent_id, type, sat_code, is_system, is_active)
|
||||
VALUES ('200', 'Pasivo', NULL, 'pasivo', '200', true, true);
|
||||
INSERT INTO accounts (code, name, parent_id, type, sat_code, is_system, is_active)
|
||||
VALUES ('210', 'Proveedores', (SELECT id FROM accounts WHERE code = '200'), 'pasivo', '210', true, true);
|
||||
INSERT INTO accounts (code, name, parent_id, type, sat_code, is_system, is_active)
|
||||
VALUES ('220', 'IVA Trasladado', (SELECT id FROM accounts WHERE code = '200'), 'pasivo', '220', true, true);
|
||||
INSERT INTO accounts (code, name, parent_id, type, sat_code, is_system, is_active)
|
||||
VALUES ('230', 'ISR por Pagar', (SELECT id FROM accounts WHERE code = '200'), 'pasivo', '230', true, true);
|
||||
|
||||
-- ============================================================
|
||||
-- CAPITAL (300)
|
||||
-- ============================================================
|
||||
('300', 'Capital', NULL, 'capital', '300', true, true),
|
||||
('310', 'Capital Social', (SELECT id FROM accounts WHERE code = '300'), 'capital', '310', true, true),
|
||||
('320', 'Resultados del Ejercicio', (SELECT id FROM accounts WHERE code = '300'), 'capital', '320', true, true),
|
||||
INSERT INTO accounts (code, name, parent_id, type, sat_code, is_system, is_active)
|
||||
VALUES ('300', 'Capital', NULL, 'capital', '300', true, true);
|
||||
INSERT INTO accounts (code, name, parent_id, type, sat_code, is_system, is_active)
|
||||
VALUES ('310', 'Capital Social', (SELECT id FROM accounts WHERE code = '300'), 'capital', '310', true, true);
|
||||
INSERT INTO accounts (code, name, parent_id, type, sat_code, is_system, is_active)
|
||||
VALUES ('320', 'Resultados del Ejercicio', (SELECT id FROM accounts WHERE code = '300'), 'capital', '320', true, true);
|
||||
|
||||
-- ============================================================
|
||||
-- INGRESOS (400)
|
||||
-- ============================================================
|
||||
('400', 'Ingresos', NULL, 'ingreso', '400', true, true),
|
||||
('410', 'Ventas', (SELECT id FROM accounts WHERE code = '400'), 'ingreso', '410', true, true),
|
||||
('420', 'Devoluciones sobre Ventas', (SELECT id FROM accounts WHERE code = '400'), 'ingreso', '420', true, true),
|
||||
INSERT INTO accounts (code, name, parent_id, type, sat_code, is_system, is_active)
|
||||
VALUES ('400', 'Ingresos', NULL, 'ingreso', '400', true, true);
|
||||
INSERT INTO accounts (code, name, parent_id, type, sat_code, is_system, is_active)
|
||||
VALUES ('410', 'Ventas', (SELECT id FROM accounts WHERE code = '400'), 'ingreso', '410', true, true);
|
||||
INSERT INTO accounts (code, name, parent_id, type, sat_code, is_system, is_active)
|
||||
VALUES ('420', 'Devoluciones sobre Ventas', (SELECT id FROM accounts WHERE code = '400'), 'ingreso', '420', true, true);
|
||||
|
||||
-- ============================================================
|
||||
-- COSTOS (500)
|
||||
-- ============================================================
|
||||
('500', 'Costos', NULL, 'costo', '500', true, true),
|
||||
('510', 'Costo de Mercancia Vendida', (SELECT id FROM accounts WHERE code = '500'), 'costo', '510', true, true),
|
||||
INSERT INTO accounts (code, name, parent_id, type, sat_code, is_system, is_active)
|
||||
VALUES ('500', 'Costos', NULL, 'costo', '500', true, true);
|
||||
INSERT INTO accounts (code, name, parent_id, type, sat_code, is_system, is_active)
|
||||
VALUES ('510', 'Costo de Mercancia Vendida', (SELECT id FROM accounts WHERE code = '500'), 'costo', '510', true, true);
|
||||
|
||||
-- ============================================================
|
||||
-- GASTOS (600)
|
||||
-- ============================================================
|
||||
('600', 'Gastos', NULL, 'gasto', '600', true, true),
|
||||
('610', 'Gastos Operativos', (SELECT id FROM accounts WHERE code = '600'), 'gasto', '610', true, true),
|
||||
('620', 'Gastos Financieros', (SELECT id FROM accounts WHERE code = '600'), 'gasto', '620', true, true);
|
||||
INSERT INTO accounts (code, name, parent_id, type, sat_code, is_system, is_active)
|
||||
VALUES ('600', 'Gastos', NULL, 'gasto', '600', true, true);
|
||||
INSERT INTO accounts (code, name, parent_id, type, sat_code, is_system, is_active)
|
||||
VALUES ('610', 'Gastos Operativos', (SELECT id FROM accounts WHERE code = '600'), 'gasto', '610', true, true);
|
||||
INSERT INTO accounts (code, name, parent_id, type, sat_code, is_system, is_active)
|
||||
VALUES ('620', 'Gastos Financieros', (SELECT id FROM accounts WHERE code = '600'), 'gasto', '620', true, true);
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user