# -*- 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()