Açıklama Yok

billing.py 5.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  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. )
  11. from billing.models import Invoice, InvoiceLine, Payout
  12. def _get_price(price_list: PriceList, material_id: int, unit: str, direction: str) -> Optional[Decimal]:
  13. item = (
  14. PriceListItem.objects.filter(
  15. price_list=price_list, material_id=material_id, unit=unit, direction=direction
  16. )
  17. .order_by("id")
  18. .first()
  19. )
  20. return item.unit_price if item else None
  21. def generate_invoice_for_pickup(pickup: PickupOrder) -> Invoice:
  22. if not pickup.weigh_tickets.exists():
  23. raise ValueError("Pickup has no weigh tickets")
  24. # If invoice already exists and not void, return it (idempotent)
  25. existing = pickup.invoices.filter(status__in=[Invoice.STATUS_DRAFT, Invoice.STATUS_ISSUED, Invoice.STATUS_PAID]).first()
  26. if existing:
  27. return existing
  28. price_list = pickup.customer.price_list
  29. if not price_list:
  30. # Fallback to any org pricelist
  31. price_list = PriceList.objects.filter(organization=pickup.organization).order_by("id").first()
  32. if not price_list:
  33. raise ValueError("No price list found for customer or organization")
  34. inv = Invoice.objects.create(
  35. organization=pickup.organization,
  36. customer=pickup.customer,
  37. pickup=pickup,
  38. currency_code=price_list.currency_code,
  39. status=Invoice.STATUS_DRAFT,
  40. )
  41. total = Decimal("0.00")
  42. payout_total = Decimal("0.00")
  43. for t in pickup.weigh_tickets.all():
  44. for line in t.lines.all():
  45. sell_price = _get_price(price_list, line.material_id, line.unit, PriceListItem.DIRECTION_SELL)
  46. buy_price = _get_price(price_list, line.material_id, line.unit, PriceListItem.DIRECTION_BUY)
  47. if sell_price is not None and sell_price > 0:
  48. line_total = (Decimal(line.quantity) * Decimal(sell_price)).quantize(Decimal("0.01"))
  49. InvoiceLine.objects.create(
  50. invoice=inv,
  51. description=f"{line.material.name} ({line.unit})",
  52. material=line.material,
  53. quantity=line.quantity,
  54. unit=line.unit,
  55. unit_price=sell_price,
  56. line_total=line_total,
  57. )
  58. total += line_total
  59. if buy_price is not None and buy_price > 0:
  60. payout_total += (Decimal(line.quantity) * Decimal(buy_price)).quantize(Decimal("0.01"))
  61. inv.total_amount = total
  62. inv.issued_at = timezone.now()
  63. inv.status = Invoice.STATUS_ISSUED if total > 0 else Invoice.STATUS_DRAFT
  64. inv.due_at = inv.issued_at + timezone.timedelta(days=14)
  65. inv.save()
  66. if payout_total > 0:
  67. Payout.objects.create(
  68. organization=pickup.organization,
  69. customer=pickup.customer,
  70. pickup=pickup,
  71. amount=payout_total,
  72. currency_code=price_list.currency_code,
  73. paid_at=timezone.now(),
  74. reference=f"PAYOUT-{pickup.id}",
  75. )
  76. # Update pickup status
  77. pickup.status = PickupOrder.STATUS_INVOICED
  78. pickup.save(update_fields=["status"])
  79. return inv
  80. def regenerate_invoice_for_pickup(pickup: PickupOrder) -> Invoice:
  81. existing = pickup.invoices.filter(status__in=[Invoice.STATUS_DRAFT, Invoice.STATUS_ISSUED, Invoice.STATUS_PAID]).first()
  82. if not existing:
  83. return generate_invoice_for_pickup(pickup)
  84. if existing.status != Invoice.STATUS_DRAFT:
  85. raise ValueError("Invoice not in draft status; cannot regenerate")
  86. price_list = pickup.customer.price_list
  87. if not price_list:
  88. price_list = PriceList.objects.filter(organization=pickup.organization).order_by("id").first()
  89. if not price_list:
  90. raise ValueError("No price list found for customer or organization")
  91. # Clear lines and rebuild
  92. InvoiceLine.objects.filter(invoice=existing).delete()
  93. total = Decimal("0.00")
  94. payout_total = Decimal("0.00")
  95. for t in pickup.weigh_tickets.all():
  96. for line in t.lines.all():
  97. sell_price = _get_price(price_list, line.material_id, line.unit, PriceListItem.DIRECTION_SELL)
  98. buy_price = _get_price(price_list, line.material_id, line.unit, PriceListItem.DIRECTION_BUY)
  99. if sell_price is not None and sell_price > 0:
  100. line_total = (Decimal(line.quantity) * Decimal(sell_price)).quantize(Decimal("0.01"))
  101. InvoiceLine.objects.create(
  102. invoice=existing,
  103. description=f"{line.material.name} ({line.unit})",
  104. material=line.material,
  105. quantity=line.quantity,
  106. unit=line.unit,
  107. unit_price=sell_price,
  108. line_total=line_total,
  109. )
  110. total += line_total
  111. if buy_price is not None and buy_price > 0:
  112. payout_total += (Decimal(line.quantity) * Decimal(buy_price)).quantize(Decimal("0.01"))
  113. existing.total_amount = total
  114. # Keep draft; do not auto issue
  115. if not existing.currency_code:
  116. existing.currency_code = price_list.currency_code
  117. existing.save(update_fields=["total_amount", "currency_code", "updated_at"])
  118. return existing