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:
@@ -100,9 +100,8 @@ def list_items():
|
||||
where_clauses = ["i.is_active = true"]
|
||||
params = []
|
||||
|
||||
if branch_id:
|
||||
where_clauses.append("i.branch_id = %s")
|
||||
params.append(branch_id)
|
||||
# branch_id no longer filters inventory rows (shared catalog).
|
||||
# It is used only to show per-branch stock.
|
||||
if search:
|
||||
where_clauses.append("(i.part_number ILIKE %s OR i.name ILIKE %s OR i.barcode ILIKE %s)")
|
||||
params.extend([f'%{search}%', f'%{search}%', f'%{search}%'])
|
||||
@@ -116,93 +115,91 @@ def list_items():
|
||||
where = " AND ".join(where_clauses)
|
||||
|
||||
if low_stock:
|
||||
# low_stock filter: JOIN with stock subquery, filter items where stock < min_stock
|
||||
# This keeps pagination accurate because the filter is in the SQL WHERE clause.
|
||||
# low_stock filter: JOIN with total stock summary
|
||||
count_sql = f"""
|
||||
SELECT count(*) FROM inventory i
|
||||
LEFT JOIN (
|
||||
SELECT inventory_id, COALESCE(SUM(quantity), 0) AS stock
|
||||
FROM inventory_operations GROUP BY inventory_id
|
||||
) s ON s.inventory_id = i.id
|
||||
LEFT JOIN inventory_stock_summary s ON s.inventory_id = i.id
|
||||
WHERE {where} AND i.min_stock IS NOT NULL AND i.min_stock > 0
|
||||
AND COALESCE(s.stock, 0) < i.min_stock
|
||||
"""
|
||||
cur.execute(count_sql, params)
|
||||
total = cur.fetchone()[0]
|
||||
|
||||
stock_join = """
|
||||
LEFT JOIN inventory_stock ist ON ist.inventory_id = i.id AND ist.branch_id = %s
|
||||
""" if branch_id else """
|
||||
LEFT JOIN inventory_stock_summary ist ON ist.inventory_id = i.id
|
||||
"""
|
||||
stock_select = "COALESCE(ist.stock, 0) AS stock"
|
||||
stock_params = [int(branch_id)] if branch_id else []
|
||||
|
||||
cur.execute(f"""
|
||||
SELECT i.id, i.branch_id, i.part_number, i.barcode, i.name, i.description,
|
||||
SELECT i.id, i.part_number, i.barcode, i.name, i.description,
|
||||
i.category_id, i.brand, i.unit, i.cost, i.price_1, i.price_2, i.price_3,
|
||||
i.tax_rate, i.min_stock, i.max_stock, i.location, i.image_url, i.catalog_part_id,
|
||||
COALESCE(s.stock, 0) AS stock
|
||||
{stock_select}
|
||||
FROM inventory i
|
||||
LEFT JOIN (
|
||||
SELECT inventory_id, COALESCE(SUM(quantity), 0) AS stock
|
||||
FROM inventory_operations GROUP BY inventory_id
|
||||
) s ON s.inventory_id = i.id
|
||||
{stock_join}
|
||||
WHERE {where} AND i.min_stock IS NOT NULL AND i.min_stock > 0
|
||||
AND COALESCE(s.stock, 0) < i.min_stock
|
||||
AND COALESCE(
|
||||
(SELECT stock FROM inventory_stock_summary WHERE inventory_id = i.id), 0
|
||||
) < i.min_stock
|
||||
ORDER BY i.name
|
||||
LIMIT %s OFFSET %s
|
||||
""", params + [per_page, (page - 1) * per_page])
|
||||
""", stock_params + params + [per_page, (page - 1) * per_page])
|
||||
|
||||
items = []
|
||||
for r in cur.fetchall():
|
||||
items.append({
|
||||
'id': r[0], 'branch_id': r[1], 'part_number': r[2], 'barcode': r[3],
|
||||
'name': r[4], 'description': r[5], 'category_id': r[6], 'brand': r[7],
|
||||
'unit': r[8], 'cost': float(r[9]) if r[9] else 0,
|
||||
'price_1': float(r[10]) if r[10] else 0,
|
||||
'price_2': float(r[11]) if r[11] else 0,
|
||||
'price_3': float(r[12]) if r[12] else 0,
|
||||
'tax_rate': float(r[13]) if r[13] else 0.16,
|
||||
'min_stock': r[14], 'max_stock': r[15], 'location': r[16],
|
||||
'image_url': r[17], 'catalog_part_id': r[18],
|
||||
'stock': r[19]
|
||||
'id': r[0], 'part_number': r[1], 'barcode': r[2],
|
||||
'name': r[3], 'description': r[4], 'category_id': r[5], 'brand': r[6],
|
||||
'unit': r[7], 'cost': float(r[8]) if r[8] else 0,
|
||||
'price_1': float(r[9]) if r[9] else 0,
|
||||
'price_2': float(r[10]) if r[10] else 0,
|
||||
'price_3': float(r[11]) if r[11] else 0,
|
||||
'tax_rate': float(r[12]) if r[12] else 0.16,
|
||||
'min_stock': r[13], 'max_stock': r[14], 'location': r[15],
|
||||
'image_url': r[16], 'catalog_part_id': r[17],
|
||||
'stock': r[18]
|
||||
})
|
||||
else:
|
||||
# Normal path: count, fetch items, then bulk-lookup stock
|
||||
cur.execute(f"SELECT count(*) FROM inventory i WHERE {where}", params)
|
||||
total = cur.fetchone()[0]
|
||||
|
||||
stock_join = """
|
||||
LEFT JOIN inventory_stock ist ON ist.inventory_id = i.id AND ist.branch_id = %s
|
||||
""" if branch_id else """
|
||||
LEFT JOIN inventory_stock_summary ist ON ist.inventory_id = i.id
|
||||
"""
|
||||
stock_select = "COALESCE(ist.stock, 0) AS stock"
|
||||
stock_params = [int(branch_id)] if branch_id else []
|
||||
|
||||
cur.execute(f"""
|
||||
SELECT i.id, i.branch_id, i.part_number, i.barcode, i.name, i.description,
|
||||
SELECT i.id, i.part_number, i.barcode, i.name, i.description,
|
||||
i.category_id, i.brand, i.unit, i.cost, i.price_1, i.price_2, i.price_3,
|
||||
i.tax_rate, i.min_stock, i.max_stock, i.location, i.image_url, i.catalog_part_id
|
||||
i.tax_rate, i.min_stock, i.max_stock, i.location, i.image_url, i.catalog_part_id,
|
||||
{stock_select}
|
||||
FROM inventory i
|
||||
{stock_join}
|
||||
WHERE {where}
|
||||
ORDER BY i.name
|
||||
LIMIT %s OFFSET %s
|
||||
""", params + [per_page, (page - 1) * per_page])
|
||||
|
||||
items_raw = cur.fetchall()
|
||||
|
||||
# Get stock for all returned items
|
||||
inv_ids = [r[0] for r in items_raw]
|
||||
stock_map = {}
|
||||
if inv_ids:
|
||||
cur.execute("""
|
||||
SELECT inventory_id, COALESCE(SUM(quantity), 0)
|
||||
FROM inventory_operations
|
||||
WHERE inventory_id = ANY(%s)
|
||||
GROUP BY inventory_id
|
||||
""", (inv_ids,))
|
||||
stock_map = {r[0]: r[1] for r in cur.fetchall()}
|
||||
""", stock_params + params + [per_page, (page - 1) * per_page])
|
||||
|
||||
items = []
|
||||
for r in items_raw:
|
||||
stock = stock_map.get(r[0], 0)
|
||||
for r in cur.fetchall():
|
||||
items.append({
|
||||
'id': r[0], 'branch_id': r[1], 'part_number': r[2], 'barcode': r[3],
|
||||
'name': r[4], 'description': r[5], 'category_id': r[6], 'brand': r[7],
|
||||
'unit': r[8], 'cost': float(r[9]) if r[9] else 0,
|
||||
'price_1': float(r[10]) if r[10] else 0,
|
||||
'price_2': float(r[11]) if r[11] else 0,
|
||||
'price_3': float(r[12]) if r[12] else 0,
|
||||
'tax_rate': float(r[13]) if r[13] else 0.16,
|
||||
'min_stock': r[14], 'max_stock': r[15], 'location': r[16],
|
||||
'image_url': r[17], 'catalog_part_id': r[18],
|
||||
'stock': stock
|
||||
'id': r[0], 'part_number': r[1], 'barcode': r[2],
|
||||
'name': r[3], 'description': r[4], 'category_id': r[5], 'brand': r[6],
|
||||
'unit': r[7], 'cost': float(r[8]) if r[8] else 0,
|
||||
'price_1': float(r[9]) if r[9] else 0,
|
||||
'price_2': float(r[10]) if r[10] else 0,
|
||||
'price_3': float(r[11]) if r[11] else 0,
|
||||
'tax_rate': float(r[12]) if r[12] else 0.16,
|
||||
'min_stock': r[13], 'max_stock': r[14], 'location': r[15],
|
||||
'image_url': r[16], 'catalog_part_id': r[17],
|
||||
'stock': r[18]
|
||||
})
|
||||
|
||||
cur.close()
|
||||
@@ -222,9 +219,8 @@ def get_item(item_id):
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
SELECT i.*, b.name as branch_name, c.name as category_name
|
||||
SELECT i.*, c.name as category_name
|
||||
FROM inventory i
|
||||
LEFT JOIN branches b ON i.branch_id = b.id
|
||||
LEFT JOIN categories c ON i.category_id = c.id
|
||||
WHERE i.id = %s
|
||||
""", (item_id,))
|
||||
@@ -240,7 +236,8 @@ def get_item(item_id):
|
||||
if item.get(k) is not None:
|
||||
item[k] = float(item[k])
|
||||
|
||||
item['stock'] = get_stock(conn, item_id, item.get('branch_id'))
|
||||
branch_id = request.args.get('branch_id', g.branch_id)
|
||||
item['stock'] = get_stock(conn, item_id, branch_id)
|
||||
item['history'] = get_movement_history(conn, item_id, limit=20)
|
||||
|
||||
cur.close()
|
||||
@@ -259,8 +256,6 @@ def create_item():
|
||||
return jsonify({'error': f'{f} required'}), 400
|
||||
|
||||
branch_id = data.get('branch_id', g.branch_id)
|
||||
if not branch_id:
|
||||
return jsonify({'error': 'branch_id required'}), 400
|
||||
|
||||
# Plan limit check
|
||||
from services.billing import check_limit, next_plan, PLANS, get_plan
|
||||
@@ -307,13 +302,13 @@ def create_item():
|
||||
try:
|
||||
cur.execute("""
|
||||
INSERT INTO inventory
|
||||
(branch_id, part_number, barcode, name, description, category_id, brand,
|
||||
(part_number, barcode, name, description, category_id, brand,
|
||||
vehicle_compatibility, unit, cost, price_1, price_2, price_3, tax_rate,
|
||||
min_stock, max_stock, location, image_url, catalog_part_id)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
||||
RETURNING id
|
||||
""", (
|
||||
branch_id, data['part_number'], barcode, data['name'],
|
||||
data['part_number'], barcode, data['name'],
|
||||
data.get('description'), data.get('category_id'), data.get('brand'),
|
||||
json.dumps(data.get('vehicle_compatibility')) if data.get('vehicle_compatibility') else None,
|
||||
data.get('unit', 'PZA'), data.get('cost', 0),
|
||||
@@ -324,9 +319,9 @@ def create_item():
|
||||
))
|
||||
item_id = cur.fetchone()[0]
|
||||
|
||||
# Record initial stock if provided
|
||||
# Record initial stock if provided (requires branch_id)
|
||||
initial_stock = data.get('initial_stock', 0)
|
||||
if initial_stock > 0:
|
||||
if initial_stock > 0 and branch_id:
|
||||
record_initial(conn, item_id, branch_id, initial_stock, data.get('cost'))
|
||||
|
||||
# Insert SKU aliases if provided
|
||||
@@ -409,8 +404,8 @@ def create_item():
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
cur.close(); conn.close()
|
||||
if 'idx_inventory_branch_part' in str(e):
|
||||
return jsonify({'error': 'Part number already exists in this branch'}), 409
|
||||
if 'idx_inventory_part_unique' in str(e):
|
||||
return jsonify({'error': 'Part number already exists'}), 409
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@@ -555,8 +550,8 @@ def bulk_import_items():
|
||||
description = str(row.get('description', '')).strip()
|
||||
category = str(row.get('category', '')).strip()
|
||||
|
||||
# Check if item already exists for this branch
|
||||
cur.execute("SELECT id FROM inventory WHERE branch_id = %s AND part_number = %s", (branch_id, part_number))
|
||||
# Check if item already exists (catalog is shared across branches)
|
||||
cur.execute("SELECT id FROM inventory WHERE part_number = %s", (part_number,))
|
||||
existing = cur.fetchone()
|
||||
|
||||
if existing:
|
||||
@@ -569,13 +564,12 @@ def bulk_import_items():
|
||||
brand = COALESCE(NULLIF(%s,''), brand),
|
||||
cost = CASE WHEN %s > 0 THEN %s ELSE cost END,
|
||||
price_1 = CASE WHEN %s > 0 THEN %s ELSE price_1 END,
|
||||
stock = stock + %s,
|
||||
location = COALESCE(NULLIF(%s,''), location),
|
||||
description = COALESCE(NULLIF(%s,''), description),
|
||||
category = COALESCE(NULLIF(%s,''), category)
|
||||
WHERE id = %s
|
||||
""",
|
||||
(name, brand, cost, cost, price_1, price_1, stock, location, description, category, item_id)
|
||||
(name, brand, cost, cost, price_1, price_1, location, description, category, item_id)
|
||||
)
|
||||
was_inserted = False
|
||||
# Record stock adjustment for existing item if stock > 0
|
||||
@@ -588,11 +582,11 @@ def bulk_import_items():
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO inventory
|
||||
(branch_id, part_number, barcode, name, brand, cost, price_1, location, description, category, unit)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
(part_number, barcode, name, brand, cost, price_1, location, description, category, unit)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""",
|
||||
(branch_id, part_number, barcode, name, brand, cost, price_1, location, description, category, 'PZA')
|
||||
(part_number, barcode, name, brand, cost, price_1, location, description, category, 'PZA')
|
||||
)
|
||||
item_id = cur.fetchone()[0]
|
||||
was_inserted = True
|
||||
@@ -1332,7 +1326,7 @@ def api_inventory_stats():
|
||||
branch_id = getattr(g, 'branch_id', None)
|
||||
|
||||
# Stock count
|
||||
cur.execute("SELECT COUNT(*) FROM inventory WHERE is_active = true AND (branch_id = %s OR %s IS NULL)", (branch_id, branch_id))
|
||||
cur.execute("SELECT COUNT(*) FROM inventory WHERE is_active = true")
|
||||
stock = cur.fetchone()[0]
|
||||
|
||||
# Operations counts by type
|
||||
@@ -1373,52 +1367,44 @@ def api_inventory_summary():
|
||||
"""Get high-level summary counts for the inventory dashboard badges."""
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
branch_id = getattr(g, 'branch_id', None)
|
||||
|
||||
where_branch = ""
|
||||
params = []
|
||||
if branch_id:
|
||||
where_branch = "AND i.branch_id = %s"
|
||||
params.append(branch_id)
|
||||
|
||||
# 1. Total active SKUs
|
||||
cur.execute(f"""
|
||||
cur.execute("""
|
||||
SELECT COUNT(*) FROM inventory i
|
||||
WHERE i.is_active = true {where_branch}
|
||||
""", params.copy())
|
||||
WHERE i.is_active = true
|
||||
""")
|
||||
total_skus = cur.fetchone()[0] or 0
|
||||
|
||||
# 2. Total inventory value (cost * stock)
|
||||
cur.execute(f"""
|
||||
cur.execute("""
|
||||
SELECT COALESCE(SUM(i.cost * COALESCE(s.stock, 0)), 0)
|
||||
FROM inventory i
|
||||
LEFT JOIN inventory_stock_summary s ON s.inventory_id = i.id
|
||||
WHERE i.is_active = true {where_branch}
|
||||
""", params.copy())
|
||||
WHERE i.is_active = true
|
||||
""")
|
||||
total_value = float(cur.fetchone()[0] or 0)
|
||||
|
||||
# 3. Low stock count (below min_stock)
|
||||
cur.execute(f"""
|
||||
cur.execute("""
|
||||
SELECT COUNT(*)
|
||||
FROM inventory i
|
||||
LEFT JOIN inventory_stock_summary s ON s.inventory_id = i.id
|
||||
WHERE i.is_active = true {where_branch}
|
||||
WHERE i.is_active = true
|
||||
AND i.min_stock IS NOT NULL AND i.min_stock > 0
|
||||
AND COALESCE(s.stock, 0) < i.min_stock
|
||||
""", params.copy())
|
||||
""")
|
||||
low_stock = cur.fetchone()[0] or 0
|
||||
|
||||
# 4. No movement in last 60 days
|
||||
cutoff = datetime.utcnow() - timedelta(days=60)
|
||||
cur.execute(f"""
|
||||
cur.execute("""
|
||||
SELECT COUNT(*)
|
||||
FROM inventory i
|
||||
WHERE i.is_active = true {where_branch}
|
||||
WHERE i.is_active = true
|
||||
AND i.id NOT IN (
|
||||
SELECT inventory_id FROM inventory_operations
|
||||
WHERE created_at > %s
|
||||
)
|
||||
""", params + [cutoff])
|
||||
""", (cutoff,))
|
||||
no_movement = cur.fetchone()[0] or 0
|
||||
|
||||
cur.close(); conn.close()
|
||||
@@ -1478,34 +1464,40 @@ def report_valuation():
|
||||
cur = conn.cursor()
|
||||
branch_id = request.args.get('branch_id', g.branch_id)
|
||||
|
||||
where = "i.is_active = true"
|
||||
params = []
|
||||
if branch_id:
|
||||
where += " AND i.branch_id = %s"
|
||||
params.append(branch_id)
|
||||
sql = """
|
||||
SELECT i.id, i.part_number, i.name, i.brand, i.cost,
|
||||
COALESCE(ist.stock, 0) AS stock,
|
||||
COALESCE(ist.stock, 0) * COALESCE(i.cost, 0) AS value
|
||||
FROM inventory i
|
||||
LEFT JOIN inventory_stock ist ON ist.inventory_id = i.id AND ist.branch_id = %s
|
||||
WHERE i.is_active = true
|
||||
ORDER BY value DESC
|
||||
"""
|
||||
params = [branch_id]
|
||||
else:
|
||||
sql = """
|
||||
SELECT i.id, i.part_number, i.name, i.brand, i.cost,
|
||||
COALESCE(s.stock, 0) AS stock,
|
||||
COALESCE(s.stock, 0) * COALESCE(i.cost, 0) AS value
|
||||
FROM inventory i
|
||||
LEFT JOIN inventory_stock_summary s ON s.inventory_id = i.id
|
||||
WHERE i.is_active = true
|
||||
ORDER BY value DESC
|
||||
"""
|
||||
params = []
|
||||
|
||||
cur.execute(f"""
|
||||
SELECT i.id, i.part_number, i.name, i.brand, i.cost, i.branch_id,
|
||||
COALESCE(s.stock, 0) AS stock,
|
||||
COALESCE(s.stock, 0) * COALESCE(i.cost, 0) AS value
|
||||
FROM inventory i
|
||||
LEFT JOIN (
|
||||
SELECT inventory_id, SUM(quantity) AS stock
|
||||
FROM inventory_operations GROUP BY inventory_id
|
||||
) s ON s.inventory_id = i.id
|
||||
WHERE {where}
|
||||
ORDER BY value DESC
|
||||
""", params)
|
||||
cur.execute(sql, params)
|
||||
|
||||
items = []
|
||||
grand_total = 0
|
||||
for r in cur.fetchall():
|
||||
val = float(r[7])
|
||||
val = float(r[6])
|
||||
grand_total += val
|
||||
items.append({
|
||||
'id': r[0], 'part_number': r[1], 'name': r[2], 'brand': r[3],
|
||||
'cost': float(r[4]) if r[4] else 0, 'branch_id': r[5],
|
||||
'stock': r[6], 'value': round(val, 2)
|
||||
'cost': float(r[4]) if r[4] else 0,
|
||||
'stock': r[5], 'value': round(val, 2)
|
||||
})
|
||||
|
||||
cur.close(); conn.close()
|
||||
@@ -1581,32 +1573,22 @@ def report_no_movement():
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
days = int(request.args.get('days', 60))
|
||||
branch_id = request.args.get('branch_id', g.branch_id)
|
||||
cutoff = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
where_branch = ""
|
||||
params_main = []
|
||||
if branch_id:
|
||||
where_branch = "AND i.branch_id = %s"
|
||||
params_main.append(branch_id)
|
||||
|
||||
cur.execute(f"""
|
||||
cur.execute("""
|
||||
SELECT i.id, i.part_number, i.name, i.brand, i.cost,
|
||||
COALESCE(s.stock, 0) AS stock,
|
||||
last_op.last_date
|
||||
FROM inventory i
|
||||
LEFT JOIN (
|
||||
SELECT inventory_id, SUM(quantity) AS stock
|
||||
FROM inventory_operations GROUP BY inventory_id
|
||||
) s ON s.inventory_id = i.id
|
||||
LEFT JOIN inventory_stock_summary s ON s.inventory_id = i.id
|
||||
LEFT JOIN (
|
||||
SELECT inventory_id, MAX(created_at) AS last_date
|
||||
FROM inventory_operations GROUP BY inventory_id
|
||||
) last_op ON last_op.inventory_id = i.id
|
||||
WHERE i.is_active = true {where_branch}
|
||||
WHERE i.is_active = true
|
||||
AND (last_op.last_date IS NULL OR last_op.last_date < %s)
|
||||
ORDER BY last_op.last_date ASC NULLS FIRST
|
||||
""", params_main + [cutoff])
|
||||
""", (cutoff,))
|
||||
|
||||
items = []
|
||||
for r in cur.fetchall():
|
||||
@@ -1626,28 +1608,17 @@ def report_low_stock():
|
||||
"""Items below their min_stock threshold."""
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
branch_id = request.args.get('branch_id', g.branch_id)
|
||||
|
||||
where_branch = ""
|
||||
params = []
|
||||
if branch_id:
|
||||
where_branch = "AND i.branch_id = %s"
|
||||
params.append(branch_id)
|
||||
|
||||
cur.execute(f"""
|
||||
cur.execute("""
|
||||
SELECT i.id, i.part_number, i.name, i.brand, i.min_stock,
|
||||
COALESCE(s.stock, 0) AS stock,
|
||||
i.min_stock - COALESCE(s.stock, 0) AS deficit
|
||||
FROM inventory i
|
||||
LEFT JOIN (
|
||||
SELECT inventory_id, SUM(quantity) AS stock
|
||||
FROM inventory_operations GROUP BY inventory_id
|
||||
) s ON s.inventory_id = i.id
|
||||
WHERE i.is_active = true {where_branch}
|
||||
LEFT JOIN inventory_stock_summary s ON s.inventory_id = i.id
|
||||
WHERE i.is_active = true
|
||||
AND i.min_stock IS NOT NULL AND i.min_stock > 0
|
||||
AND COALESCE(s.stock, 0) < i.min_stock
|
||||
ORDER BY deficit DESC
|
||||
""", params)
|
||||
""")
|
||||
|
||||
items = []
|
||||
for r in cur.fetchall():
|
||||
@@ -1668,15 +1639,13 @@ def report_branch_comparison():
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("""
|
||||
SELECT i.id, i.part_number, i.name, i.brand, i.branch_id,
|
||||
SELECT i.id, i.part_number, i.name, i.brand,
|
||||
ist.branch_id,
|
||||
b.name AS branch_name,
|
||||
COALESCE(s.stock, 0) AS stock
|
||||
COALESCE(ist.stock, 0) AS stock
|
||||
FROM inventory i
|
||||
LEFT JOIN branches b ON i.branch_id = b.id
|
||||
LEFT JOIN (
|
||||
SELECT inventory_id, SUM(quantity) AS stock
|
||||
FROM inventory_operations GROUP BY inventory_id
|
||||
) s ON s.inventory_id = i.id
|
||||
LEFT JOIN inventory_stock ist ON ist.inventory_id = i.id
|
||||
LEFT JOIN branches b ON ist.branch_id = b.id
|
||||
WHERE i.is_active = true
|
||||
ORDER BY i.part_number, b.name
|
||||
""")
|
||||
@@ -1687,10 +1656,11 @@ def report_branch_comparison():
|
||||
pn = r[1]
|
||||
if pn not in by_part:
|
||||
by_part[pn] = {'part_number': pn, 'name': r[2], 'brand': r[3], 'branches': []}
|
||||
by_part[pn]['branches'].append({
|
||||
'inventory_id': r[0], 'branch_id': r[4],
|
||||
'branch_name': r[5], 'stock': r[6]
|
||||
})
|
||||
if r[4] is not None:
|
||||
by_part[pn]['branches'].append({
|
||||
'inventory_id': r[0], 'branch_id': r[4],
|
||||
'branch_name': r[5], 'stock': r[6]
|
||||
})
|
||||
|
||||
cur.close(); conn.close()
|
||||
items = list(by_part.values())
|
||||
@@ -1753,12 +1723,11 @@ def api_stock_by_branch():
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
SELECT b.id, b.name, b.address,
|
||||
COALESCE(SUM(io.quantity), 0) as stock
|
||||
COALESCE(ist.stock, 0) as stock
|
||||
FROM branches b
|
||||
LEFT JOIN inventory_operations io
|
||||
ON io.branch_id = b.id AND io.inventory_id = %s
|
||||
LEFT JOIN inventory_stock ist
|
||||
ON ist.branch_id = b.id AND ist.inventory_id = %s
|
||||
WHERE b.is_active = true
|
||||
GROUP BY b.id, b.name, b.address
|
||||
ORDER BY b.name
|
||||
""", (inventory_id,))
|
||||
data = []
|
||||
|
||||
Reference in New Issue
Block a user