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/ # Python 包
│ ├── __init__.py
│ └── init.py # 模块初始化 + ServerEnv 注册
├── wwwroot/ # 前端文件
│ ├── index.ui # 模块入口页
│ ├── menu.ui # 导航菜单
│ ├── suppliers.ui # 供应商管理页
│ ├── supply_contracts.ui # 供销合同页
│ ├── sub_distributors.ui # 二级分销商页
│ ├── distribution_agreements.ui # 分销协议页
│ ├── 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 权限管理脚本
├── supplychain/
│ ├── __init__.py # Package init
│ └── init.py # Module init + ServerEnv registration
├── wwwroot/
│ ├── index.ui # Module entry point
│ ├── menu.ui # Navigation menu
│ └── api/ # CRUD API endpoints (.dspy files)
├── models/ # Table definitions (JSON)
├── json/ # CRUD definitions (JSON)
├── init/ # Initialization data
├── pyproject.toml
├── build.sh
└── README.md
```
## 数据库表
| 表名 | 说明 | 类别 |
|------|------|------|
| 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. 克隆模块
## Sage Integration
### 1. Install module
```bash
cd ~/repos
git clone git@git.opencomputing.cn:yumoqing/supplychain.git
cd ~/repos/sage/pkgs
git clone https://git.opencomputing.cn/yumoqing/supplychain
cd supplychain
../../py3/bin/pip install .
```
### 2. 构建模块
### 2. Generate DDL and apply to database
```bash
cd ~/repos/supplychain
chmod +x build.sh
./build.sh
cd ~/repos/sage/pkgs/supplychain/models
../../py3/bin/json2ddl mysql . > mysql.ddl.sql
mysql -h <db_host> -u <db_user> -p sage < mysql.ddl.sql
```
### 3. 生成 DDL 并创建数据库表
### 3. Generate CRUD UI files
```bash
cd ~/repos/supplychain/models
mysql -h <host> -u <user> -p <dbname> < mysql.ddl.sql
cd ~/repos/sage/pkgs/supplychain/json
../../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
from supplychain.init import load_supplychain
# 在 init() 函数中添加:
# In init():
load_supplychain()
```
**b. 修改 `build.sh`** (Sage 根目录):
```bash
for m in ... supplychain
### 6. Register RBAC permissions
Add to `~/repos/sage/load_path.py`:
```
/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 权限
模块提供 `scripts/load_path.py` 权限管理脚本,自动注册所有路径:
Then run:
```bash
cd ~/repos/sage
./py3/bin/python ~/repos/supplychain/scripts/load_path.py
cd ~/repos/sage && ./py3/bin/python load_path.py
```
脚本按角色分类注册:
- `any`: 静态资源/CRUD 别名目录
- `logined`: 所有页面和 API
- `reseller.operator`: 供应商、供销合同管理
- `reseller.sale`: 二级分销商、分销协议管理
**维护规则**: 每次代码变更如有新 path 出现,需同步更新 `scripts/load_path.py` 中的路径列表。
### 6. 重启 Sage
### 7. Restart Sage
```bash
cd ~/repos/sage
./stop.sh && ./start.sh
cd ~/repos/sage && ./stop.sh && ./start.sh
```
## 产品模块引用
## API for External Modules
本模块的 `productid``prodtypeid` 字段引用 product 模块的 `products``product_types` 表。
已在以下模型的 `codes` 段添加引用配置:
- `models/supply_contract_items.json`
- `models/distribution_agreement_items.json`
- `models/supplychain_accounting.json`
Other modules can call `calculate_sale_amounts()` to compute supply/distribution amounts during sales:
```python
env = ServerEnv()
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
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Find Sage root directory
# Find Sage root
SAGE_ROOT=""
for candidate in "$SCRIPT_DIR/../.." "$HOME/repos/sage" "$HOME/sage"; do
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
if [ -z "$SAGE_ROOT" ]; then
echo "ERROR: Cannot find Sage root directory"
echo "ERROR: Sage root not found"
exit 1
fi
echo "Sage root: $SAGE_ROOT"
# Install module
cd "$SCRIPT_DIR"
$SAGE_ROOT/py3/bin/pip install -e .
# Generate DDL from models
# Generate DDL from models if present
if [ -d "$SCRIPT_DIR/models" ]; then
echo "Generating DDL..."
echo "Generating DDL from models..."
cd "$SCRIPT_DIR/models"
$SAGE_ROOT/py3/bin/json2ddl mysql . > mysql.ddl.sql
echo "DDL generated: $SCRIPT_DIR/models/mysql.ddl.sql"
if ls *.xlsx 1>/dev/null 2>&1; then
"$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
# Generate CRUD UI from json definitions
# Generate CRUD UI from json definitions if present
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."
if [ -d "$SCRIPT_DIR/models" ]; then
"$SAGE_ROOT/py3/bin/xls2ui" -m "$SCRIPT_DIR/models" -o "$SCRIPT_DIR/wwwroot" supplychain *.json
echo "CRUD UI files generated in wwwroot/"
fi
fi
# Create symlink in Sage wwwroot
echo "Creating wwwroot symlink..."
# Link wwwroot to Sage
echo "Linking wwwroot to Sage..."
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",
"title": "分销协议产品折扣",
"params": {
"sortby": [
"created_at desc"
],
"sortby": ["prodtypeid", "productid"],
"logined_userorgid": "resellerid",
"browserfields": {
"exclouded": [
"created_at"
]
"exclouded": ["id", "agreement_id", "resellerid"]
},
"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')}}",

View File

@ -3,47 +3,37 @@
"alias": "distribution_agreements_list",
"title": "分销协议管理",
"params": {
"sortby": [
"created_at desc"
],
"sortby": ["created_at desc"],
"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": {
"exclouded": [
"created_by",
"created_at",
"updated_at"
],
"exclouded": ["id"],
"alters": {
"status": {
"uitype": "code",
"data": [
{
"value": "1",
"text": "有效"
},
{
"value": "0",
"text": "无效"
},
{
"value": "2",
"text": "已过期"
}
{"value": "1", "text": "生效中"},
{"value": "2", "text": "已到期"},
{"value": "0", "text": "已终止"}
]
}
}
},
"editexclouded": [
"id",
"resellerid",
"created_by",
"created_at",
"updated_at"
],
"subtables": [
{
"field": "id",
"title": "产品折扣",
"field": "agreement_id",
"title": "产品分销折扣",
"url": "{{entire_url('../distribution_agreement_items_list')}}",
"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",
"title": "供应商管理",
"params": {
"sortby": [
"created_at desc"
],
"sortby": ["created_at desc"],
"logined_userorgid": "resellerid",
"browserfields": {
"exclouded": [
"created_by",
"created_at",
"updated_at"
"data_filter": {
"AND": [
{"field": "supplier_name", "op": "LIKE", "var": "supplier_name"},
{"field": "status", "op": "=", "var": "status"}
]
},
"editexclouded": [
"id",
"resellerid",
"created_by",
"created_at",
"updated_at"
],
"filter_labels": {
"supplier_name": "供应商名称",
"status": "状态"
},
"browserfields": {
"exclouded": ["id"],
"alters": {
"status": {
"uitype": "code",
"data": [
{"value": "1", "text": "正常"},
{"value": "0", "text": "停用"}
]
}
}
},
"editable": {
"new_data_url": "{{entire_url('../api/suppliers_create.dspy')}}",
"update_data_url": "{{entire_url('../api/suppliers_update.dspy')}}",

View File

@ -3,21 +3,11 @@
"alias": "supply_contract_items_list",
"title": "供销合同产品折扣",
"params": {
"sortby": [
"created_at desc"
],
"sortby": ["prodtypeid", "productid"],
"logined_userorgid": "resellerid",
"browserfields": {
"exclouded": [
"created_at"
]
"exclouded": ["id", "contract_id", "resellerid"]
},
"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')}}",

View File

@ -3,47 +3,37 @@
"alias": "supply_contracts_list",
"title": "供销合同管理",
"params": {
"sortby": [
"created_at desc"
],
"sortby": ["created_at desc"],
"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": {
"exclouded": [
"created_by",
"created_at",
"updated_at"
],
"exclouded": ["id"],
"alters": {
"status": {
"uitype": "code",
"data": [
{
"value": "1",
"text": "有效"
},
{
"value": "0",
"text": "无效"
},
{
"value": "2",
"text": "已过期"
}
{"value": "1", "text": "生效中"},
{"value": "2", "text": "已到期"},
{"value": "0", "text": "已终止"}
]
}
}
},
"editexclouded": [
"id",
"resellerid",
"created_by",
"created_at",
"updated_at"
],
"subtables": [
{
"field": "id",
"title": "产品折扣",
"field": "contract_id",
"title": "产品折扣明细",
"url": "{{entire_url('../supply_contract_items_list')}}",
"subtable": "supply_contract_items"
}

View File

@ -24,7 +24,7 @@
},
{
"name": "resellerid",
"title": "所属分销商机构ID",
"title": "所属分销商机构ID",
"type": "str",
"length": 32,
"nullable": "no"
@ -51,8 +51,13 @@
"default": "1.0000"
},
{
"name": "settlement_price",
"title": "结算单价",
"name": "min_order_qty",
"title": "最小订购量",
"type": "int"
},
{
"name": "sale_price",
"title": "分销指导价",
"type": "double",
"length": 15,
"dec": 4
@ -87,18 +92,6 @@
"table": "distribution_agreements",
"valuefield": "id",
"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",
"title": "所属分销商机构ID",
"title": "所属分销商机构ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "sub_distributor_id",
"name": "sub_reseller_id",
"title": "二级分销商ID",
"type": "str",
"length": 32,
@ -105,9 +105,9 @@
"idxfields": ["resellerid"]
},
{
"name": "idx_da_subdist",
"name": "idx_da_sub_reseller",
"idxtype": "index",
"idxfields": ["sub_distributor_id"]
"idxfields": ["sub_reseller_id"]
},
{
"name": "idx_da_code",
@ -117,10 +117,10 @@
],
"codes": [
{
"field": "sub_distributor_id",
"table": "sub_distributors",
"field": "sub_reseller_id",
"table": "sub_resellers",
"valuefield": "id",
"textfield": "sub_dist_name"
"textfield": "sub_reseller_name"
},
{
"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]
name = "supplychain"
version = "1.0.0"
description = "供应商和分销商管理模块 — 供销合同、分销协议、产品折扣及供销记账"
description = "Supply chain management module - suppliers, contracts, sub-resellers, distribution agreements, and sales ledger"
requires-python = ">=3.8"
dependencies = [
"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.dictObject import DictObject
from appPublic.uniqueID import getID
from sqlor.dbpools import DBPools
from datetime import datetime
import json
from ahserver.serverenv import ServerEnv
MODULE_NAME = "supplychain"
MODULE_VERSION = "1.0.0"
def _get_dbname():
"""Get the database name for the supplychain module."""
"""Get module database name dynamically."""
env = ServerEnv()
return env.get_module_dbname('supplychain')
return env.get_module_dbname(MODULE_NAME)
def get_db_context():
"""Get a database context manager for the supplychain module."""
config = getConfig('.')
DBPools(config.databases)
dbname = _get_dbname()
return db.sqlorContext(dbname)
def _get_sor():
"""Get a sqlor context for this module's database."""
config = getConfig()
db = DBPools()
db.databases = config.databases
return db, _get_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.
def _generate_supplier_code(resellerid):
"""Generate unique supplier code: SUP-{YYYYMMDD}-{seq}."""
env = ServerEnv()
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
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})
def _generate_contract_code(resellerid):
"""Generate unique contract code: SC-{YYYYMMDD}-{seq}."""
env = ServerEnv()
today = env.strdate(env.today())
prefix = f"SC-{today.replace('-', '')}"
db, dbname = _get_sor()
with db.sqlorContext(dbname) as sor:
sql = """SELECT COUNT(*) as cnt FROM supply_contracts
WHERE resellerid = ${resellerid}$ AND contract_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}"
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,
def _generate_sub_reseller_code(resellerid):
"""Generate unique sub-reseller code: SR-{YYYYMMDD}-{seq}."""
env = ServerEnv()
today = env.strdate(env.today())
prefix = f"SR-{today.replace('-', '')}"
db, dbname = _get_sor()
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:
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,
async def update_supplier(request, params_kw):
"""Update a supplier record."""
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 ["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,
}
return None
await sor.C("supply_contracts", rec)
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):
"""
Get active distribution agreement discount for a product.
Priority: exact product > product type > agreement default.
async def update_supply_contract(request, params_kw):
"""Update a supply contract."""
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 ["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
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)
async def delete_supply_contract(request, params_kw):
"""Delete a supply contract and its items."""
data = params_kw
db, dbname = _get_sor()
async with db.sqlorContext(dbname) as sor:
await sor.D("supply_contract_items", {"contract_id": data["id"]})
await sor.D("supply_contracts", {"id": data["id"]})
return json.dumps({"status": "ok", "message": "删除成功"})
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,
# ============================================================
# Supply Contract Items APIs
# ============================================================
async def create_supply_contract_item(request, params_kw):
"""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:
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,
async def update_supply_contract_item(request, params_kw):
"""Update a supply contract 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", "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,
}
return None
await sor.C("sub_resellers", rec)
return json.dumps({"status": "ok", "data": rec, "message": "创建成功"})
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=""):
async def update_sub_reseller(request, params_kw):
"""Update a sub-reseller."""
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_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": "更新成功"})
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
supply_discount = float(data.get("supply_discount", 1.0))
supply_amount = total_amount * supply_discount
distribution_discount = float(data.get("distribution_discount", 1.0))
distribution_amount = total_amount * distribution_discount
profit_amount = distribution_amount - supply_amount
rec = {
"id": getID(),
"resellerid": resellerid,
"sub_reseller_id": data.get("sub_reseller_id"),
"supplier_id": data.get("supplier_id"),
"agreement_id": data.get("agreement_id"),
"contract_id": data.get("contract_id"),
"prodtypeid": data.get("prodtypeid"),
"productid": data.get("productid"),
"sale_date": data.get("sale_date"),
"quantity": quantity,
"unit_price": unit_price,
"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),
"settlement_status": data.get("settlement_status", "0"),
"remark": data.get("remark"),
"created_by": user_id,
"created_at": now,
"updated_at": now,
}
await sor.C("sales_ledger", rec)
return json.dumps({"status": "ok", "data": rec, "message": "记账成功"})
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 and record accounting for a product sale.
Calculate supply and distribution amounts for a sale.
Called by accounting/other modules during product sales.
Returns: the created accounting record as dict
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
"""
if sale_date is None:
sale_date = datetime.now().strftime("%Y-%m-%d")
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
# Get supply discount
supply_info = await get_active_supply_discount(sor, resellerid, productid, prodtypeid, sale_date)
db, dbname = _get_sor()
async with db.sqlorContext(dbname) as sor:
biz_date = await env.get_business_date(sor)
supply_contract_id = None
supply_contract_item_id = None
supplier_id = None
supply_discount = 1.0
supply_amount = total_amount
# 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 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
if recs_sc:
contract_id = recs_sc[0].id
supply_discount = float(recs_sc[0].default_discount)
# Get distribution discount
distribution_agreement_id = None
distribution_agreement_item_id = None
dist_discount = 1.0
dist_amount = total_amount
# 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)
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
supply_amount = total_amount * supply_discount
profit_amount = dist_amount - supply_amount
# 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
})
# 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")
}
if recs_da:
agreement_id = recs_da[0].id
distribution_discount = float(recs_da[0].default_discount)
await sor.C("supplychain_accounting", record)
return record
# 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():
"""Register all functions with ServerEnv."""
"""Register all supplychain 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
# Supplier
env.create_supplier = create_supplier
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

View File

@ -1,23 +1,9 @@
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_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})
from ahserver.serverenv import ServerEnv
env = ServerEnv()
create_func = getattr(env, 'create_distribution_agreement_items', None)
if create_func is None:
print(json.dumps({"status": "error", "message": "create_distribution_agreement_items function not found"}))
else:
result = await create_func(request, params_kw)
print(result)

View File

@ -1,19 +1,9 @@
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"})
from ahserver.serverenv import ServerEnv
env = ServerEnv()
delete_func = getattr(env, 'delete_distribution_agreement_items', None)
if delete_func is None:
print(json.dumps({"status": "error", "message": "delete_distribution_agreement_items function not found"}))
else:
result = await delete_func(request, params_kw)
print(result)

View File

@ -1,27 +1,9 @@
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})
from ahserver.serverenv import ServerEnv
env = ServerEnv()
update_func = getattr(env, 'update_distribution_agreement_items', None)
if update_func is None:
print(json.dumps({"status": "error", "message": "update_distribution_agreement_items function not found"}))
else:
result = await update_func(request, params_kw)
print(result)

View File

@ -1,28 +1,9 @@
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 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})
from ahserver.serverenv import ServerEnv
env = ServerEnv()
create_func = getattr(env, 'create_distribution_agreements', None)
if create_func is None:
print(json.dumps({"status": "error", "message": "create_distribution_agreements function not found"}))
else:
result = await create_func(request, params_kw)
print(result)

View File

@ -1,19 +1,9 @@
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"})
from ahserver.serverenv import ServerEnv
env = ServerEnv()
delete_func = getattr(env, 'delete_distribution_agreements', None)
if delete_func is None:
print(json.dumps({"status": "error", "message": "delete_distribution_agreements function not found"}))
else:
result = await delete_func(request, params_kw)
print(result)

View File

@ -1,27 +1,9 @@
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})
from ahserver.serverenv import ServerEnv
env = ServerEnv()
update_func = getattr(env, 'update_distribution_agreements', None)
if update_func is None:
print(json.dumps({"status": "error", "message": "update_distribution_agreements 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_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
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 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})
from ahserver.serverenv import ServerEnv
env = ServerEnv()
create_func = getattr(env, 'create_suppliers', None)
if create_func is None:
print(json.dumps({"status": "error", "message": "create_suppliers function not found"}))
else:
result = await create_func(request, params_kw)
print(result)

View File

@ -1,19 +1,9 @@
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"})
from ahserver.serverenv import ServerEnv
env = ServerEnv()
delete_func = getattr(env, 'delete_suppliers', None)
if delete_func is None:
print(json.dumps({"status": "error", "message": "delete_suppliers function not found"}))
else:
result = await delete_func(request, params_kw)
print(result)

View File

@ -1,27 +1,9 @@
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})
from ahserver.serverenv import ServerEnv
env = ServerEnv()
update_func = getattr(env, 'update_suppliers', None)
if update_func is None:
print(json.dumps({"status": "error", "message": "update_suppliers function not found"}))
else:
result = await update_func(request, params_kw)
print(result)

View File

@ -1,23 +1,9 @@
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_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})
from ahserver.serverenv import ServerEnv
env = ServerEnv()
create_func = getattr(env, 'create_supply_contract_items', None)
if create_func is None:
print(json.dumps({"status": "error", "message": "create_supply_contract_items function not found"}))
else:
result = await create_func(request, params_kw)
print(result)

View File

@ -1,19 +1,9 @@
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"})
from ahserver.serverenv import ServerEnv
env = ServerEnv()
delete_func = getattr(env, 'delete_supply_contract_items', None)
if delete_func is None:
print(json.dumps({"status": "error", "message": "delete_supply_contract_items function not found"}))
else:
result = await delete_func(request, params_kw)
print(result)

View File

@ -1,27 +1,9 @@
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})
from ahserver.serverenv import ServerEnv
env = ServerEnv()
update_func = getattr(env, 'update_supply_contract_items', None)
if update_func is None:
print(json.dumps({"status": "error", "message": "update_supply_contract_items function not found"}))
else:
result = await update_func(request, params_kw)
print(result)

View File

@ -1,28 +1,9 @@
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 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})
from ahserver.serverenv import ServerEnv
env = ServerEnv()
create_func = getattr(env, 'create_supply_contracts', None)
if create_func is None:
print(json.dumps({"status": "error", "message": "create_supply_contracts function not found"}))
else:
result = await create_func(request, params_kw)
print(result)

View File

@ -1,19 +1,9 @@
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"})
from ahserver.serverenv import ServerEnv
env = ServerEnv()
delete_func = getattr(env, 'delete_supply_contracts', None)
if delete_func is None:
print(json.dumps({"status": "error", "message": "delete_supply_contracts function not found"}))
else:
result = await delete_func(request, params_kw)
print(result)

View File

@ -1,27 +1,9 @@
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})
from ahserver.serverenv import ServerEnv
env = ServerEnv()
update_func = getattr(env, 'update_supply_contracts', None)
if update_func is None:
print(json.dumps({"status": "error", "message": "update_supply_contracts function not found"}))
else:
result = await update_func(request, params_kw)
print(result)

View File

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

View File

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