feat: Implement supplier invoice case traceability and purchase line classification

This commit is contained in:
Christian 2026-04-12 09:26:35 +02:00
parent ceb560e2f2
commit 13dc1736b4
4 changed files with 448 additions and 30 deletions

View File

@ -22,6 +22,121 @@ import re
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
_PURCHASE_CASE_TYPE = "indkøb"
def _resolve_procurement_customer_id() -> int:
"""Find customer used for internally-owned procurement cases."""
configured_id = getattr(settings, "PROCUREMENT_CASE_CUSTOMER_ID", None)
if configured_id:
row = execute_query_single(
"SELECT id FROM customers WHERE id = %s AND is_active = true",
(configured_id,)
)
if row:
return int(row["id"])
bmc_row = execute_query_single(
"""
SELECT id
FROM customers
WHERE is_active = true
AND LOWER(name) LIKE %s
ORDER BY CASE WHEN LOWER(name) LIKE %s THEN 0 ELSE 1 END, id
LIMIT 1
""",
("%bmc%", "%bmc networks%")
)
if bmc_row:
return int(bmc_row["id"])
fallback = execute_query_single(
"SELECT id FROM customers WHERE is_active = true ORDER BY id LIMIT 1"
)
if fallback:
return int(fallback["id"])
raise ValueError("No active customer available for procurement case creation")
def _ensure_case_for_supplier_invoice(
invoice_id: int,
invoice_number: str,
vendor_name: Optional[str],
total_amount,
currency: Optional[str],
file_id: Optional[int] = None,
) -> Optional[int]:
"""Create and link a procurement case if missing for a supplier invoice."""
existing = execute_query_single(
"SELECT sag_id FROM supplier_invoices WHERE id = %s",
(invoice_id,)
)
if not existing:
return None
current_sag_id = existing.get("sag_id")
if current_sag_id:
return int(current_sag_id)
customer_id = _resolve_procurement_customer_id()
vendor_label = (vendor_name or "Ukendt leverandør").strip()
currency_label = (currency or "DKK").strip()
amount_label = f"{Decimal(total_amount or 0):.2f}"
case_title = f"Leverandørfaktura {invoice_number} - {vendor_label}"
case_description = (
"Auto-oprettet fra leverandørfaktura\n"
f"Faktura: {invoice_number}\n"
f"Leverandør: {vendor_label}\n"
f"Beløb: {amount_label} {currency_label}\n"
f"Invoice ID: {invoice_id}\n"
f"Fil ID: {file_id or '-'}"
)
case_row = execute_query_single(
"""
INSERT INTO sag_sager (titel, beskrivelse, type, status, customer_id)
VALUES (%s, %s, %s, %s, %s)
RETURNING id
""",
(case_title, case_description, _PURCHASE_CASE_TYPE, "åben", customer_id)
)
if not case_row:
return None
sag_id = int(case_row["id"])
execute_update(
"UPDATE supplier_invoices SET sag_id = %s WHERE id = %s",
(sag_id, invoice_id)
)
try:
execute_update(
"""
INSERT INTO sag_kommentarer (sag_id, forfatter, indhold, er_system_besked)
VALUES (%s, %s, %s, %s)
""",
(
sag_id,
"Invoice Bot",
(
"🔗 Automatisk oprettet fra leverandørfaktura\n"
f"Faktura: {invoice_number}\n"
f"Leverandør: {vendor_label}\n"
f"Beløb: {amount_label} {currency_label}\n"
f"Invoice ID: {invoice_id}"
),
True,
),
)
except Exception as comment_error:
logger.warning("⚠️ Could not create case comment for supplier invoice %s: %s", invoice_id, comment_error)
logger.info("✅ Linked supplier invoice %s to SAG-%s", invoice_id, sag_id)
return sag_id
def _smart_extract_lines(text: str) -> List[Dict]: def _smart_extract_lines(text: str) -> List[Dict]:
""" """
@ -957,7 +1072,7 @@ async def create_invoice_from_extraction(file_id: int):
if not extraction: if not extraction:
raise HTTPException(status_code=404, detail="Ingen extraction fundet for denne fil") raise HTTPException(status_code=404, detail="Ingen extraction fundet for denne fil")
extraction_data = extraction[0] extraction_data = extraction
# Check if vendor is matched # Check if vendor is matched
if not extraction_data['vendor_matched_id']: if not extraction_data['vendor_matched_id']:
@ -975,7 +1090,7 @@ async def create_invoice_from_extraction(file_id: int):
raise HTTPException(status_code=400, detail="Faktura er allerede oprettet fra denne extraction") raise HTTPException(status_code=400, detail="Faktura er allerede oprettet fra denne extraction")
# Get extraction lines # Get extraction lines
lines = execute_query_single( lines = execute_query(
"""SELECT * FROM extraction_lines """SELECT * FROM extraction_lines
WHERE extraction_id = %s WHERE extraction_id = %s
ORDER BY line_number""", ORDER BY line_number""",
@ -1037,6 +1152,15 @@ async def create_invoice_from_extraction(file_id: int):
) )
) )
sag_id = _ensure_case_for_supplier_invoice(
invoice_id=invoice_id,
invoice_number=invoice_number,
vendor_name=extraction.get("vendor_name"),
total_amount=extraction_data.get("total_amount"),
currency=extraction_data.get("currency"),
file_id=file_id,
)
# Create invoice lines # Create invoice lines
if lines: if lines:
for line in lines: for line in lines:
@ -1067,6 +1191,7 @@ async def create_invoice_from_extraction(file_id: int):
return { return {
"status": "success", "status": "success",
"invoice_id": invoice_id, "invoice_id": invoice_id,
"sag_id": sag_id,
"invoice_number": invoice_number, "invoice_number": invoice_number,
"vendor_name": extraction['vendor_name'], "vendor_name": extraction['vendor_name'],
"total_amount": extraction['total_amount'], "total_amount": extraction['total_amount'],
@ -3089,6 +3214,15 @@ async def create_invoice_from_file(file_id: int, vendor_id: int) -> int:
) )
) )
_ensure_case_for_supplier_invoice(
invoice_id=invoice_id,
invoice_number=f"PENDING-{file_id}",
vendor_name=None,
total_amount=0,
currency="DKK",
file_id=file_id,
)
logger.info(f"✅ Created minimal invoice {invoice_id} for file {file_id}, vendor {vendor_id}") logger.info(f"✅ Created minimal invoice {invoice_id} for file {file_id}, vendor {vendor_id}")
return invoice_id return invoice_id

View File

@ -2240,6 +2240,73 @@ async def get_case_calendar_events(sag_id: int, include_children: bool = True):
# VAREKØB & SALG - CRUD (Case-linked sale items) # VAREKØB & SALG - CRUD (Case-linked sale items)
# ============================================================================ # ============================================================================
_PURCHASE_PURPOSE_VALUES = {
"salg",
"lager",
"asset",
"intern_brug",
"retur_reklamation",
"projekt_omkostning",
}
def _normalize_purchase_purpose(value: Optional[object], item_type: str) -> Optional[str]:
if item_type != "purchase":
return None
if value is None:
return None
normalized = str(value).strip().lower()
if not normalized:
return None
if normalized not in _PURCHASE_PURPOSE_VALUES:
allowed = ", ".join(sorted(_PURCHASE_PURPOSE_VALUES))
raise HTTPException(status_code=400, detail=f"purchase_purpose must be one of: {allowed}")
return normalized
def _resolve_purchase_traceability(
supplier_invoice_id_value: Optional[object],
supplier_invoice_line_id_value: Optional[object],
) -> tuple[Optional[int], Optional[int]]:
supplier_invoice_id = _coerce_optional_int(supplier_invoice_id_value, "supplier_invoice_id")
supplier_invoice_line_id = _coerce_optional_int(supplier_invoice_line_id_value, "supplier_invoice_line_id")
if supplier_invoice_line_id is not None and supplier_invoice_id is None:
line_row = execute_query_single(
"SELECT supplier_invoice_id FROM supplier_invoice_lines WHERE id = %s",
(supplier_invoice_line_id,)
)
if not line_row:
raise HTTPException(status_code=400, detail="Invalid supplier_invoice_line_id")
supplier_invoice_id = int(line_row["supplier_invoice_id"])
if supplier_invoice_id is not None:
invoice_exists = execute_query_single(
"SELECT id FROM supplier_invoices WHERE id = %s",
(supplier_invoice_id,)
)
if not invoice_exists:
raise HTTPException(status_code=400, detail="Invalid supplier_invoice_id")
if supplier_invoice_line_id is not None:
line_exists = execute_query_single(
"SELECT id, supplier_invoice_id FROM supplier_invoice_lines WHERE id = %s",
(supplier_invoice_line_id,)
)
if not line_exists:
raise HTTPException(status_code=400, detail="Invalid supplier_invoice_line_id")
if supplier_invoice_id is not None and int(line_exists["supplier_invoice_id"]) != supplier_invoice_id:
raise HTTPException(
status_code=400,
detail="supplier_invoice_line_id does not belong to supplier_invoice_id",
)
return supplier_invoice_id, supplier_invoice_line_id
@router.get("/sag/{sag_id}/sale-items") @router.get("/sag/{sag_id}/sale-items")
async def list_sale_items(sag_id: int): async def list_sale_items(sag_id: int):
"""List sale items for a case.""" """List sale items for a case."""
@ -2290,6 +2357,47 @@ async def create_sale_item(sag_id: int, data: dict):
if status not in ("draft", "confirmed", "cancelled"): if status not in ("draft", "confirmed", "cancelled"):
raise HTTPException(status_code=400, detail="status must be draft, confirmed, or cancelled") raise HTTPException(status_code=400, detail="status must be draft, confirmed, or cancelled")
has_purchase_columns = table_has_column("sag_salgsvarer", "purchase_purpose")
purchase_purpose = None
supplier_invoice_id = None
supplier_invoice_line_id = None
if has_purchase_columns:
purchase_purpose = _normalize_purchase_purpose(data.get("purchase_purpose"), item_type)
supplier_invoice_id, supplier_invoice_line_id = _resolve_purchase_traceability(
data.get("supplier_invoice_id"),
data.get("supplier_invoice_line_id"),
)
if item_type != "purchase":
supplier_invoice_id = None
supplier_invoice_line_id = None
if has_purchase_columns:
query = """
INSERT INTO sag_salgsvarer
(sag_id, type, description, quantity, unit, unit_price, amount, currency, status, line_date, external_ref, product_id,
purchase_purpose, supplier_invoice_id, supplier_invoice_line_id)
VALUES
(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING *
"""
params = (
sag_id,
item_type,
description,
data.get("quantity"),
data.get("unit"),
data.get("unit_price"),
amount,
data.get("currency", "DKK"),
status,
data.get("line_date"),
data.get("external_ref"),
data.get("product_id"),
purchase_purpose,
supplier_invoice_id,
supplier_invoice_line_id,
)
else:
query = """ query = """
INSERT INTO sag_salgsvarer INSERT INTO sag_salgsvarer
(sag_id, type, description, quantity, unit, unit_price, amount, currency, status, line_date, external_ref, product_id) (sag_id, type, description, quantity, unit, unit_price, amount, currency, status, line_date, external_ref, product_id)
@ -2349,7 +2457,7 @@ async def update_sale_item(sag_id: int, item_id: int, updates: dict):
"""Update a sale item for a case.""" """Update a sale item for a case."""
try: try:
check = execute_query( check = execute_query(
"SELECT id FROM sag_salgsvarer WHERE id = %s AND sag_id = %s", "SELECT id, type FROM sag_salgsvarer WHERE id = %s AND sag_id = %s",
(item_id, sag_id) (item_id, sag_id)
) )
if not check: if not check:
@ -2367,10 +2475,18 @@ async def update_sale_item(sag_id: int, item_id: int, updates: dict):
"line_date", "line_date",
"external_ref", "external_ref",
"product_id", "product_id",
"purchase_purpose",
"supplier_invoice_id",
"supplier_invoice_line_id",
] ]
set_clauses = [] set_clauses = []
params = [] params = []
has_purchase_columns = table_has_column("sag_salgsvarer", "purchase_purpose")
current_type = (check[0].get("type") or "sale").lower()
next_type = current_type
if "type" in updates:
next_type = (updates.get("type") or "").lower()
for field in allowed_fields: for field in allowed_fields:
if field in updates: if field in updates:
@ -2386,10 +2502,56 @@ async def update_sale_item(sag_id: int, item_id: int, updates: dict):
raise HTTPException(status_code=400, detail="status must be draft, confirmed, or cancelled") raise HTTPException(status_code=400, detail="status must be draft, confirmed, or cancelled")
set_clauses.append("status = %s") set_clauses.append("status = %s")
params.append(value) params.append(value)
elif field == "purchase_purpose":
if not has_purchase_columns:
continue
value = _normalize_purchase_purpose(updates.get(field), next_type)
set_clauses.append("purchase_purpose = %s")
params.append(value)
elif field in ("supplier_invoice_id", "supplier_invoice_line_id"):
# handled together below to keep consistency between IDs
continue
else: else:
set_clauses.append(f"{field} = %s") set_clauses.append(f"{field} = %s")
params.append(updates[field]) params.append(updates[field])
if has_purchase_columns and (
"supplier_invoice_id" in updates
or "supplier_invoice_line_id" in updates
or next_type != current_type
):
raw_supplier_invoice_id = updates.get("supplier_invoice_id") if "supplier_invoice_id" in updates else None
raw_supplier_invoice_line_id = updates.get("supplier_invoice_line_id") if "supplier_invoice_line_id" in updates else None
if "supplier_invoice_id" not in updates or "supplier_invoice_line_id" not in updates:
current_refs = execute_query_single(
"SELECT supplier_invoice_id, supplier_invoice_line_id FROM sag_salgsvarer WHERE id = %s AND sag_id = %s",
(item_id, sag_id)
) or {}
if "supplier_invoice_id" not in updates:
raw_supplier_invoice_id = current_refs.get("supplier_invoice_id")
if "supplier_invoice_line_id" not in updates:
raw_supplier_invoice_line_id = current_refs.get("supplier_invoice_line_id")
supplier_invoice_id, supplier_invoice_line_id = _resolve_purchase_traceability(
raw_supplier_invoice_id,
raw_supplier_invoice_line_id,
)
if next_type != "purchase":
supplier_invoice_id = None
supplier_invoice_line_id = None
set_clauses.append("supplier_invoice_id = %s")
params.append(supplier_invoice_id)
set_clauses.append("supplier_invoice_line_id = %s")
params.append(supplier_invoice_line_id)
if has_purchase_columns and next_type != "purchase":
if "purchase_purpose" not in updates:
set_clauses.append("purchase_purpose = %s")
params.append(None)
if not set_clauses: if not set_clauses:
raise HTTPException(status_code=400, detail="No valid fields to update") raise HTTPException(status_code=400, detail="No valid fields to update")

View File

@ -6024,6 +6024,7 @@
<th>Enhed</th> <th>Enhed</th>
<th>Enhedspris</th> <th>Enhedspris</th>
<th>Linjesum</th> <th>Linjesum</th>
<th>Formål</th>
<th>Kilde-sag</th> <th>Kilde-sag</th>
<th>Status</th> <th>Status</th>
<th class="text-end pe-4">Handlinger</th> <th class="text-end pe-4">Handlinger</th>
@ -6031,7 +6032,7 @@
</thead> </thead>
<tbody id="saleItemsPurchaseBody"> <tbody id="saleItemsPurchaseBody">
<tr> <tr>
<td colspan="9" class="text-center py-4 text-muted">Indlæser indkøbslinjer...</td> <td colspan="10" class="text-center py-4 text-muted">Indlæser indkøbslinjer...</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -7282,6 +7283,20 @@
let saleItemsCache = []; let saleItemsCache = [];
const purchasePurposeLabels = {
salg: 'Salg',
lager: 'Lager',
asset: 'Asset',
intern_brug: 'Intern brug',
retur_reklamation: 'Retur/reklamation',
projekt_omkostning: 'Projektomkostning'
};
function purchasePurposeLabel(value) {
if (!value) return '-';
return purchasePurposeLabels[value] || value;
}
async function loadVarekobSalg() { async function loadVarekobSalg() {
try { try {
const res = await fetch(`/api/v1/sag/${salesCaseId}/varekob-salg?include_subcases=true`); const res = await fetch(`/api/v1/sag/${salesCaseId}/varekob-salg?include_subcases=true`);
@ -7323,9 +7338,10 @@
const salesItems = items.filter(item => (item.type || '').toLowerCase() !== 'purchase'); const salesItems = items.filter(item => (item.type || '').toLowerCase() !== 'purchase');
const purchaseItems = items.filter(item => (item.type || '').toLowerCase() === 'purchase'); const purchaseItems = items.filter(item => (item.type || '').toLowerCase() === 'purchase');
const renderRows = (list) => { const renderRows = (list, isPurchase) => {
if (!list.length) { if (!list.length) {
return '<tr><td colspan="9" class="text-center py-4 text-muted">Ingen linjer</td></tr>'; const columns = isPurchase ? 10 : 9;
return `<tr><td colspan="${columns}" class="text-center py-4 text-muted">Ingen linjer</td></tr>`;
} }
return list.map(item => { return list.map(item => {
@ -7334,6 +7350,9 @@
const sourceBadge = isSubcase const sourceBadge = isSubcase
? `<span class="badge bg-warning text-dark ms-2">Under-sag</span>` ? `<span class="badge bg-warning text-dark ms-2">Under-sag</span>`
: `<span class="badge bg-light text-dark border ms-2">Denne sag</span>`; : `<span class="badge bg-light text-dark border ms-2">Denne sag</span>`;
const purposeCell = isPurchase
? `<td><span class="badge bg-info-subtle text-dark border">${purchasePurposeLabel(item.purchase_purpose)}</span></td>`
: '';
return ` return `
<tr> <tr>
<td class="ps-4">${item.line_date || '-'}</td> <td class="ps-4">${item.line_date || '-'}</td>
@ -7342,6 +7361,7 @@
<td>${item.unit || '-'}</td> <td>${item.unit || '-'}</td>
<td>${item.unit_price != null ? formatCurrency(item.unit_price) : '-'}</td> <td>${item.unit_price != null ? formatCurrency(item.unit_price) : '-'}</td>
<td class="fw-bold">${formatCurrency(item.amount)}</td> <td class="fw-bold">${formatCurrency(item.amount)}</td>
${purposeCell}
<td>${item.source_sag_titel || '-'}${sourceBadge}</td> <td>${item.source_sag_titel || '-'}${sourceBadge}</td>
<td><span class="badge bg-light text-dark border">${statusLabel}</span></td> <td><span class="badge bg-light text-dark border">${statusLabel}</span></td>
<td class="text-end pe-4"> <td class="text-end pe-4">
@ -7355,8 +7375,8 @@
}).join(''); }).join('');
}; };
salesBody.innerHTML = renderRows(salesItems); salesBody.innerHTML = renderRows(salesItems, false);
purchaseBody.innerHTML = renderRows(purchaseItems); purchaseBody.innerHTML = renderRows(purchaseItems, true);
const salesSum = salesItems.reduce((sum, item) => sum + Number(item.amount || 0), 0); const salesSum = salesItems.reduce((sum, item) => sum + Number(item.amount || 0), 0);
const purchaseSum = purchaseItems.reduce((sum, item) => sum + Number(item.amount || 0), 0); const purchaseSum = purchaseItems.reduce((sum, item) => sum + Number(item.amount || 0), 0);
@ -7400,10 +7420,30 @@
document.getElementById('sale_amount').value = item?.amount ?? ''; document.getElementById('sale_amount').value = item?.amount ?? '';
document.getElementById('sale_currency').value = item?.currency || 'DKK'; document.getElementById('sale_currency').value = item?.currency || 'DKK';
document.getElementById('sale_external_ref').value = item?.external_ref || ''; document.getElementById('sale_external_ref').value = item?.external_ref || '';
document.getElementById('sale_purchase_purpose').value = item?.purchase_purpose || '';
document.getElementById('sale_supplier_invoice_id').value = item?.supplier_invoice_id || '';
document.getElementById('sale_supplier_invoice_line_id').value = item?.supplier_invoice_line_id || '';
togglePurchaseFields();
new bootstrap.Modal(document.getElementById('saleItemModal')).show(); new bootstrap.Modal(document.getElementById('saleItemModal')).show();
} }
function togglePurchaseFields() {
const type = document.getElementById('sale_type').value;
const purchaseFields = document.getElementById('sale_purchase_fields');
if (!purchaseFields) return;
if (type === 'purchase') {
purchaseFields.classList.remove('d-none');
} else {
purchaseFields.classList.add('d-none');
document.getElementById('sale_purchase_purpose').value = '';
document.getElementById('sale_supplier_invoice_id').value = '';
document.getElementById('sale_supplier_invoice_line_id').value = '';
}
}
function openSaleItemModalById(itemId) { function openSaleItemModalById(itemId) {
const item = saleItemsCache.find((entry) => entry.id === itemId); const item = saleItemsCache.find((entry) => entry.id === itemId);
openSaleItemModal(item || null); openSaleItemModal(item || null);
@ -7429,9 +7469,18 @@
unit_price: document.getElementById('sale_unit_price').value || null, unit_price: document.getElementById('sale_unit_price').value || null,
amount: document.getElementById('sale_amount').value, amount: document.getElementById('sale_amount').value,
currency: document.getElementById('sale_currency').value || 'DKK', currency: document.getElementById('sale_currency').value || 'DKK',
external_ref: document.getElementById('sale_external_ref').value || null external_ref: document.getElementById('sale_external_ref').value || null,
purchase_purpose: document.getElementById('sale_purchase_purpose').value || null,
supplier_invoice_id: document.getElementById('sale_supplier_invoice_id').value || null,
supplier_invoice_line_id: document.getElementById('sale_supplier_invoice_line_id').value || null
}; };
if (payload.type !== 'purchase') {
payload.purchase_purpose = null;
payload.supplier_invoice_id = null;
payload.supplier_invoice_line_id = null;
}
if (!payload.description || !payload.amount) { if (!payload.description || !payload.amount) {
alert('Beskrivelse og linjesum er påkrævet.'); alert('Beskrivelse og linjesum er påkrævet.');
return; return;
@ -7470,8 +7519,10 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const qtyInput = document.getElementById('sale_quantity'); const qtyInput = document.getElementById('sale_quantity');
const priceInput = document.getElementById('sale_unit_price'); const priceInput = document.getElementById('sale_unit_price');
const typeInput = document.getElementById('sale_type');
if (qtyInput) qtyInput.addEventListener('input', updateSaleAmount); if (qtyInput) qtyInput.addEventListener('input', updateSaleAmount);
if (priceInput) priceInput.addEventListener('input', updateSaleAmount); if (priceInput) priceInput.addEventListener('input', updateSaleAmount);
if (typeInput) typeInput.addEventListener('change', togglePurchaseFields);
loadVarekobSalg(); loadVarekobSalg();
}); });
</script> </script>
@ -8769,6 +8820,30 @@
<label class="form-label">Reference</label> <label class="form-label">Reference</label>
<input type="text" class="form-control" id="sale_external_ref" placeholder="Valgfri reference"> <input type="text" class="form-control" id="sale_external_ref" placeholder="Valgfri reference">
</div> </div>
<div class="col-12 d-none" id="sale_purchase_fields">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Indkøbsformål</label>
<select class="form-select" id="sale_purchase_purpose">
<option value="">Vælg formål</option>
<option value="salg">Salg</option>
<option value="lager">Lager</option>
<option value="asset">Asset</option>
<option value="intern_brug">Intern brug</option>
<option value="retur_reklamation">Retur/reklamation</option>
<option value="projekt_omkostning">Projektomkostning</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Leverandørfaktura ID</label>
<input type="number" class="form-control" id="sale_supplier_invoice_id" min="1" step="1" placeholder="Fx 1254">
</div>
<div class="col-md-4">
<label class="form-label">Fakturalinje ID</label>
<input type="number" class="form-control" id="sale_supplier_invoice_line_id" min="1" step="1" placeholder="Fx 8921">
</div>
</div>
</div>
</div> </div>
</form> </form>
</div> </div>

View File

@ -0,0 +1,47 @@
-- Migration 169: Supplier invoice -> case traceability and purchase line classification
-- Created: 2026-04-12
-- Link supplier invoices to cases for procurement workflow
ALTER TABLE supplier_invoices
ADD COLUMN IF NOT EXISTS sag_id INTEGER REFERENCES sag_sager(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_supplier_invoices_sag_id
ON supplier_invoices(sag_id);
-- Add explicit purchase line classification + invoice traceability on case purchase lines
ALTER TABLE sag_salgsvarer
ADD COLUMN IF NOT EXISTS purchase_purpose VARCHAR(50),
ADD COLUMN IF NOT EXISTS supplier_invoice_id INTEGER REFERENCES supplier_invoices(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS supplier_invoice_line_id INTEGER REFERENCES supplier_invoice_lines(id) ON DELETE SET NULL;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'chk_sag_salgsvarer_purchase_purpose'
) THEN
ALTER TABLE sag_salgsvarer
ADD CONSTRAINT chk_sag_salgsvarer_purchase_purpose
CHECK (
purchase_purpose IS NULL
OR purchase_purpose IN (
'salg',
'lager',
'asset',
'intern_brug',
'retur_reklamation',
'projekt_omkostning'
)
);
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_sag_salgsvarer_purchase_purpose
ON sag_salgsvarer(purchase_purpose);
CREATE INDEX IF NOT EXISTS idx_sag_salgsvarer_supplier_invoice_id
ON sag_salgsvarer(supplier_invoice_id);
CREATE INDEX IF NOT EXISTS idx_sag_salgsvarer_supplier_invoice_line_id
ON sag_salgsvarer(supplier_invoice_line_id);