- models/*.json: 在supply_contract_items, distribution_agreement_items, supplychain_accounting的codes段添加products/product_types引用 - scripts/load_path.py: 模块RBAC权限管理脚本, 包含any/logined/operator/sale四类权限 - supplychain/init.py: 重命名get_module_dbname为_get_dbname避免覆盖全局函数 - wwwroot/api/*_create.dspy: 修复自动编号生成逻辑(移除死代码条件判断)
253 lines
10 KiB
Python
253 lines
10 KiB
Python
from ahserver.serverenv import ServerEnv
|
|
from appPublic.jsonConfig import getConfig
|
|
from appPublic.dictObject import DictObject
|
|
from appPublic.uniqueID import getID
|
|
from sqlor.dbpools import DBPools
|
|
from datetime import datetime
|
|
import json
|
|
|
|
MODULE_NAME = "supplychain"
|
|
MODULE_VERSION = "1.0.0"
|
|
|
|
|
|
def _get_dbname():
|
|
"""Get the database name for the supplychain module."""
|
|
env = ServerEnv()
|
|
return env.get_module_dbname('supplychain')
|
|
|
|
|
|
def get_db_context():
|
|
"""Get a database context manager for the supplychain module."""
|
|
config = getConfig('.')
|
|
DBPools(config.databases)
|
|
dbname = _get_dbname()
|
|
return db.sqlorContext(dbname)
|
|
|
|
|
|
async def get_active_supply_discount(sor, resellerid, productid, prodtypeid=None, sale_date=None):
|
|
"""
|
|
Get active supply contract discount for a product.
|
|
Priority: exact product > product type > contract default.
|
|
|
|
Returns: dict with contract_id, discount, settlement_price, supplier_id
|
|
"""
|
|
if sale_date is None:
|
|
sale_date = datetime.now().strftime("%Y-%m-%d")
|
|
|
|
# Try exact product match
|
|
sql = """SELECT sci.id, sci.contract_id, sci.discount, sci.settlement_price, sc.supplier_id
|
|
FROM supply_contract_items sci
|
|
JOIN supply_contracts sc ON sci.contract_id = sc.id
|
|
WHERE sci.resellerid = ${resellerid}$
|
|
AND sc.status = '1'
|
|
AND sc.start_date <= ${sale_date}$
|
|
AND (sc.end_date IS NULL OR sc.end_date >= ${sale_date}$)
|
|
AND sci.productid = ${productid}$
|
|
ORDER BY sc.start_date DESC LIMIT 1"""
|
|
recs = await sor.sqlExe(sql, {"resellerid": resellerid, "sale_date": sale_date, "productid": productid})
|
|
|
|
if not recs and prodtypeid:
|
|
sql = """SELECT sci.id, sci.contract_id, sci.discount, sci.settlement_price, sc.supplier_id
|
|
FROM supply_contract_items sci
|
|
JOIN supply_contracts sc ON sci.contract_id = sc.id
|
|
WHERE sci.resellerid = ${resellerid}$
|
|
AND sc.status = '1'
|
|
AND sc.start_date <= ${sale_date}$
|
|
AND (sc.end_date IS NULL OR sc.end_date >= ${sale_date}$)
|
|
AND sci.prodtypeid = ${prodtypeid}$
|
|
ORDER BY sc.start_date DESC LIMIT 1"""
|
|
recs = await sor.sqlExe(sql, {"resellerid": resellerid, "sale_date": sale_date, "prodtypeid": prodtypeid})
|
|
|
|
if recs:
|
|
return {
|
|
"contract_item_id": recs[0].id,
|
|
"contract_id": recs[0].contract_id,
|
|
"discount": float(recs[0].discount) if recs[0].discount else 1.0,
|
|
"settlement_price": float(recs[0].settlement_price) if recs[0].settlement_price else None,
|
|
"supplier_id": recs[0].supplier_id,
|
|
}
|
|
|
|
# Fallback to contract default
|
|
sql = """SELECT id, supplier_id, default_discount FROM supply_contracts
|
|
WHERE resellerid = ${resellerid}$
|
|
AND status = '1'
|
|
AND start_date <= ${sale_date}$
|
|
AND (end_date IS NULL OR end_date >= ${sale_date}$)
|
|
ORDER BY start_date DESC LIMIT 1"""
|
|
recs = await sor.sqlExe(sql, {"resellerid": resellerid, "sale_date": sale_date})
|
|
|
|
if recs:
|
|
return {
|
|
"contract_item_id": None,
|
|
"contract_id": recs[0].id,
|
|
"discount": float(recs[0].default_discount) if recs[0].default_discount else 1.0,
|
|
"settlement_price": None,
|
|
"supplier_id": recs[0].supplier_id,
|
|
}
|
|
|
|
return None
|
|
|
|
|
|
async def get_active_dist_discount(sor, resellerid, sub_distributor_id, productid, prodtypeid=None, sale_date=None):
|
|
"""
|
|
Get active distribution agreement discount for a product.
|
|
Priority: exact product > product type > agreement default.
|
|
|
|
Returns: dict with agreement_id, discount, settlement_price
|
|
"""
|
|
if sale_date is None:
|
|
sale_date = datetime.now().strftime("%Y-%m-%d")
|
|
|
|
# Try exact product match
|
|
sql = """SELECT dai.id, dai.agreement_id, dai.discount, dai.settlement_price
|
|
FROM distribution_agreement_items dai
|
|
JOIN distribution_agreements da ON dai.agreement_id = da.id
|
|
WHERE dai.resellerid = ${resellerid}$
|
|
AND da.sub_distributor_id = ${sub_distributor_id}$
|
|
AND da.status = '1'
|
|
AND da.start_date <= ${sale_date}$
|
|
AND (da.end_date IS NULL OR da.end_date >= ${sale_date}$)
|
|
AND dai.productid = ${productid}$
|
|
ORDER BY da.start_date DESC LIMIT 1"""
|
|
ns = {"resellerid": resellerid, "sub_distributor_id": sub_distributor_id,
|
|
"sale_date": sale_date, "productid": productid}
|
|
recs = await sor.sqlExe(sql, ns)
|
|
|
|
if not recs and prodtypeid:
|
|
sql = """SELECT dai.id, dai.agreement_id, dai.discount, dai.settlement_price
|
|
FROM distribution_agreement_items dai
|
|
JOIN distribution_agreements da ON dai.agreement_id = da.id
|
|
WHERE dai.resellerid = ${resellerid}$
|
|
AND da.sub_distributor_id = ${sub_distributor_id}$
|
|
AND da.status = '1'
|
|
AND da.start_date <= ${sale_date}$
|
|
AND (da.end_date IS NULL OR da.end_date >= ${sale_date}$)
|
|
AND dai.prodtypeid = ${prodtypeid}$
|
|
ORDER BY da.start_date DESC LIMIT 1"""
|
|
ns["productid"] = None
|
|
recs = await sor.sqlExe(sql, ns)
|
|
|
|
if recs:
|
|
return {
|
|
"agreement_item_id": recs[0].id,
|
|
"agreement_id": recs[0].agreement_id,
|
|
"discount": float(recs[0].discount) if recs[0].discount else 1.0,
|
|
"settlement_price": float(recs[0].settlement_price) if recs[0].settlement_price else None,
|
|
}
|
|
|
|
# Fallback to agreement default
|
|
sql = """SELECT id, default_discount FROM distribution_agreements
|
|
WHERE resellerid = ${resellerid}$
|
|
AND sub_distributor_id = ${sub_distributor_id}$
|
|
AND status = '1'
|
|
AND start_date <= ${sale_date}$
|
|
AND (end_date IS NULL OR end_date >= ${sale_date}$)
|
|
ORDER BY start_date DESC LIMIT 1"""
|
|
recs = await sor.sqlExe(sql, {"resellerid": resellerid,
|
|
"sub_distributor_id": sub_distributor_id,
|
|
"sale_date": sale_date})
|
|
|
|
if recs:
|
|
return {
|
|
"agreement_item_id": None,
|
|
"agreement_id": recs[0].id,
|
|
"discount": float(recs[0].default_discount) if recs[0].default_discount else 1.0,
|
|
"settlement_price": None,
|
|
}
|
|
|
|
return None
|
|
|
|
|
|
async def calculate_sale_accounting(sor, resellerid, productid, quantity, unit_price,
|
|
sub_distributor_id=None, prodtypeid=None,
|
|
sale_date=None, source_type="2", source_id=None,
|
|
created_by=None, remark=""):
|
|
"""
|
|
Calculate and record accounting for a product sale.
|
|
|
|
Returns: the created accounting record as dict
|
|
"""
|
|
if sale_date is None:
|
|
sale_date = datetime.now().strftime("%Y-%m-%d")
|
|
|
|
total_amount = quantity * unit_price
|
|
|
|
# Get supply discount
|
|
supply_info = await get_active_supply_discount(sor, resellerid, productid, prodtypeid, sale_date)
|
|
|
|
supply_contract_id = None
|
|
supply_contract_item_id = None
|
|
supplier_id = None
|
|
supply_discount = 1.0
|
|
supply_amount = total_amount
|
|
|
|
if supply_info:
|
|
supply_contract_id = supply_info["contract_id"]
|
|
supply_contract_item_id = supply_info["contract_item_id"]
|
|
supplier_id = supply_info["supplier_id"]
|
|
supply_discount = supply_info["discount"]
|
|
if supply_info["settlement_price"]:
|
|
supply_amount = supply_info["settlement_price"] * quantity
|
|
else:
|
|
supply_amount = total_amount * supply_discount
|
|
|
|
# Get distribution discount
|
|
distribution_agreement_id = None
|
|
distribution_agreement_item_id = None
|
|
dist_discount = 1.0
|
|
dist_amount = total_amount
|
|
|
|
if sub_distributor_id:
|
|
dist_info = await get_active_dist_discount(sor, resellerid, sub_distributor_id,
|
|
productid, prodtypeid, sale_date)
|
|
if dist_info:
|
|
distribution_agreement_id = dist_info["agreement_id"]
|
|
distribution_agreement_item_id = dist_info["agreement_item_id"]
|
|
dist_discount = dist_info["discount"]
|
|
if dist_info["settlement_price"]:
|
|
dist_amount = dist_info["settlement_price"] * quantity
|
|
else:
|
|
dist_amount = total_amount * dist_discount
|
|
|
|
profit_amount = dist_amount - supply_amount
|
|
|
|
# Create accounting record
|
|
accounting_id = getID()
|
|
record = {
|
|
"id": accounting_id,
|
|
"resellerid": resellerid,
|
|
"supply_contract_id": supply_contract_id,
|
|
"supply_contract_item_id": supply_contract_item_id,
|
|
"distribution_agreement_id": distribution_agreement_id,
|
|
"distribution_agreement_item_id": distribution_agreement_item_id,
|
|
"sub_distributor_id": sub_distributor_id,
|
|
"supplier_id": supplier_id,
|
|
"prodtypeid": prodtypeid,
|
|
"productid": productid,
|
|
"quantity": quantity,
|
|
"unit_price": unit_price,
|
|
"supply_discount": supply_discount,
|
|
"supply_amount": supply_amount,
|
|
"dist_discount": dist_discount,
|
|
"dist_amount": dist_amount,
|
|
"profit_amount": profit_amount,
|
|
"sale_date": sale_date,
|
|
"source_type": source_type,
|
|
"source_id": source_id,
|
|
"remark": remark,
|
|
"created_by": created_by,
|
|
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
}
|
|
|
|
await sor.C("supplychain_accounting", record)
|
|
return record
|
|
|
|
|
|
def load_supplychain():
|
|
"""Register all functions with ServerEnv."""
|
|
env = ServerEnv()
|
|
env.get_active_supply_discount = get_active_supply_discount
|
|
env.get_active_dist_discount = get_active_dist_discount
|
|
env.calculate_sale_accounting = calculate_sale_accounting
|
|
return True
|