from __future__ import annotations from decimal import Decimal from typing import Optional from django.utils import timezone from ..models import ( PickupOrder, PriceList, PriceListItem, WeighTicket, ) from billing.models import Invoice, InvoiceLine, Payout def _get_price(price_list: PriceList, material_id: int, unit: str, direction: str) -> Optional[Decimal]: item = ( PriceListItem.objects.filter( price_list=price_list, material_id=material_id, unit=unit, direction=direction ) .order_by("id") .first() ) return item.unit_price if item else None def generate_invoice_for_pickup(pickup: PickupOrder) -> Invoice: if not pickup.weigh_tickets.exists(): raise ValueError("Pickup has no weigh tickets") # If invoice already exists and not void, return it (idempotent) existing = pickup.invoices.filter(status__in=[Invoice.STATUS_DRAFT, Invoice.STATUS_ISSUED, Invoice.STATUS_PAID]).first() if existing: return existing price_list = pickup.customer.price_list if not price_list: # Fallback to any org pricelist price_list = PriceList.objects.filter(organization=pickup.organization).order_by("id").first() if not price_list: raise ValueError("No price list found for customer or organization") inv = Invoice.objects.create( organization=pickup.organization, customer=pickup.customer, pickup=pickup, currency_code=price_list.currency_code, status=Invoice.STATUS_DRAFT, ) total = Decimal("0.00") payout_total = Decimal("0.00") for t in pickup.weigh_tickets.all(): for line in t.lines.all(): sell_price = _get_price(price_list, line.material_id, line.unit, PriceListItem.DIRECTION_SELL) buy_price = _get_price(price_list, line.material_id, line.unit, PriceListItem.DIRECTION_BUY) if sell_price is not None and sell_price > 0: line_total = (Decimal(line.quantity) * Decimal(sell_price)).quantize(Decimal("0.01")) InvoiceLine.objects.create( invoice=inv, description=f"{line.material.name} ({line.unit})", material=line.material, quantity=line.quantity, unit=line.unit, unit_price=sell_price, line_total=line_total, ) total += line_total if buy_price is not None and buy_price > 0: payout_total += (Decimal(line.quantity) * Decimal(buy_price)).quantize(Decimal("0.01")) inv.total_amount = total inv.issued_at = timezone.now() inv.status = Invoice.STATUS_ISSUED if total > 0 else Invoice.STATUS_DRAFT inv.due_at = inv.issued_at + timezone.timedelta(days=14) inv.save() if payout_total > 0: Payout.objects.create( organization=pickup.organization, customer=pickup.customer, pickup=pickup, amount=payout_total, currency_code=price_list.currency_code, paid_at=timezone.now(), reference=f"PAYOUT-{pickup.id}", ) # Update pickup status pickup.status = PickupOrder.STATUS_INVOICED pickup.save(update_fields=["status"]) return inv def regenerate_invoice_for_pickup(pickup: PickupOrder) -> Invoice: existing = pickup.invoices.filter(status__in=[Invoice.STATUS_DRAFT, Invoice.STATUS_ISSUED, Invoice.STATUS_PAID]).first() if not existing: return generate_invoice_for_pickup(pickup) if existing.status != Invoice.STATUS_DRAFT: raise ValueError("Invoice not in draft status; cannot regenerate") price_list = pickup.customer.price_list if not price_list: price_list = PriceList.objects.filter(organization=pickup.organization).order_by("id").first() if not price_list: raise ValueError("No price list found for customer or organization") # Clear lines and rebuild InvoiceLine.objects.filter(invoice=existing).delete() total = Decimal("0.00") payout_total = Decimal("0.00") for t in pickup.weigh_tickets.all(): for line in t.lines.all(): sell_price = _get_price(price_list, line.material_id, line.unit, PriceListItem.DIRECTION_SELL) buy_price = _get_price(price_list, line.material_id, line.unit, PriceListItem.DIRECTION_BUY) if sell_price is not None and sell_price > 0: line_total = (Decimal(line.quantity) * Decimal(sell_price)).quantize(Decimal("0.01")) InvoiceLine.objects.create( invoice=existing, description=f"{line.material.name} ({line.unit})", material=line.material, quantity=line.quantity, unit=line.unit, unit_price=sell_price, line_total=line_total, ) total += line_total if buy_price is not None and buy_price > 0: payout_total += (Decimal(line.quantity) * Decimal(buy_price)).quantize(Decimal("0.01")) existing.total_amount = total # Keep draft; do not auto issue if not existing.currency_code: existing.currency_code = price_list.currency_code existing.save(update_fields=["total_amount", "currency_code", "updated_at"]) return existing