From d7dff943fc249b4222d80c5a9f1b7f14fb5e4fa7 Mon Sep 17 00:00:00 2001 From: dxc Date: Mon, 2 Mar 2026 12:22:30 +0800 Subject: [PATCH] feat: use highest historical unit price for material bases in export --- .../app/services/inbound/base_service.py | 151 +++++++++--------- 1 file changed, 73 insertions(+), 78 deletions(-) diff --git a/inventory-backend/app/services/inbound/base_service.py b/inventory-backend/app/services/inbound/base_service.py index eda5da9..6dec304 100644 --- a/inventory-backend/app/services/inbound/base_service.py +++ b/inventory-backend/app/services/inbound/base_service.py @@ -6,7 +6,7 @@ from app.models.inbound.buy import StockBuy from app.models.inbound.semi import StockSemi from app.models.inbound.product import StockProduct # from app.models.inbound.service import StockService -from sqlalchemy import or_, and_ +from sqlalchemy import or_, and_, func import traceback import json import io @@ -114,7 +114,6 @@ class MaterialBaseService: """ 获取基础信息列表 (带分页和筛选) """ - from sqlalchemy import func try: # 构建聚合子查询 buy_sub = db.session.query( @@ -148,9 +147,9 @@ class MaterialBaseService: MaterialBase, total_inv.label('total_inv'), total_avail.label('total_avail') - ).outerjoin(buy_sub, MaterialBase.id == buy_sub.c.base_id)\ - .outerjoin(semi_sub, MaterialBase.id == semi_sub.c.base_id)\ - .outerjoin(prod_sub, MaterialBase.id == prod_sub.c.base_id) + ).outerjoin(buy_sub, MaterialBase.id == buy_sub.c.base_id) \ + .outerjoin(semi_sub, MaterialBase.id == semi_sub.c.base_id) \ + .outerjoin(prod_sub, MaterialBase.id == prod_sub.c.base_id) if filters: # 1. 关键词模糊搜索 @@ -263,7 +262,6 @@ class MaterialBaseService: raise ValueError(f"已存在相同名称和规格的数据 (ID: {exist.id})") new_material = MaterialBase( - # [修改] 移除了 'IRIS' 默认值 company_name=data.get('companyName'), name=data['name'], common_name=data.get('commonName'), @@ -353,14 +351,13 @@ class MaterialBaseService: raise e # ============================================================================== - # [核心修改] 统一资产统计导出(增加用户权限脱敏) + # [核心修改] 统一资产统计导出(增加最高单价计算逻辑) # ============================================================================== @staticmethod def export_excel(filters=None, user_permissions=None): """ 全口径资产统计报表: - 逻辑:先查库存表 (Buy, Semi, Product),关联基础信息,只导出实际存在的库存记录。 - 并根据用户权限对字段进行脱敏。 + 根据基础信息列表和库存表,计算出每个物料的最高历史单价并进行导出 """ try: # 1. 构造基础信息的筛选条件 (用于过滤库存) @@ -412,65 +409,61 @@ class MaterialBaseService: query_product = query_product.filter(cond) list_product = query_product.all() - # 2.4 计算每个物料基础的最高单价/成本 - buy_max = defaultdict(float) + # ==================================================== + # [核心新增] 预先计算每个 base_id 的全局最高历史单价 + # 优先级:采购件 > 半成品 > 成品 + # ==================================================== + buy_max_prices = {} for stock, base in list_buy: - p = float(stock.pre_tax_unit_price or 0) - if p > buy_max[base.id]: - buy_max[base.id] = p + price = float(stock.pre_tax_unit_price or 0) + if price > buy_max_prices.get(base.id, 0): + buy_max_prices[base.id] = price - semi_max = defaultdict(float) + semi_max_prices = {} for stock, base in list_semi: - p = float(stock.raw_material_cost or 0) + float(stock.manual_cost or 0) - if p > semi_max[base.id]: - semi_max[base.id] = p - - product_max = defaultdict(float) - for stock, base in list_product: - p = float(stock.raw_material_cost or 0) + float(stock.manual_cost or 0) - if p > product_max[base.id]: - product_max[base.id] = p - - highest_price = {} - all_base_ids = set() - for stock, base in list_buy: - all_base_ids.add(base.id) - for stock, base in list_semi: - all_base_ids.add(base.id) - for stock, base in list_product: - all_base_ids.add(base.id) - - for base_id in all_base_ids: - price = None - buy_val = buy_max.get(base_id) - if buy_val is not None and buy_val > 0: - price = buy_val + # 兼容如果有 unit_total_cost 字段则用,否则用 原料+人工 + if hasattr(stock, 'unit_total_cost') and stock.unit_total_cost is not None: + price = float(stock.unit_total_cost) else: - semi_val = semi_max.get(base_id) - if semi_val is not None and semi_val > 0: - price = semi_val - else: - prod_val = product_max.get(base_id) - if prod_val is not None and prod_val > 0: - price = prod_val - highest_price[base_id] = price or 0.0 + price = float(stock.raw_material_cost or 0) + float(stock.manual_cost or 0) + + if price > semi_max_prices.get(base.id, 0): + semi_max_prices[base.id] = price + + product_max_prices = {} + for stock, base in list_product: + if hasattr(stock, 'unit_total_cost') and stock.unit_total_cost is not None: + price = float(stock.unit_total_cost) + else: + price = float(stock.raw_material_cost or 0) + float(stock.manual_cost or 0) + + if price > product_max_prices.get(base.id, 0): + product_max_prices[base.id] = price + + # 构造获取某个物料最高价的闭包函数 + def get_highest_price(base_id): + if base_id in buy_max_prices and buy_max_prices[base_id] > 0: + return buy_max_prices[base_id] + if base_id in semi_max_prices and semi_max_prices[base_id] > 0: + return semi_max_prices[base_id] + if base_id in product_max_prices and product_max_prices[base_id] > 0: + return product_max_prices[base_id] + return 0.0 # 3. 数据整合 all_rows = [] # 处理采购件 for stock, base in list_buy: - # 价格计算:使用当前物料基础的最高单价 - unit_price = highest_price.get(base.id, 0.0) - tax_rate = float(stock.tax_rate or 0) - # 根据新单价重新计算含税单价 - price_incl = unit_price * (1 + tax_rate / 100.0) qty = float(stock.stock_quantity or 0) + # 使用该物料的全局最高单价作为不含税单价 + highest_excl_price = get_highest_price(base.id) + tax_rate = float(stock.tax_rate or 0) - # 计算不含税总价 = 数量 * 不含税单价 - total_val_excl = qty * unit_price - # 计算含税总价 = 数量 * 含税单价 - total_val_incl = qty * price_incl + # 计算含税单价和总额 + highest_incl_price = highest_excl_price * (1 + tax_rate / 100.0) + total_val_excl = qty * highest_excl_price + total_val_incl = qty * highest_incl_price ident = stock.batch_number or stock.serial_number or stock.barcode or stock.sku @@ -483,23 +476,21 @@ class MaterialBaseService: "date": stock.in_date, "qty": qty, "avail": float(stock.available_quantity or 0), - "price_excl": unit_price, + "price_excl": highest_excl_price, "total_val_excl": total_val_excl, "tax": tax_rate, - "price_incl": price_incl, + "price_incl": highest_incl_price, "total_val": total_val_incl }) # 处理半成品 for stock, base in list_semi: - # 使用当前物料基础的最高单价 - unit_price = highest_price.get(base.id, 0.0) qty = float(stock.stock_quantity or 0) + # 使用该物料的全局最高单价作为成本 + highest_cost = get_highest_price(base.id) - # 半成品不含税总价 = 数量 * 单价 - total_val_excl = qty * unit_price - # 含税总价同上 (税率0) - total_val_incl = qty * unit_price + total_val_excl = qty * highest_cost + total_val_incl = qty * highest_cost # 半成品无税 ident = stock.batch_number or stock.serial_number or stock.barcode or stock.sku @@ -512,21 +503,21 @@ class MaterialBaseService: "date": stock.production_date, "qty": qty, "avail": float(stock.available_quantity or 0), - "price_excl": unit_price, + "price_excl": highest_cost, "total_val_excl": total_val_excl, "tax": 0.0, - "price_incl": unit_price, + "price_incl": highest_cost, "total_val": total_val_incl }) # 处理成品 for stock, base in list_product: - # 使用当前物料基础的最高单价 - unit_price = highest_price.get(base.id, 0.0) qty = float(stock.stock_quantity or 0) + # 使用该物料的全局最高单价作为成本 + highest_cost = get_highest_price(base.id) - total_val_excl = qty * unit_price - total_val_incl = qty * unit_price + total_val_excl = qty * highest_cost + total_val_incl = qty * highest_cost ident = stock.serial_number or stock.barcode or stock.sku @@ -539,10 +530,10 @@ class MaterialBaseService: "date": stock.production_date, "qty": qty, "avail": float(stock.available_quantity or 0), - "price_excl": unit_price, + "price_excl": highest_cost, "total_val_excl": total_val_excl, "tax": 0.0, - "price_incl": unit_price, + "price_incl": highest_cost, "total_val": total_val_incl }) @@ -559,7 +550,7 @@ class MaterialBaseService: ws = wb.active ws.title = "库存统计" - # 表头 + # 表头 (严格对应你的图 5) headers = [ "所属公司", "资产名称", "规格型号", "物料类型", "类别一级", "类别二级", "类别三级", "类别四级", "类别五级", @@ -582,7 +573,7 @@ class MaterialBaseService: col_idx['spec'] = idx elif header == "物料类型": col_idx['type'] = idx - elif header in ("类别一级","类别二级","类别三级","类别四级","类别五级"): + elif header in ("类别一级", "类别二级", "类别三级", "类别四级", "类别五级"): col_idx.setdefault('category_cols', []).append(idx) elif header == "计量单位": col_idx['unit'] = idx @@ -679,13 +670,17 @@ class MaterialBaseService: # 根据数据来源检查对应模块的权限 if row_type == '采购件': # 校验采购模块的价格权限 - has_price_perm = any(p in user_permissions for p in ['inbound_buy:postTaxUnitPrice', 'inbound_buy:preTaxUnitPrice', 'inbound_buy:totalAmount']) + has_price_perm = any(p in user_permissions for p in + ['inbound_buy:postTaxUnitPrice', 'inbound_buy:preTaxUnitPrice', + 'inbound_buy:totalAmount']) elif row_type == '半成品': # 校验半成品模块的成本权限 - has_price_perm = any(p in user_permissions for p in ['inbound_semi:rawMaterialCost', 'inbound_semi:manualCost']) + has_price_perm = any(p in user_permissions for p in + ['inbound_semi:rawMaterialCost', 'inbound_semi:manualCost']) elif row_type == '成品': # 校验成品模块的成本权限 - has_price_perm = any(p in user_permissions for p in ['inbound_product:rawMaterialCost', 'inbound_product:manualCost']) + has_price_perm = any(p in user_permissions for p in + ['inbound_product:rawMaterialCost', 'inbound_product:manualCost']) else: # 未知类型,默认隐藏价格列 has_price_perm = False @@ -714,4 +709,4 @@ class MaterialBaseService: except Exception as e: traceback.print_exc() - raise e + raise e \ No newline at end of file