feat: cash advance

This commit is contained in:
Efril
2026-08-13 14:38:28 +07:00
parent e6078e3c0b
commit 2c6864147b
36 changed files with 2355 additions and 186 deletions
@@ -0,0 +1 @@
DROP TABLE IF EXISTS cash_advances;
@@ -0,0 +1,45 @@
-- A cash advance is money handed to a team up front so the team can go shopping
-- (kasbon in the Indonesian UI). It is deliberately not an expense: while the money
-- sits with the team it is still the outlet's, and what was actually spent is read
-- from the purchase orders and expenses charged back to the advance. Nothing about
-- that spending is copied here.
CREATE TABLE cash_advances (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
outlet_id UUID NOT NULL REFERENCES outlets(id) ON DELETE CASCADE,
code_number VARCHAR(50) NOT NULL,
-- Same team shape as purchase_orders, with one difference: an advance is handed
-- to a team, so there is no "no team chosen yet" state and team_scope is NOT NULL.
team_scope VARCHAR(20) NOT NULL,
team_category_id UUID REFERENCES categories(id) ON DELETE RESTRICT,
amount DECIMAL(15,2) NOT NULL DEFAULT 0,
-- Cash the team brought back unspent. Spending is not stored: it is summed from
-- the purchase orders and expenses that point at this advance.
returned_amount DECIMAL(15,2) NOT NULL DEFAULT 0,
issued_date DATE NOT NULL,
due_date DATE,
status VARCHAR(20) NOT NULL DEFAULT 'draft',
description TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- Written as a CASE for the same reason as purchase_orders: an OR chain would
-- evaluate to NULL for an unexpected scope and a CHECK only rejects FALSE.
CONSTRAINT chk_cash_advances_team CHECK (
CASE
WHEN team_scope = 'category' THEN team_category_id IS NOT NULL
WHEN team_scope = 'central' THEN team_category_id IS NULL
ELSE false
END
),
CONSTRAINT chk_cash_advances_amounts CHECK (amount >= 0 AND returned_amount >= 0)
);
-- Leading with organization_id means this also serves the plain per-organization
-- lookups, so there is no separate index on that column.
CREATE UNIQUE INDEX idx_cash_advances_organization_id_code_number ON cash_advances(organization_id, code_number);
CREATE INDEX idx_cash_advances_outlet_id ON cash_advances(outlet_id);
CREATE INDEX idx_cash_advances_team_category_id ON cash_advances(team_category_id);
CREATE INDEX idx_cash_advances_team_scope ON cash_advances(team_scope);
CREATE INDEX idx_cash_advances_issued_date ON cash_advances(issued_date);
CREATE INDEX idx_cash_advances_status ON cash_advances(status);
@@ -0,0 +1,15 @@
DROP INDEX IF EXISTS idx_expenses_cash_advance_id;
ALTER TABLE expenses
DROP CONSTRAINT IF EXISTS fk_expenses_cash_advance;
ALTER TABLE expenses
DROP COLUMN IF EXISTS cash_advance_id;
DROP INDEX IF EXISTS idx_purchase_orders_cash_advance_id;
ALTER TABLE purchase_orders
DROP CONSTRAINT IF EXISTS fk_purchase_orders_cash_advance;
ALTER TABLE purchase_orders
DROP COLUMN IF EXISTS cash_advance_id;
@@ -0,0 +1,24 @@
-- Spending paid out of a cash advance points back at it. This is how an advance is
-- accounted for: the team's purchases and expenses are the settlement, so the advance
-- itself never carries a copy of what was bought.
-- RESTRICT rather than SET NULL: dropping an advance that still has spending on it
-- would leave that spending looking like it came straight out of the drawer.
ALTER TABLE purchase_orders
ADD COLUMN IF NOT EXISTS cash_advance_id UUID;
ALTER TABLE purchase_orders
ADD CONSTRAINT fk_purchase_orders_cash_advance
FOREIGN KEY (cash_advance_id) REFERENCES cash_advances(id) ON DELETE RESTRICT;
CREATE INDEX IF NOT EXISTS idx_purchase_orders_cash_advance_id
ON purchase_orders(cash_advance_id);
ALTER TABLE expenses
ADD COLUMN IF NOT EXISTS cash_advance_id UUID;
ALTER TABLE expenses
ADD CONSTRAINT fk_expenses_cash_advance
FOREIGN KEY (cash_advance_id) REFERENCES cash_advances(id) ON DELETE RESTRICT;
CREATE INDEX IF NOT EXISTS idx_expenses_cash_advance_id
ON expenses(cash_advance_id);