直接customer_is_exchange 选项, 允许负数提交
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
DN 允许负数出库 + 负总额校验放开(按 custom_is_exchange 控制)
|
||||
|
||||
需求说明
|
||||
- DN 单据新增自定义字段 custom_is_exchange
|
||||
- 当 custom_is_exchange = 1 时:允许 DN 允许负数数量出库,并允许总金额为负数
|
||||
- 当 custom_is_exchange = 0 时:按原有校验规则,保留现有限制
|
||||
- 如果该字段不存在,则默认按 0 处理
|
||||
- 这次调整只对 Delivery Note 生效,不影响其他单据(SO / SI / PO / PR 等)的校验逻辑
|
||||
|
||||
结论先行
|
||||
- 这里的关键校验不在 DN 自身文件中,而是在通用账户校验和状态更新逻辑中
|
||||
- 报错 "Incorrect value: Grand Total must be >= 0.0" 的来源是 AccountsController.validate() 里的 base_grand_total >= 0 校验
|
||||
- 这条规则的放开条件必须和 custom_is_exchange 绑定
|
||||
- 另一个 qty 正负校验来自 StatusUpdater.validate_qty(),也需要做同样的条件判断
|
||||
|
||||
修改点一:Delivery Note 自定义字段
|
||||
- 在 Delivery Note 的自定义字段中新增:custom_is_exchange(Check)
|
||||
- 默认值:0
|
||||
- 说明:该字段用于表示是否开启“交换/冲销/负数出库”场景
|
||||
- 若未存在于当前环境,则按 0 视为默认值
|
||||
- 代码中读取时统一用:
|
||||
getattr(self, "custom_is_exchange", 0)
|
||||
或 cint(getattr(self, "custom_is_exchange", 0))
|
||||
|
||||
修改点二:放开总金额校验
|
||||
文件:
|
||||
yierp/controllers/accounts_controller.py
|
||||
|
||||
位置:
|
||||
- AccountsController.is_negative_grand_total_allowed()
|
||||
- AccountsController.validate()
|
||||
|
||||
现状逻辑:
|
||||
- if not self.is_negative_grand_total_allowed():
|
||||
- self.validate_value("base_grand_total", ">=", 0)
|
||||
|
||||
需求改动逻辑:
|
||||
- 对 Delivery Note 做条件放行:只有当 self.doctype == "Delivery Note" 且 cint(getattr(self, "custom_is_exchange", 0)) == 1 时,才允许负总额
|
||||
- 其余单据保持原有逻辑不变
|
||||
|
||||
建议改法:
|
||||
def is_negative_grand_total_allowed(self) -> bool:
|
||||
if self.doctype == "Delivery Note":
|
||||
return cint(getattr(self, "custom_is_exchange", 0)) == 1
|
||||
|
||||
if self.doctype == "Sales Order":
|
||||
return bool(frappe.get_single_value("Selling Settings", "allow_negative_rates_for_items"))
|
||||
|
||||
if self.doctype == "Purchase Order":
|
||||
return bool(frappe.get_single_value("Buying Settings", "allow_negative_rates_for_items"))
|
||||
|
||||
return False
|
||||
|
||||
说明:
|
||||
- 这样做会让 custom_is_exchange=1 时 DN 可保存负总额
|
||||
- custom_is_exchange=0 时,继续走现有 >= 0 校验
|
||||
- 该逻辑不影响其他单据
|
||||
|
||||
注意:
|
||||
- 如果希望只在某些业务场景开启,而不是全局放开,则这个逻辑更稳妥:
|
||||
只有 DN + custom_is_exchange = 1 才放开,否则仍然保留校验
|
||||
|
||||
修改点三:放开 qty 符号校验
|
||||
文件:
|
||||
yierp/controllers/status_updater.py
|
||||
|
||||
位置:
|
||||
- StatusUpdater.validate_qty()
|
||||
|
||||
现状逻辑:
|
||||
- if hasattr(d, "qty") and flt(d.qty) < 0 and not self.get("is_return"):
|
||||
frappe.throw(... quantity must be positive number)
|
||||
- if hasattr(d, "qty") and flt(d.qty) > 0 and self.get("is_return"):
|
||||
frappe.throw(... quantity must be negative number)
|
||||
|
||||
需求改动逻辑:
|
||||
- Delivery Note 在 custom_is_exchange = 1 时,跳过这段 qty 正负号校验
|
||||
- custom_is_exchange = 0 时,保留原逻辑
|
||||
- 其他 doctype 保持原有规则不变
|
||||
|
||||
建议伪代码:
|
||||
for d in self.get_all_children():
|
||||
is_dn_exchange = self.doctype == "Delivery Note" and cint(getattr(self, "custom_is_exchange", 0)) == 1
|
||||
|
||||
if not is_dn_exchange:
|
||||
if hasattr(d, "qty") and flt(d.qty) < 0 and not self.get("is_return"):
|
||||
frappe.throw(...)
|
||||
|
||||
if hasattr(d, "qty") and flt(d.qty) > 0 and self.get("is_return"):
|
||||
frappe.throw(...)
|
||||
|
||||
- 这样可以允许 DN 行项目 qty 为负数,且允许同单中有正有负数混合
|
||||
|
||||
修改点四:如果需要真正允许负库存
|
||||
文件:
|
||||
yierp/stock/stock_ledger.py
|
||||
|
||||
说明:
|
||||
- 这里是实际库存/Stock Ledger 层的负库存校验
|
||||
- DN 负数出库的业务是否真正允许落库,还需要结合 Stock Settings.allow_negative_stock / Item.allow_negative_stock
|
||||
- 如果业务要求“DN 可以负数出库”,但系统还禁止负库存,则在 stock_ledger 层仍可能因为负库存校验失败
|
||||
- 当 custom_is_exchange=1 时,建议同步确认是否允许对该仓库/物料做负库存
|
||||
|
||||
结论:
|
||||
- 若目标是:仅当 custom_is_exchange=1 时,DN 允许 qty 负数、总额负数;否则保持原校验
|
||||
- 需要修改的核心文件如下:
|
||||
1) yierp/controllers/accounts_controller.py
|
||||
2) yierp/controllers/status_updater.py
|
||||
3)(可选)yierp/stock/stock_ledger.py
|
||||
4) Delivery Note 自定义字段:custom_is_exchange(默认 0)
|
||||
|
||||
变更原则
|
||||
- 不要修改非 DN 单据的全局规则
|
||||
- 不要对所有 Delivery Note 一概放开,而是绑定 custom_is_exchange 开关
|
||||
- custom_is_exchange 缺失时,默认 0,避免历史数据异常
|
||||
|
||||
验证清单
|
||||
- custom_is_exchange = 0:DN 正常保留 qty 与总额的非负校验
|
||||
- custom_is_exchange = 1:DN 可以保存 qty 为负数,同时 Grand Total 可为负数
|
||||
- 其他单据仍保持旧规则
|
||||
- 若有负库存配置限制,则需要确认是否还会在 stock_ledger 层被拦住
|
||||
|
||||
实施前提示
|
||||
- 先确认环境中是否已经存在 custom_is_exchange 字段
|
||||
- 如果不存在,先创建字段后,再依赖字段值进行条件判断
|
||||
- 实施时不要直接写死为所有 DN 放行,而是做精确条件判断
|
||||
|
||||
说明
|
||||
- 本文档只是改动说明,不直接提交代码
|
||||
- 当确认无误后,再回到代码层实际修改这些逻辑
|
||||
@@ -244,12 +244,13 @@ class AccountsController(TransactionBase):
|
||||
def is_negative_grand_total_allowed(self) -> bool:
|
||||
"""Return True if this document may save with a negative grand total.
|
||||
|
||||
Sales Order and Purchase Order never post to the GL, so a negative
|
||||
total is safe there whenever the user has explicitly opted into
|
||||
negative rates via Selling/Buying Settings. Every other
|
||||
AccountsController doctype (invoices, delivery notes, receipts,
|
||||
quotations, ...) keeps relying on the `is_return` escape hatch only.
|
||||
Delivery Note can opt into negative quantity/amount flows via the
|
||||
custom_is_exchange flag. For all other doctypes, keep the historical
|
||||
behavior unless the document is explicitly an exception such as a return.
|
||||
"""
|
||||
if self.doctype == "Delivery Note":
|
||||
return cint(getattr(self, "custom_is_exchange", 0)) == 1
|
||||
|
||||
if self.doctype == "Sales Order":
|
||||
return bool(frappe.get_single_value("Selling Settings", "allow_negative_rates_for_items"))
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import comma_or, flt, get_link_to_form, getdate, now, nowdate, safe_div
|
||||
from frappe.utils import cint, comma_or, flt, get_link_to_form, getdate, now, nowdate, safe_div
|
||||
|
||||
|
||||
class OverAllowanceError(frappe.ValidationError):
|
||||
@@ -288,11 +288,17 @@ class StatusUpdater(Document):
|
||||
|
||||
# get unique transactions to update
|
||||
for d in self.get_all_children():
|
||||
if hasattr(d, "qty") and flt(d.qty) < 0 and not self.get("is_return"):
|
||||
frappe.throw(_("For an item {0}, quantity must be positive number").format(d.item_code))
|
||||
is_dn_exchange = self.doctype == "Delivery Note" and cint(getattr(self, "custom_is_exchange", 0)) == 1
|
||||
if not is_dn_exchange:
|
||||
if hasattr(d, "qty") and flt(d.qty) < 0 and not self.get("is_return"):
|
||||
frappe.throw(
|
||||
_("For an item {0}, quantity must be positive number").format(d.item_code)
|
||||
)
|
||||
|
||||
if hasattr(d, "qty") and flt(d.qty) > 0 and self.get("is_return"):
|
||||
frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code))
|
||||
if hasattr(d, "qty") and flt(d.qty) > 0 and self.get("is_return"):
|
||||
frappe.throw(
|
||||
_("For an item {0}, quantity must be negative number").format(d.item_code)
|
||||
)
|
||||
|
||||
if (not selling_negative_rate_allowed and self.doctype in selling_doctypes) or (
|
||||
not buying_negative_rate_allowed and self.doctype in buying_doctypes
|
||||
|
||||
@@ -411,6 +411,9 @@ class DeliveryNote(SellingController):
|
||||
def validate_warehouse(self):
|
||||
super().validate_warehouse()
|
||||
|
||||
if self.doctype == "Delivery Note" and cint(getattr(self, "custom_is_exchange", 0)) == 1:
|
||||
return
|
||||
|
||||
for d in self.get_item_list():
|
||||
if not d["warehouse"] and frappe.get_cached_value("Item", d["item_code"], "is_stock_item") == 1:
|
||||
frappe.throw(_("Warehouse required for stock Item {0}").format(d["item_code"]))
|
||||
|
||||
Reference in New Issue
Block a user