dy-dashboard/export_finished_inventory.py
2026-10-28 11:14:17 +08:00

164 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""导出所有成品库存数据(总库存 + 可发库存 + 30天销量 + 可支撑天数 + 单仓库明细)
口径说明:
- 成品定义:products.product_type IN ('成品','清库存','现货','赠品') 且 deleted_at IS NULL
- 建档时间:products.created_at
- 成本价:命中特殊规则用 SPEC 正确成本价,否则用最新库存日 inventory_detail.cost_price
- 总库存:最新库存日所有仓库 available_qty 合计(含15号加工厂储存仓)
- 可发库存:最新库存日排除15号仓库 available_qty 合计(含 EXCLUDE_WAREHOUSE_20_PRODUCTS 特例排除)
- 30天总销量:daily_outbound 近30天 SUM(货品数量),排除内料(名称含"内料"或编码含 -NL)
- 可支撑天数:可发库存 / (30天总销量/30),无销量=999
"""
import io
import os
import sys
from datetime import datetime
sys.path.insert(0, '/home/gxd/.openclaw/workspace/dy-dashboard')
from services.db import query
from services.inventory_calc import (
resolve_cost_price, SPECIAL_COST_PRICES, EXCLUDE_WAREHOUSE_20_PRODUCTS,
_get_latest_inv_date,
)
FINISHED_TYPES = ('成品', '清库存', '现货', '赠品')
def main():
latest_date = _get_latest_inv_date()
print(f'最新库存日期: {latest_date}')
# 1. 成品列表
products = query("""
SELECT product_code, product_name, created_at
FROM products
WHERE product_type IN ('成品','清库存','现货','赠品') AND deleted_at IS NULL
ORDER BY product_code ASC
""")
print(f'成品数: {len(products)}')
# 2. 最新库存日各仓库库存(pivot)
inv_rows = query("""
SELECT product_code, warehouse_code, available_qty
FROM inventory_detail
WHERE data_date = %s
""", (latest_date,))
from collections import defaultdict
per_wh = defaultdict(dict) # code -> {wh: qty}
total_qty = defaultdict(int) # code -> 所有仓库合计(含15号)
avail_qty = defaultdict(int) # code -> 排除15号 + 20号特例
for r in inv_rows:
code = r['product_code']
wh = r['warehouse_code']
qty = int(r['available_qty'] or 0)
per_wh[code][wh] = per_wh[code].get(wh, 0) + qty
total_qty[code] += qty
if wh != '15':
if code in EXCLUDE_WAREHOUSE_20_PRODUCTS and str(wh).zfill(2) == '20':
continue
avail_qty[code] += qty
# 3. 最新库存日成本价
cost_rows = query("""
SELECT product_code, MAX(cost_price) AS pr
FROM inventory_detail WHERE data_date = %s
GROUP BY product_code
""", (latest_date,))
cost_map = {r['product_code']: float(r['pr'] or 0) for r in cost_rows}
# 4. 30天总销量(排除内料)
sales_rows = query("""
SELECT 货品编号 AS code, SUM(货品数量) AS qty
FROM daily_outbound
WHERE 日期 >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
AND 货品名称 NOT LIKE %s AND 货品编号 NOT LIKE %s
GROUP BY 货品编号
""", ('%内料%', '%-NL'))
sales_30 = {r['code']: float(r['qty'] or 0) for r in sales_rows}
# 5. 仓库列(最新库存日实际出现的仓库,按编码排序)
wh_codes = sorted({wh for d in per_wh.values() for wh in d.keys()})
wh_names = {w['warehouse_code']: w['warehouse_name'] for w in query(
"SELECT warehouse_code, warehouse_name FROM warehouses")}
wh_cols = []
for wh in wh_codes:
name = wh_names.get(wh, '')
if str(wh).zfill(2) in wh_names:
name = wh_names[str(wh).zfill(2)]
elif str(wh).lstrip('0') in wh_names:
name = wh_names[str(wh).lstrip('0')]
wh_cols.append((wh, name))
print(f'仓库列数: {len(wh_cols)}')
# 6. 生成 Excel
import openpyxl
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
from openpyxl.utils import get_column_letter
wb = openpyxl.Workbook()
ws = wb.active
ws.title = '成品库存明细'
headers = ['货品编码', '货品名称', '建档时间', '成本价', '总库存', '可发库存',
'可支撑天数', '30天总销量']
for wh, name in wh_cols:
headers.append(f'{wh} {name}')
ws.append(headers)
header_fill = PatternFill('solid', fgColor='4472C4')
header_font = Font(name='微软雅黑', bold=True, color='FFFFFF', size=11)
thin = Side(style='thin', color='D9D9D9')
border = Border(left=thin, right=thin, top=thin, bottom=thin)
for c in ws[1]:
c.fill = header_fill
c.font = header_font
c.alignment = Alignment(horizontal='center', vertical='center')
c.border = border
for p in products:
code = p['product_code']
name = p['product_name'] or ''
created = p['created_at']
created_str = created.strftime('%Y-%m-%d') if hasattr(created, 'strftime') else str(created)[:10] if created else ''
price = resolve_cost_price(code, cost_map.get(code, 0.0))
total = total_qty.get(code, 0)
avail = avail_qty.get(code, 0)
s30 = int(sales_30.get(code, 0))
daily = s30 / 30.0
support = round(avail / daily, 1) if daily > 0 else 999
row = [code, name, created_str, price, total, avail, support, s30]
for wh, _ in wh_cols:
row.append(per_wh[code].get(wh, 0))
ws.append(row)
# 列宽
widths = {'货品编码': 24, '货品名称': 40, '建档时间': 12, '成本价': 10,
'总库存': 10, '可发库存': 10, '可支撑天数': 12, '30天总销量': 12}
for i, h in enumerate(headers, start=1):
ws.column_dimensions[get_column_letter(i)].width = widths.get(h, 16)
# 冻结首行
ws.freeze_panes = 'A2'
# 数值格式
for row in ws.iter_rows(min_row=2):
row[3].number_format = '0.0000' # 成本价
for cell in row[4:]:
cell.number_format = '#,##0'
out_path = None
if len(sys.argv) > 1 and sys.argv[1].startswith('--out='):
out_path = sys.argv[1].split('=', 1)[1]
if not out_path:
out_path = f'/home/gxd/.openclaw/workspace/dingtalk-assistant/tmp_export/成品库存明细_{datetime.now().strftime("%Y%m%d")}.xlsx'
os.makedirs(os.path.dirname(out_path), exist_ok=True)
wb.save(out_path)
print(f'已生成: {out_path}')
print(f'行数: {len(products)}, 列数: {len(headers)}')
if __name__ == '__main__':
main()