feat: Fase 1-3 completas - precios proveedor, multi-sucursal, factura global

Fase 1: Lista de precios de proveedor
- Tabla supplier_catalog_prices en master DB
- Endpoints GET/POST/PUT/DELETE /supplier-catalog/prices
- Upload CSV/Excel de precios de proveedor
- Visualizacion de supplier_price en catalogo y POS

Fase 2: Multi-sucursal completo
- Migracion v4.0: inventory.branch_id=NULL, tabla inventory_stock
- Campos fiscales en branches (RFC, regimen, CP, serie CFDI, certificados)
- Trigger trg_update_inventory_stock para sincronizar stock por sucursal
- Backend config_bp.py con CRUD de sucursales fiscales
- Backend inventory_bp.py y pos_bp.py refactorizados para inventario compartido
- Backend invoicing_bp.py usa datos fiscales de la sucursal de la venta
- Frontend config.html/js con modal de sucursales expandido

Fase 3: Factura global mensual
- Migracion v4.1: tablas global_invoice_sales, sales.global_invoiced_at
- build_global_invoice_xml() con InformacionGlobal SAT-compliant
- Servicio global_invoice.py para agrupar ventas PUE <=000
- Endpoints POST/GET /global-invoice y /global-invoice/eligible-sales
- Frontend invoicing.html/js con boton y modal de factura global
This commit is contained in:
2026-06-11 08:59:56 +00:00
parent ea29cc31c0
commit 2b73c2c6db
23 changed files with 1665 additions and 230 deletions

View File

@@ -13,15 +13,53 @@ config_bp = Blueprint('config', __name__, url_prefix='/pos/api/config')
def list_branches():
conn = get_tenant_conn(g.tenant_id)
cur = conn.cursor()
cur.execute("SELECT id, name, address, phone, is_active FROM branches ORDER BY id")
cur.execute("""
SELECT id, name, address, phone, is_active, is_main,
rfc, razon_social, regimen_fiscal, codigo_postal,
serie_cfdi, folio_inicial, licencia_fiscal
FROM branches ORDER BY id
""")
branches = []
for r in cur.fetchall():
branches.append({'id': r[0], 'name': r[1], 'address': r[2], 'phone': r[3], 'is_active': r[4]})
branches.append({
'id': r[0], 'name': r[1], 'address': r[2], 'phone': r[3],
'is_active': r[4], 'is_main': r[5],
'rfc': r[6], 'razon_social': r[7], 'regimen_fiscal': r[8],
'codigo_postal': r[9], 'serie_cfdi': r[10],
'folio_inicial': r[11], 'licencia_fiscal': r[12],
})
cur.close()
conn.close()
return jsonify({'data': branches})
@config_bp.route('/branches/<int:branch_id>', methods=['GET'])
@require_auth('config.view')
def get_branch(branch_id):
conn = get_tenant_conn(g.tenant_id)
cur = conn.cursor()
cur.execute("""
SELECT id, name, address, phone, is_active, is_main,
rfc, razon_social, regimen_fiscal, codigo_postal,
serie_cfdi, folio_inicial, licencia_fiscal,
certificado_pem, llave_pem
FROM branches WHERE id = %s
""", (branch_id,))
r = cur.fetchone()
cur.close()
conn.close()
if not r:
return jsonify({'error': 'Branch not found'}), 404
return jsonify({
'id': r[0], 'name': r[1], 'address': r[2], 'phone': r[3],
'is_active': r[4], 'is_main': r[5],
'rfc': r[6], 'razon_social': r[7], 'regimen_fiscal': r[8],
'codigo_postal': r[9], 'serie_cfdi': r[10],
'folio_inicial': r[11], 'licencia_fiscal': r[12],
'certificado_pem': r[13], 'llave_pem': r[14],
})
@config_bp.route('/branches', methods=['POST'])
@require_auth('config.edit')
def create_branch():
@@ -47,10 +85,25 @@ def create_branch():
conn = get_tenant_conn(g.tenant_id)
cur = conn.cursor()
# If setting as main, clear any existing main
if data.get('is_main'):
cur.execute("UPDATE branches SET is_main = false WHERE is_main = true")
cur.execute("""
INSERT INTO branches (name, address, phone)
VALUES (%s, %s, %s) RETURNING id
""", (data['name'], data.get('address'), data.get('phone')))
INSERT INTO branches (
name, address, phone, is_main,
rfc, razon_social, regimen_fiscal, codigo_postal,
serie_cfdi, folio_inicial, licencia_fiscal,
certificado_pem, llave_pem
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id
""", (
data['name'], data.get('address'), data.get('phone'), bool(data.get('is_main')),
data.get('rfc'), data.get('razon_social'), data.get('regimen_fiscal'), data.get('codigo_postal'),
data.get('serie_cfdi'), data.get('folio_inicial'), data.get('licencia_fiscal'),
data.get('certificado_pem'), data.get('llave_pem'),
))
branch_id = cur.fetchone()[0]
conn.commit()
cur.close()
@@ -58,6 +111,50 @@ def create_branch():
return jsonify({'id': branch_id, 'message': 'Branch created'}), 201
@config_bp.route('/branches/<int:branch_id>', methods=['PUT'])
@require_auth('config.edit')
def update_branch(branch_id):
data = request.get_json() or {}
conn = get_tenant_conn(g.tenant_id)
cur = conn.cursor()
cur.execute("SELECT id FROM branches WHERE id = %s", (branch_id,))
if not cur.fetchone():
cur.close(); conn.close()
return jsonify({'error': 'Branch not found'}), 404
# If setting as main, clear any existing main
if data.get('is_main'):
cur.execute("UPDATE branches SET is_main = false WHERE is_main = true AND id <> %s", (branch_id,))
updates = []
params = []
field_map = {
'name': 'name', 'address': 'address', 'phone': 'phone',
'is_active': 'is_active', 'is_main': 'is_main',
'rfc': 'rfc', 'razon_social': 'razon_social',
'regimen_fiscal': 'regimen_fiscal', 'codigo_postal': 'codigo_postal',
'serie_cfdi': 'serie_cfdi', 'folio_inicial': 'folio_inicial',
'licencia_fiscal': 'licencia_fiscal',
'certificado_pem': 'certificado_pem', 'llave_pem': 'llave_pem',
}
for json_key, col in field_map.items():
if json_key in data:
updates.append(f"{col} = %s")
params.append(data[json_key])
if not updates:
cur.close(); conn.close()
return jsonify({'error': 'Nothing to update'}), 400
params.append(branch_id)
cur.execute(f"UPDATE branches SET {', '.join(updates)} WHERE id = %s", params)
conn.commit()
cur.close()
conn.close()
return jsonify({'ok': True, 'message': 'Branch updated'})
@config_bp.route('/employees', methods=['GET'])
@require_auth('config.view')
def list_employees():