增加委外管理模块

This commit is contained in:
zzh 2026-03-13 10:48:38 +08:00
parent 23cf2ed453
commit a04c222f0a
2 changed files with 514 additions and 0 deletions

View File

@ -152,6 +152,26 @@
<a href="#/plan-mgmt/reconciliation" class="dropdown-item" data-route="plan-mgmt-reconciliation">对账单</a>
</div>
</div>
<div class="topnav-item has-dropdown" data-menu="outsourcing">
<span class="nav-left">
<span class="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M22 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
</svg>
</span>
<span class="topnav-text">委外管理</span>
</span>
<span class="topnav-caret"></span>
<div class="topnav-dropdown">
<a href="#/outsourcing-mgmt/orders" class="dropdown-item" data-route="outsourcing-mgmt-orders">委外工单</a>
<a href="#/outsourcing-mgmt/material-issue" class="dropdown-item" data-route="outsourcing-mgmt-material-issue">委外发料</a>
<a href="#/outsourcing-mgmt/finished-goods-receipt" class="dropdown-item" data-route="outsourcing-mgmt-finished-goods-receipt">成品入库</a>
<a href="#/outsourcing-mgmt/wip-stock" class="dropdown-item" data-route="outsourcing-mgmt-wip-stock">委外在制库存</a>
</div>
</div>
<div class="topnav-item has-dropdown" data-menu="collect">
<span class="nav-left">
<span class="nav-icon" aria-hidden="true">
@ -475,6 +495,10 @@
<script src="./js/components/purchase-demand.js?v=20260312" defer></script>
<script src="./js/components/customer-order.js" defer></script>
<script src="./js/components/reconciliation.js" defer></script>
<script src="./js/components/outsourcing-orders.js" defer></script>
<script src="./js/components/outsourcing-material-issue.js" defer></script>
<script src="./js/components/finished-goods-receipt.js" defer></script>
<script src="./js/components/outsourcing-wip-stock.js" defer></script>
<script src="./js/components/export.js" defer></script>
<script src="./js/components/settings.js" defer></script>
<script src="./js/components/notifications.js" defer></script>

View File

@ -314,6 +314,65 @@ def init_db():
updated_at TEXT
)''')
# 委外工单表 - 记录委外生产订单
c.execute('''CREATE TABLE IF NOT EXISTS outsourcing_orders(
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_no TEXT NOT NULL UNIQUE,
customer_order_no TEXT NOT NULL,
product_code TEXT NOT NULL,
product_name TEXT NOT NULL,
production_qty INTEGER NOT NULL,
outsourcing_factory TEXT NOT NULL,
delivery_date TEXT NOT NULL,
status TEXT DEFAULT 'pending',
created_by TEXT,
created_at TEXT,
updated_at TEXT
)''')
# 委外发料表 - 记录发给外协厂的物料
c.execute('''CREATE TABLE IF NOT EXISTS outsourcing_material_issue(
id INTEGER PRIMARY KEY AUTOINCREMENT,
issue_no TEXT NOT NULL UNIQUE,
outsourcing_order_no TEXT NOT NULL,
material_code TEXT NOT NULL,
material_name TEXT NOT NULL,
issue_qty INTEGER NOT NULL,
unit TEXT DEFAULT 'pcs',
issue_date TEXT NOT NULL,
created_by TEXT,
created_at TEXT,
updated_at TEXT,
FOREIGN KEY(outsourcing_order_no) REFERENCES outsourcing_orders(order_no)
)''')
# 委外在制库存表 - 记录发给外协厂但未完工的物料
c.execute('''CREATE TABLE IF NOT EXISTS outsourcing_wip_stock(
id INTEGER PRIMARY KEY AUTOINCREMENT,
outsourcing_order_no TEXT NOT NULL,
material_code TEXT NOT NULL UNIQUE,
material_name TEXT NOT NULL,
wip_qty INTEGER DEFAULT 0,
unit TEXT DEFAULT 'pcs',
updated_at TEXT,
FOREIGN KEY(outsourcing_order_no) REFERENCES outsourcing_orders(order_no)
)''')
# 成品入库表 - 记录外协厂完工的整机入库
c.execute('''CREATE TABLE IF NOT EXISTS finished_goods_receipt(
id INTEGER PRIMARY KEY AUTOINCREMENT,
receipt_no TEXT NOT NULL UNIQUE,
outsourcing_order_no TEXT NOT NULL,
product_code TEXT NOT NULL,
product_name TEXT NOT NULL,
receipt_qty INTEGER NOT NULL,
receipt_date TEXT NOT NULL,
created_by TEXT,
created_at TEXT,
updated_at TEXT,
FOREIGN KEY(outsourcing_order_no) REFERENCES outsourcing_orders(order_no)
)''')
conn.commit()
# create default admin
c.execute('SELECT id FROM users WHERE username=?', ('admin',))
@ -7159,6 +7218,437 @@ def import_purchase_demand():
conn.close()
# ==================== 委外工单管理 API ====================
@app.get('/api/outsourcing-orders')
@require_login
@require_any_role('superadmin', 'admin')
def get_outsourcing_orders():
"""获取委外工单列表"""
conn = get_db()
c = conn.cursor()
c.execute('SELECT * FROM outsourcing_orders ORDER BY created_at DESC')
rows = c.fetchall()
conn.close()
return jsonify({'list': [dict(r) for r in rows]})
@app.post('/api/outsourcing-orders')
@require_login
@require_any_role('superadmin', 'admin')
def create_outsourcing_order():
"""创建委外工单"""
data = request.get_json() or {}
customer_order_no = data.get('customer_order_no', '').strip()
product_code = data.get('product_code', '').strip()
product_name = data.get('product_name', '').strip()
production_qty = data.get('production_qty', 0)
outsourcing_factory = data.get('outsourcing_factory', '').strip()
delivery_date = data.get('delivery_date', '').strip()
if not all([customer_order_no, product_code, product_name, outsourcing_factory, delivery_date]):
return jsonify({'error': '请填写所有必填字段'}), 400
try:
production_qty = int(production_qty)
if production_qty <= 0:
return jsonify({'error': '生产数量必须大于0'}), 400
except (ValueError, TypeError):
return jsonify({'error': '生产数量必须是有效整数'}), 400
conn = get_db()
c = conn.cursor()
# 生成委外工单号 WW+年月日+4位流水号
now = get_beijing_time()
date_str = datetime.now().strftime('%Y%m%d')
c.execute("SELECT COUNT(*) FROM outsourcing_orders WHERE order_no LIKE ?", (f'WW{date_str}%',))
count = c.fetchone()[0]
order_no = f'WW{date_str}{str(count + 1).zfill(4)}'
username = session.get('username', '')
try:
c.execute('''INSERT INTO outsourcing_orders(
order_no, customer_order_no, product_code, product_name,
production_qty, outsourcing_factory, delivery_date, status,
created_by, created_at, updated_at
) VALUES(?,?,?,?,?,?,?,?,?,?,?)''', (
order_no, customer_order_no, product_code, product_name,
production_qty, outsourcing_factory, delivery_date, 'pending',
username, now, now
))
conn.commit()
conn.close()
log('create_outsourcing_order', f'创建委外工单: {order_no}')
return jsonify({'ok': True, 'order_no': order_no, 'message': '委外工单创建成功'})
except Exception as e:
conn.close()
return jsonify({'error': f'创建失败: {str(e)}'}), 500
@app.get('/api/outsourcing-orders/<int:order_id>')
@require_login
@require_any_role('superadmin', 'admin')
def get_outsourcing_order_detail(order_id):
"""获取委外工单详情及物料明细"""
conn = get_db()
c = conn.cursor()
# 获取工单基本信息
c.execute('SELECT * FROM outsourcing_orders WHERE id=?', (order_id,))
order = c.fetchone()
if not order:
conn.close()
return jsonify({'error': '委外工单不存在'}), 404
# 获取该产品的BOM物料清单
c.execute('SELECT * FROM bom WHERE product_code=?', (order['product_code'],))
bom_list = c.fetchall()
# 计算每种物料的需发数量 = BOM用量 * 生产数量
materials = []
for bom in bom_list:
need_qty = int(math.ceil(bom['unit_qty'] * order['production_qty']))
materials.append({
'material_code': bom['material_code'],
'material_name': bom['material_name'],
'bom_unit_qty': bom['unit_qty'],
'need_qty': need_qty,
'unit': bom['unit']
})
conn.close()
return jsonify({
'order': dict(order),
'materials': materials
})
@app.put('/api/outsourcing-orders/<int:order_id>')
@require_login
@require_any_role('superadmin', 'admin')
def update_outsourcing_order(order_id):
"""更新委外工单"""
data = request.get_json() or {}
conn = get_db()
c = conn.cursor()
c.execute('SELECT * FROM outsourcing_orders WHERE id=?', (order_id,))
existing = c.fetchone()
if not existing:
conn.close()
return jsonify({'error': '委外工单不存在'}), 404
update_fields = []
update_values = []
if 'production_qty' in data:
update_fields.append('production_qty=?')
update_values.append(int(data['production_qty']))
if 'outsourcing_factory' in data:
update_fields.append('outsourcing_factory=?')
update_values.append(data['outsourcing_factory'])
if 'delivery_date' in data:
update_fields.append('delivery_date=?')
update_values.append(data['delivery_date'])
if 'status' in data:
update_fields.append('status=?')
update_values.append(data['status'])
update_fields.append('updated_at=?')
update_values.append(get_beijing_time())
update_values.append(order_id)
c.execute(f'UPDATE outsourcing_orders SET {", ".join(update_fields)} WHERE id=?', update_values)
conn.commit()
conn.close()
log('update_outsourcing_order', f'更新委外工单 ID: {order_id}')
return jsonify({'ok': True, 'message': '更新成功'})
@app.delete('/api/outsourcing-orders/<int:order_id>')
@require_login
@require_any_role('superadmin', 'admin')
def delete_outsourcing_order(order_id):
"""删除委外工单"""
conn = get_db()
c = conn.cursor()
c.execute('SELECT order_no FROM outsourcing_orders WHERE id=?', (order_id,))
row = c.fetchone()
if not row:
conn.close()
return jsonify({'error': '委外工单不存在'}), 404
c.execute('DELETE FROM outsourcing_orders WHERE id=?', (order_id,))
conn.commit()
conn.close()
log('delete_outsourcing_order', f'删除委外工单 ID: {order_id}')
return jsonify({'ok': True, 'message': '删除成功'})
# ==================== 委外发料管理 API ====================
@app.get('/api/outsourcing-material-issue')
@require_login
@require_any_role('superadmin', 'admin')
def get_material_issue_list():
"""获取委外发料列表"""
conn = get_db()
c = conn.cursor()
c.execute('SELECT * FROM outsourcing_material_issue ORDER BY created_at DESC')
rows = c.fetchall()
conn.close()
return jsonify({'list': [dict(r) for r in rows]})
@app.post('/api/outsourcing-material-issue')
@require_login
@require_any_role('superadmin', 'admin')
def create_material_issue():
"""创建委外发料单"""
data = request.get_json() or {}
outsourcing_order_no = data.get('outsourcing_order_no', '').strip()
materials = data.get('materials', [])
issue_date = data.get('issue_date', '').strip()
if not outsourcing_order_no or not materials or not issue_date:
return jsonify({'error': '请填写所有必填字段'}), 400
conn = get_db()
c = conn.cursor()
# 验证委外工单是否存在
c.execute('SELECT * FROM outsourcing_orders WHERE order_no=?', (outsourcing_order_no,))
order = c.fetchone()
if not order:
conn.close()
return jsonify({'error': '委外工单不存在'}), 404
# 生成发料单号 FL+年月日+4位流水号
now = get_beijing_time()
date_str = datetime.now().strftime('%Y%m%d')
c.execute("SELECT COUNT(*) FROM outsourcing_material_issue WHERE issue_no LIKE ?", (f'FL{date_str}%',))
count = c.fetchone()[0]
issue_no = f'FL{date_str}{str(count + 1).zfill(4)}'
username = session.get('username', '')
try:
for material in materials:
material_code = material.get('material_code', '').strip()
material_name = material.get('material_name', '').strip()
issue_qty = int(material.get('issue_qty', 0))
unit = material.get('unit', 'pcs').strip()
if not material_code or issue_qty <= 0:
continue
# 插入发料记录
c.execute('''INSERT INTO outsourcing_material_issue(
issue_no, outsourcing_order_no, material_code, material_name,
issue_qty, unit, issue_date, created_by, created_at, updated_at
) VALUES(?,?,?,?,?,?,?,?,?,?)''', (
issue_no, outsourcing_order_no, material_code, material_name,
issue_qty, unit, issue_date, username, now, now
))
# 扣减期初库存
c.execute('SELECT stock_qty FROM initial_stock WHERE material_code=?', (material_code,))
stock_row = c.fetchone()
if stock_row:
new_stock = stock_row['stock_qty'] - issue_qty
c.execute('UPDATE initial_stock SET stock_qty=?, updated_at=? WHERE material_code=?',
(new_stock, now, material_code))
# 增加委外在制库存
c.execute('SELECT wip_qty FROM outsourcing_wip_stock WHERE material_code=?', (material_code,))
wip_row = c.fetchone()
if wip_row:
new_wip = wip_row['wip_qty'] + issue_qty
c.execute('UPDATE outsourcing_wip_stock SET wip_qty=?, updated_at=? WHERE material_code=?',
(new_wip, now, material_code))
else:
c.execute('''INSERT INTO outsourcing_wip_stock(
outsourcing_order_no, material_code, material_name, wip_qty, unit, updated_at
) VALUES(?,?,?,?,?,?)''', (
outsourcing_order_no, material_code, material_name, issue_qty, unit, now
))
conn.commit()
conn.close()
log('create_material_issue', f'创建委外发料单: {issue_no}')
return jsonify({'ok': True, 'issue_no': issue_no, 'message': '发料成功'})
except Exception as e:
conn.rollback()
conn.close()
return jsonify({'error': f'发料失败: {str(e)}'}), 500
# ==================== 成品入库管理 API ====================
@app.get('/api/finished-goods-receipt')
@require_login
@require_any_role('superadmin', 'admin')
def get_finished_goods_receipt_list():
"""获取成品入库列表"""
conn = get_db()
c = conn.cursor()
c.execute('SELECT * FROM finished_goods_receipt ORDER BY created_at DESC')
rows = c.fetchall()
conn.close()
return jsonify({'list': [dict(r) for r in rows]})
@app.post('/api/finished-goods-receipt')
@require_login
@require_any_role('superadmin', 'admin')
def create_finished_goods_receipt():
"""创建成品入库单"""
data = request.get_json() or {}
outsourcing_order_no = data.get('outsourcing_order_no', '').strip()
receipt_qty = data.get('receipt_qty', 0)
receipt_date = data.get('receipt_date', '').strip()
if not outsourcing_order_no or not receipt_date:
return jsonify({'error': '请填写所有必填字段'}), 400
try:
receipt_qty = int(receipt_qty)
if receipt_qty <= 0:
return jsonify({'error': '入库数量必须大于0'}), 400
except (ValueError, TypeError):
return jsonify({'error': '入库数量必须是有效整数'}), 400
conn = get_db()
c = conn.cursor()
# 验证委外工单是否存在
c.execute('SELECT * FROM outsourcing_orders WHERE order_no=?', (outsourcing_order_no,))
order = c.fetchone()
if not order:
conn.close()
return jsonify({'error': '委外工单不存在'}), 404
# 生成入库单号 RK+年月日+4位流水号
now = get_beijing_time()
date_str = datetime.now().strftime('%Y%m%d')
c.execute("SELECT COUNT(*) FROM finished_goods_receipt WHERE receipt_no LIKE ?", (f'RK{date_str}%',))
count = c.fetchone()[0]
receipt_no = f'RK{date_str}{str(count + 1).zfill(4)}'
username = session.get('username', '')
try:
# 插入入库记录
c.execute('''INSERT INTO finished_goods_receipt(
receipt_no, outsourcing_order_no, product_code, product_name,
receipt_qty, receipt_date, created_by, created_at, updated_at
) VALUES(?,?,?,?,?,?,?,?,?)''', (
receipt_no, outsourcing_order_no, order['product_code'], order['product_name'],
receipt_qty, receipt_date, username, now, now
))
# 获取该产品的BOM计算物料消耗
c.execute('SELECT * FROM bom WHERE product_code=?', (order['product_code'],))
bom_list = c.fetchall()
material_consumption = []
for bom in bom_list:
material_code = bom['material_code']
consumed_qty = int(math.ceil(bom['unit_qty'] * receipt_qty))
# 扣减委外在制库存
c.execute('SELECT wip_qty FROM outsourcing_wip_stock WHERE material_code=?', (material_code,))
wip_row = c.fetchone()
if wip_row:
new_wip = max(0, wip_row['wip_qty'] - consumed_qty)
c.execute('UPDATE outsourcing_wip_stock SET wip_qty=?, updated_at=? WHERE material_code=?',
(new_wip, now, material_code))
material_consumption.append({
'material_code': material_code,
'material_name': bom['material_name'],
'consumed_qty': consumed_qty
})
# 增加成品库存(如果需要跟踪成品库存)
c.execute('SELECT stock_qty FROM initial_stock WHERE material_code=?', (order['product_code'],))
product_stock = c.fetchone()
if product_stock:
new_stock = product_stock['stock_qty'] + receipt_qty
c.execute('UPDATE initial_stock SET stock_qty=?, updated_at=? WHERE material_code=?',
(new_stock, now, order['product_code']))
else:
c.execute('''INSERT INTO initial_stock(
material_code, material_name, stock_qty, unit, created_at, updated_at
) VALUES(?,?,?,?,?,?)''', (
order['product_code'], order['product_name'], receipt_qty, 'pcs', now, now
))
conn.commit()
conn.close()
log('create_finished_goods_receipt', f'创建成品入库单: {receipt_no}')
return jsonify({
'ok': True,
'receipt_no': receipt_no,
'material_consumption': material_consumption,
'message': '入库成功'
})
except Exception as e:
conn.rollback()
conn.close()
return jsonify({'error': f'入库失败: {str(e)}'}), 500
# ==================== 委外在制库存查询 API ====================
@app.get('/api/outsourcing-wip-stock')
@require_login
@require_any_role('superadmin', 'admin')
def get_outsourcing_wip_stock():
"""获取委外在制库存列表"""
conn = get_db()
c = conn.cursor()
c.execute('SELECT * FROM outsourcing_wip_stock WHERE wip_qty > 0 ORDER BY updated_at DESC')
rows = c.fetchall()
conn.close()
return jsonify({'list': [dict(r) for r in rows]})
# ==================== 客户订单查询 API用于委外工单关联 ====================
@app.get('/api/customer-orders-for-outsourcing')
@require_login
@require_any_role('superadmin', 'admin')
def get_customer_orders_for_outsourcing():
"""获取客户订单列表(用于委外工单关联)"""
conn = get_db()
c = conn.cursor()
c.execute('SELECT DISTINCT order_no, customer_name, material FROM customer_orders ORDER BY created_at DESC LIMIT 100')
rows = c.fetchall()
conn.close()
return jsonify({'list': [dict(r) for r in rows]})
@app.errorhandler(404)
def not_found(e):
# 如果请求的是 HTML 页面(通过 Accept header 判断),返回 index.html