Initial commit: supplychain module - supplier, sub-reseller, contract, agreement, and sales ledger management

This commit is contained in:
yumoqing 2026-05-26 14:21:31 +08:00
parent 58f427d530
commit ed9c96d719
40 changed files with 1533 additions and 1001 deletions

329
README.md
View File

@ -1,236 +1,167 @@
# supplychain — 供应商和分销商管理模块 # Supplychain Module
## 概述 Supplier and Reseller Management module for Sage platform.
supplychain 模块为 Sage 平台提供供应商和分销商的全链路管理功能,包括: ## Overview
- **供应商管理**:添加和管理供应商基本信息、联系方式、财务信息 Manages the complete supply chain workflow for reseller/distributor organizations:
- **供销合同管理**:运营人员创建与供应商的供销合同,设置合同有效期和产品折扣 - **Operators** manage suppliers and supply contracts with product-level discount terms
- **二级分销商管理**:销售人员添加和管理二级分销商 - **Sales** manage sub-resellers (tier-2 distributors) and distribution agreements with product-level discount terms
- **分销协议管理**:销售人员与二级分销商签署分销协议,设置产品分销折扣 - **Sales ledger** records transactions and calculates supply/distribution amounts for accounting
- **供销记账**:产品销售时自动计算供销关系的记账金额(进货金额、分销金额、利润)
## 目录结构 ## Architecture
### Tables
| Table | Description |
|-------|-------------|
| `suppliers` | Supplier master data (contact, bank, tax info) |
| `supply_contracts` | Supply contracts between reseller and supplier |
| `supply_contract_items` | Product-level discount details per supply contract |
| `sub_resellers` | Sub-reseller (tier-2 distributor) master data |
| `distribution_agreements` | Distribution agreements between reseller and sub-reseller |
| `distribution_agreement_items` | Product-level discount details per distribution agreement |
| `sales_ledger` | Sales transaction records with calculated amounts |
### Discount Calculation Logic
`calculate_sale_amounts()` API is called during product sales:
1. Finds active supply contract for the supplier (filtered by date and status)
2. Looks up product-specific discount in contract items (priority: exact product > product type > default)
3. Finds active distribution agreement for the sub-reseller
4. Looks up product-specific discount in agreement items (same priority)
5. Calculates: supply_amount = total * supply_discount, distribution_amount = total * distribution_discount, profit = distribution - supply
### Role-Based Access
- `reseller.operator` — manage suppliers, supply contracts, and contract items
- `reseller.sale` — manage sub-resellers, distribution agreements, and agreement items
- `reseller.accountant` — view and manage sales ledger entries
## Directory Structure
``` ```
supplychain/ supplychain/
├── supplychain/ # Python 包 ├── supplychain/
│ ├── __init__.py │ ├── __init__.py # Package init
│ └── init.py # 模块初始化 + ServerEnv 注册 │ └── init.py # Module init + ServerEnv registration
├── wwwroot/ # 前端文件 ├── wwwroot/
│ ├── index.ui # 模块入口页 │ ├── index.ui # Module entry point
│ ├── menu.ui # 导航菜单 │ ├── menu.ui # Navigation menu
│ ├── suppliers.ui # 供应商管理页 │ └── api/ # CRUD API endpoints (.dspy files)
│ ├── supply_contracts.ui # 供销合同页 ├── models/ # Table definitions (JSON)
│ ├── sub_distributors.ui # 二级分销商页 ├── json/ # CRUD definitions (JSON)
│ ├── distribution_agreements.ui # 分销协议页 ├── init/ # Initialization data
│ ├── accounting.ui # 供销记账页
│ └── api/ # API 端点
│ ├── *_create.dspy
│ ├── *_update.dspy
│ ├── *_delete.dspy
│ ├── calculate_accounting.dspy # 记账计算 API
│ ├── query_supply_discount.dspy # 查询进货折扣
│ └── query_dist_discount.dspy # 查询分销折扣
├── models/ # 数据库表定义
│ ├── suppliers.json
│ ├── supply_contracts.json
│ ├── supply_contract_items.json
│ ├── sub_distributors.json
│ ├── distribution_agreements.json
│ ├── distribution_agreement_items.json
│ └── supplychain_accounting.json
├── json/ # CRUD 配置
│ ├── suppliers_list.json
│ ├── supply_contracts_list.json
│ ├── supply_contract_items_list.json
│ ├── sub_distributors_list.json
│ ├── distribution_agreements_list.json
│ ├── distribution_agreement_items_list.json
│ └── supplychain_accounting_list.json
├── init/
│ └── data.json # 初始化数据(可选)
├── scripts/
│ └── load_path.py # RBAC 权限管理脚本
├── pyproject.toml ├── pyproject.toml
├── build.sh ├── build.sh
└── README.md └── README.md
``` ```
## 数据库表 ## Sage Integration
| 表名 | 说明 | 类别 |
|------|------|------|
| suppliers | 供应商表 | entity |
| supply_contracts | 供销合同表 | entity |
| supply_contract_items | 供销合同产品折扣明细 | relation |
| sub_distributors | 二级分销商表 | entity |
| distribution_agreements | 分销协议表 | entity |
| distribution_agreement_items | 分销协议产品折扣明细 | relation |
| supplychain_accounting | 供销记账表 | relation |
## 角色权限
| 角色 | 权限范围 |
|------|----------|
| 运营 (operator) | 供应商管理、供销合同管理(含产品折扣) |
| 销售 (sale) | 二级分销商管理、分销协议管理(含产品折扣) |
| 所有登录用户 | 供销记账查看 |
## 折扣计算规则
### 进货折扣(供应商 → 分销商)
查找优先级:
1. 供销合同 → 产品明细(精确匹配 productid
2. 供销合同 → 产品明细(匹配 prodtypeid
3. 供销合同 → 默认折扣default_discount
### 分销折扣(分销商 → 二级分销商)
查找优先级:
1. 分销协议 → 产品明细(精确匹配 productid
2. 分销协议 → 产品明细(匹配 prodtypeid
3. 分销协议 → 默认折扣default_discount
### 记账金额计算
```
销售总额 = 单价 × 数量
进货金额 = 销售总额 × 进货折扣 (或 结算单价 × 数量)
分销金额 = 销售总额 × 分销折扣 (或 结算单价 × 数量)
利润金额 = 分销金额 - 进货金额
```
## API 接口
### 记账计算 API
**端点**: `/supplychain/api/calculate_accounting.dspy`
**请求方法**: POST
**参数**:
```json
{
"productid": "产品ID",
"prodtypeid": "产品分类ID (可选)",
"quantity": 10,
"unit_price": 100.00,
"sub_distributor_id": "二级分销商ID (可选)",
"sale_date": "2026-05-25 (可选, 默认今天)",
"source_type": "2 (来源类型: 1=手动, 2=API)",
"source_id": "来源记录ID (可选)",
"remark": "备注 (可选)"
}
```
**返回**:
```json
{
"status": "ok",
"data": {...},
"summary": {
"total_amount": 1000.00,
"supply_amount": 700.00,
"dist_amount": 900.00,
"profit_amount": 200.00,
"supply_discount": 0.7,
"dist_discount": 0.9
}
}
```
### 折扣查询 API
**进货折扣查询**: `/supplychain/api/query_supply_discount.dspy`
- 参数: `productid`, `prodtypeid` (可选)
**分销折扣查询**: `/supplychain/api/query_dist_discount.dspy`
- 参数: `sub_distributor_id`, `productid`, `prodtypeid` (可选)
## Python 模块函数
通过 `ServerEnv` 注册后可在 .dspy 文件中直接调用:
```python
# 获取进货折扣
supply_info = await get_active_supply_discount(sor, resellerid, productid, prodtypeid, sale_date)
# 获取分销折扣
dist_info = await get_active_dist_discount(sor, resellerid, sub_distributor_id, productid, prodtypeid, sale_date)
# 计算并创建记账记录
record = await calculate_sale_accounting(sor, resellerid, productid, quantity, unit_price,
sub_distributor_id, prodtypeid, sale_date, source_type, source_id, created_by, remark)
```
## 安装步骤
### 1. 克隆模块
### 1. Install module
```bash ```bash
cd ~/repos cd ~/repos/sage/pkgs
git clone git@git.opencomputing.cn:yumoqing/supplychain.git git clone https://git.opencomputing.cn/yumoqing/supplychain
cd supplychain
../../py3/bin/pip install .
``` ```
### 2. 构建模块 ### 2. Generate DDL and apply to database
```bash ```bash
cd ~/repos/supplychain cd ~/repos/sage/pkgs/supplychain/models
chmod +x build.sh ../../py3/bin/json2ddl mysql . > mysql.ddl.sql
./build.sh mysql -h <db_host> -u <db_user> -p sage < mysql.ddl.sql
``` ```
### 3. 生成 DDL 并创建数据库表 ### 3. Generate CRUD UI files
```bash ```bash
cd ~/repos/supplychain/models cd ~/repos/sage/pkgs/supplychain/json
mysql -h <host> -u <user> -p <dbname> < mysql.ddl.sql ../../py3/bin/xls2ui -m ../models -o ../wwwroot supplychain *.json
``` ```
### 4. 集成到 Sage ### 4. Link wwwroot
```bash
cd ~/repos/sage/wwwroot
ln -sf ../pkgs/supplychain/wwwroot supplychain
```
**a. 修改 `app/sage.py`**: ### 5. Register module in Sage
Add to `~/repos/sage/app/sage.py`:
```python ```python
from supplychain.init import load_supplychain from supplychain.init import load_supplychain
# 在 init() 函数中添加: # In init():
load_supplychain() load_supplychain()
``` ```
**b. 修改 `build.sh`** (Sage 根目录): ### 6. Register RBAC permissions
```bash Add to `~/repos/sage/load_path.py`:
for m in ... supplychain ```
/supplychain logined
/supplychain/index.ui logined
/supplychain/menu.ui any
/supplychain/suppliers_list logined
/supplychain/suppliers_list/index.ui logined
/supplychain/supply_contracts_list logined
/supplychain/supply_contracts_list/index.ui logined
/supplychain/supply_contract_items_list logined
/supplychain/supply_contract_items_list/index.ui logined
/supplychain/sub_resellers_list logined
/supplychain/sub_resellers_list/index.ui logined
/supplychain/distribution_agreements_list logined
/supplychain/distribution_agreements_list/index.ui logined
/supplychain/distribution_agreement_items_list logined
/supplychain/distribution_agreement_items_list/index.ui logined
/supplychain/sales_ledger_list logined
/supplychain/sales_ledger_list/index.ui logined
/supplychain/api/suppliers_create.dspy logined
/supplychain/api/suppliers_update.dspy logined
/supplychain/api/suppliers_delete.dspy logined
/supplychain/api/supply_contracts_create.dspy logined
/supplychain/api/supply_contracts_update.dspy logined
/supplychain/api/supply_contracts_delete.dspy logined
/supplychain/api/supply_contract_items_create.dspy logined
/supplychain/api/supply_contract_items_update.dspy logined
/supplychain/api/supply_contract_items_delete.dspy logined
/supplychain/api/sub_resellers_create.dspy logined
/supplychain/api/sub_resellers_update.dspy logined
/supplychain/api/sub_resellers_delete.dspy logined
/supplychain/api/distribution_agreements_create.dspy logined
/supplychain/api/distribution_agreements_update.dspy logined
/supplychain/api/distribution_agreements_delete.dspy logined
/supplychain/api/distribution_agreement_items_create.dspy logined
/supplychain/api/distribution_agreement_items_update.dspy logined
/supplychain/api/distribution_agreement_items_delete.dspy logined
/supplychain/api/sales_ledger_create.dspy logined
/supplychain/api/sales_ledger_update.dspy logined
/supplychain/api/sales_ledger_delete.dspy logined
``` ```
### 5. 注册 RBAC 权限 Then run:
模块提供 `scripts/load_path.py` 权限管理脚本,自动注册所有路径:
```bash ```bash
cd ~/repos/sage cd ~/repos/sage && ./py3/bin/python load_path.py
./py3/bin/python ~/repos/supplychain/scripts/load_path.py
``` ```
脚本按角色分类注册: ### 7. Restart Sage
- `any`: 静态资源/CRUD 别名目录
- `logined`: 所有页面和 API
- `reseller.operator`: 供应商、供销合同管理
- `reseller.sale`: 二级分销商、分销协议管理
**维护规则**: 每次代码变更如有新 path 出现,需同步更新 `scripts/load_path.py` 中的路径列表。
### 6. 重启 Sage
```bash ```bash
cd ~/repos/sage cd ~/repos/sage && ./stop.sh && ./start.sh
./stop.sh && ./start.sh
``` ```
## 产品模块引用 ## API for External Modules
本模块的 `productid``prodtypeid` 字段引用 product 模块的 `products``product_types` 表。 Other modules can call `calculate_sale_amounts()` to compute supply/distribution amounts during sales:
已在以下模型的 `codes` 段添加引用配置:
- `models/supply_contract_items.json` ```python
- `models/distribution_agreement_items.json` env = ServerEnv()
- `models/supplychain_accounting.json` result = await env.calculate_sale_amounts(request, {
"resellerid": org_id,
"sub_reseller_id": sub_reseller_id, # optional
"supplier_id": supplier_id, # optional
"prodtypeid": product_type_id,
"productid": product_id,
"quantity": qty,
"unit_price": price,
})
# Returns: {contract_id, agreement_id, total_amount, supply_discount, supply_amount, distribution_discount, distribution_amount, profit_amount}
```

View File

@ -1,10 +1,12 @@
#!/usr/bin/env bash #!/bin/bash
# Supplychain module build script
# Links wwwroot files to Sage and generates DDL/CRUD UI files
set -e set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Find Sage root directory # Find Sage root
SAGE_ROOT="" SAGE_ROOT=""
for candidate in "$SCRIPT_DIR/../.." "$HOME/repos/sage" "$HOME/sage"; do for candidate in "$SCRIPT_DIR/../.." "$HOME/repos/sage" "$HOME/sage"; do
if [ -d "$candidate/wwwroot" ] && [ -d "$candidate/py3/bin" ]; then if [ -d "$candidate/wwwroot" ] && [ -d "$candidate/py3/bin" ]; then
@ -14,38 +16,38 @@ for candidate in "$SCRIPT_DIR/../.." "$HOME/repos/sage" "$HOME/sage"; do
done done
if [ -z "$SAGE_ROOT" ]; then if [ -z "$SAGE_ROOT" ]; then
echo "ERROR: Cannot find Sage root directory" echo "ERROR: Sage root not found"
exit 1 exit 1
fi fi
echo "Sage root: $SAGE_ROOT" echo "Sage root: $SAGE_ROOT"
# Install module # Generate DDL from models if present
cd "$SCRIPT_DIR"
$SAGE_ROOT/py3/bin/pip install -e .
# Generate DDL from models
if [ -d "$SCRIPT_DIR/models" ]; then if [ -d "$SCRIPT_DIR/models" ]; then
echo "Generating DDL..." echo "Generating DDL from models..."
cd "$SCRIPT_DIR/models" cd "$SCRIPT_DIR/models"
$SAGE_ROOT/py3/bin/json2ddl mysql . > mysql.ddl.sql if ls *.xlsx 1>/dev/null 2>&1; then
echo "DDL generated: $SCRIPT_DIR/models/mysql.ddl.sql" "$SAGE_ROOT/py3/bin/xls2ddl" mysql . > mysql.ddl.sql
echo "DDL generated: models/mysql.ddl.sql"
elif ls *.json 1>/dev/null 2>&1; then
"$SAGE_ROOT/py3/bin/json2ddl" mysql . > mysql.ddl.sql
echo "DDL generated: models/mysql.ddl.sql"
fi
fi fi
# Generate CRUD UI from json definitions # Generate CRUD UI from json definitions if present
if [ -d "$SCRIPT_DIR/json" ]; then if [ -d "$SCRIPT_DIR/json" ]; then
echo "Generating CRUD UI files..." echo "Generating CRUD UI files..."
cd "$SCRIPT_DIR/json" cd "$SCRIPT_DIR/json"
for f in *.json; do if [ -d "$SCRIPT_DIR/models" ]; then
echo " Processing $f..." "$SAGE_ROOT/py3/bin/xls2ui" -m "$SCRIPT_DIR/models" -o "$SCRIPT_DIR/wwwroot" supplychain *.json
done echo "CRUD UI files generated in wwwroot/"
$SAGE_ROOT/py3/bin/xls2ui -m ../models -o ../wwwroot supplychain *.json fi
echo "CRUD UI files generated."
fi fi
# Create symlink in Sage wwwroot # Link wwwroot to Sage
echo "Creating wwwroot symlink..." echo "Linking wwwroot to Sage..."
rm -f "$SAGE_ROOT/wwwroot/supplychain" rm -f "$SAGE_ROOT/wwwroot/supplychain"
ln -s "$SCRIPT_DIR/wwwroot" "$SAGE_ROOT/wwwroot/supplychain" ln -sf "$SCRIPT_DIR/wwwroot" "$SAGE_ROOT/wwwroot/supplychain"
echo "Build complete." echo "Supplychain module build complete."

View File

@ -3,21 +3,11 @@
"alias": "distribution_agreement_items_list", "alias": "distribution_agreement_items_list",
"title": "分销协议产品折扣", "title": "分销协议产品折扣",
"params": { "params": {
"sortby": [ "sortby": ["prodtypeid", "productid"],
"created_at desc"
],
"logined_userorgid": "resellerid", "logined_userorgid": "resellerid",
"browserfields": { "browserfields": {
"exclouded": [ "exclouded": ["id", "agreement_id", "resellerid"]
"created_at"
]
}, },
"editexclouded": [
"id",
"resellerid",
"agreement_id",
"created_at"
],
"editable": { "editable": {
"new_data_url": "{{entire_url('../api/distribution_agreement_items_create.dspy')}}", "new_data_url": "{{entire_url('../api/distribution_agreement_items_create.dspy')}}",
"update_data_url": "{{entire_url('../api/distribution_agreement_items_update.dspy')}}", "update_data_url": "{{entire_url('../api/distribution_agreement_items_update.dspy')}}",

View File

@ -3,47 +3,37 @@
"alias": "distribution_agreements_list", "alias": "distribution_agreements_list",
"title": "分销协议管理", "title": "分销协议管理",
"params": { "params": {
"sortby": [ "sortby": ["created_at desc"],
"created_at desc"
],
"logined_userorgid": "resellerid", "logined_userorgid": "resellerid",
"data_filter": {
"AND": [
{"field": "agreement_name", "op": "LIKE", "var": "agreement_name"},
{"field": "agreement_code", "op": "LIKE", "var": "agreement_code"},
{"field": "status", "op": "=", "var": "status"}
]
},
"filter_labels": {
"agreement_name": "协议名称",
"agreement_code": "协议编号",
"status": "状态"
},
"browserfields": { "browserfields": {
"exclouded": [ "exclouded": ["id"],
"created_by",
"created_at",
"updated_at"
],
"alters": { "alters": {
"status": { "status": {
"uitype": "code", "uitype": "code",
"data": [ "data": [
{ {"value": "1", "text": "生效中"},
"value": "1", {"value": "2", "text": "已到期"},
"text": "有效" {"value": "0", "text": "已终止"}
},
{
"value": "0",
"text": "无效"
},
{
"value": "2",
"text": "已过期"
}
] ]
} }
} }
}, },
"editexclouded": [
"id",
"resellerid",
"created_by",
"created_at",
"updated_at"
],
"subtables": [ "subtables": [
{ {
"field": "id", "field": "agreement_id",
"title": "产品折扣", "title": "产品分销折扣",
"url": "{{entire_url('../distribution_agreement_items_list')}}", "url": "{{entire_url('../distribution_agreement_items_list')}}",
"subtable": "distribution_agreement_items" "subtable": "distribution_agreement_items"
} }

View File

@ -0,0 +1,40 @@
{
"tblname": "sales_ledger",
"alias": "sales_ledger_list",
"title": "销售记账",
"params": {
"sortby": ["sale_date desc", "created_at desc"],
"logined_userorgid": "resellerid",
"data_filter": {
"AND": [
{"field": "sale_date", "op": ">=", "var": "sale_date_start"},
{"field": "sale_date", "op": "<=", "var": "sale_date_end"},
{"field": "productid", "op": "=", "var": "productid"},
{"field": "settlement_status", "op": "=", "var": "settlement_status"}
]
},
"filter_labels": {
"sale_date_start": "销售开始日期",
"sale_date_end": "销售结束日期",
"productid": "产品ID",
"settlement_status": "结算状态"
},
"browserfields": {
"exclouded": ["id"],
"alters": {
"settlement_status": {
"uitype": "code",
"data": [
{"value": "0", "text": "未结算"},
{"value": "1", "text": "已结算"}
]
}
}
},
"editable": {
"new_data_url": "{{entire_url('../api/sales_ledger_create.dspy')}}",
"update_data_url": "{{entire_url('../api/sales_ledger_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/sales_ledger_delete.dspy')}}"
}
}
}

View File

@ -0,0 +1,36 @@
{
"tblname": "sub_resellers",
"alias": "sub_resellers_list",
"title": "二级分销商管理",
"params": {
"sortby": ["created_at desc"],
"logined_userorgid": "resellerid",
"data_filter": {
"AND": [
{"field": "sub_reseller_name", "op": "LIKE", "var": "sub_reseller_name"},
{"field": "status", "op": "=", "var": "status"}
]
},
"filter_labels": {
"sub_reseller_name": "二级分销商名称",
"status": "状态"
},
"browserfields": {
"exclouded": ["id"],
"alters": {
"status": {
"uitype": "code",
"data": [
{"value": "1", "text": "正常"},
{"value": "0", "text": "停用"}
]
}
}
},
"editable": {
"new_data_url": "{{entire_url('../api/sub_resellers_create.dspy')}}",
"update_data_url": "{{entire_url('../api/sub_resellers_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/sub_resellers_delete.dspy')}}"
}
}
}

View File

@ -3,24 +3,30 @@
"alias": "suppliers_list", "alias": "suppliers_list",
"title": "供应商管理", "title": "供应商管理",
"params": { "params": {
"sortby": [ "sortby": ["created_at desc"],
"created_at desc"
],
"logined_userorgid": "resellerid", "logined_userorgid": "resellerid",
"browserfields": { "data_filter": {
"exclouded": [ "AND": [
"created_by", {"field": "supplier_name", "op": "LIKE", "var": "supplier_name"},
"created_at", {"field": "status", "op": "=", "var": "status"}
"updated_at"
] ]
}, },
"editexclouded": [ "filter_labels": {
"id", "supplier_name": "供应商名称",
"resellerid", "status": "状态"
"created_by", },
"created_at", "browserfields": {
"updated_at" "exclouded": ["id"],
], "alters": {
"status": {
"uitype": "code",
"data": [
{"value": "1", "text": "正常"},
{"value": "0", "text": "停用"}
]
}
}
},
"editable": { "editable": {
"new_data_url": "{{entire_url('../api/suppliers_create.dspy')}}", "new_data_url": "{{entire_url('../api/suppliers_create.dspy')}}",
"update_data_url": "{{entire_url('../api/suppliers_update.dspy')}}", "update_data_url": "{{entire_url('../api/suppliers_update.dspy')}}",

View File

@ -3,21 +3,11 @@
"alias": "supply_contract_items_list", "alias": "supply_contract_items_list",
"title": "供销合同产品折扣", "title": "供销合同产品折扣",
"params": { "params": {
"sortby": [ "sortby": ["prodtypeid", "productid"],
"created_at desc"
],
"logined_userorgid": "resellerid", "logined_userorgid": "resellerid",
"browserfields": { "browserfields": {
"exclouded": [ "exclouded": ["id", "contract_id", "resellerid"]
"created_at"
]
}, },
"editexclouded": [
"id",
"resellerid",
"contract_id",
"created_at"
],
"editable": { "editable": {
"new_data_url": "{{entire_url('../api/supply_contract_items_create.dspy')}}", "new_data_url": "{{entire_url('../api/supply_contract_items_create.dspy')}}",
"update_data_url": "{{entire_url('../api/supply_contract_items_update.dspy')}}", "update_data_url": "{{entire_url('../api/supply_contract_items_update.dspy')}}",

View File

@ -3,47 +3,37 @@
"alias": "supply_contracts_list", "alias": "supply_contracts_list",
"title": "供销合同管理", "title": "供销合同管理",
"params": { "params": {
"sortby": [ "sortby": ["created_at desc"],
"created_at desc"
],
"logined_userorgid": "resellerid", "logined_userorgid": "resellerid",
"data_filter": {
"AND": [
{"field": "contract_name", "op": "LIKE", "var": "contract_name"},
{"field": "contract_code", "op": "LIKE", "var": "contract_code"},
{"field": "status", "op": "=", "var": "status"}
]
},
"filter_labels": {
"contract_name": "合同名称",
"contract_code": "合同编号",
"status": "状态"
},
"browserfields": { "browserfields": {
"exclouded": [ "exclouded": ["id"],
"created_by",
"created_at",
"updated_at"
],
"alters": { "alters": {
"status": { "status": {
"uitype": "code", "uitype": "code",
"data": [ "data": [
{ {"value": "1", "text": "生效中"},
"value": "1", {"value": "2", "text": "已到期"},
"text": "有效" {"value": "0", "text": "已终止"}
},
{
"value": "0",
"text": "无效"
},
{
"value": "2",
"text": "已过期"
}
] ]
} }
} }
}, },
"editexclouded": [
"id",
"resellerid",
"created_by",
"created_at",
"updated_at"
],
"subtables": [ "subtables": [
{ {
"field": "id", "field": "contract_id",
"title": "产品折扣", "title": "产品折扣明细",
"url": "{{entire_url('../supply_contract_items_list')}}", "url": "{{entire_url('../supply_contract_items_list')}}",
"subtable": "supply_contract_items" "subtable": "supply_contract_items"
} }

View File

@ -24,7 +24,7 @@
}, },
{ {
"name": "resellerid", "name": "resellerid",
"title": "所属分销商机构ID", "title": "所属分销商机构ID",
"type": "str", "type": "str",
"length": 32, "length": 32,
"nullable": "no" "nullable": "no"
@ -51,8 +51,13 @@
"default": "1.0000" "default": "1.0000"
}, },
{ {
"name": "settlement_price", "name": "min_order_qty",
"title": "结算单价", "title": "最小订购量",
"type": "int"
},
{
"name": "sale_price",
"title": "分销指导价",
"type": "double", "type": "double",
"length": 15, "length": 15,
"dec": 4 "dec": 4
@ -87,18 +92,6 @@
"table": "distribution_agreements", "table": "distribution_agreements",
"valuefield": "id", "valuefield": "id",
"textfield": "agreement_name" "textfield": "agreement_name"
},
{
"field": "prodtypeid",
"table": "product_types",
"valuefield": "id",
"textfield": "type_name"
},
{
"field": "productid",
"table": "products",
"valuefield": "id",
"textfield": "product_name"
} }
] ]
} }

View File

@ -17,13 +17,13 @@
}, },
{ {
"name": "resellerid", "name": "resellerid",
"title": "所属分销商机构ID", "title": "所属分销商机构ID",
"type": "str", "type": "str",
"length": 32, "length": 32,
"nullable": "no" "nullable": "no"
}, },
{ {
"name": "sub_distributor_id", "name": "sub_reseller_id",
"title": "二级分销商ID", "title": "二级分销商ID",
"type": "str", "type": "str",
"length": 32, "length": 32,
@ -105,9 +105,9 @@
"idxfields": ["resellerid"] "idxfields": ["resellerid"]
}, },
{ {
"name": "idx_da_subdist", "name": "idx_da_sub_reseller",
"idxtype": "index", "idxtype": "index",
"idxfields": ["sub_distributor_id"] "idxfields": ["sub_reseller_id"]
}, },
{ {
"name": "idx_da_code", "name": "idx_da_code",
@ -117,10 +117,10 @@
], ],
"codes": [ "codes": [
{ {
"field": "sub_distributor_id", "field": "sub_reseller_id",
"table": "sub_distributors", "table": "sub_resellers",
"valuefield": "id", "valuefield": "id",
"textfield": "sub_dist_name" "textfield": "sub_reseller_name"
}, },
{ {
"field": "resellerid", "field": "resellerid",

208
models/sales_ledger.json Normal file
View File

@ -0,0 +1,208 @@
{
"summary": [
{
"name": "sales_ledger",
"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": "sub_reseller_id",
"title": "二级分销商ID",
"type": "str",
"length": 32
},
{
"name": "supplier_id",
"title": "供应商ID",
"type": "str",
"length": 32
},
{
"name": "agreement_id",
"title": "分销协议ID",
"type": "str",
"length": 32
},
{
"name": "contract_id",
"title": "供销合同ID",
"type": "str",
"length": 32
},
{
"name": "prodtypeid",
"title": "产品分类ID",
"type": "str",
"length": 32
},
{
"name": "productid",
"title": "产品ID",
"type": "str",
"length": 32
},
{
"name": "sale_date",
"title": "销售日期",
"type": "date",
"nullable": "no"
},
{
"name": "quantity",
"title": "销售数量",
"type": "double",
"length": 15,
"dec": 2,
"nullable": "no"
},
{
"name": "unit_price",
"title": "销售单价",
"type": "double",
"length": 15,
"dec": 4,
"nullable": "no"
},
{
"name": "supply_discount",
"title": "进货折扣",
"type": "double",
"length": 5,
"dec": 4
},
{
"name": "supply_amount",
"title": "进货金额",
"type": "double",
"length": 15,
"dec": 2
},
{
"name": "distribution_discount",
"title": "分销折扣",
"type": "double",
"length": 5,
"dec": 4
},
{
"name": "distribution_amount",
"title": "分销金额",
"type": "double",
"length": 15,
"dec": 2
},
{
"name": "profit_amount",
"title": "利润金额",
"type": "double",
"length": 15,
"dec": 2
},
{
"name": "settlement_status",
"title": "结算状态",
"type": "char",
"length": 1,
"nullable": "no",
"default": "0"
},
{
"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_sl_reseller",
"idxtype": "index",
"idxfields": ["resellerid"]
},
{
"name": "idx_sl_sale_date",
"idxtype": "index",
"idxfields": ["sale_date"]
},
{
"name": "idx_sl_product",
"idxtype": "index",
"idxfields": ["prodtypeid", "productid"]
},
{
"name": "idx_sl_sub_reseller",
"idxtype": "index",
"idxfields": ["sub_reseller_id"]
},
{
"name": "idx_sl_supplier",
"idxtype": "index",
"idxfields": ["supplier_id"]
}
],
"codes": [
{
"field": "sub_reseller_id",
"table": "sub_resellers",
"valuefield": "id",
"textfield": "sub_reseller_name"
},
{
"field": "supplier_id",
"table": "suppliers",
"valuefield": "id",
"textfield": "supplier_name"
},
{
"field": "agreement_id",
"table": "distribution_agreements",
"valuefield": "id",
"textfield": "agreement_name"
},
{
"field": "contract_id",
"table": "supply_contracts",
"valuefield": "id",
"textfield": "contract_name"
},
{
"field": "resellerid",
"table": "organization",
"valuefield": "id",
"textfield": "orgname"
}
]
}

132
models/sub_resellers.json Normal file
View File

@ -0,0 +1,132 @@
{
"summary": [
{
"name": "sub_resellers",
"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_reseller_code",
"title": "二级分销商编号",
"type": "str",
"length": 64,
"nullable": "no"
},
{
"name": "sub_reseller_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_sr_reseller",
"idxtype": "index",
"idxfields": ["resellerid"]
},
{
"name": "idx_sr_code",
"idxtype": "unique",
"idxfields": ["resellerid", "sub_reseller_code"]
}
],
"codes": [
{
"field": "resellerid",
"table": "organization",
"valuefield": "id",
"textfield": "orgname"
}
]
}

View File

@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "supplychain" name = "supplychain"
version = "1.0.0" version = "1.0.0"
description = "供应商和分销商管理模块 — 供销合同、分销协议、产品折扣及供销记账" description = "Supply chain management module - suppliers, contracts, sub-resellers, distribution agreements, and sales ledger"
requires-python = ">=3.8" requires-python = ">=3.8"
dependencies = [ dependencies = [
"sqlor", "sqlor",

View File

@ -0,0 +1 @@
# supplychain Python package

View File

@ -1,252 +1,668 @@
from ahserver.serverenv import ServerEnv """
Supplychain module - Supplier and Reseller Management
Handles suppliers, supply contracts, sub-resellers, distribution agreements,
and sales ledger for accounting calculations.
"""
import json
from datetime import datetime, date
from appPublic.uniqueID import getID
from appPublic.jsonConfig import getConfig from appPublic.jsonConfig import getConfig
from appPublic.dictObject import DictObject from appPublic.dictObject import DictObject
from appPublic.uniqueID import getID
from sqlor.dbpools import DBPools from sqlor.dbpools import DBPools
from datetime import datetime from ahserver.serverenv import ServerEnv
import json
MODULE_NAME = "supplychain" MODULE_NAME = "supplychain"
MODULE_VERSION = "1.0.0"
def _get_dbname(): def _get_dbname():
"""Get the database name for the supplychain module.""" """Get module database name dynamically."""
env = ServerEnv() env = ServerEnv()
return env.get_module_dbname('supplychain') return env.get_module_dbname(MODULE_NAME)
def get_db_context(): def _get_sor():
"""Get a database context manager for the supplychain module.""" """Get a sqlor context for this module's database."""
config = getConfig('.') config = getConfig()
DBPools(config.databases) db = DBPools()
dbname = _get_dbname() db.databases = config.databases
return db.sqlorContext(dbname) return db, _get_dbname()
async def get_active_supply_discount(sor, resellerid, productid, prodtypeid=None, sale_date=None): def _generate_supplier_code(resellerid):
""" """Generate unique supplier code: SUP-{YYYYMMDD}-{seq}."""
Get active supply contract discount for a product. env = ServerEnv()
Priority: exact product > product type > contract default. today = env.strdate(env.today())
prefix = f"SUP-{today.replace('-', '')}"
db, dbname = _get_sor()
with db.sqlorContext(dbname) as sor:
sql = """SELECT COUNT(*) as cnt FROM suppliers
WHERE resellerid = ${resellerid}$ AND supplier_code LIKE ${prefix}$"""
recs = sor.sqlExe(sql, {"resellerid": resellerid, "prefix": prefix + "%"})
seq = (recs[0].cnt if recs else 0) + 1
return f"{prefix}-{seq:04d}"
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 def _generate_contract_code(resellerid):
sql = """SELECT sci.id, sci.contract_id, sci.discount, sci.settlement_price, sc.supplier_id """Generate unique contract code: SC-{YYYYMMDD}-{seq}."""
FROM supply_contract_items sci env = ServerEnv()
JOIN supply_contracts sc ON sci.contract_id = sc.id today = env.strdate(env.today())
WHERE sci.resellerid = ${resellerid}$ prefix = f"SC-{today.replace('-', '')}"
AND sc.status = '1' db, dbname = _get_sor()
AND sc.start_date <= ${sale_date}$ with db.sqlorContext(dbname) as sor:
AND (sc.end_date IS NULL OR sc.end_date >= ${sale_date}$) sql = """SELECT COUNT(*) as cnt FROM supply_contracts
AND sci.productid = ${productid}$ WHERE resellerid = ${resellerid}$ AND contract_code LIKE ${prefix}$"""
ORDER BY sc.start_date DESC LIMIT 1""" recs = sor.sqlExe(sql, {"resellerid": resellerid, "prefix": prefix + "%"})
recs = await sor.sqlExe(sql, {"resellerid": resellerid, "sale_date": sale_date, "productid": productid}) seq = (recs[0].cnt if recs else 0) + 1
return f"{prefix}-{seq:04d}"
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: def _generate_sub_reseller_code(resellerid):
return { """Generate unique sub-reseller code: SR-{YYYYMMDD}-{seq}."""
"contract_item_id": recs[0].id, env = ServerEnv()
"contract_id": recs[0].contract_id, today = env.strdate(env.today())
"discount": float(recs[0].discount) if recs[0].discount else 1.0, prefix = f"SR-{today.replace('-', '')}"
"settlement_price": float(recs[0].settlement_price) if recs[0].settlement_price else None, db, dbname = _get_sor()
"supplier_id": recs[0].supplier_id, with db.sqlorContext(dbname) as sor:
sql = """SELECT COUNT(*) as cnt FROM sub_resellers
WHERE resellerid = ${resellerid}$ AND sub_reseller_code LIKE ${prefix}$"""
recs = sor.sqlExe(sql, {"resellerid": resellerid, "prefix": prefix + "%"})
seq = (recs[0].cnt if recs else 0) + 1
return f"{prefix}-{seq:04d}"
def _generate_agreement_code(resellerid):
"""Generate unique agreement code: DA-{YYYYMMDD}-{seq}."""
env = ServerEnv()
today = env.strdate(env.today())
prefix = f"DA-{today.replace('-', '')}"
db, dbname = _get_sor()
with db.sqlorContext(dbname) as sor:
sql = """SELECT COUNT(*) as cnt FROM distribution_agreements
WHERE resellerid = ${resellerid}$ AND agreement_code LIKE ${prefix}$"""
recs = sor.sqlExe(sql, {"resellerid": resellerid, "prefix": prefix + "%"})
seq = (recs[0].cnt if recs else 0) + 1
return f"{prefix}-{seq:04d}"
# ============================================================
# Supplier APIs
# ============================================================
async def create_supplier(request, params_kw):
"""Create a new supplier record."""
env = ServerEnv()
user_id = await env.get_user()
resellerid = await env.get_userorgid()
data = params_kw
db, dbname = _get_sor()
async with db.sqlorContext(dbname) as sor:
supplier_code = data.get("supplier_code") or _generate_supplier_code(resellerid)
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
rec = {
"id": getID(),
"resellerid": resellerid,
"supplier_code": supplier_code,
"supplier_name": data.get("supplier_name"),
"contact_person": data.get("contact_person"),
"contact_phone": data.get("contact_phone"),
"contact_email": data.get("contact_email"),
"address": data.get("address"),
"tax_number": data.get("tax_number"),
"bank_name": data.get("bank_name"),
"bank_account": data.get("bank_account"),
"status": data.get("status", "1"),
"remark": data.get("remark"),
"created_by": user_id,
"created_at": now,
"updated_at": now,
} }
await sor.C("suppliers", rec)
return json.dumps({"status": "ok", "data": rec, "message": "创建成功"})
# 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: async def update_supplier(request, params_kw):
return { """Update a supplier record."""
"contract_item_id": None, data = params_kw
"contract_id": recs[0].id, db, dbname = _get_sor()
"discount": float(recs[0].default_discount) if recs[0].default_discount else 1.0, async with db.sqlorContext(dbname) as sor:
"settlement_price": None, now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
"supplier_id": recs[0].supplier_id, rec = {"id": data["id"], "updated_at": now}
for key in ["supplier_name", "contact_person", "contact_phone", "contact_email",
"address", "tax_number", "bank_name", "bank_account", "status", "remark"]:
if key in data:
rec[key] = data[key]
await sor.U("suppliers", rec)
return json.dumps({"status": "ok", "message": "更新成功"})
async def delete_supplier(request, params_kw):
"""Delete a supplier record."""
data = params_kw
db, dbname = _get_sor()
async with db.sqlorContext(dbname) as sor:
await sor.D("suppliers", {"id": data["id"]})
return json.dumps({"status": "ok", "message": "删除成功"})
# ============================================================
# Supply Contract APIs
# ============================================================
async def create_supply_contract(request, params_kw):
"""Create a new supply contract."""
env = ServerEnv()
user_id = await env.get_user()
resellerid = await env.get_userorgid()
data = params_kw
db, dbname = _get_sor()
async with db.sqlorContext(dbname) as sor:
contract_code = data.get("contract_code") or _generate_contract_code(resellerid)
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
rec = {
"id": getID(),
"resellerid": resellerid,
"supplier_id": data.get("supplier_id"),
"contract_code": contract_code,
"contract_name": data.get("contract_name"),
"sign_date": data.get("sign_date"),
"start_date": data.get("start_date"),
"end_date": data.get("end_date"),
"status": data.get("status", "1"),
"default_discount": data.get("default_discount", 1.0),
"remark": data.get("remark"),
"created_by": user_id,
"created_at": now,
"updated_at": now,
} }
await sor.C("supply_contracts", rec)
return None return json.dumps({"status": "ok", "data": rec, "message": "创建成功"})
async def get_active_dist_discount(sor, resellerid, sub_distributor_id, productid, prodtypeid=None, sale_date=None): async def update_supply_contract(request, params_kw):
""" """Update a supply contract."""
Get active distribution agreement discount for a product. data = params_kw
Priority: exact product > product type > agreement default. db, dbname = _get_sor()
async with db.sqlorContext(dbname) as sor:
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
rec = {"id": data["id"], "updated_at": now}
for key in ["supplier_id", "contract_name", "sign_date", "start_date",
"end_date", "status", "default_discount", "remark"]:
if key in data:
rec[key] = data[key]
await sor.U("supply_contracts", rec)
return json.dumps({"status": "ok", "message": "更新成功"})
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 async def delete_supply_contract(request, params_kw):
sql = """SELECT dai.id, dai.agreement_id, dai.discount, dai.settlement_price """Delete a supply contract and its items."""
FROM distribution_agreement_items dai data = params_kw
JOIN distribution_agreements da ON dai.agreement_id = da.id db, dbname = _get_sor()
WHERE dai.resellerid = ${resellerid}$ async with db.sqlorContext(dbname) as sor:
AND da.sub_distributor_id = ${sub_distributor_id}$ await sor.D("supply_contract_items", {"contract_id": data["id"]})
AND da.status = '1' await sor.D("supply_contracts", {"id": data["id"]})
AND da.start_date <= ${sale_date}$ return json.dumps({"status": "ok", "message": "删除成功"})
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 { # Supply Contract Items APIs
"agreement_item_id": recs[0].id, # ============================================================
"agreement_id": recs[0].agreement_id,
"discount": float(recs[0].discount) if recs[0].discount else 1.0, async def create_supply_contract_item(request, params_kw):
"settlement_price": float(recs[0].settlement_price) if recs[0].settlement_price else None, """Create a supply contract product discount item."""
data = params_kw
db, dbname = _get_sor()
async with db.sqlorContext(dbname) as sor:
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
rec = {
"id": getID(),
"contract_id": data["contract_id"],
"resellerid": data.get("resellerid", ""),
"prodtypeid": data.get("prodtypeid"),
"productid": data.get("productid"),
"discount": data.get("discount", 1.0),
"settlement_price": data.get("settlement_price"),
"remark": data.get("remark"),
"created_at": now,
} }
await sor.C("supply_contract_items", rec)
return json.dumps({"status": "ok", "data": rec, "message": "创建成功"})
# 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: async def update_supply_contract_item(request, params_kw):
return { """Update a supply contract item."""
"agreement_item_id": None, data = params_kw
"agreement_id": recs[0].id, db, dbname = _get_sor()
"discount": float(recs[0].default_discount) if recs[0].default_discount else 1.0, async with db.sqlorContext(dbname) as sor:
"settlement_price": None, rec = {"id": data["id"]}
for key in ["prodtypeid", "productid", "discount", "settlement_price", "remark"]:
if key in data:
rec[key] = data[key]
await sor.U("supply_contract_items", rec)
return json.dumps({"status": "ok", "message": "更新成功"})
async def delete_supply_contract_item(request, params_kw):
"""Delete a supply contract item."""
data = params_kw
db, dbname = _get_sor()
async with db.sqlorContext(dbname) as sor:
await sor.D("supply_contract_items", {"id": data["id"]})
return json.dumps({"status": "ok", "message": "删除成功"})
# ============================================================
# Sub-Reseller APIs
# ============================================================
async def create_sub_reseller(request, params_kw):
"""Create a new sub-reseller."""
env = ServerEnv()
user_id = await env.get_user()
resellerid = await env.get_userorgid()
data = params_kw
db, dbname = _get_sor()
async with db.sqlorContext(dbname) as sor:
sub_reseller_code = data.get("sub_reseller_code") or _generate_sub_reseller_code(resellerid)
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
rec = {
"id": getID(),
"resellerid": resellerid,
"sub_reseller_code": sub_reseller_code,
"sub_reseller_name": data.get("sub_reseller_name"),
"contact_person": data.get("contact_person"),
"contact_phone": data.get("contact_phone"),
"contact_email": data.get("contact_email"),
"address": data.get("address"),
"tax_number": data.get("tax_number"),
"bank_name": data.get("bank_name"),
"bank_account": data.get("bank_account"),
"status": data.get("status", "1"),
"remark": data.get("remark"),
"created_by": user_id,
"created_at": now,
"updated_at": now,
} }
await sor.C("sub_resellers", rec)
return None return json.dumps({"status": "ok", "data": rec, "message": "创建成功"})
async def calculate_sale_accounting(sor, resellerid, productid, quantity, unit_price, async def update_sub_reseller(request, params_kw):
sub_distributor_id=None, prodtypeid=None, """Update a sub-reseller."""
sale_date=None, source_type="2", source_id=None, data = params_kw
created_by=None, remark=""): db, dbname = _get_sor()
""" async with db.sqlorContext(dbname) as sor:
Calculate and record accounting for a product sale. now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
rec = {"id": data["id"], "updated_at": now}
for key in ["sub_reseller_name", "contact_person", "contact_phone", "contact_email",
"address", "tax_number", "bank_name", "bank_account", "status", "remark"]:
if key in data:
rec[key] = data[key]
await sor.U("sub_resellers", rec)
return json.dumps({"status": "ok", "message": "更新成功"})
Returns: the created accounting record as dict
"""
if sale_date is None:
sale_date = datetime.now().strftime("%Y-%m-%d")
async def delete_sub_reseller(request, params_kw):
"""Delete a sub-reseller."""
data = params_kw
db, dbname = _get_sor()
async with db.sqlorContext(dbname) as sor:
await sor.D("sub_resellers", {"id": data["id"]})
return json.dumps({"status": "ok", "message": "删除成功"})
# ============================================================
# Distribution Agreement APIs
# ============================================================
async def create_distribution_agreement(request, params_kw):
"""Create a new distribution agreement."""
env = ServerEnv()
user_id = await env.get_user()
resellerid = await env.get_userorgid()
data = params_kw
db, dbname = _get_sor()
async with db.sqlorContext(dbname) as sor:
agreement_code = data.get("agreement_code") or _generate_agreement_code(resellerid)
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
rec = {
"id": getID(),
"resellerid": resellerid,
"sub_reseller_id": data.get("sub_reseller_id"),
"agreement_code": agreement_code,
"agreement_name": data.get("agreement_name"),
"sign_date": data.get("sign_date"),
"start_date": data.get("start_date"),
"end_date": data.get("end_date"),
"status": data.get("status", "1"),
"default_discount": data.get("default_discount", 1.0),
"remark": data.get("remark"),
"created_by": user_id,
"created_at": now,
"updated_at": now,
}
await sor.C("distribution_agreements", rec)
return json.dumps({"status": "ok", "data": rec, "message": "创建成功"})
async def update_distribution_agreement(request, params_kw):
"""Update a distribution agreement."""
data = params_kw
db, dbname = _get_sor()
async with db.sqlorContext(dbname) as sor:
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
rec = {"id": data["id"], "updated_at": now}
for key in ["sub_reseller_id", "agreement_name", "sign_date", "start_date",
"end_date", "status", "default_discount", "remark"]:
if key in data:
rec[key] = data[key]
await sor.U("distribution_agreements", rec)
return json.dumps({"status": "ok", "message": "更新成功"})
async def delete_distribution_agreement(request, params_kw):
"""Delete a distribution agreement and its items."""
data = params_kw
db, dbname = _get_sor()
async with db.sqlorContext(dbname) as sor:
await sor.D("distribution_agreement_items", {"agreement_id": data["id"]})
await sor.D("distribution_agreements", {"id": data["id"]})
return json.dumps({"status": "ok", "message": "删除成功"})
# ============================================================
# Distribution Agreement Items APIs
# ============================================================
async def create_distribution_agreement_item(request, params_kw):
"""Create a distribution agreement product discount item."""
data = params_kw
db, dbname = _get_sor()
async with db.sqlorContext(dbname) as sor:
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
rec = {
"id": getID(),
"agreement_id": data["agreement_id"],
"resellerid": data.get("resellerid", ""),
"prodtypeid": data.get("prodtypeid"),
"productid": data.get("productid"),
"discount": data.get("discount", 1.0),
"min_order_qty": data.get("min_order_qty"),
"sale_price": data.get("sale_price"),
"remark": data.get("remark"),
"created_at": now,
}
await sor.C("distribution_agreement_items", rec)
return json.dumps({"status": "ok", "data": rec, "message": "创建成功"})
async def update_distribution_agreement_item(request, params_kw):
"""Update a distribution agreement item."""
data = params_kw
db, dbname = _get_sor()
async with db.sqlorContext(dbname) as sor:
rec = {"id": data["id"]}
for key in ["prodtypeid", "productid", "discount", "min_order_qty",
"sale_price", "remark"]:
if key in data:
rec[key] = data[key]
await sor.U("distribution_agreement_items", rec)
return json.dumps({"status": "ok", "message": "更新成功"})
async def delete_distribution_agreement_item(request, params_kw):
"""Delete a distribution agreement item."""
data = params_kw
db, dbname = _get_sor()
async with db.sqlorContext(dbname) as sor:
await sor.D("distribution_agreement_items", {"id": data["id"]})
return json.dumps({"status": "ok", "message": "删除成功"})
# ============================================================
# Sales Ledger APIs
# ============================================================
async def create_sales_ledger(request, params_kw):
"""Create a sales ledger entry."""
env = ServerEnv()
user_id = await env.get_user()
resellerid = await env.get_userorgid()
data = params_kw
db, dbname = _get_sor()
async with db.sqlorContext(dbname) as sor:
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
quantity = float(data.get("quantity", 0))
unit_price = float(data.get("unit_price", 0))
total_amount = quantity * unit_price total_amount = quantity * unit_price
# Get supply discount supply_discount = float(data.get("supply_discount", 1.0))
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 supply_amount = total_amount * supply_discount
# Get distribution discount distribution_discount = float(data.get("distribution_discount", 1.0))
distribution_agreement_id = None distribution_amount = total_amount * distribution_discount
distribution_agreement_item_id = None
dist_discount = 1.0
dist_amount = total_amount
if sub_distributor_id: profit_amount = distribution_amount - supply_amount
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 rec = {
"id": getID(),
# Create accounting record
accounting_id = getID()
record = {
"id": accounting_id,
"resellerid": resellerid, "resellerid": resellerid,
"supply_contract_id": supply_contract_id, "sub_reseller_id": data.get("sub_reseller_id"),
"supply_contract_item_id": supply_contract_item_id, "supplier_id": data.get("supplier_id"),
"distribution_agreement_id": distribution_agreement_id, "agreement_id": data.get("agreement_id"),
"distribution_agreement_item_id": distribution_agreement_item_id, "contract_id": data.get("contract_id"),
"sub_distributor_id": sub_distributor_id, "prodtypeid": data.get("prodtypeid"),
"supplier_id": supplier_id, "productid": data.get("productid"),
"prodtypeid": prodtypeid, "sale_date": data.get("sale_date"),
"productid": productid,
"quantity": quantity, "quantity": quantity,
"unit_price": unit_price, "unit_price": unit_price,
"supply_discount": supply_discount, "supply_discount": supply_discount,
"supply_amount": supply_amount, "supply_amount": round(supply_amount, 2),
"dist_discount": dist_discount, "distribution_discount": distribution_discount,
"dist_amount": dist_amount, "distribution_amount": round(distribution_amount, 2),
"profit_amount": profit_amount, "profit_amount": round(profit_amount, 2),
"sale_date": sale_date, "settlement_status": data.get("settlement_status", "0"),
"source_type": source_type, "remark": data.get("remark"),
"source_id": source_id, "created_by": user_id,
"remark": remark, "created_at": now,
"created_by": created_by, "updated_at": now,
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
} }
await sor.C("sales_ledger", rec)
return json.dumps({"status": "ok", "data": rec, "message": "记账成功"})
await sor.C("supplychain_accounting", record)
return record
async def update_sales_ledger(request, params_kw):
"""Update a sales ledger entry."""
data = params_kw
db, dbname = _get_sor()
async with db.sqlorContext(dbname) as sor:
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
rec = {"id": data["id"], "updated_at": now}
for key in ["sub_reseller_id", "supplier_id", "agreement_id", "contract_id",
"prodtypeid", "productid", "sale_date", "quantity", "unit_price",
"supply_discount", "supply_amount", "distribution_discount",
"distribution_amount", "profit_amount", "settlement_status", "remark"]:
if key in data:
rec[key] = data[key]
await sor.U("sales_ledger", rec)
return json.dumps({"status": "ok", "message": "更新成功"})
async def delete_sales_ledger(request, params_kw):
"""Delete a sales ledger entry."""
data = params_kw
db, dbname = _get_sor()
async with db.sqlorContext(dbname) as sor:
await sor.D("sales_ledger", {"id": data["id"]})
return json.dumps({"status": "ok", "message": "删除成功"})
# ============================================================
# Discount Calculation API (called by other modules during sales)
# ============================================================
async def calculate_sale_amounts(request, params_kw):
"""
Calculate supply and distribution amounts for a sale.
Called by accounting/other modules during product sales.
Parameters:
resellerid: 分销商机构ID
sub_reseller_id: 二级分销商ID可选
supplier_id: 供应商ID可选
prodtypeid: 产品分类ID
productid: 产品ID
quantity: 销售数量
unit_price: 销售单价
Returns: supply_discount, supply_amount, distribution_discount, distribution_amount, profit_amount
"""
env = ServerEnv()
data = params_kw
resellerid = data.get("resellerid")
sub_reseller_id = data.get("sub_reseller_id")
supplier_id = data.get("supplier_id")
prodtypeid = data.get("prodtypeid")
productid = data.get("productid")
quantity = float(data.get("quantity", 0))
unit_price = float(data.get("unit_price", 0))
total_amount = quantity * unit_price
db, dbname = _get_sor()
async with db.sqlorContext(dbname) as sor:
biz_date = await env.get_business_date(sor)
# Step 1: Find active supply contract for this supplier
supply_discount = 1.0
contract_id = None
if supplier_id:
sql_sc = """SELECT id, default_discount FROM supply_contracts
WHERE resellerid = ${resellerid}$
AND supplier_id = ${supplier_id}$
AND status = '1'
AND start_date <= ${biz_date}$
AND (end_date IS NULL OR end_date > ${biz_date}$)"""
recs_sc = await sor.sqlExe(sql_sc, {
"resellerid": resellerid,
"supplier_id": supplier_id,
"biz_date": biz_date
})
if recs_sc:
contract_id = recs_sc[0].id
supply_discount = float(recs_sc[0].default_discount)
# Try to find product-specific discount in contract items
# Priority: exact product > product type > NULL (default)
sql_sci = """SELECT discount FROM supply_contract_items
WHERE contract_id = ${contract_id}$
AND ((prodtypeid = ${prodtypeid}$ AND productid = ${productid}$)
OR (prodtypeid = ${prodtypeid}$ AND productid IS NULL)
OR (prodtypeid IS NULL AND productid IS NULL))
ORDER BY
CASE WHEN prodtypeid IS NOT NULL AND productid IS NOT NULL THEN 1
WHEN prodtypeid IS NOT NULL AND productid IS NULL THEN 2
ELSE 3 END
LIMIT 1"""
recs_sci = await sor.sqlExe(sql_sci, {
"contract_id": contract_id,
"prodtypeid": prodtypeid,
"productid": productid
})
if recs_sci:
supply_discount = float(recs_sci[0].discount)
supply_amount = total_amount * supply_discount
# Step 2: Find active distribution agreement for this sub-reseller
distribution_discount = 1.0
agreement_id = None
if sub_reseller_id:
sql_da = """SELECT id, default_discount FROM distribution_agreements
WHERE resellerid = ${resellerid}$
AND sub_reseller_id = ${sub_reseller_id}$
AND status = '1'
AND start_date <= ${biz_date}$
AND (end_date IS NULL OR end_date > ${biz_date}$)"""
recs_da = await sor.sqlExe(sql_da, {
"resellerid": resellerid,
"sub_reseller_id": sub_reseller_id,
"biz_date": biz_date
})
if recs_da:
agreement_id = recs_da[0].id
distribution_discount = float(recs_da[0].default_discount)
# Try to find product-specific discount with priority ordering
sql_dai = """SELECT discount FROM distribution_agreement_items
WHERE agreement_id = ${agreement_id}$
AND ((prodtypeid = ${prodtypeid}$ AND productid = ${productid}$)
OR (prodtypeid = ${prodtypeid}$ AND productid IS NULL)
OR (prodtypeid IS NULL AND productid IS NULL))
ORDER BY
CASE WHEN prodtypeid IS NOT NULL AND productid IS NOT NULL THEN 1
WHEN prodtypeid IS NOT NULL AND productid IS NULL THEN 2
ELSE 3 END
LIMIT 1"""
recs_dai = await sor.sqlExe(sql_dai, {
"agreement_id": agreement_id,
"prodtypeid": prodtypeid,
"productid": productid
})
if recs_dai:
distribution_discount = float(recs_dai[0].discount)
distribution_amount = total_amount * distribution_discount
profit_amount = distribution_amount - supply_amount
result = {
"contract_id": contract_id,
"agreement_id": agreement_id,
"total_amount": round(total_amount, 2),
"supply_discount": supply_discount,
"supply_amount": round(supply_amount, 2),
"distribution_discount": distribution_discount,
"distribution_amount": round(distribution_amount, 2),
"profit_amount": round(profit_amount, 2),
}
return json.dumps({"status": "ok", "data": result})
# ============================================================
# Register functions with ServerEnv
# ============================================================
def load_supplychain(): def load_supplychain():
"""Register all functions with ServerEnv.""" """Register all supplychain functions with ServerEnv."""
env = ServerEnv() env = ServerEnv()
env.get_active_supply_discount = get_active_supply_discount # Supplier
env.get_active_dist_discount = get_active_dist_discount env.create_supplier = create_supplier
env.calculate_sale_accounting = calculate_sale_accounting env.update_supplier = update_supplier
env.delete_supplier = delete_supplier
# Supply Contract
env.create_supply_contract = create_supply_contract
env.update_supply_contract = update_supply_contract
env.delete_supply_contract = delete_supply_contract
# Supply Contract Items
env.create_supply_contract_item = create_supply_contract_item
env.update_supply_contract_item = update_supply_contract_item
env.delete_supply_contract_item = delete_supply_contract_item
# Sub-Reseller
env.create_sub_reseller = create_sub_reseller
env.update_sub_reseller = update_sub_reseller
env.delete_sub_reseller = delete_sub_reseller
# Distribution Agreement
env.create_distribution_agreement = create_distribution_agreement
env.update_distribution_agreement = update_distribution_agreement
env.delete_distribution_agreement = delete_distribution_agreement
# Distribution Agreement Items
env.create_distribution_agreement_item = create_distribution_agreement_item
env.update_distribution_agreement_item = update_distribution_agreement_item
env.delete_distribution_agreement_item = delete_distribution_agreement_item
# Sales Ledger
env.create_sales_ledger = create_sales_ledger
env.update_sales_ledger = update_sales_ledger
env.delete_sales_ledger = delete_sales_ledger
# Calculation API
env.calculate_sale_amounts = calculate_sale_amounts
return True return True

View File

@ -1,23 +1,9 @@
import json import json
from appPublic.uniqueID import getID from ahserver.serverenv import ServerEnv
from datetime import datetime env = ServerEnv()
create_func = getattr(env, 'create_distribution_agreement_items', None)
async def main(request, params_kw): if create_func is None:
"""Create a new distribution_agreement_items record.""" print(json.dumps({"status": "error", "message": "create_distribution_agreement_items function not found"}))
user_id = await get_user() else:
user_orgid = await get_userorgid() result = await create_func(request, params_kw)
dbname = get_module_dbname('supplychain') print(result)
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
data["id"] = getID()
data["resellerid"] = user_orgid
data["created_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
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

@ -1,19 +1,9 @@
import json import json
from ahserver.serverenv import ServerEnv
async def main(request, params_kw): env = ServerEnv()
"""Delete a distribution_agreement_items record.""" delete_func = getattr(env, 'delete_distribution_agreement_items', None)
dbname = get_module_dbname('supplychain') if delete_func is None:
print(json.dumps({"status": "error", "message": "delete_distribution_agreement_items function not found"}))
data = params_kw.get("data", "{}") else:
if isinstance(data, str): result = await delete_func(request, params_kw)
data = json.loads(data) print(result)
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

@ -1,27 +1,9 @@
import json import json
from datetime import datetime from ahserver.serverenv import ServerEnv
env = ServerEnv()
async def main(request, params_kw): update_func = getattr(env, 'update_distribution_agreement_items', None)
"""Update a distribution_agreement_items record.""" if update_func is None:
user_id = await get_user() print(json.dumps({"status": "error", "message": "update_distribution_agreement_items function not found"}))
dbname = get_module_dbname('supplychain') else:
result = await update_func(request, params_kw)
data = params_kw.get("data", "{}") print(result)
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

@ -1,28 +1,9 @@
import json import json
from appPublic.uniqueID import getID from ahserver.serverenv import ServerEnv
from datetime import datetime env = ServerEnv()
create_func = getattr(env, 'create_distribution_agreements', None)
async def main(request, params_kw): if create_func is None:
"""Create a new distribution_agreements record.""" print(json.dumps({"status": "error", "message": "create_distribution_agreements function not found"}))
user_id = await get_user() else:
user_orgid = await get_userorgid() result = await create_func(request, params_kw)
dbname = get_module_dbname('supplychain') print(result)
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 agreement code
if not data.get("agreement_code"):
data["agreement_code"] = f"DA-{datetime.now().strftime('%Y%m%d')}-{getID()[:4].upper()}"
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

@ -1,19 +1,9 @@
import json import json
from ahserver.serverenv import ServerEnv
async def main(request, params_kw): env = ServerEnv()
"""Delete a distribution_agreements record.""" delete_func = getattr(env, 'delete_distribution_agreements', None)
dbname = get_module_dbname('supplychain') if delete_func is None:
print(json.dumps({"status": "error", "message": "delete_distribution_agreements function not found"}))
data = params_kw.get("data", "{}") else:
if isinstance(data, str): result = await delete_func(request, params_kw)
data = json.loads(data) print(result)
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

@ -1,27 +1,9 @@
import json import json
from datetime import datetime from ahserver.serverenv import ServerEnv
env = ServerEnv()
async def main(request, params_kw): update_func = getattr(env, 'update_distribution_agreements', None)
"""Update a distribution_agreements record.""" if update_func is None:
user_id = await get_user() print(json.dumps({"status": "error", "message": "update_distribution_agreements function not found"}))
dbname = get_module_dbname('supplychain') else:
result = await update_func(request, params_kw)
data = params_kw.get("data", "{}") print(result)
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,9 @@
import json
from ahserver.serverenv import ServerEnv
env = ServerEnv()
create_func = getattr(env, 'create_sales_ledger', None)
if create_func is None:
print(json.dumps({"status": "error", "message": "create_sales_ledger function not found"}))
else:
result = await create_func(request, params_kw)
print(result)

View File

@ -0,0 +1,9 @@
import json
from ahserver.serverenv import ServerEnv
env = ServerEnv()
delete_func = getattr(env, 'delete_sales_ledger', None)
if delete_func is None:
print(json.dumps({"status": "error", "message": "delete_sales_ledger function not found"}))
else:
result = await delete_func(request, params_kw)
print(result)

View File

@ -0,0 +1,9 @@
import json
from ahserver.serverenv import ServerEnv
env = ServerEnv()
update_func = getattr(env, 'update_sales_ledger', None)
if update_func is None:
print(json.dumps({"status": "error", "message": "update_sales_ledger function not found"}))
else:
result = await update_func(request, params_kw)
print(result)

View File

@ -0,0 +1,9 @@
import json
from ahserver.serverenv import ServerEnv
env = ServerEnv()
create_func = getattr(env, 'create_sub_resellers', None)
if create_func is None:
print(json.dumps({"status": "error", "message": "create_sub_resellers function not found"}))
else:
result = await create_func(request, params_kw)
print(result)

View File

@ -0,0 +1,9 @@
import json
from ahserver.serverenv import ServerEnv
env = ServerEnv()
delete_func = getattr(env, 'delete_sub_resellers', None)
if delete_func is None:
print(json.dumps({"status": "error", "message": "delete_sub_resellers function not found"}))
else:
result = await delete_func(request, params_kw)
print(result)

View File

@ -0,0 +1,9 @@
import json
from ahserver.serverenv import ServerEnv
env = ServerEnv()
update_func = getattr(env, 'update_sub_resellers', None)
if update_func is None:
print(json.dumps({"status": "error", "message": "update_sub_resellers function not found"}))
else:
result = await update_func(request, params_kw)
print(result)

View File

@ -1,28 +1,9 @@
import json import json
from appPublic.uniqueID import getID from ahserver.serverenv import ServerEnv
from datetime import datetime env = ServerEnv()
create_func = getattr(env, 'create_suppliers', None)
async def main(request, params_kw): if create_func is None:
"""Create a new suppliers record.""" print(json.dumps({"status": "error", "message": "create_suppliers function not found"}))
user_id = await get_user() else:
user_orgid = await get_userorgid() result = await create_func(request, params_kw)
dbname = get_module_dbname('supplychain') print(result)
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 supplier code
if not data.get("supplier_code"):
data["supplier_code"] = f"SUP-{datetime.now().strftime('%Y%m%d')}-{getID()[:4].upper()}"
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

@ -1,19 +1,9 @@
import json import json
from ahserver.serverenv import ServerEnv
async def main(request, params_kw): env = ServerEnv()
"""Delete a suppliers record.""" delete_func = getattr(env, 'delete_suppliers', None)
dbname = get_module_dbname('supplychain') if delete_func is None:
print(json.dumps({"status": "error", "message": "delete_suppliers function not found"}))
data = params_kw.get("data", "{}") else:
if isinstance(data, str): result = await delete_func(request, params_kw)
data = json.loads(data) print(result)
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

@ -1,27 +1,9 @@
import json import json
from datetime import datetime from ahserver.serverenv import ServerEnv
env = ServerEnv()
async def main(request, params_kw): update_func = getattr(env, 'update_suppliers', None)
"""Update a suppliers record.""" if update_func is None:
user_id = await get_user() print(json.dumps({"status": "error", "message": "update_suppliers function not found"}))
dbname = get_module_dbname('supplychain') else:
result = await update_func(request, params_kw)
data = params_kw.get("data", "{}") print(result)
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

@ -1,23 +1,9 @@
import json import json
from appPublic.uniqueID import getID from ahserver.serverenv import ServerEnv
from datetime import datetime env = ServerEnv()
create_func = getattr(env, 'create_supply_contract_items', None)
async def main(request, params_kw): if create_func is None:
"""Create a new supply_contract_items record.""" print(json.dumps({"status": "error", "message": "create_supply_contract_items function not found"}))
user_id = await get_user() else:
user_orgid = await get_userorgid() result = await create_func(request, params_kw)
dbname = get_module_dbname('supplychain') print(result)
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
data["id"] = getID()
data["resellerid"] = user_orgid
data["created_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
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

@ -1,19 +1,9 @@
import json import json
from ahserver.serverenv import ServerEnv
async def main(request, params_kw): env = ServerEnv()
"""Delete a supply_contract_items record.""" delete_func = getattr(env, 'delete_supply_contract_items', None)
dbname = get_module_dbname('supplychain') if delete_func is None:
print(json.dumps({"status": "error", "message": "delete_supply_contract_items function not found"}))
data = params_kw.get("data", "{}") else:
if isinstance(data, str): result = await delete_func(request, params_kw)
data = json.loads(data) print(result)
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

@ -1,27 +1,9 @@
import json import json
from datetime import datetime from ahserver.serverenv import ServerEnv
env = ServerEnv()
async def main(request, params_kw): update_func = getattr(env, 'update_supply_contract_items', None)
"""Update a supply_contract_items record.""" if update_func is None:
user_id = await get_user() print(json.dumps({"status": "error", "message": "update_supply_contract_items function not found"}))
dbname = get_module_dbname('supplychain') else:
result = await update_func(request, params_kw)
data = params_kw.get("data", "{}") print(result)
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

@ -1,28 +1,9 @@
import json import json
from appPublic.uniqueID import getID from ahserver.serverenv import ServerEnv
from datetime import datetime env = ServerEnv()
create_func = getattr(env, 'create_supply_contracts', None)
async def main(request, params_kw): if create_func is None:
"""Create a new supply_contracts record.""" print(json.dumps({"status": "error", "message": "create_supply_contracts function not found"}))
user_id = await get_user() else:
user_orgid = await get_userorgid() result = await create_func(request, params_kw)
dbname = get_module_dbname('supplychain') print(result)
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 contract code
if not data.get("contract_code"):
data["contract_code"] = f"SC-{datetime.now().strftime('%Y%m%d')}-{getID()[:4].upper()}"
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

@ -1,19 +1,9 @@
import json import json
from ahserver.serverenv import ServerEnv
async def main(request, params_kw): env = ServerEnv()
"""Delete a supply_contracts record.""" delete_func = getattr(env, 'delete_supply_contracts', None)
dbname = get_module_dbname('supplychain') if delete_func is None:
print(json.dumps({"status": "error", "message": "delete_supply_contracts function not found"}))
data = params_kw.get("data", "{}") else:
if isinstance(data, str): result = await delete_func(request, params_kw)
data = json.loads(data) print(result)
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

@ -1,27 +1,9 @@
import json import json
from datetime import datetime from ahserver.serverenv import ServerEnv
env = ServerEnv()
async def main(request, params_kw): update_func = getattr(env, 'update_supply_contracts', None)
"""Update a supply_contracts record.""" if update_func is None:
user_id = await get_user() print(json.dumps({"status": "error", "message": "update_supply_contracts function not found"}))
dbname = get_module_dbname('supplychain') else:
result = await update_func(request, params_kw)
data = params_kw.get("data", "{}") print(result)
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

@ -3,55 +3,31 @@
"options": { "options": {
"width": "100%", "width": "100%",
"height": "100%", "height": "100%",
"padding": "0", "padding": "20px"
"bgcolor": "#0B1120"
}, },
"subwidgets": [ "subwidgets": [
{
"widgettype": "HBox",
"options": {
"width": "100%",
"alignItems": "center",
"padding": "16px 24px",
"marginBottom": "0"
},
"subwidgets": [
{
"widgettype": "Title2",
"options": {
"text": "供销链管理",
"color": "#F1F5F9",
"fontWeight": "700"
}
},
{
"widgettype": "Filler"
},
{ {
"widgettype": "Text", "widgettype": "Text",
"options": { "options": {
"text": "供应商、合同、二级分销商与记账管理", "text": "供销链管理",
"fontSize": "14px", "fontSize": "24px",
"color": "#64748B" "fontWeight": "bold",
"marginBottom": "20px"
} }
}
]
}, },
{ {
"widgettype": "ResponsableBox", "widgettype": "ResponsableBox",
"options": { "options": {
"gap": "12px", "gap": "16px",
"minWidth": "180px", "minWidth": "250px"
"padding": "0 24px 16px 24px"
}, },
"subwidgets": [ "subwidgets": [
{ {
"widgettype": "VBox", "widgettype": "VBox",
"options": { "options": {
"bgcolor": "#1E293B", "bgcolor": "#FFFFFF",
"padding": "16px 24px", "padding": "20px",
"borderRadius": "8px", "borderRadius": "8px",
"border": "1px solid #334155",
"cursor": "pointer" "cursor": "pointer"
}, },
"binds": [ "binds": [
@ -61,26 +37,27 @@
"actiontype": "urlwidget", "actiontype": "urlwidget",
"target": "app.supplychain_content", "target": "app.supplychain_content",
"options": { "options": {
"url": "{{entire_url('suppliers.ui')}}" "url": "{{entire_url('suppliers_list')}}"
}, },
"mode": "replace" "mode": "replace"
} }
], ],
"subwidgets": [ "subwidgets": [
{ {
"widgettype": "Title5", "widgettype": "Text",
"options": { "options": {
"text": "供应商管理", "text": "供应商管理",
"color": "#F1F5F9", "fontSize": "16px",
"fontWeight": "600" "fontWeight": "bold"
} }
}, },
{ {
"widgettype": "Text", "widgettype": "Text",
"options": { "options": {
"text": "添加和管理供应商信息", "text": "管理供应商信息、联系方式、银行账户等",
"fontSize": "12px", "fontSize": "12px",
"color": "#94A3B8" "color": "#666666",
"marginTop": "8px"
} }
} }
] ]
@ -88,10 +65,9 @@
{ {
"widgettype": "VBox", "widgettype": "VBox",
"options": { "options": {
"bgcolor": "#1E293B", "bgcolor": "#FFFFFF",
"padding": "16px 24px", "padding": "20px",
"borderRadius": "8px", "borderRadius": "8px",
"border": "1px solid #334155",
"cursor": "pointer" "cursor": "pointer"
}, },
"binds": [ "binds": [
@ -101,18 +77,18 @@
"actiontype": "urlwidget", "actiontype": "urlwidget",
"target": "app.supplychain_content", "target": "app.supplychain_content",
"options": { "options": {
"url": "{{entire_url('supply_contracts.ui')}}" "url": "{{entire_url('supply_contracts_list')}}"
}, },
"mode": "replace" "mode": "replace"
} }
], ],
"subwidgets": [ "subwidgets": [
{ {
"widgettype": "Title5", "widgettype": "Text",
"options": { "options": {
"text": "供销合同", "text": "供销合同",
"color": "#F1F5F9", "fontSize": "16px",
"fontWeight": "600" "fontWeight": "bold"
} }
}, },
{ {
@ -120,7 +96,8 @@
"options": { "options": {
"text": "管理与供应商的供销合同及产品折扣", "text": "管理与供应商的供销合同及产品折扣",
"fontSize": "12px", "fontSize": "12px",
"color": "#94A3B8" "color": "#666666",
"marginTop": "8px"
} }
} }
] ]
@ -128,10 +105,9 @@
{ {
"widgettype": "VBox", "widgettype": "VBox",
"options": { "options": {
"bgcolor": "#1E293B", "bgcolor": "#FFFFFF",
"padding": "16px 24px", "padding": "20px",
"borderRadius": "8px", "borderRadius": "8px",
"border": "1px solid #334155",
"cursor": "pointer" "cursor": "pointer"
}, },
"binds": [ "binds": [
@ -141,26 +117,27 @@
"actiontype": "urlwidget", "actiontype": "urlwidget",
"target": "app.supplychain_content", "target": "app.supplychain_content",
"options": { "options": {
"url": "{{entire_url('sub_distributors.ui')}}" "url": "{{entire_url('sub_resellers_list')}}"
}, },
"mode": "replace" "mode": "replace"
} }
], ],
"subwidgets": [ "subwidgets": [
{ {
"widgettype": "Title5", "widgettype": "Text",
"options": { "options": {
"text": "二级分销商", "text": "二级分销商",
"color": "#F1F5F9", "fontSize": "16px",
"fontWeight": "600" "fontWeight": "bold"
} }
}, },
{ {
"widgettype": "Text", "widgettype": "Text",
"options": { "options": {
"text": "添加和管理二级分销商", "text": "管理二级分销商信息",
"fontSize": "12px", "fontSize": "12px",
"color": "#94A3B8" "color": "#666666",
"marginTop": "8px"
} }
} }
] ]
@ -168,10 +145,9 @@
{ {
"widgettype": "VBox", "widgettype": "VBox",
"options": { "options": {
"bgcolor": "#1E293B", "bgcolor": "#FFFFFF",
"padding": "16px 24px", "padding": "20px",
"borderRadius": "8px", "borderRadius": "8px",
"border": "1px solid #334155",
"cursor": "pointer" "cursor": "pointer"
}, },
"binds": [ "binds": [
@ -181,18 +157,18 @@
"actiontype": "urlwidget", "actiontype": "urlwidget",
"target": "app.supplychain_content", "target": "app.supplychain_content",
"options": { "options": {
"url": "{{entire_url('distribution_agreements.ui')}}" "url": "{{entire_url('distribution_agreements_list')}}"
}, },
"mode": "replace" "mode": "replace"
} }
], ],
"subwidgets": [ "subwidgets": [
{ {
"widgettype": "Title5", "widgettype": "Text",
"options": { "options": {
"text": "分销协议", "text": "分销协议",
"color": "#F1F5F9", "fontSize": "16px",
"fontWeight": "600" "fontWeight": "bold"
} }
}, },
{ {
@ -200,7 +176,8 @@
"options": { "options": {
"text": "管理与二级分销商的分销协议及产品折扣", "text": "管理与二级分销商的分销协议及产品折扣",
"fontSize": "12px", "fontSize": "12px",
"color": "#94A3B8" "color": "#666666",
"marginTop": "8px"
} }
} }
] ]
@ -208,10 +185,9 @@
{ {
"widgettype": "VBox", "widgettype": "VBox",
"options": { "options": {
"bgcolor": "#1E293B", "bgcolor": "#FFFFFF",
"padding": "16px 24px", "padding": "20px",
"borderRadius": "8px", "borderRadius": "8px",
"border": "1px solid #334155",
"cursor": "pointer" "cursor": "pointer"
}, },
"binds": [ "binds": [
@ -221,26 +197,27 @@
"actiontype": "urlwidget", "actiontype": "urlwidget",
"target": "app.supplychain_content", "target": "app.supplychain_content",
"options": { "options": {
"url": "{{entire_url('accounting.ui')}}" "url": "{{entire_url('sales_ledger_list')}}"
}, },
"mode": "replace" "mode": "replace"
} }
], ],
"subwidgets": [ "subwidgets": [
{ {
"widgettype": "Title5", "widgettype": "Text",
"options": { "options": {
"text": "销记账", "text": "销记账",
"color": "#F1F5F9", "fontSize": "16px",
"fontWeight": "600" "fontWeight": "bold"
} }
}, },
{ {
"widgettype": "Text", "widgettype": "Text",
"options": { "options": {
"text": "查看供销关系记账记录和利润统计", "text": "查看和管理销售记账记录",
"fontSize": "12px", "fontSize": "12px",
"color": "#94A3B8" "color": "#666666",
"marginTop": "8px"
} }
} }
] ]
@ -249,11 +226,11 @@
}, },
{ {
"widgettype": "VBox", "widgettype": "VBox",
"id": "supplychain_content", "id": "app.supplychain_content",
"css": "filler",
"options": { "options": {
"width": "100%", "width": "100%",
"overflowY": "auto" "flex": "1",
"marginTop": "20px"
} }
} }
] ]

View File

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