Нема описа

billing.py 3.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. from __future__ import annotations
  2. from decimal import Decimal
  3. from typing import Optional
  4. from django.utils import timezone
  5. from ..models import (
  6. PickupOrder,
  7. PriceList,
  8. PriceListItem,
  9. WeighTicket,
  10. Invoice,
  11. InvoiceLine,
  12. Payout,
  13. )
  14. def _get_price(price_list: PriceList, material_id: int, unit: str, direction: str) -> Optional[Decimal]:
  15. item = (
  16. PriceListItem.objects.filter(
  17. price_list=price_list, material_id=material_id, unit=unit, direction=direction
  18. )
  19. .order_by("id")
  20. .first()
  21. )
  22. return item.unit_price if item else None
  23. def generate_invoice_for_pickup(pickup: PickupOrder) -> Invoice:
  24. if getattr(pickup, "weigh_ticket", None) is None:
  25. raise ValueError("Pickup has no weigh ticket")
  26. # If invoice already exists and not void, return it (idempotent)
  27. existing = pickup.invoices.filter(status__in=[Invoice.STATUS_DRAFT, Invoice.STATUS_ISSUED, Invoice.STATUS_PAID]).first()
  28. if existing:
  29. return existing
  30. price_list = pickup.customer.price_list
  31. if not price_list:
  32. # Fallback to any org pricelist
  33. price_list = PriceList.objects.filter(organization=pickup.organization).order_by("id").first()
  34. if not price_list:
  35. raise ValueError("No price list found for customer or organization")
  36. inv = Invoice.objects.create(
  37. organization=pickup.organization,
  38. customer=pickup.customer,
  39. pickup=pickup,
  40. currency_code=price_list.currency_code,
  41. status=Invoice.STATUS_DRAFT,
  42. )
  43. total = Decimal("0.00")
  44. payout_total = Decimal("0.00")
  45. for line in pickup.weigh_ticket.lines.all():
  46. sell_price = _get_price(price_list, line.material_id, line.unit, PriceListItem.DIRECTION_SELL)
  47. buy_price = _get_price(price_list, line.material_id, line.unit, PriceListItem.DIRECTION_BUY)
  48. if sell_price is not None and sell_price > 0:
  49. line_total = (Decimal(line.quantity) * Decimal(sell_price)).quantize(Decimal("0.01"))
  50. InvoiceLine.objects.create(
  51. invoice=inv,
  52. description=f"{line.material.name} ({line.unit})",
  53. material=line.material,
  54. quantity=line.quantity,
  55. unit=line.unit,
  56. unit_price=sell_price,
  57. line_total=line_total,
  58. )
  59. total += line_total
  60. if buy_price is not None and buy_price > 0:
  61. payout_total += (Decimal(line.quantity) * Decimal(buy_price)).quantize(Decimal("0.01"))
  62. inv.total_amount = total
  63. inv.issued_at = timezone.now()
  64. inv.status = Invoice.STATUS_ISSUED if total > 0 else Invoice.STATUS_DRAFT
  65. inv.due_at = inv.issued_at + timezone.timedelta(days=14)
  66. inv.save()
  67. if payout_total > 0:
  68. Payout.objects.create(
  69. organization=pickup.organization,
  70. customer=pickup.customer,
  71. pickup=pickup,
  72. amount=payout_total,
  73. currency_code=price_list.currency_code,
  74. paid_at=timezone.now(),
  75. reference=f"PAYOUT-{pickup.id}",
  76. )
  77. # Update pickup status
  78. pickup.status = PickupOrder.STATUS_INVOICED
  79. pickup.save(update_fields=["status"])
  80. return inv