| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145 |
- 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
|