Files

153 lines
5.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""采购业务服务:确认订单 / 进货单过账(写库存 + 生成应付)。"""
from __future__ import annotations
from datetime import date
from decimal import Decimal
from django.db import transaction
from apps.inventory import services as inv_services
from apps.finance import services as fin_services
from apps.core.services import (
compute_tax,
parse_price,
resolve_line_quantity,
resolve_tax_rate,
)
from .models import PurchaseOrder, PurchaseOrderLine, PurchaseBill, PurchaseBillLine
def _resolve_line(product, ln):
"""解析采购行:换算基本单位数量 + 录入单位/数量;价格按录入单位。
返回 (qty, price, src_unit, src_qty, amount, tax_rate, tax_amount)(D4 加税)。
"""
parsed = resolve_line_quantity(product, ln)
qty = parsed["quantity"]
src_unit = parsed["source_unit"]
src_qty = parsed["source_quantity"]
# 单价校验(与销售侧同口径):必须 > 0,否则拒绝
# (此前直接 Decimal(str(...)) 未校验,0 价只能靠下游"金额>0"兜住,
# 错误码不精确;0 成本入库还会污染加权平均成本)
price = parse_price(product, ln.get("unit_price"))
if src_unit is not None:
from apps.catalog.models import UnitConversion
conv = UnitConversion.objects.filter(product=product, unit=src_unit).first()
rate = conv.rate if conv else None
amount = src_qty * price
else:
rate = None
amount = qty * price
tax_rate = resolve_tax_rate(product, ln)
tax = compute_tax(amount, tax_rate)
return qty, price, src_unit, src_qty, amount, tax_rate, tax["tax"]
def _generate_bill_no(tenant, prefix: str) -> str:
"""取候选单号(唯一性由 create_with_unique_bill_no 的重试保证)。"""
Model = PurchaseOrder if prefix == "PO" else PurchaseBill
from apps.core.services import next_bill_no
return next_bill_no(tenant, prefix, Model)
@transaction.atomic
def create_purchase_bill(
*, tenant, supplier, warehouse, bill_date=None, remark="", lines=None,
order=None,
) -> PurchaseBill:
"""新建一张进货单(草稿态)。lines: list of dict(product, quantity, unit_price)。"""
bill_date = bill_date or date.today()
from apps.core.services import create_with_unique_bill_no
bill = create_with_unique_bill_no(
PurchaseBill,
tenant=tenant,
prefix="PB",
defaults=dict(
supplier=supplier,
warehouse=warehouse,
order=order,
bill_date=bill_date,
total_amount=Decimal("0"),
state="draft",
remark=remark,
),
)
total = Decimal("0")
for ln in lines or []:
product = ln["product"]
qty, price, src_unit, src_qty, amount, tax_rate, tax_amount = _resolve_line(product, ln)
PurchaseBillLine.objects.create(
tenant=tenant,
bill=bill,
product=product,
quantity=qty,
unit_price=price,
amount=amount,
batch_no=str(ln.get("batch_no") or ""),
production_date=ln.get("production_date"),
expiry_date=ln.get("expiry_date"),
source_unit=src_unit,
source_quantity=src_qty,
tax_rate=tax_rate,
tax_amount=tax_amount,
)
total += amount
if total <= 0:
raise ValueError(f"进货单金额必须大于 0(行合计 {total});请检查数量与单价")
bill.total_amount = total
bill.save(update_fields=["total_amount", "updated_at"])
return bill
@transaction.atomic
def confirm_purchase_bill(bill: PurchaseBill, *, user=None) -> PurchaseBill:
"""进货单过账:写库存 + 生成应付单。
必须状态是 draft;过账后变 confirmed。
"""
if bill.state != "draft":
raise ValueError(f"purchase bill {bill.bill_no} not in draft state")
# 1. 写库存
for line in bill.lines.select_related("product").all():
inv_services.inbound(
tenant=bill.tenant,
warehouse=bill.warehouse,
product=line.product,
quantity=line.quantity,
unit_cost=line.unit_price,
source_type="purchase",
source_ref=bill.bill_no,
batch_no=line.batch_no,
production_date=line.production_date,
expiry_date=line.expiry_date,
)
# 2. 生成应付(D4:把行税额合计传给凭证做进项税拆分)
from django.db.models import Sum as _Sum
tax_total = bill.lines.aggregate(t=_Sum("tax_amount"))["t"] or Decimal("0")
fin_services.create_payable_from_purchase(
tenant=bill.tenant,
supplier=bill.supplier,
total_amount=bill.total_amount,
source_ref=bill.bill_no,
bill_date=bill.bill_date,
tax_amount=tax_total,
)
# 3. 改状态
bill.state = "confirmed"
bill.save(update_fields=["state", "updated_at"])
from apps.core import audit as audit_log
audit_log.log_bill_posted(
tenant=bill.tenant, bill=bill, user=user,
extra={"lines": bill.lines.count()},
)
return bill