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
247 lines
11 KiB
PL/PgSQL
247 lines
11 KiB
PL/PgSQL
-- v4.0_multi_branch.sql
|
|
-- Multi-branch overhaul: branch fiscal data + shared inventory with per-branch stock.
|
|
-- WARNING: this migration restructures inventory data. A full DB backup is required.
|
|
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
-- 1. BRANCHES: fiscal fields + main flag
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
ALTER TABLE branches
|
|
ADD COLUMN IF NOT EXISTS is_main BOOLEAN DEFAULT FALSE,
|
|
ADD COLUMN IF NOT EXISTS rfc VARCHAR(13),
|
|
ADD COLUMN IF NOT EXISTS razon_social VARCHAR(300),
|
|
ADD COLUMN IF NOT EXISTS regimen_fiscal VARCHAR(10),
|
|
ADD COLUMN IF NOT EXISTS cp VARCHAR(5),
|
|
ADD COLUMN IF NOT EXISTS direccion_fiscal TEXT,
|
|
ADD COLUMN IF NOT EXISTS serie_cfdi VARCHAR(10) DEFAULT 'A',
|
|
ADD COLUMN IF NOT EXISTS folio_inicio INTEGER DEFAULT 1,
|
|
ADD COLUMN IF NOT EXISTS folio_actual INTEGER DEFAULT 1,
|
|
ADD COLUMN IF NOT EXISTS email VARCHAR(200);
|
|
|
|
-- Ensure at least one branch is marked main (the first one created).
|
|
DO $$
|
|
DECLARE
|
|
main_branch_id INTEGER;
|
|
branch_count INTEGER;
|
|
BEGIN
|
|
SELECT COUNT(*) INTO branch_count FROM branches;
|
|
|
|
IF branch_count = 0 THEN
|
|
INSERT INTO branches (name, is_main)
|
|
VALUES ('Principal', TRUE);
|
|
ELSE
|
|
SELECT id INTO main_branch_id FROM branches ORDER BY id LIMIT 1;
|
|
|
|
UPDATE branches SET is_main = FALSE;
|
|
UPDATE branches SET is_main = TRUE WHERE id = main_branch_id;
|
|
END IF;
|
|
END $$;
|
|
|
|
-- Constraint: only one main branch per tenant.
|
|
-- Because this runs inside a single tenant DB, a simple partial unique index is enough.
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_branches_single_main
|
|
ON branches (is_main)
|
|
WHERE is_main = TRUE;
|
|
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
-- 2. INVENTORY STOCK: new per-branch stock table
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
CREATE TABLE IF NOT EXISTS inventory_stock (
|
|
id SERIAL PRIMARY KEY,
|
|
inventory_id INTEGER NOT NULL REFERENCES inventory(id) ON DELETE CASCADE,
|
|
branch_id INTEGER NOT NULL REFERENCES branches(id) ON DELETE CASCADE,
|
|
stock INTEGER DEFAULT 0,
|
|
location VARCHAR(50),
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(inventory_id, branch_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_inventory_stock_branch ON inventory_stock(branch_id);
|
|
CREATE INDEX IF NOT EXISTS idx_inventory_stock_inventory ON inventory_stock(inventory_id);
|
|
|
|
CREATE OR REPLACE FUNCTION update_inventory_stock_updated_at()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = NOW();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS trg_inventory_stock_updated_at ON inventory_stock;
|
|
CREATE TRIGGER trg_inventory_stock_updated_at
|
|
BEFORE UPDATE ON inventory_stock
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_inventory_stock_updated_at();
|
|
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
-- 3. INVENTORY: make branch_id nullable + prepare for consolidation
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
-- Drop the old unique constraint that forces one record per (branch, part_number).
|
|
DROP INDEX IF EXISTS idx_inventory_branch_part;
|
|
|
|
-- Make branch_id nullable so we can have master records without a branch.
|
|
ALTER TABLE inventory ALTER COLUMN branch_id DROP NOT NULL;
|
|
|
|
-- Add unique constraint on part_number at tenant level so a product exists once.
|
|
-- If duplicates still exist this will fail, so we consolidate below first.
|
|
-- We create it at the end of this migration after deduplication.
|
|
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
-- 4. DATA MIGRATION: consolidate duplicated inventory rows by part_number
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
-- Build a mapping: for each duplicated part_number, choose the master record.
|
|
-- Master = record belonging to the main branch; fallback = oldest id.
|
|
CREATE TEMP TABLE _inventory_master_map AS
|
|
SELECT DISTINCT ON (part_number)
|
|
id AS master_id,
|
|
part_number
|
|
FROM inventory
|
|
ORDER BY part_number,
|
|
CASE WHEN branch_id = (SELECT id FROM branches WHERE is_main = TRUE LIMIT 1) THEN 0 ELSE 1 END,
|
|
id ASC;
|
|
|
|
-- Create temp table of duplicates (all rows that are NOT the master for their part_number).
|
|
CREATE TEMP TABLE _inventory_duplicates AS
|
|
SELECT i.id AS duplicate_id, m.master_id
|
|
FROM inventory i
|
|
JOIN _inventory_master_map m ON i.part_number = m.part_number
|
|
WHERE i.id <> m.master_id;
|
|
|
|
|
|
|
|
-- Compute per-duplicate stock and insert into inventory_stock against master_id + duplicate's branch.
|
|
INSERT INTO inventory_stock (inventory_id, branch_id, stock, location)
|
|
SELECT
|
|
dups.master_id,
|
|
dups.branch_id,
|
|
GREATEST(0, COALESCE(stock_by_dup.stock, 0))::int,
|
|
dups.location
|
|
FROM (
|
|
SELECT d.master_id, d.duplicate_id, i.branch_id, i.location
|
|
FROM _inventory_duplicates d
|
|
JOIN inventory i ON i.id = d.duplicate_id
|
|
) dups
|
|
JOIN LATERAL (
|
|
SELECT COALESCE(SUM(quantity), 0) AS stock
|
|
FROM inventory_operations
|
|
WHERE inventory_id = dups.duplicate_id AND branch_id = dups.branch_id
|
|
) stock_by_dup ON TRUE
|
|
ON CONFLICT (inventory_id, branch_id) DO UPDATE
|
|
SET stock = inventory_stock.stock + EXCLUDED.stock;
|
|
|
|
-- Also migrate stock from master records themselves (they were already in inventory.branch_id).
|
|
INSERT INTO inventory_stock (inventory_id, branch_id, stock, location)
|
|
SELECT
|
|
i.id,
|
|
i.branch_id,
|
|
GREATEST(0, COALESCE(stock_by_inv.stock, 0))::int,
|
|
i.location
|
|
FROM inventory i
|
|
JOIN _inventory_master_map m ON i.id = m.master_id
|
|
JOIN LATERAL (
|
|
SELECT COALESCE(SUM(quantity), 0) AS stock
|
|
FROM inventory_operations
|
|
WHERE inventory_id = i.id AND branch_id = i.branch_id
|
|
) stock_by_inv ON TRUE
|
|
WHERE i.branch_id IS NOT NULL
|
|
ON CONFLICT (inventory_id, branch_id) DO UPDATE
|
|
SET stock = EXCLUDED.stock;
|
|
|
|
-- Handle inventory_stock_summary specially: it has PK on inventory_id.
|
|
-- If master already has a summary row, add duplicate's stock and remove duplicate row.
|
|
-- Otherwise repoint the duplicate row to master.
|
|
UPDATE inventory_stock_summary s
|
|
SET stock = s.stock + d.stock
|
|
FROM (
|
|
SELECT duplicate_id, master_id, stock FROM inventory_stock_summary ss
|
|
JOIN _inventory_duplicates m ON ss.inventory_id = m.duplicate_id
|
|
WHERE EXISTS (SELECT 1 FROM inventory_stock_summary sm WHERE sm.inventory_id = m.master_id)
|
|
) d
|
|
WHERE s.inventory_id = d.master_id;
|
|
|
|
DELETE FROM inventory_stock_summary
|
|
WHERE inventory_id IN (
|
|
SELECT m.duplicate_id
|
|
FROM _inventory_duplicates m
|
|
WHERE EXISTS (SELECT 1 FROM inventory_stock_summary sm WHERE sm.inventory_id = m.master_id)
|
|
);
|
|
|
|
UPDATE inventory_stock_summary
|
|
SET inventory_id = m.master_id
|
|
FROM _inventory_duplicates m
|
|
WHERE inventory_id = m.duplicate_id;
|
|
|
|
-- Update FK references from duplicate inventory rows to master inventory rows.
|
|
-- We use dynamic SQL to update every known referencing table.
|
|
DO $$
|
|
DECLARE
|
|
rec RECORD;
|
|
fk_sql TEXT;
|
|
BEGIN
|
|
FOR rec IN
|
|
SELECT
|
|
tc.table_name,
|
|
kcu.column_name
|
|
FROM information_schema.table_constraints tc
|
|
JOIN information_schema.key_column_usage kcu
|
|
ON tc.constraint_name = kcu.constraint_name
|
|
JOIN information_schema.constraint_column_usage ccu
|
|
ON ccu.constraint_name = tc.constraint_name
|
|
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
AND ccu.table_name = 'inventory'
|
|
AND tc.table_name <> 'inventory_stock_summary'
|
|
LOOP
|
|
fk_sql := format(
|
|
'UPDATE %I SET %I = m.master_id FROM _inventory_duplicates m WHERE %I = m.duplicate_id',
|
|
rec.table_name, rec.column_name, rec.column_name
|
|
);
|
|
EXECUTE fk_sql;
|
|
END LOOP;
|
|
END $$;
|
|
|
|
-- Delete duplicate inventory rows now that FKs are repointed.
|
|
DELETE FROM inventory
|
|
WHERE id IN (SELECT duplicate_id FROM _inventory_duplicates);
|
|
|
|
-- Clean up master records: remove branch_id so they become shared catalog items.
|
|
UPDATE inventory SET branch_id = NULL WHERE branch_id IS NOT NULL;
|
|
|
|
-- Now safe to enforce uniqueness at tenant level.
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_inventory_part_unique ON inventory (part_number);
|
|
|
|
-- Clean temp tables.
|
|
DROP TABLE IF EXISTS _inventory_master_map;
|
|
DROP TABLE IF EXISTS _inventory_duplicates;
|
|
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
-- 5. CFDI_QUEUE: allow sale_id to be NULL for global invoices (Phase 3 prep)
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
ALTER TABLE cfdi_queue ALTER COLUMN sale_id DROP NOT NULL;
|
|
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
-- 6. TRIGGER: Keep inventory_stock in sync with inventory_operations
|
|
-- ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
CREATE OR REPLACE FUNCTION update_inventory_stock()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
INSERT INTO inventory_stock (inventory_id, branch_id, stock)
|
|
VALUES (NEW.inventory_id, NEW.branch_id, NEW.quantity)
|
|
ON CONFLICT (inventory_id, branch_id) DO UPDATE
|
|
SET stock = inventory_stock.stock + EXCLUDED.stock,
|
|
updated_at = NOW();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS trg_update_inventory_stock ON inventory_operations;
|
|
CREATE TRIGGER trg_update_inventory_stock
|
|
AFTER INSERT ON inventory_operations
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_inventory_stock();
|