diff --git a/browser_login/fetch_receipt_details_full.py b/browser_login/fetch_receipt_details_full.py index 7ff9c94..30157da 100644 --- a/browser_login/fetch_receipt_details_full.py +++ b/browser_login/fetch_receipt_details_full.py @@ -178,7 +178,7 @@ def fetch_receipt_details_full(): "转换单位": item.get("convertUnitName"), "收货单价": item.get("receivePrice"), "收货时间": item.get("receiptTime"), - "进货数量": item.get("convertPlannedPurchaseQuantity") if item.get("convertPlannedPurchaseQuantity") is not None else item.get("plannedPurchaseQuantity"), + "进货数量": item.get("plannedPurchaseQuantity"), "收货数量": item.get("convertGoodsQuantity") if item.get("convertGoodsQuantity") is not None else item.get("goodsQuantity"), "收货总金额": item.get("receiveAmount") }) diff --git a/browser_login/fetch_receipt_details_incremental.py b/browser_login/fetch_receipt_details_incremental.py index 729c2f3..4d8637d 100644 --- a/browser_login/fetch_receipt_details_incremental.py +++ b/browser_login/fetch_receipt_details_incremental.py @@ -173,7 +173,8 @@ def fetch_receipt_details_incremental(): cursor.execute('SELECT id FROM receipt_details WHERE purchase_order_code = ? AND row_no = ? AND material_code = ?', (po_code, row_no, mat_code)) existing_record = cursor.fetchone() - p_qty = item.get("convertPlannedPurchaseQuantity") if item.get("convertPlannedPurchaseQuantity") is not None else item.get("plannedPurchaseQuantity") + # 进货数量(件数)永远只取原始的 plannedPurchaseQuantity,不取转换后的 + p_qty = item.get("plannedPurchaseQuantity") r_qty = item.get("convertGoodsQuantity") if item.get("convertGoodsQuantity") is not None else item.get("goodsQuantity") if existing_record: diff --git a/browser_login/import_to_sqlite.py b/browser_login/import_to_sqlite.py index 049882f..5b2d759 100644 --- a/browser_login/import_to_sqlite.py +++ b/browser_login/import_to_sqlite.py @@ -40,22 +40,17 @@ def init_db(): cursor.execute('CREATE INDEX IF NOT EXISTS idx_receipt_supplier_name ON receipt_details(supplier_name)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_receipt_time ON receipt_details(receipt_time)') - # 创建 BOM 成本表(父件表) - cursor.execute('DROP TABLE IF EXISTS bom_child') - cursor.execute('DROP TABLE IF EXISTS bom_parent') - + # 注意:为了在打包部署时不丢失用户已抓取的数据,改为 IF NOT EXISTS cursor.execute(''' - CREATE TABLE bom_parent ( + CREATE TABLE IF NOT EXISTS bom_parent ( id INTEGER PRIMARY KEY AUTOINCREMENT, parent_material_code TEXT UNIQUE, parent_material_name TEXT ) ''') - # 创建 BOM 成本表(子件明细表) - # 由于是树状结构,我们采用“邻接表”模型,记录每个节点的 parent_id cursor.execute(''' - CREATE TABLE bom_child ( + CREATE TABLE IF NOT EXISTS bom_child ( id INTEGER PRIMARY KEY AUTOINCREMENT, parent_material_code TEXT, -- 归属的最顶层父件 node_material_code TEXT, diff --git a/web_ui/app.py b/web_ui/app.py index 76a2528..515c80f 100644 --- a/web_ui/app.py +++ b/web_ui/app.py @@ -40,6 +40,20 @@ DB_PATH.parent.mkdir(parents=True, exist_ok=True) # 抓取脚本目录 (属于代码文件,从 BASE_DIR 加载) BROWSER_LOGIN_DIR = BASE_DIR / "browser_login" +def auto_init_db(): + """如果是新环境首次运行,自动初始化数据库表结构""" + if str(BROWSER_LOGIN_DIR) not in sys.path: + sys.path.insert(0, str(BROWSER_LOGIN_DIR)) + try: + from import_to_sqlite import init_db + conn = init_db() + conn.close() + except Exception as e: + print(f"⚠️ 初始化数据库表结构失败: {e}") + +# 在应用启动前执行初始化 +auto_init_db() + def background_sync_job(): """APScheduler 后台定时任务执行增量抓取""" print("[定时任务] 正在执行后台增量数据同步...") @@ -284,15 +298,19 @@ def get_bom_tree(parent_code): if material_codes: # 使用 IN 语句批量查询,并按物料分组取最新时间的价格 placeholders = ','.join(['?'] * len(material_codes)) - # SQLite 分组取最新记录的经典写法 + # SQLite 分组取最新记录的安全写法(使用窗口函数 ROW_NUMBER) price_records = conn.execute(f''' - SELECT material_code, - ROUND(COALESCE(total_amount / NULLIF(receive_qty, 0), receive_price, 0), 2) AS receive_price, - receipt_time, purchase_order_code, supplier_name - FROM receipt_details - WHERE material_code IN ({placeholders}) - GROUP BY material_code - HAVING receipt_time = MAX(receipt_time) + WITH RankedRecords AS ( + SELECT material_code, + ROUND(COALESCE(total_amount / NULLIF(receive_qty, 0), receive_price, 0), 2) AS receive_price, + receipt_time, purchase_order_code, supplier_name, + ROW_NUMBER() OVER(PARTITION BY material_code ORDER BY receipt_time DESC, id DESC) as rn + FROM receipt_details + WHERE material_code IN ({placeholders}) + ) + SELECT material_code, receive_price, receipt_time, purchase_order_code, supplier_name + FROM RankedRecords + WHERE rn = 1 ''', material_codes).fetchall() for r in price_records: @@ -478,15 +496,19 @@ def get_bom_tree_compare(parent_code): # --- A. 查询历史最新价格及铸件重量/单位 --- latest_records = conn.execute(f''' - SELECT material_code, - unit_name, - purchase_qty, - receive_qty, - ROUND(COALESCE(total_amount / NULLIF(receive_qty, 0), receive_price, 0), 2) AS receive_price - FROM receipt_details - WHERE material_code IN ({placeholders}) - GROUP BY material_code - HAVING receipt_time = MAX(receipt_time) + WITH RankedRecords AS ( + SELECT material_code, + unit_name, + purchase_qty, + receive_qty, + ROUND(COALESCE(total_amount / NULLIF(receive_qty, 0), receive_price, 0), 2) AS receive_price, + ROW_NUMBER() OVER(PARTITION BY material_code ORDER BY receipt_time DESC, id DESC) as rn + FROM receipt_details + WHERE material_code IN ({placeholders}) + ) + SELECT material_code, unit_name, purchase_qty, receive_qty, receive_price + FROM RankedRecords + WHERE rn = 1 ''', unique_codes).fetchall() for r in latest_records: @@ -510,13 +532,17 @@ def get_bom_tree_compare(parent_code): # 1. 先查询该期间内的最新收货价 params = unique_codes + [start_date, end_date_full] period_latest_records = conn.execute(f''' - SELECT material_code, - ROUND(COALESCE(total_amount / NULLIF(receive_qty, 0), receive_price, 0), 2) AS receive_price - FROM receipt_details - WHERE material_code IN ({placeholders}) - AND receipt_time >= ? AND receipt_time <= ? - GROUP BY material_code - HAVING receipt_time = MAX(receipt_time) + WITH RankedRecords AS ( + SELECT material_code, + ROUND(COALESCE(total_amount / NULLIF(receive_qty, 0), receive_price, 0), 2) AS receive_price, + ROW_NUMBER() OVER(PARTITION BY material_code ORDER BY receipt_time DESC, id DESC) as rn + FROM receipt_details + WHERE material_code IN ({placeholders}) + AND receipt_time >= ? AND receipt_time <= ? + ) + SELECT material_code, receive_price + FROM RankedRecords + WHERE rn = 1 ''', params).fetchall() found_codes = set() @@ -532,13 +558,17 @@ def get_bom_tree_compare(parent_code): missing_ph = ','.join(['?'] * len(missing_codes)) fallback_params = missing_codes + [start_date] fallback_records = conn.execute(f''' - SELECT material_code, - ROUND(COALESCE(total_amount / NULLIF(receive_qty, 0), receive_price, 0), 2) AS receive_price - FROM receipt_details - WHERE material_code IN ({missing_ph}) - AND receipt_time < ? - GROUP BY material_code - HAVING receipt_time = MAX(receipt_time) + WITH RankedRecords AS ( + SELECT material_code, + ROUND(COALESCE(total_amount / NULLIF(receive_qty, 0), receive_price, 0), 2) AS receive_price, + ROW_NUMBER() OVER(PARTITION BY material_code ORDER BY receipt_time DESC, id DESC) as rn + FROM receipt_details + WHERE material_code IN ({missing_ph}) + AND receipt_time < ? + ) + SELECT material_code, receive_price + FROM RankedRecords + WHERE rn = 1 ''', fallback_params).fetchall() fallback_found = set() @@ -631,10 +661,19 @@ def get_bom_tree_compare(parent_code): is_real_purchased_leaf = (not has_children) and (node['latestUnitPrice'] > 0 or node['periodAStatus'] != 'missing' or node['periodBStatus'] != 'missing') if not has_children: - # 叶子节点,总成本 = 单价 * 耗用量 - node['totalLatest'] = node['latestUnitPrice'] * usage_qty - node['totalPeriodA'] = node['periodAUnitPrice'] * usage_qty - node['totalPeriodB'] = node['periodBUnitPrice'] * usage_qty + # 叶子节点,核心逻辑修改:区分是否是 1PZJ 铸件 + if code and code.startswith('1PZJ') and node.get('castingWeight'): + # 铸件:总成本 = BOM 用量(件) * 单件重量(KG/件) * 采购单价(元/KG) + multiplier = usage_qty * float(node['castingWeight']) + node['calcType'] = 'casting' + else: + # 普通件:总成本 = BOM 用量 * 采购单价 + multiplier = usage_qty + node['calcType'] = 'normal' + + node['totalLatest'] = node['latestUnitPrice'] * multiplier + node['totalPeriodA'] = node['periodAUnitPrice'] * multiplier + node['totalPeriodB'] = node['periodBUnitPrice'] * multiplier # 只要它是叶子节点,我们就给它标颜色。 # 如果它真的是虚拟件(基准价是 0,且 A B 都是 missing),那就大大方方给它标红点 diff --git a/web_ui/templates/bom_compare.html b/web_ui/templates/bom_compare.html index 4fcb454..fefc49b 100644 --- a/web_ui/templates/bom_compare.html +++ b/web_ui/templates/bom_compare.html @@ -320,45 +320,76 @@ - + + - + + + + + + + + + + + + + + - + - +