Нет описания

billing.py 3.2KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  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 getattr(pickup, "weigh_ticket", None) is None:
  23. raise ValueError("Pickup has no weigh ticket")
  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 line in pickup.weigh_ticket.lines.all():
  44. sell_price = _get_price(price_list, line.material_id, line.unit, PriceListItem.DIRECTION_SELL)
  45. buy_price = _get_price(price_list, line.material_id, line.unit, PriceListItem.DIRECTION_BUY)
  46. if sell_price is not None and sell_price > 0:
  47. line_total = (Decimal(line.quantity) * Decimal(sell_price)).quantize(Decimal("0.01"))
  48. InvoiceLine.objects.create(
  49. invoice=inv,
  50. description=f"{line.material.name} ({line.unit})",
  51. material=line.material,
  52. quantity=line.quantity,
  53. unit=line.unit,
  54. unit_price=sell_price,
  55. line_total=line_total,
  56. )
  57. total += line_total
  58. if buy_price is not None and buy_price > 0:
  59. payout_total += (Decimal(line.quantity) * Decimal(buy_price)).quantize(Decimal("0.01"))
  60. inv.total_amount = total
  61. inv.issued_at = timezone.now()
  62. inv.status = Invoice.STATUS_ISSUED if total > 0 else Invoice.STATUS_DRAFT
  63. inv.due_at = inv.issued_at + timezone.timedelta(days=14)
  64. inv.save()
  65. if payout_total > 0:
  66. Payout.objects.create(
  67. organization=pickup.organization,
  68. customer=pickup.customer,
  69. pickup=pickup,
  70. amount=payout_total,
  71. currency_code=price_list.currency_code,
  72. paid_at=timezone.now(),
  73. reference=f"PAYOUT-{pickup.id}",
  74. )
  75. # Update pickup status
  76. pickup.status = PickupOrder.STATUS_INVOICED
  77. pickup.save(update_fields=["status"])
  78. return inv