baseline: 批次A-D 成果 + membership 半成品(测试红)
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
"""采购业务服务:确认订单 / 进货单过账(写库存 + 生成应付)。"""
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user