feat: 供应商和分销商管理模块 (supplychain)

- 7个数据库表: suppliers, supply_contracts, supply_contract_items,
  sub_distributors, distribution_agreements, distribution_agreement_items,
  supplychain_accounting
- CRUD JSON配置 (7个列表 + editable段)
- API端点: create/update/delete (21个) + calculate_accounting + query_discount (2个)
- 前端页面: index.ui + 5个功能页 + menu.ui
- 记账计算: 自动查找有效合同/协议折扣,计算进货金额、分销金额、利润
- 折扣查找优先级: 精确产品 > 产品分类 > 默认折扣
- productid/prodtypeid引用product模块(即将开发)
This commit is contained in:
yumoqing 2026-05-25 15:14:09 +08:00
parent 4c1efe56a8
commit da32159ad9
51 changed files with 2836 additions and 0 deletions

51
build.sh Normal file
View File

@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
# Find Sage root directory
SAGE_ROOT=""
for candidate in "$SCRIPT_DIR/../.." "$HOME/repos/sage" "$HOME/sage"; do
if [ -d "$candidate/wwwroot" ] && [ -d "$candidate/py3/bin" ]; then
SAGE_ROOT="$(cd "$candidate" && pwd)"
break
fi
done
if [ -z "$SAGE_ROOT" ]; then
echo "ERROR: Cannot find Sage root directory"
exit 1
fi
echo "Sage root: $SAGE_ROOT"
# Install module
cd "$SCRIPT_DIR"
$SAGE_ROOT/py3/bin/pip install -e .
# Generate DDL from models
if [ -d "$SCRIPT_DIR/models" ]; then
echo "Generating DDL..."
cd "$SCRIPT_DIR/models"
$SAGE_ROOT/py3/bin/json2ddl mysql . > mysql.ddl.sql
echo "DDL generated: $SCRIPT_DIR/models/mysql.ddl.sql"
fi
# Generate CRUD UI from json definitions
if [ -d "$SCRIPT_DIR/json" ]; then
echo "Generating CRUD UI files..."
cd "$SCRIPT_DIR/json"
for f in *.json; do
echo " Processing $f..."
done
$SAGE_ROOT/py3/bin/xls2ui -m ../models -o ../wwwroot supplychain *.json
echo "CRUD UI files generated."
fi
# Create symlink in Sage wwwroot
echo "Creating wwwroot symlink..."
rm -f "$SAGE_ROOT/wwwroot/supplychain"
ln -s "$SCRIPT_DIR/wwwroot" "$SAGE_ROOT/wwwroot/supplychain"
echo "Build complete."

1
init/data.json Normal file
View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,27 @@
{
"tblname": "distribution_agreement_items",
"alias": "distribution_agreement_items_list",
"title": "分销协议产品折扣",
"params": {
"sortby": [
"created_at desc"
],
"logined_userorgid": "resellerid",
"browserfields": {
"exclouded": [
"created_at"
]
},
"editexclouded": [
"id",
"resellerid",
"agreement_id",
"created_at"
],
"editable": {
"new_data_url": "{{entire_url('../api/distribution_agreement_items_create.dspy')}}",
"update_data_url": "{{entire_url('../api/distribution_agreement_items_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/distribution_agreement_items_delete.dspy')}}"
}
}
}

View File

@ -0,0 +1,57 @@
{
"tblname": "distribution_agreements",
"alias": "distribution_agreements_list",
"title": "分销协议管理",
"params": {
"sortby": [
"created_at desc"
],
"logined_userorgid": "resellerid",
"browserfields": {
"exclouded": [
"created_by",
"created_at",
"updated_at"
],
"alters": {
"status": {
"uitype": "code",
"data": [
{
"value": "1",
"text": "有效"
},
{
"value": "0",
"text": "无效"
},
{
"value": "2",
"text": "已过期"
}
]
}
}
},
"editexclouded": [
"id",
"resellerid",
"created_by",
"created_at",
"updated_at"
],
"subtables": [
{
"field": "id",
"title": "产品折扣",
"url": "{{entire_url('../distribution_agreement_items_list')}}",
"subtable": "distribution_agreement_items"
}
],
"editable": {
"new_data_url": "{{entire_url('../api/distribution_agreements_create.dspy')}}",
"update_data_url": "{{entire_url('../api/distribution_agreements_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/distribution_agreements_delete.dspy')}}"
}
}
}

View File

@ -0,0 +1,30 @@
{
"tblname": "sub_distributors",
"alias": "sub_distributors_list",
"title": "二级分销商管理",
"params": {
"sortby": [
"created_at desc"
],
"logined_userorgid": "resellerid",
"browserfields": {
"exclouded": [
"created_by",
"created_at",
"updated_at"
]
},
"editexclouded": [
"id",
"resellerid",
"created_by",
"created_at",
"updated_at"
],
"editable": {
"new_data_url": "{{entire_url('../api/sub_distributors_create.dspy')}}",
"update_data_url": "{{entire_url('../api/sub_distributors_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/sub_distributors_delete.dspy')}}"
}
}
}

30
json/suppliers_list.json Normal file
View File

@ -0,0 +1,30 @@
{
"tblname": "suppliers",
"alias": "suppliers_list",
"title": "供应商管理",
"params": {
"sortby": [
"created_at desc"
],
"logined_userorgid": "resellerid",
"browserfields": {
"exclouded": [
"created_by",
"created_at",
"updated_at"
]
},
"editexclouded": [
"id",
"resellerid",
"created_by",
"created_at",
"updated_at"
],
"editable": {
"new_data_url": "{{entire_url('../api/suppliers_create.dspy')}}",
"update_data_url": "{{entire_url('../api/suppliers_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/suppliers_delete.dspy')}}"
}
}
}

View File

@ -0,0 +1,27 @@
{
"tblname": "supply_contract_items",
"alias": "supply_contract_items_list",
"title": "供销合同产品折扣",
"params": {
"sortby": [
"created_at desc"
],
"logined_userorgid": "resellerid",
"browserfields": {
"exclouded": [
"created_at"
]
},
"editexclouded": [
"id",
"resellerid",
"contract_id",
"created_at"
],
"editable": {
"new_data_url": "{{entire_url('../api/supply_contract_items_create.dspy')}}",
"update_data_url": "{{entire_url('../api/supply_contract_items_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/supply_contract_items_delete.dspy')}}"
}
}
}

View File

@ -0,0 +1,57 @@
{
"tblname": "supply_contracts",
"alias": "supply_contracts_list",
"title": "供销合同管理",
"params": {
"sortby": [
"created_at desc"
],
"logined_userorgid": "resellerid",
"browserfields": {
"exclouded": [
"created_by",
"created_at",
"updated_at"
],
"alters": {
"status": {
"uitype": "code",
"data": [
{
"value": "1",
"text": "有效"
},
{
"value": "0",
"text": "无效"
},
{
"value": "2",
"text": "已过期"
}
]
}
}
},
"editexclouded": [
"id",
"resellerid",
"created_by",
"created_at",
"updated_at"
],
"subtables": [
{
"field": "id",
"title": "产品折扣",
"url": "{{entire_url('../supply_contract_items_list')}}",
"subtable": "supply_contract_items"
}
],
"editable": {
"new_data_url": "{{entire_url('../api/supply_contracts_create.dspy')}}",
"update_data_url": "{{entire_url('../api/supply_contracts_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/supply_contracts_delete.dspy')}}"
}
}
}

View File

@ -0,0 +1,29 @@
{
"tblname": "supplychain_accounting",
"alias": "supplychain_accounting_list",
"title": "供销记账",
"params": {
"sortby": [
"sale_date desc",
"created_at desc"
],
"logined_userorgid": "resellerid",
"browserfields": {
"exclouded": [
"created_by",
"created_at"
]
},
"editexclouded": [
"id",
"resellerid",
"created_by",
"created_at"
],
"editable": {
"new_data_url": "{{entire_url('../api/supplychain_accounting_create.dspy')}}",
"update_data_url": "{{entire_url('../api/supplychain_accounting_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/supplychain_accounting_delete.dspy')}}"
}
}
}

View File

@ -0,0 +1,92 @@
{
"summary": [
{
"name": "distribution_agreement_items",
"title": "分销协议产品折扣明细表",
"primary": ["id"],
"catelog": "relation"
}
],
"fields": [
{
"name": "id",
"title": "主键ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "agreement_id",
"title": "分销协议ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "resellerid",
"title": "所属主分销商机构ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "prodtypeid",
"title": "产品分类ID",
"type": "str",
"length": 32
},
{
"name": "productid",
"title": "产品ID",
"type": "str",
"length": 32
},
{
"name": "discount",
"title": "分销折扣",
"type": "double",
"length": 5,
"dec": 4,
"nullable": "no",
"default": "1.0000"
},
{
"name": "settlement_price",
"title": "结算单价",
"type": "double",
"length": 15,
"dec": 4
},
{
"name": "remark",
"title": "备注",
"type": "text"
},
{
"name": "created_at",
"title": "创建时间",
"type": "datetime",
"nullable": "no"
}
],
"indexes": [
{
"name": "idx_dai_agreement",
"idxtype": "index",
"idxfields": ["agreement_id"]
},
{
"name": "idx_dai_product",
"idxtype": "index",
"idxfields": ["agreement_id", "prodtypeid", "productid"]
}
],
"codes": [
{
"field": "agreement_id",
"table": "distribution_agreements",
"valuefield": "id",
"textfield": "agreement_name"
}
]
}

View File

@ -0,0 +1,132 @@
{
"summary": [
{
"name": "distribution_agreements",
"title": "分销协议表",
"primary": ["id"],
"catelog": "entity"
}
],
"fields": [
{
"name": "id",
"title": "主键ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "resellerid",
"title": "所属主分销商机构ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "sub_distributor_id",
"title": "二级分销商ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "agreement_code",
"title": "协议编号",
"type": "str",
"length": 64,
"nullable": "no"
},
{
"name": "agreement_name",
"title": "协议名称",
"type": "str",
"length": 255,
"nullable": "no"
},
{
"name": "sign_date",
"title": "签署日期",
"type": "date"
},
{
"name": "start_date",
"title": "生效日期",
"type": "date",
"nullable": "no"
},
{
"name": "end_date",
"title": "到期日期",
"type": "date"
},
{
"name": "status",
"title": "状态",
"type": "char",
"length": 1,
"nullable": "no",
"default": "1"
},
{
"name": "default_discount",
"title": "默认分销折扣",
"type": "double",
"length": 5,
"dec": 4,
"default": "1.0000"
},
{
"name": "remark",
"title": "备注",
"type": "text"
},
{
"name": "created_by",
"title": "创建人",
"type": "str",
"length": 32
},
{
"name": "created_at",
"title": "创建时间",
"type": "datetime",
"nullable": "no"
},
{
"name": "updated_at",
"title": "更新时间",
"type": "datetime"
}
],
"indexes": [
{
"name": "idx_da_reseller",
"idxtype": "index",
"idxfields": ["resellerid"]
},
{
"name": "idx_da_subdist",
"idxtype": "index",
"idxfields": ["sub_distributor_id"]
},
{
"name": "idx_da_code",
"idxtype": "unique",
"idxfields": ["resellerid", "agreement_code"]
}
],
"codes": [
{
"field": "sub_distributor_id",
"table": "sub_distributors",
"valuefield": "id",
"textfield": "sub_dist_name"
},
{
"field": "resellerid",
"table": "organization",
"valuefield": "id",
"textfield": "orgname"
}
]
}

View File

@ -0,0 +1,143 @@
{
"summary": [
{
"name": "sub_distributors",
"title": "二级分销商表",
"primary": ["id"],
"catelog": "entity"
}
],
"fields": [
{
"name": "id",
"title": "主键ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "resellerid",
"title": "所属主分销商机构ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "sub_dist_code",
"title": "二级分销商编号",
"type": "str",
"length": 64,
"nullable": "no"
},
{
"name": "sub_dist_name",
"title": "二级分销商名称",
"type": "str",
"length": 255,
"nullable": "no"
},
{
"name": "contact_person",
"title": "联系人",
"type": "str",
"length": 100
},
{
"name": "contact_phone",
"title": "联系电话",
"type": "str",
"length": 50
},
{
"name": "contact_email",
"title": "联系邮箱",
"type": "str",
"length": 255
},
{
"name": "address",
"title": "地址",
"type": "str",
"length": 500
},
{
"name": "tax_number",
"title": "税号",
"type": "str",
"length": 64
},
{
"name": "bank_name",
"title": "开户银行",
"type": "str",
"length": 255
},
{
"name": "bank_account",
"title": "银行账号",
"type": "str",
"length": 64
},
{
"name": "managed_by",
"title": "负责销售ID",
"type": "str",
"length": 32
},
{
"name": "status",
"title": "状态",
"type": "char",
"length": 1,
"nullable": "no",
"default": "1"
},
{
"name": "remark",
"title": "备注",
"type": "text"
},
{
"name": "created_by",
"title": "创建人",
"type": "str",
"length": 32
},
{
"name": "created_at",
"title": "创建时间",
"type": "datetime",
"nullable": "no"
},
{
"name": "updated_at",
"title": "更新时间",
"type": "datetime"
}
],
"indexes": [
{
"name": "idx_sd_reseller",
"idxtype": "index",
"idxfields": ["resellerid"]
},
{
"name": "idx_sd_code",
"idxtype": "unique",
"idxfields": ["resellerid", "sub_dist_code"]
},
{
"name": "idx_sd_manager",
"idxtype": "index",
"idxfields": ["managed_by"]
}
],
"codes": [
{
"field": "resellerid",
"table": "organization",
"valuefield": "id",
"textfield": "orgname"
}
]
}

132
models/suppliers.json Normal file
View File

@ -0,0 +1,132 @@
{
"summary": [
{
"name": "suppliers",
"title": "供应商表",
"primary": ["id"],
"catelog": "entity"
}
],
"fields": [
{
"name": "id",
"title": "主键ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "resellerid",
"title": "所属分销商机构ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "supplier_code",
"title": "供应商编号",
"type": "str",
"length": 64,
"nullable": "no"
},
{
"name": "supplier_name",
"title": "供应商名称",
"type": "str",
"length": 255,
"nullable": "no"
},
{
"name": "contact_person",
"title": "联系人",
"type": "str",
"length": 100
},
{
"name": "contact_phone",
"title": "联系电话",
"type": "str",
"length": 50
},
{
"name": "contact_email",
"title": "联系邮箱",
"type": "str",
"length": 255
},
{
"name": "address",
"title": "地址",
"type": "str",
"length": 500
},
{
"name": "tax_number",
"title": "税号",
"type": "str",
"length": 64
},
{
"name": "bank_name",
"title": "开户银行",
"type": "str",
"length": 255
},
{
"name": "bank_account",
"title": "银行账号",
"type": "str",
"length": 64
},
{
"name": "status",
"title": "状态",
"type": "char",
"length": 1,
"nullable": "no",
"default": "1"
},
{
"name": "remark",
"title": "备注",
"type": "text"
},
{
"name": "created_by",
"title": "创建人",
"type": "str",
"length": 32
},
{
"name": "created_at",
"title": "创建时间",
"type": "datetime",
"nullable": "no"
},
{
"name": "updated_at",
"title": "更新时间",
"type": "datetime"
}
],
"indexes": [
{
"name": "idx_suppliers_reseller",
"idxtype": "index",
"idxfields": ["resellerid"]
},
{
"name": "idx_suppliers_code",
"idxtype": "unique",
"idxfields": ["resellerid", "supplier_code"]
}
],
"codes": [
{
"field": "resellerid",
"table": "organization",
"valuefield": "id",
"textfield": "orgname"
}
]
}

View File

@ -0,0 +1,92 @@
{
"summary": [
{
"name": "supply_contract_items",
"title": "供销合同产品折扣明细表",
"primary": ["id"],
"catelog": "relation"
}
],
"fields": [
{
"name": "id",
"title": "主键ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "contract_id",
"title": "供销合同ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "resellerid",
"title": "所属分销商机构ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "prodtypeid",
"title": "产品分类ID",
"type": "str",
"length": 32
},
{
"name": "productid",
"title": "产品ID",
"type": "str",
"length": 32
},
{
"name": "discount",
"title": "进货折扣",
"type": "double",
"length": 5,
"dec": 4,
"nullable": "no",
"default": "1.0000"
},
{
"name": "settlement_price",
"title": "结算单价",
"type": "double",
"length": 15,
"dec": 4
},
{
"name": "remark",
"title": "备注",
"type": "text"
},
{
"name": "created_at",
"title": "创建时间",
"type": "datetime",
"nullable": "no"
}
],
"indexes": [
{
"name": "idx_sci_contract",
"idxtype": "index",
"idxfields": ["contract_id"]
},
{
"name": "idx_sci_product",
"idxtype": "index",
"idxfields": ["contract_id", "prodtypeid", "productid"]
}
],
"codes": [
{
"field": "contract_id",
"table": "supply_contracts",
"valuefield": "id",
"textfield": "contract_name"
}
]
}

View File

@ -0,0 +1,132 @@
{
"summary": [
{
"name": "supply_contracts",
"title": "供销合同表",
"primary": ["id"],
"catelog": "entity"
}
],
"fields": [
{
"name": "id",
"title": "主键ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "resellerid",
"title": "所属分销商机构ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "supplier_id",
"title": "供应商ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "contract_code",
"title": "合同编号",
"type": "str",
"length": 64,
"nullable": "no"
},
{
"name": "contract_name",
"title": "合同名称",
"type": "str",
"length": 255,
"nullable": "no"
},
{
"name": "sign_date",
"title": "签署日期",
"type": "date"
},
{
"name": "start_date",
"title": "生效日期",
"type": "date",
"nullable": "no"
},
{
"name": "end_date",
"title": "到期日期",
"type": "date"
},
{
"name": "status",
"title": "状态",
"type": "char",
"length": 1,
"nullable": "no",
"default": "1"
},
{
"name": "default_discount",
"title": "默认折扣",
"type": "double",
"length": 5,
"dec": 4,
"default": "1.0000"
},
{
"name": "remark",
"title": "备注",
"type": "text"
},
{
"name": "created_by",
"title": "创建人",
"type": "str",
"length": 32
},
{
"name": "created_at",
"title": "创建时间",
"type": "datetime",
"nullable": "no"
},
{
"name": "updated_at",
"title": "更新时间",
"type": "datetime"
}
],
"indexes": [
{
"name": "idx_sc_reseller",
"idxtype": "index",
"idxfields": ["resellerid"]
},
{
"name": "idx_sc_supplier",
"idxtype": "index",
"idxfields": ["supplier_id"]
},
{
"name": "idx_sc_code",
"idxtype": "unique",
"idxfields": ["resellerid", "contract_code"]
}
],
"codes": [
{
"field": "supplier_id",
"table": "suppliers",
"valuefield": "id",
"textfield": "supplier_name"
},
{
"field": "resellerid",
"table": "organization",
"valuefield": "id",
"textfield": "orgname"
}
]
}

View File

@ -0,0 +1,233 @@
{
"summary": [
{
"name": "supplychain_accounting",
"title": "供销记账表",
"primary": ["id"],
"catelog": "relation"
}
],
"fields": [
{
"name": "id",
"title": "主键ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "resellerid",
"title": "所属主分销商机构ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "supply_contract_id",
"title": "供销合同ID",
"type": "str",
"length": 32
},
{
"name": "supply_contract_item_id",
"title": "供销合同产品明细ID",
"type": "str",
"length": 32
},
{
"name": "distribution_agreement_id",
"title": "分销协议ID",
"type": "str",
"length": 32
},
{
"name": "distribution_agreement_item_id",
"title": "分销协议产品明细ID",
"type": "str",
"length": 32
},
{
"name": "sub_distributor_id",
"title": "二级分销商ID",
"type": "str",
"length": 32
},
{
"name": "supplier_id",
"title": "供应商ID",
"type": "str",
"length": 32
},
{
"name": "prodtypeid",
"title": "产品分类ID",
"type": "str",
"length": 32
},
{
"name": "productid",
"title": "产品ID",
"type": "str",
"length": 32
},
{
"name": "quantity",
"title": "数量",
"type": "double",
"length": 15,
"dec": 4,
"nullable": "no",
"default": "0"
},
{
"name": "unit_price",
"title": "销售单价",
"type": "double",
"length": 15,
"dec": 4,
"nullable": "no",
"default": "0"
},
{
"name": "supply_discount",
"title": "进货折扣",
"type": "double",
"length": 5,
"dec": 4
},
{
"name": "supply_amount",
"title": "进货金额(应付供应商)",
"type": "double",
"length": 15,
"dec": 2,
"nullable": "no",
"default": "0"
},
{
"name": "dist_discount",
"title": "分销折扣",
"type": "double",
"length": 5,
"dec": 4
},
{
"name": "dist_amount",
"title": "分销金额(二级分销商应付)",
"type": "double",
"length": 15,
"dec": 2,
"nullable": "no",
"default": "0"
},
{
"name": "profit_amount",
"title": "利润金额",
"type": "double",
"length": 15,
"dec": 2,
"nullable": "no",
"default": "0"
},
{
"name": "sale_date",
"title": "销售日期",
"type": "date",
"nullable": "no"
},
{
"name": "source_type",
"title": "来源类型",
"type": "char",
"length": 1,
"default": "1"
},
{
"name": "source_id",
"title": "来源记录ID",
"type": "str",
"length": 32
},
{
"name": "remark",
"title": "备注",
"type": "text"
},
{
"name": "created_by",
"title": "创建人",
"type": "str",
"length": 32
},
{
"name": "created_at",
"title": "创建时间",
"type": "datetime",
"nullable": "no"
}
],
"indexes": [
{
"name": "idx_sa_reseller",
"idxtype": "index",
"idxfields": ["resellerid"]
},
{
"name": "idx_sa_sale_date",
"idxtype": "index",
"idxfields": ["sale_date"]
},
{
"name": "idx_sa_product",
"idxtype": "index",
"idxfields": ["productid"]
},
{
"name": "idx_sa_subdist",
"idxtype": "index",
"idxfields": ["sub_distributor_id"]
},
{
"name": "idx_sa_supplier",
"idxtype": "index",
"idxfields": ["supplier_id"]
},
{
"name": "idx_sa_source",
"idxtype": "index",
"idxfields": ["source_type", "source_id"]
}
],
"codes": [
{
"field": "supply_contract_id",
"table": "supply_contracts",
"valuefield": "id",
"textfield": "contract_name"
},
{
"field": "distribution_agreement_id",
"table": "distribution_agreements",
"valuefield": "id",
"textfield": "agreement_name"
},
{
"field": "sub_distributor_id",
"table": "sub_distributors",
"valuefield": "id",
"textfield": "sub_dist_name"
},
{
"field": "supplier_id",
"table": "suppliers",
"valuefield": "id",
"textfield": "supplier_name"
},
{
"field": "resellerid",
"table": "organization",
"valuefield": "id",
"textfield": "orgname"
}
]
}

17
pyproject.toml Normal file
View File

@ -0,0 +1,17 @@
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "supplychain"
version = "1.0.0"
description = "供应商和分销商管理模块 — 供销合同、分销协议、产品折扣及供销记账"
requires-python = ">=3.8"
dependencies = [
"sqlor",
"bricks_for_python",
]
[tool.setuptools.packages.find]
where = ["."]
include = ["supplychain*"]

60
skill/SKILL.md Normal file
View File

@ -0,0 +1,60 @@
---
name: supplychain-module
version: 1.0.0
description: 供应商和分销商管理模块 — 供销合同、分销协议、产品折扣及供销记账
trigger_conditions:
- 需要管理供应商信息
- 需要创建供销合同或分销协议
- 需要为产品设置进货或分销折扣
- 需要计算产品销售时的供销记账金额
---
# supplychain 模块技能文档
## 模块概述
supplychain 模块为分销商提供供应链管理功能,包括供应商管理、供销合同管理、二级分销商管理、分销协议管理,以及产品销售时的供销记账金额计算。
## 数据表结构
### 实体表
- **suppliers** — 供应商(基本信息、联系方式、财务信息)
- **supply_contracts** — 供销合同(运营创建,关联供应商,设置有效期)
- **sub_distributors** — 二级分销商(销售创建,基本信息)
- **distribution_agreements** — 分销协议(销售创建,关联二级分销商)
### 关系表
- **supply_contract_items** — 供销合同产品折扣明细(合同下的产品级折扣)
- **distribution_agreement_items** — 分销协议产品折扣明细(协议下的产品级折扣)
- **supplychain_accounting** — 供销记账记录(销售时自动计算)
## 折扣查找优先级
无论是进货折扣还是分销折扣,查找优先级均为:
1. 精确匹配 productid
2. 匹配 prodtypeid产品分类级折扣
3. 使用合同/协议的 default_discount
## API 调用
### 记账计算
```
POST /supplychain/api/calculate_accounting.dspy
Body: {productid, prodtypeid?, quantity, unit_price, sub_distributor_id?, sale_date?, ...}
```
### Python 函数调用
```python
# 在 .dspy 文件中
record = await calculate_sale_accounting(sor, resellerid, productid, quantity, unit_price,
sub_distributor_id, prodtypeid, sale_date, ...)
```
## 注意事项
- productid 和 prodtypeid 引用 product 模块(即将开发)
- 所有数据通过 resellerid 隔离(机构级多租户)
- 合同/协议编号自动生成
- 折扣值范围 0-11.0 表示无折扣)

0
supplychain/__init__.py Normal file
View File

253
supplychain/init.py Normal file
View File

@ -0,0 +1,253 @@
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_module_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_module_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
env.get_module_dbname = get_module_dbname
return True

25
wwwroot/accounting.ui Normal file
View File

@ -0,0 +1,25 @@
{
"widgettype": "VBox",
"options": {
"width": "100%",
"height": "100%",
"padding": "16px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "供销记账",
"fontSize": "20px",
"fontWeight": "bold",
"marginBottom": "16px"
}
},
{
"widgettype": "DataViewer",
"options": {
"url": "{{entire_url('supplychain_accounting_list/index.ui')}}"
}
}
]
}

View File

@ -0,0 +1,222 @@
import json
from appPublic.uniqueID import getID
from datetime import datetime
async def main(request, params_kw):
"""
产品销售时调用此API计算供销记账金额。
输入参数:
productid: 产品ID
prodtypeid: 产品分类ID (可选)
quantity: 销售数量
unit_price: 销售单价
sub_distributor_id: 二级分销商ID (可选, 如果是直接销售则为空)
sale_date: 销售日期 (可选, 默认今天)
source_type: 来源类型 (1=手动, 2=API调用)
source_id: 来源记录ID (可选)
计算逻辑:
1. 查找有效的供销合同及对应产品折扣
2. 查找有效的分销协议及对应产品折扣 (如果有二级分销商)
3. 计算: 进货金额 = 单价 * 数量 * 进货折扣
4. 计算: 分销金额 = 单价 * 数量 * 分销折扣
5. 计算: 利润金额 = 分销金额 - 进货金额
6. 创建记账记录
返回: 记账记录数据
"""
user_id = await get_user()
user_orgid = await get_userorgid()
dbname = get_module_dbname('supplychain')
# Parse input
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
productid = data.get("productid")
prodtypeid = data.get("prodtypeid")
quantity = float(data.get("quantity", 0))
unit_price = float(data.get("unit_price", 0))
sub_distributor_id = data.get("sub_distributor_id")
sale_date = data.get("sale_date", datetime.now().strftime("%Y-%m-%d"))
source_type = data.get("source_type", "2")
source_id = data.get("source_id")
remark = data.get("remark", "")
if not productid or quantity <= 0 or unit_price <= 0:
return json.dumps({"status": "error", "message": "缺少必要参数: productid, quantity, unit_price"})
config = getConfig(".")
DBPools(config.databases)
total_amount = quantity * unit_price
async with db.sqlorContext(dbname) as sor:
# Step 1: Find active supply contract with product discount
# Priority: exact product > product type > default contract discount
supply_contract_id = None
supply_contract_item_id = None
supplier_id = None
supply_discount = 1.0
supply_amount = total_amount
# Find supply contract items matching this product
if prodtypeid:
sql_sci = """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 sci.created_at DESC LIMIT 1"""
ns_sci = {"resellerid": user_orgid, "sale_date": sale_date, "productid": productid}
sci_recs = await sor.sqlExe(sql_sci, ns_sci)
if not prodtypeid or not sci_recs:
sql_sci = """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 sci.created_at DESC LIMIT 1"""
ns_sci = {"resellerid": user_orgid, "sale_date": sale_date, "prodtypeid": prodtypeid}
sci_recs = await sor.sqlExe(sql_sci, ns_sci)
if sci_recs:
supply_contract_item_id = sci_recs[0].id
supply_contract_id = sci_recs[0].contract_id
supplier_id = sci_recs[0].supplier_id
supply_discount = float(sci_recs[0].discount) if sci_recs[0].discount else 1.0
if sci_recs[0].settlement_price:
supply_amount = float(sci_recs[0].settlement_price) * quantity
else:
supply_amount = total_amount * supply_discount
else:
# Fallback: find any active supply contract with default discount
sql_sc = """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 created_at DESC LIMIT 1"""
sc_recs = await sor.sqlExe(sql_sc, {"resellerid": user_orgid, "sale_date": sale_date})
if sc_recs:
supply_contract_id = sc_recs[0].id
supplier_id = sc_recs[0].supplier_id
supply_discount = float(sc_recs[0].default_discount) if sc_recs[0].default_discount else 1.0
supply_amount = total_amount * supply_discount
# Step 2: Find active distribution agreement with product discount (if sub_distributor)
distribution_agreement_id = None
distribution_agreement_item_id = None
dist_discount = 1.0
dist_amount = total_amount
if sub_distributor_id:
# Find distribution agreement items matching this product
sql_dai = """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 dai.created_at DESC LIMIT 1"""
ns_dai = {"resellerid": user_orgid, "sub_distributor_id": sub_distributor_id,
"sale_date": sale_date, "productid": productid}
dai_recs = await sor.sqlExe(sql_dai, ns_dai)
if not dai_recs and prodtypeid:
sql_dai = """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 dai.created_at DESC LIMIT 1"""
ns_dai["productid"] = None
dai_recs = await sor.sqlExe(sql_dai, ns_dai)
if dai_recs:
distribution_agreement_item_id = dai_recs[0].id
distribution_agreement_id = dai_recs[0].agreement_id
dist_discount = float(dai_recs[0].discount) if dai_recs[0].discount else 1.0
if dai_recs[0].settlement_price:
dist_amount = float(dai_recs[0].settlement_price) * quantity
else:
dist_amount = total_amount * dist_discount
else:
# Fallback: find active distribution agreement with default discount
sql_da = """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 created_at DESC LIMIT 1"""
da_recs = await sor.sqlExe(sql_da, {"resellerid": user_orgid,
"sub_distributor_id": sub_distributor_id,
"sale_date": sale_date})
if da_recs:
distribution_agreement_id = da_recs[0].id
dist_discount = float(da_recs[0].default_discount) if da_recs[0].default_discount else 1.0
dist_amount = total_amount * dist_discount
# Step 3: Calculate profit
profit_amount = dist_amount - supply_amount
# Step 4: Create accounting record
accounting_id = getID()
record = {
"id": accounting_id,
"resellerid": user_orgid,
"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": user_id,
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
await sor.C("supplychain_accounting", record)
result = {
"status": "ok",
"data": record,
"summary": {
"total_amount": total_amount,
"supply_amount": supply_amount,
"dist_amount": dist_amount,
"profit_amount": profit_amount,
"supply_discount": supply_discount,
"dist_discount": dist_discount
}
}
return json.dumps(result)

View File

@ -0,0 +1,34 @@
import json
from appPublic.uniqueID import getID
from datetime import datetime
async def main(request, params_kw):
"""Create a new distribution_agreement_items record."""
user_id = await get_user()
user_orgid = await get_userorgid()
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
data["id"] = getID()
data["resellerid"] = user_orgid
data["created_by"] = user_id
data["created_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Auto-generate codes if needed
if "distribution_agreement_items" == "suppliers" and not data.get("supplier_code"):
data["supplier_code"] = f"SUP-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
if "distribution_agreement_items" == "sub_distributors" and not data.get("sub_dist_code"):
data["sub_dist_code"] = f"SUB-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
if "distribution_agreement_items" == "supply_contracts" and not data.get("contract_code"):
data["contract_code"] = f"SC-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
if "distribution_agreement_items" == "distribution_agreements" and not data.get("agreement_code"):
data["agreement_code"] = f"DA-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.C("distribution_agreement_items", data)
return json.dumps({"status": "ok", "data": data})

View File

@ -0,0 +1,19 @@
import json
async def main(request, params_kw):
"""Delete a distribution_agreement_items record."""
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
record_id = data.get("id")
if not record_id:
return json.dumps({"status": "error", "message": "Missing record id"})
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.D("distribution_agreement_items", {"id": record_id})
return json.dumps({"status": "ok", "message": "Deleted successfully"})

View File

@ -0,0 +1,27 @@
import json
from datetime import datetime
async def main(request, params_kw):
"""Update a distribution_agreement_items record."""
user_id = await get_user()
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
record_id = data.get("id")
if not record_id:
return json.dumps({"status": "error", "message": "Missing record id"})
data["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Remove fields that should not be updated
for key in ["id", "resellerid", "created_by", "created_at"]:
data.pop(key, None)
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.U("distribution_agreement_items", data)
return json.dumps({"status": "ok", "data": data})

View File

@ -0,0 +1,34 @@
import json
from appPublic.uniqueID import getID
from datetime import datetime
async def main(request, params_kw):
"""Create a new distribution_agreements record."""
user_id = await get_user()
user_orgid = await get_userorgid()
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
data["id"] = getID()
data["resellerid"] = user_orgid
data["created_by"] = user_id
data["created_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Auto-generate codes if needed
if "distribution_agreements" == "suppliers" and not data.get("supplier_code"):
data["supplier_code"] = f"SUP-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
if "distribution_agreements" == "sub_distributors" and not data.get("sub_dist_code"):
data["sub_dist_code"] = f"SUB-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
if "distribution_agreements" == "supply_contracts" and not data.get("contract_code"):
data["contract_code"] = f"SC-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
if "distribution_agreements" == "distribution_agreements" and not data.get("agreement_code"):
data["agreement_code"] = f"DA-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.C("distribution_agreements", data)
return json.dumps({"status": "ok", "data": data})

View File

@ -0,0 +1,19 @@
import json
async def main(request, params_kw):
"""Delete a distribution_agreements record."""
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
record_id = data.get("id")
if not record_id:
return json.dumps({"status": "error", "message": "Missing record id"})
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.D("distribution_agreements", {"id": record_id})
return json.dumps({"status": "ok", "message": "Deleted successfully"})

View File

@ -0,0 +1,27 @@
import json
from datetime import datetime
async def main(request, params_kw):
"""Update a distribution_agreements record."""
user_id = await get_user()
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
record_id = data.get("id")
if not record_id:
return json.dumps({"status": "error", "message": "Missing record id"})
data["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Remove fields that should not be updated
for key in ["id", "resellerid", "created_by", "created_at"]:
data.pop(key, None)
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.U("distribution_agreements", data)
return json.dumps({"status": "ok", "data": data})

View File

@ -0,0 +1,74 @@
import json
async def main(request, params_kw):
"""
查询某二级分销商在某产品上的分销协议折扣。
参数: sub_distributor_id, productid, prodtypeid(可选)
折扣查找优先级:
1. 精确匹配 productid
2. 匹配 prodtypeid
3. 使用协议默认折扣
"""
user_orgid = await get_userorgid()
dbname = get_module_dbname('supplychain')
sub_distributor_id = params_kw.get("sub_distributor_id")
productid = params_kw.get("productid")
prodtypeid = params_kw.get("prodtypeid")
if not sub_distributor_id or not productid:
return json.dumps({"status": "error", "message": "缺少sub_distributor_id或productid参数"})
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
# Try exact product match
sql = """SELECT dai.id, dai.agreement_id, dai.discount, dai.settlement_price,
da.agreement_code, da.agreement_name
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 <= CURDATE()
AND (da.end_date IS NULL OR da.end_date >= CURDATE())
AND dai.productid = ${productid}$
ORDER BY da.start_date DESC"""
recs = await sor.sqlExe(sql, {"resellerid": user_orgid,
"sub_distributor_id": sub_distributor_id,
"productid": productid})
if not recs and prodtypeid:
sql = """SELECT dai.id, dai.agreement_id, dai.discount, dai.settlement_price,
da.agreement_code, da.agreement_name
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 <= CURDATE()
AND (da.end_date IS NULL OR da.end_date >= CURDATE())
AND dai.prodtypeid = ${prodtypeid}$
ORDER BY da.start_date DESC"""
recs = await sor.sqlExe(sql, {"resellerid": user_orgid,
"sub_distributor_id": sub_distributor_id,
"prodtypeid": prodtypeid})
if not recs:
sql = """SELECT id as agreement_id, agreement_code, agreement_name,
default_discount as discount, NULL as settlement_price
FROM distribution_agreements
WHERE resellerid = ${resellerid}$
AND sub_distributor_id = ${sub_distributor_id}$
AND status = '1'
AND start_date <= CURDATE()
AND (end_date IS NULL OR end_date >= CURDATE())
ORDER BY start_date DESC"""
recs = await sor.sqlExe(sql, {"resellerid": user_orgid,
"sub_distributor_id": sub_distributor_id})
result = [dict(r) for r in recs] if recs else []
return json.dumps({"status": "ok", "data": result})

View File

@ -0,0 +1,70 @@
import json
async def main(request, params_kw):
"""
查询某产品在有效供销合同下的折扣信息。
参数: productid, prodtypeid(可选)
折扣查找优先级:
1. 精确匹配 productid
2. 匹配 prodtypeid
3. 使用合同默认折扣
"""
user_orgid = await get_userorgid()
dbname = get_module_dbname('supplychain')
productid = params_kw.get("productid")
prodtypeid = params_kw.get("prodtypeid")
if not productid:
return json.dumps({"status": "error", "message": "缺少productid参数"})
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
# Try exact product match
sql = """SELECT sci.id, sci.contract_id, sci.discount, sci.settlement_price,
sc.contract_code, sc.contract_name, sc.supplier_id,
s.supplier_name
FROM supply_contract_items sci
JOIN supply_contracts sc ON sci.contract_id = sc.id
LEFT JOIN suppliers s ON sc.supplier_id = s.id
WHERE sci.resellerid = ${resellerid}$
AND sc.status = '1'
AND sc.start_date <= CURDATE()
AND (sc.end_date IS NULL OR sc.end_date >= CURDATE())
AND sci.productid = ${productid}$
ORDER BY sc.start_date DESC"""
recs = await sor.sqlExe(sql, {"resellerid": user_orgid, "productid": productid})
if not recs and prodtypeid:
sql = """SELECT sci.id, sci.contract_id, sci.discount, sci.settlement_price,
sc.contract_code, sc.contract_name, sc.supplier_id,
s.supplier_name
FROM supply_contract_items sci
JOIN supply_contracts sc ON sci.contract_id = sc.id
LEFT JOIN suppliers s ON sc.supplier_id = s.id
WHERE sci.resellerid = ${resellerid}$
AND sc.status = '1'
AND sc.start_date <= CURDATE()
AND (sc.end_date IS NULL OR sc.end_date >= CURDATE())
AND sci.prodtypeid = ${prodtypeid}$
ORDER BY sc.start_date DESC"""
recs = await sor.sqlExe(sql, {"resellerid": user_orgid, "prodtypeid": prodtypeid})
if not recs:
# Fallback to contract default
sql = """SELECT id as contract_id, contract_code, contract_name,
supplier_id, default_discount as discount, NULL as settlement_price
FROM supply_contracts
WHERE resellerid = ${resellerid}$
AND status = '1'
AND start_date <= CURDATE()
AND (end_date IS NULL OR end_date >= CURDATE())
ORDER BY start_date DESC"""
recs = await sor.sqlExe(sql, {"resellerid": user_orgid})
result = [dict(r) for r in recs] if recs else []
return json.dumps({"status": "ok", "data": result})

View File

@ -0,0 +1,34 @@
import json
from appPublic.uniqueID import getID
from datetime import datetime
async def main(request, params_kw):
"""Create a new sub_distributors record."""
user_id = await get_user()
user_orgid = await get_userorgid()
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
data["id"] = getID()
data["resellerid"] = user_orgid
data["created_by"] = user_id
data["created_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Auto-generate codes if needed
if "sub_distributors" == "suppliers" and not data.get("supplier_code"):
data["supplier_code"] = f"SUP-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
if "sub_distributors" == "sub_distributors" and not data.get("sub_dist_code"):
data["sub_dist_code"] = f"SUB-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
if "sub_distributors" == "supply_contracts" and not data.get("contract_code"):
data["contract_code"] = f"SC-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
if "sub_distributors" == "distribution_agreements" and not data.get("agreement_code"):
data["agreement_code"] = f"DA-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.C("sub_distributors", data)
return json.dumps({"status": "ok", "data": data})

View File

@ -0,0 +1,19 @@
import json
async def main(request, params_kw):
"""Delete a sub_distributors record."""
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
record_id = data.get("id")
if not record_id:
return json.dumps({"status": "error", "message": "Missing record id"})
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.D("sub_distributors", {"id": record_id})
return json.dumps({"status": "ok", "message": "Deleted successfully"})

View File

@ -0,0 +1,27 @@
import json
from datetime import datetime
async def main(request, params_kw):
"""Update a sub_distributors record."""
user_id = await get_user()
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
record_id = data.get("id")
if not record_id:
return json.dumps({"status": "error", "message": "Missing record id"})
data["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Remove fields that should not be updated
for key in ["id", "resellerid", "created_by", "created_at"]:
data.pop(key, None)
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.U("sub_distributors", data)
return json.dumps({"status": "ok", "data": data})

View File

@ -0,0 +1,34 @@
import json
from appPublic.uniqueID import getID
from datetime import datetime
async def main(request, params_kw):
"""Create a new suppliers record."""
user_id = await get_user()
user_orgid = await get_userorgid()
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
data["id"] = getID()
data["resellerid"] = user_orgid
data["created_by"] = user_id
data["created_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Auto-generate codes if needed
if "suppliers" == "suppliers" and not data.get("supplier_code"):
data["supplier_code"] = f"SUP-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
if "suppliers" == "sub_distributors" and not data.get("sub_dist_code"):
data["sub_dist_code"] = f"SUB-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
if "suppliers" == "supply_contracts" and not data.get("contract_code"):
data["contract_code"] = f"SC-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
if "suppliers" == "distribution_agreements" and not data.get("agreement_code"):
data["agreement_code"] = f"DA-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.C("suppliers", data)
return json.dumps({"status": "ok", "data": data})

View File

@ -0,0 +1,19 @@
import json
async def main(request, params_kw):
"""Delete a suppliers record."""
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
record_id = data.get("id")
if not record_id:
return json.dumps({"status": "error", "message": "Missing record id"})
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.D("suppliers", {"id": record_id})
return json.dumps({"status": "ok", "message": "Deleted successfully"})

View File

@ -0,0 +1,27 @@
import json
from datetime import datetime
async def main(request, params_kw):
"""Update a suppliers record."""
user_id = await get_user()
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
record_id = data.get("id")
if not record_id:
return json.dumps({"status": "error", "message": "Missing record id"})
data["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Remove fields that should not be updated
for key in ["id", "resellerid", "created_by", "created_at"]:
data.pop(key, None)
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.U("suppliers", data)
return json.dumps({"status": "ok", "data": data})

View File

@ -0,0 +1,34 @@
import json
from appPublic.uniqueID import getID
from datetime import datetime
async def main(request, params_kw):
"""Create a new supply_contract_items record."""
user_id = await get_user()
user_orgid = await get_userorgid()
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
data["id"] = getID()
data["resellerid"] = user_orgid
data["created_by"] = user_id
data["created_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Auto-generate codes if needed
if "supply_contract_items" == "suppliers" and not data.get("supplier_code"):
data["supplier_code"] = f"SUP-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
if "supply_contract_items" == "sub_distributors" and not data.get("sub_dist_code"):
data["sub_dist_code"] = f"SUB-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
if "supply_contract_items" == "supply_contracts" and not data.get("contract_code"):
data["contract_code"] = f"SC-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
if "supply_contract_items" == "distribution_agreements" and not data.get("agreement_code"):
data["agreement_code"] = f"DA-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.C("supply_contract_items", data)
return json.dumps({"status": "ok", "data": data})

View File

@ -0,0 +1,19 @@
import json
async def main(request, params_kw):
"""Delete a supply_contract_items record."""
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
record_id = data.get("id")
if not record_id:
return json.dumps({"status": "error", "message": "Missing record id"})
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.D("supply_contract_items", {"id": record_id})
return json.dumps({"status": "ok", "message": "Deleted successfully"})

View File

@ -0,0 +1,27 @@
import json
from datetime import datetime
async def main(request, params_kw):
"""Update a supply_contract_items record."""
user_id = await get_user()
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
record_id = data.get("id")
if not record_id:
return json.dumps({"status": "error", "message": "Missing record id"})
data["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Remove fields that should not be updated
for key in ["id", "resellerid", "created_by", "created_at"]:
data.pop(key, None)
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.U("supply_contract_items", data)
return json.dumps({"status": "ok", "data": data})

View File

@ -0,0 +1,34 @@
import json
from appPublic.uniqueID import getID
from datetime import datetime
async def main(request, params_kw):
"""Create a new supply_contracts record."""
user_id = await get_user()
user_orgid = await get_userorgid()
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
data["id"] = getID()
data["resellerid"] = user_orgid
data["created_by"] = user_id
data["created_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Auto-generate codes if needed
if "supply_contracts" == "suppliers" and not data.get("supplier_code"):
data["supplier_code"] = f"SUP-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
if "supply_contracts" == "sub_distributors" and not data.get("sub_dist_code"):
data["sub_dist_code"] = f"SUB-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
if "supply_contracts" == "supply_contracts" and not data.get("contract_code"):
data["contract_code"] = f"SC-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
if "supply_contracts" == "distribution_agreements" and not data.get("agreement_code"):
data["agreement_code"] = f"DA-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.C("supply_contracts", data)
return json.dumps({"status": "ok", "data": data})

View File

@ -0,0 +1,19 @@
import json
async def main(request, params_kw):
"""Delete a supply_contracts record."""
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
record_id = data.get("id")
if not record_id:
return json.dumps({"status": "error", "message": "Missing record id"})
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.D("supply_contracts", {"id": record_id})
return json.dumps({"status": "ok", "message": "Deleted successfully"})

View File

@ -0,0 +1,27 @@
import json
from datetime import datetime
async def main(request, params_kw):
"""Update a supply_contracts record."""
user_id = await get_user()
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
record_id = data.get("id")
if not record_id:
return json.dumps({"status": "error", "message": "Missing record id"})
data["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Remove fields that should not be updated
for key in ["id", "resellerid", "created_by", "created_at"]:
data.pop(key, None)
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.U("supply_contracts", data)
return json.dumps({"status": "ok", "data": data})

View File

@ -0,0 +1,34 @@
import json
from appPublic.uniqueID import getID
from datetime import datetime
async def main(request, params_kw):
"""Create a new supplychain_accounting record."""
user_id = await get_user()
user_orgid = await get_userorgid()
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
data["id"] = getID()
data["resellerid"] = user_orgid
data["created_by"] = user_id
data["created_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Auto-generate codes if needed
if "supplychain_accounting" == "suppliers" and not data.get("supplier_code"):
data["supplier_code"] = f"SUP-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
if "supplychain_accounting" == "sub_distributors" and not data.get("sub_dist_code"):
data["sub_dist_code"] = f"SUB-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
if "supplychain_accounting" == "supply_contracts" and not data.get("contract_code"):
data["contract_code"] = f"SC-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
if "supplychain_accounting" == "distribution_agreements" and not data.get("agreement_code"):
data["agreement_code"] = f"DA-{datetime.now().strftime('%Y%m%d')}-{getID()[:4]}"
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.C("supplychain_accounting", data)
return json.dumps({"status": "ok", "data": data})

View File

@ -0,0 +1,19 @@
import json
async def main(request, params_kw):
"""Delete a supplychain_accounting record."""
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
record_id = data.get("id")
if not record_id:
return json.dumps({"status": "error", "message": "Missing record id"})
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.D("supplychain_accounting", {"id": record_id})
return json.dumps({"status": "ok", "message": "Deleted successfully"})

View File

@ -0,0 +1,27 @@
import json
from datetime import datetime
async def main(request, params_kw):
"""Update a supplychain_accounting record."""
user_id = await get_user()
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
record_id = data.get("id")
if not record_id:
return json.dumps({"status": "error", "message": "Missing record id"})
data["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Remove fields that should not be updated
for key in ["id", "resellerid", "created_by", "created_at"]:
data.pop(key, None)
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.U("supplychain_accounting", data)
return json.dumps({"status": "ok", "data": data})

View File

@ -0,0 +1,25 @@
{
"widgettype": "VBox",
"options": {
"width": "100%",
"height": "100%",
"padding": "16px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "分销协议管理",
"fontSize": "20px",
"fontWeight": "bold",
"marginBottom": "16px"
}
},
{
"widgettype": "DataViewer",
"options": {
"url": "{{entire_url('distribution_agreements_list/index.ui')}}"
}
}
]
}

163
wwwroot/index.ui Normal file
View File

@ -0,0 +1,163 @@
{
"widgettype": "VBox",
"options": {
"width": "100%",
"height": "100%",
"padding": "20px",
"bgcolor": "#F5F5F5"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "供销链管理",
"fontSize": "24px",
"fontWeight": "bold",
"color": "#333333",
"marginBottom": "20px"
}
},
{
"widgettype": "Text",
"options": {
"text": "供应商、合同、二级分销商、分销协议及供销记账",
"fontSize": "14px",
"color": "#666666",
"marginBottom": "24px"
}
},
{
"widgettype": "ResponsableBox",
"options": {
"gap": "16px",
"minWidth": "250px"
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {
"bgcolor": "#FFFFFF",
"padding": "20px",
"borderRadius": "8px",
"cursor": "pointer",
"boxShadow": "0 2px 4px rgba(0,0,0,0.1)"
},
"binds": [{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.supplychain_content",
"options": {"url": "{{entire_url('suppliers.ui')}}"},
"mode": "replace"
}],
"subwidgets": [
{"widgettype": "Text", "options": {"text": "🏭", "fontSize": "32px", "textAlign": "center"}},
{"widgettype": "Text", "options": {"text": "供应商管理", "fontSize": "16px", "fontWeight": "bold", "textAlign": "center", "marginTop": "8px"}},
{"widgettype": "Text", "options": {"text": "添加和管理供应商信息", "fontSize": "12px", "color": "#999999", "textAlign": "center"}}
]
},
{
"widgettype": "VBox",
"options": {
"bgcolor": "#FFFFFF",
"padding": "20px",
"borderRadius": "8px",
"cursor": "pointer",
"boxShadow": "0 2px 4px rgba(0,0,0,0.1)"
},
"binds": [{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.supplychain_content",
"options": {"url": "{{entire_url('supply_contracts.ui')}}"},
"mode": "replace"
}],
"subwidgets": [
{"widgettype": "Text", "options": {"text": "📋", "fontSize": "32px", "textAlign": "center"}},
{"widgettype": "Text", "options": {"text": "供销合同", "fontSize": "16px", "fontWeight": "bold", "textAlign": "center", "marginTop": "8px"}},
{"widgettype": "Text", "options": {"text": "管理与供应商的供销合同及产品折扣", "fontSize": "12px", "color": "#999999", "textAlign": "center"}}
]
},
{
"widgettype": "VBox",
"options": {
"bgcolor": "#FFFFFF",
"padding": "20px",
"borderRadius": "8px",
"cursor": "pointer",
"boxShadow": "0 2px 4px rgba(0,0,0,0.1)"
},
"binds": [{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.supplychain_content",
"options": {"url": "{{entire_url('sub_distributors.ui')}}"},
"mode": "replace"
}],
"subwidgets": [
{"widgettype": "Text", "options": {"text": "🏪", "fontSize": "32px", "textAlign": "center"}},
{"widgettype": "Text", "options": {"text": "二级分销商", "fontSize": "16px", "fontWeight": "bold", "textAlign": "center", "marginTop": "8px"}},
{"widgettype": "Text", "options": {"text": "添加和管理二级分销商", "fontSize": "12px", "color": "#999999", "textAlign": "center"}}
]
},
{
"widgettype": "VBox",
"options": {
"bgcolor": "#FFFFFF",
"padding": "20px",
"borderRadius": "8px",
"cursor": "pointer",
"boxShadow": "0 2px 4px rgba(0,0,0,0.1)"
},
"binds": [{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.supplychain_content",
"options": {"url": "{{entire_url('distribution_agreements.ui')}}"},
"mode": "replace"
}],
"subwidgets": [
{"widgettype": "Text", "options": {"text": "📝", "fontSize": "32px", "textAlign": "center"}},
{"widgettype": "Text", "options": {"text": "分销协议", "fontSize": "16px", "fontWeight": "bold", "textAlign": "center", "marginTop": "8px"}},
{"widgettype": "Text", "options": {"text": "管理与二级分销商的分销协议及产品折扣", "fontSize": "12px", "color": "#999999", "textAlign": "center"}}
]
},
{
"widgettype": "VBox",
"options": {
"bgcolor": "#FFFFFF",
"padding": "20px",
"borderRadius": "8px",
"cursor": "pointer",
"boxShadow": "0 2px 4px rgba(0,0,0,0.1)"
},
"binds": [{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.supplychain_content",
"options": {"url": "{{entire_url('accounting.ui')}}"},
"mode": "replace"
}],
"subwidgets": [
{"widgettype": "Text", "options": {"text": "💰", "fontSize": "32px", "textAlign": "center"}},
{"widgettype": "Text", "options": {"text": "供销记账", "fontSize": "16px", "fontWeight": "bold", "textAlign": "center", "marginTop": "8px"}},
{"widgettype": "Text", "options": {"text": "查看供销关系记账记录和利润统计", "fontSize": "12px", "color": "#999999", "textAlign": "center"}}
]
}
]
},
{
"widgettype": "VBox",
"id": "supplychain_content",
"options": {
"width": "100%",
"flex": "1",
"marginTop": "20px"
}
}
]
}

27
wwwroot/menu.ui Normal file
View File

@ -0,0 +1,27 @@
{
"widgettype": "Menu",
"options": {
"items": [
{
"name": "供应商管理",
"url": "{{entire_url('suppliers.ui')}}"
},
{
"name": "供销合同",
"url": "{{entire_url('supply_contracts.ui')}}"
},
{
"name": "二级分销商",
"url": "{{entire_url('sub_distributors.ui')}}"
},
{
"name": "分销协议",
"url": "{{entire_url('distribution_agreements.ui')}}"
},
{
"name": "供销记账",
"url": "{{entire_url('accounting.ui')}}"
}
]
}
}

View File

@ -0,0 +1,25 @@
{
"widgettype": "VBox",
"options": {
"width": "100%",
"height": "100%",
"padding": "16px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "二级分销商管理",
"fontSize": "20px",
"fontWeight": "bold",
"marginBottom": "16px"
}
},
{
"widgettype": "DataViewer",
"options": {
"url": "{{entire_url('sub_distributors_list/index.ui')}}"
}
}
]
}

25
wwwroot/suppliers.ui Normal file
View File

@ -0,0 +1,25 @@
{
"widgettype": "VBox",
"options": {
"width": "100%",
"height": "100%",
"padding": "16px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "供应商管理",
"fontSize": "20px",
"fontWeight": "bold",
"marginBottom": "16px"
}
},
{
"widgettype": "DataViewer",
"options": {
"url": "{{entire_url('suppliers_list/index.ui')}}"
}
}
]
}

View File

@ -0,0 +1,25 @@
{
"widgettype": "VBox",
"options": {
"width": "100%",
"height": "100%",
"padding": "16px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "供销合同管理",
"fontSize": "20px",
"fontWeight": "bold",
"marginBottom": "16px"
}
},
{
"widgettype": "DataViewer",
"options": {
"url": "{{entire_url('supply_contracts_list/index.ui')}}"
}
}
]
}