Compare commits

14 Commits

Author SHA1 Message Date
dxc
2f140e112f fix: remove total_price from product inbound service
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-03-02 12:12:57 +08:00
dxc
8264867b1c fix: add total_price field to product inbound creation and update calculation
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-03-02 12:10:41 +08:00
dxc
d993e6796e refactor: remove total_price from product inbound service 2026-03-02 12:09:24 +08:00
dxc
4e05734865 fix: split cost fields into multiple rows in product.vue
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-03-02 12:03:06 +08:00
dxc
7f19867139 fix: adjust product service to use manual_cost instead of unit_total_cost
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-03-02 11:57:14 +08:00
dxc
bcd39729f8 fix: adjust BOM cost calculation SQL and refactor for consistency
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-03-02 11:52:24 +08:00
dxc
9cfbdc7d13 feat: refactor cost handling and add BOM cost calculation 2026-03-02 11:51:24 +08:00
dxc
d3510b0261 fix: correct BOM cost calculation by using raw SQL and manual_cost
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-03-02 11:47:44 +08:00
dxc
7b0082c6e0 feat: add BOM cost calculation for product inbound service 2026-03-02 11:44:50 +08:00
dxc
b08196c479 refactor: replace manual_cost with unit_total_cost and total_price
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-03-02 11:39:49 +08:00
dxc
68ea351c99 refactor: replace manual_cost with unit_total_cost and total_price
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-03-02 11:35:55 +08:00
dxc
f001be9eef feat: replace manual cost with unit total cost in inbound forms
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-03-02 10:28:43 +08:00
dxc
545cd86632 refactor: simplify cost calculation to 3 fields, drop manual_cost
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-03-02 10:24:51 +08:00
dxc
b688480892 refactor: use highest unit price per material base in export
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-03-02 09:55:49 +08:00
5 changed files with 294 additions and 179 deletions

View File

@ -14,6 +14,7 @@ import datetime
# 需要 pip install openpyxl # 需要 pip install openpyxl
from openpyxl import Workbook from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
from collections import defaultdict
class MaterialBaseService: class MaterialBaseService:
@ -411,15 +412,59 @@ class MaterialBaseService:
query_product = query_product.filter(cond) query_product = query_product.filter(cond)
list_product = query_product.all() list_product = query_product.all()
# 2.4 计算每个物料基础的最高单价/成本
buy_max = defaultdict(float)
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
semi_max = defaultdict(float)
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
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
# 3. 数据整合 # 3. 数据整合
all_rows = [] all_rows = []
# 处理采购件 # 处理采购件
for stock, base in list_buy: for stock, base in list_buy:
# 价格计算 # 价格计算:使用当前物料基础的最高单价
unit_price = float(stock.pre_tax_unit_price or 0) unit_price = highest_price.get(base.id, 0.0)
tax_rate = float(stock.tax_rate or 0) tax_rate = float(stock.tax_rate or 0)
price_incl = float(stock.post_tax_unit_price or (unit_price * (1 + tax_rate / 100.0))) # 根据新单价重新计算含税单价
price_incl = unit_price * (1 + tax_rate / 100.0)
qty = float(stock.stock_quantity or 0) qty = float(stock.stock_quantity or 0)
# 计算不含税总价 = 数量 * 不含税单价 # 计算不含税总价 = 数量 * 不含税单价
@ -447,13 +492,14 @@ class MaterialBaseService:
# 处理半成品 # 处理半成品
for stock, base in list_semi: for stock, base in list_semi:
cost = float(stock.raw_material_cost or 0) + float(stock.manual_cost or 0) # 使用当前物料基础的最高单价
unit_price = highest_price.get(base.id, 0.0)
qty = float(stock.stock_quantity or 0) qty = float(stock.stock_quantity or 0)
# 半成品不含税总价 = 数量 * 成本 # 半成品不含税总价 = 数量 * 单价
total_val_excl = qty * cost total_val_excl = qty * unit_price
# 含税总价同上 (税率0) # 含税总价同上 (税率0)
total_val_incl = qty * cost total_val_incl = qty * unit_price
ident = stock.batch_number or stock.serial_number or stock.barcode or stock.sku ident = stock.batch_number or stock.serial_number or stock.barcode or stock.sku
@ -466,20 +512,21 @@ class MaterialBaseService:
"date": stock.production_date, "date": stock.production_date,
"qty": qty, "qty": qty,
"avail": float(stock.available_quantity or 0), "avail": float(stock.available_quantity or 0),
"price_excl": cost, "price_excl": unit_price,
"total_val_excl": total_val_excl, "total_val_excl": total_val_excl,
"tax": 0.0, "tax": 0.0,
"price_incl": cost, "price_incl": unit_price,
"total_val": total_val_incl "total_val": total_val_incl
}) })
# 处理成品 # 处理成品
for stock, base in list_product: for stock, base in list_product:
cost = float(stock.raw_material_cost or 0) + float(stock.manual_cost or 0) # 使用当前物料基础的最高单价
unit_price = highest_price.get(base.id, 0.0)
qty = float(stock.stock_quantity or 0) qty = float(stock.stock_quantity or 0)
total_val_excl = qty * cost total_val_excl = qty * unit_price
total_val_incl = qty * cost total_val_incl = qty * unit_price
ident = stock.serial_number or stock.barcode or stock.sku ident = stock.serial_number or stock.barcode or stock.sku
@ -492,10 +539,10 @@ class MaterialBaseService:
"date": stock.production_date, "date": stock.production_date,
"qty": qty, "qty": qty,
"avail": float(stock.available_quantity or 0), "avail": float(stock.available_quantity or 0),
"price_excl": cost, "price_excl": unit_price,
"total_val_excl": total_val_excl, "total_val_excl": total_val_excl,
"tax": 0.0, "tax": 0.0,
"price_incl": cost, "price_incl": unit_price,
"total_val": total_val_incl "total_val": total_val_incl
}) })

View File

@ -7,11 +7,9 @@ from sqlalchemy import or_, func, text, and_
import traceback import traceback
import json import json
class ProductInboundService: class ProductInboundService:
# ============================================================
# 0. 辅助:唯一性校验
# ============================================================
@staticmethod @staticmethod
def _check_unique(serial_number, exclude_id=None): def _check_unique(serial_number, exclude_id=None):
from app.models.inbound.product import StockProduct from app.models.inbound.product import StockProduct
@ -24,9 +22,6 @@ class ProductInboundService:
occupied_name = exists.base.name if (hasattr(exists, 'base') and exists.base) else "未知物料" occupied_name = exists.base.name if (hasattr(exists, 'base') and exists.base) else "未知物料"
raise ValueError(f"序列号【{serial_number}】已存在!被成品 [{occupied_name}] 占用,请核查。") raise ValueError(f"序列号【{serial_number}】已存在!被成品 [{occupied_name}] 占用,请核查。")
# ============================================================
# 1. 基础物料搜索 (已修改支持分页)
# ============================================================
@staticmethod @staticmethod
def search_base_material(keyword, page=1, limit=50): def search_base_material(keyword, page=1, limit=50):
try: try:
@ -37,7 +32,7 @@ class ProductInboundService:
or_( or_(
MaterialBase.name.ilike(kw), MaterialBase.name.ilike(kw),
MaterialBase.spec_model.ilike(kw), MaterialBase.spec_model.ilike(kw),
MaterialBase.company_name.ilike(kw) # [新增] MaterialBase.company_name.ilike(kw)
) )
) )
query = query.order_by(MaterialBase.id.desc()) query = query.order_by(MaterialBase.id.desc())
@ -46,7 +41,7 @@ class ProductInboundService:
for item in pagination.items: for item in pagination.items:
results.append({ results.append({
'id': item.id, 'id': item.id,
'company_name': item.company_name, # [新增] 'company_name': item.company_name,
'name': item.name, 'name': item.name,
'spec': item.spec_model, 'spec': item.spec_model,
'category': item.category, 'category': item.category,
@ -64,9 +59,6 @@ class ProductInboundService:
traceback.print_exc() traceback.print_exc()
return {"items": [], "total": 0, "page": 1, "has_next": False} return {"items": [], "total": 0, "page": 1, "has_next": False}
# ============================================================
# 1.5 BOM 搜索逻辑
# ============================================================
@staticmethod @staticmethod
def search_bom_options(keyword): def search_bom_options(keyword):
from app.models.bom import BomTable from app.models.bom import BomTable
@ -103,9 +95,6 @@ class ProductInboundService:
traceback.print_exc() traceback.print_exc()
return [] return []
# ============================================================
# 2. 新增入库逻辑
# ============================================================
@staticmethod @staticmethod
def handle_inbound(data): def handle_inbound(data):
from app.models.inbound.product import StockProduct from app.models.inbound.product import StockProduct
@ -137,6 +126,11 @@ class ProductInboundService:
in_date_val = current_time in_date_val = current_time
in_qty = float(data.get('in_quantity') or 0) in_qty = float(data.get('in_quantity') or 0)
raw_cost = float(data.get('raw_material_cost') or 0)
manual_cost = 0.0 # 字段已弃用,保持向后兼容
unit_total_cost = float(data.get('unit_total_cost') or raw_cost or 0)
total_price = unit_total_cost * in_qty
p_start = data.get('production_start_time', '') p_start = data.get('production_start_time', '')
p_end = data.get('production_end_time', '') p_end = data.get('production_end_time', '')
time_range = f"{p_start} ~ {p_end}" if p_start or p_end else None time_range = f"{p_start} ~ {p_end}" if p_start or p_end else None
@ -175,8 +169,8 @@ class ProductInboundService:
work_order_code=data.get('work_order_code'), work_order_code=data.get('work_order_code'),
production_manager=data.get('production_manager'), production_manager=data.get('production_manager'),
production_time_range=time_range, production_time_range=time_range,
raw_material_cost=float(data.get('raw_material_cost') or 0), raw_material_cost=raw_cost,
manual_cost=float(data.get('manual_cost') or 0), manual_cost=unit_total_cost,
quality_status=data.get('quality_status', '合格'), quality_status=data.get('quality_status', '合格'),
product_photo=json.dumps(photo_list), product_photo=json.dumps(photo_list),
quality_report_link=json.dumps(quality_list), quality_report_link=json.dumps(quality_list),
@ -193,9 +187,6 @@ class ProductInboundService:
db.session.rollback() db.session.rollback()
raise e raise e
# ============================================================
# 3. 更新逻辑
# ============================================================
@staticmethod @staticmethod
def update_inbound(stock_id, data): def update_inbound(stock_id, data):
from app.models.inbound.product import StockProduct from app.models.inbound.product import StockProduct
@ -230,7 +221,7 @@ class ProductInboundService:
if 'sale_price' in data: stock.sale_price = float(data['sale_price']) if 'sale_price' in data: stock.sale_price = float(data['sale_price'])
if 'raw_material_cost' in data: stock.raw_material_cost = float(data['raw_material_cost']) if 'raw_material_cost' in data: stock.raw_material_cost = float(data['raw_material_cost'])
if 'manual_cost' in data: stock.manual_cost = float(data['manual_cost']) if 'unit_total_cost' in data: stock.manual_cost = float(data['unit_total_cost']) # 映射到 manual_cost 物理字段
if 'in_quantity' in data: if 'in_quantity' in data:
new_qty = float(data['in_quantity']) new_qty = float(data['in_quantity'])
@ -239,6 +230,7 @@ class ProductInboundService:
stock.stock_quantity = float(stock.stock_quantity) + diff stock.stock_quantity = float(stock.stock_quantity) + diff
stock.available_quantity = float(stock.available_quantity) + diff stock.available_quantity = float(stock.available_quantity) + diff
if 'production_start_time' in data or 'production_end_time' in data: if 'production_start_time' in data or 'production_end_time' in data:
old_range = stock.production_time_range or " ~ " old_range = stock.production_time_range or " ~ "
parts = old_range.split(' ~ ') parts = old_range.split(' ~ ')
@ -254,9 +246,6 @@ class ProductInboundService:
db.session.rollback() db.session.rollback()
raise e raise e
# ============================================================
# 4. 删除逻辑
# ============================================================
@staticmethod @staticmethod
def delete_inbound(stock_id): def delete_inbound(stock_id):
from app.models.inbound.product import StockProduct from app.models.inbound.product import StockProduct
@ -270,9 +259,6 @@ class ProductInboundService:
db.session.rollback() db.session.rollback()
raise e raise e
# ============================================================
# 5. 出库历史
# ============================================================
@staticmethod @staticmethod
def get_outbound_history(stock_id): def get_outbound_history(stock_id):
try: try:
@ -283,9 +269,6 @@ class ProductInboundService:
except: except:
return [] return []
# ============================================================
# 6. 获取列表
# ============================================================
@staticmethod @staticmethod
def get_list(page, limit, keyword=None, statuses=None, category=None, material_type=None, company=None): def get_list(page, limit, keyword=None, statuses=None, category=None, material_type=None, company=None):
from app.models.inbound.product import StockProduct from app.models.inbound.product import StockProduct
@ -296,7 +279,7 @@ class ProductInboundService:
query = query.filter(or_( query = query.filter(or_(
MaterialBase.name.ilike(kw), MaterialBase.name.ilike(kw),
MaterialBase.spec_model.ilike(kw), MaterialBase.spec_model.ilike(kw),
MaterialBase.company_name.ilike(kw), # [新增] MaterialBase.company_name.ilike(kw),
StockProduct.serial_number.ilike(kw), StockProduct.serial_number.ilike(kw),
StockProduct.work_order_code.ilike(kw), StockProduct.work_order_code.ilike(kw),
StockProduct.order_id.ilike(kw), StockProduct.order_id.ilike(kw),
@ -307,7 +290,6 @@ class ProductInboundService:
if material_type and material_type.strip(): if material_type and material_type.strip():
query = query.filter(MaterialBase.material_type == material_type.strip()) query = query.filter(MaterialBase.material_type == material_type.strip())
# [新增]
if company and company.strip(): if company and company.strip():
query = query.filter(MaterialBase.company_name == company.strip()) query = query.filter(MaterialBase.company_name == company.strip())
@ -335,7 +317,9 @@ class ProductInboundService:
items = [] items = []
for item in current_items: for item in current_items:
items.append(item.to_dict()) # 使用 Model to_dict item_dict = item.to_dict()
item_dict['unit_total_cost'] = float(item.manual_cost or 0)
items.append(item_dict)
return {"total": pagination.total, "items": items} return {"total": pagination.total, "items": items}
except: except:
traceback.print_exc() traceback.print_exc()
@ -363,29 +347,20 @@ class ProductInboundService:
except Exception: except Exception:
return [] return []
# ============================================================
# 7. 获取筛选项
# ============================================================
@staticmethod @staticmethod
def get_filter_options(): def get_filter_options():
try: try:
from app.models.base import MaterialBase from app.models.base import MaterialBase
# 类别 categories = db.session.query(MaterialBase.category).filter(MaterialBase.category != None,
categories = db.session.query(MaterialBase.category) \ MaterialBase.category != '').distinct().all()
.filter(MaterialBase.category != None, MaterialBase.category != '') \
.distinct().all()
sorted_categories = sorted([r[0] for r in categories]) sorted_categories = sorted([r[0] for r in categories])
# 类型 types = db.session.query(MaterialBase.material_type).filter(MaterialBase.material_type != None,
types = db.session.query(MaterialBase.material_type) \ MaterialBase.material_type != '').distinct().all()
.filter(MaterialBase.material_type != None, MaterialBase.material_type != '') \
.distinct().all()
sorted_types = sorted([r[0] for r in types]) sorted_types = sorted([r[0] for r in types])
# [新增] 公司 companies = db.session.query(MaterialBase.company_name).filter(MaterialBase.company_name != None,
companies = db.session.query(MaterialBase.company_name) \ MaterialBase.company_name != '').distinct().all()
.filter(MaterialBase.company_name != None, MaterialBase.company_name != '') \
.distinct().all()
sorted_companies = sorted([r[0] for r in companies]) sorted_companies = sorted([r[0] for r in companies])
return { return {
@ -398,9 +373,6 @@ class ProductInboundService:
traceback.print_exc() traceback.print_exc()
return {"categories": [], "types": [], "companies": []} return {"categories": [], "types": [], "companies": []}
# ============================================================
# 8. 获取历史负责人建议 (修改为全局查询)
# ============================================================
@staticmethod @staticmethod
def get_history_managers(keyword=None): def get_history_managers(keyword=None):
from app.models.inbound.product import StockProduct from app.models.inbound.product import StockProduct
@ -415,4 +387,55 @@ class ProductInboundService:
return [r[0] for r in records if r[0]] return [r[0] for r in records if r[0]]
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
return [] return []
# ============================================================
# 9. BOM 原材料成本自动核算 (新增)
# ============================================================
@staticmethod
def calculate_bom_cost(bom_no, bom_version):
"""
根据 BOM 编号和版本计算原材料总成本
遍历 BOM 子件,使用原生 SQL 查物理表 bom_table取每个子件在采购、半成品、成品三个表中的最高单价乘以用量后累加
"""
from app.models.inbound.buy import StockBuy
from app.models.inbound.semi import StockSemi
from app.models.inbound.product import StockProduct
from sqlalchemy import func, text
try:
# 使用原生 SQL 精准查询 bom_table避免模型映射错误
sql = text("""
SELECT child_id, dosage
FROM bom_table
WHERE bom_no = :bom_no AND version = :version
""")
bom_lines = db.session.execute(sql, {'bom_no': bom_no, 'version': bom_version}).fetchall()
total_cost = 0.0
for line in bom_lines:
component_base_id = line[0] # child_id
usage_qty = float(line[1] or 1.0) # dosage
# 1. 查采购表最高价 (不含税)
buy_price = db.session.query(func.max(StockBuy.pre_tax_unit_price)).filter(
StockBuy.base_id == component_base_id
).scalar() or 0.0
# 2. 查半成品表最高价 (单件成本映射存在 manual_cost 里了)
semi_price = db.session.query(func.max(StockSemi.manual_cost)).filter(
StockSemi.base_id == component_base_id
).scalar() or 0.0
# 3. 查成品表最高价 (同样存储在 manual_cost 字段里)
product_price = db.session.query(func.max(StockProduct.manual_cost)).filter(
StockProduct.base_id == component_base_id
).scalar() or 0.0
# 4. 取三个表中的最大值,乘以用量 (dosage)
max_price = max(float(buy_price), float(semi_price), float(product_price))
total_cost += max_price * usage_qty
return round(total_cost, 2)
except Exception as e:
traceback.print_exc()
raise e

View File

@ -7,11 +7,9 @@ from sqlalchemy import or_, func, text, and_
import traceback import traceback
import json import json
class SemiInboundService: class SemiInboundService:
# ============================================================
# 0. 辅助:唯一性校验
# ============================================================
@staticmethod @staticmethod
def _check_unique(base_id, serial_number, batch_number, exclude_id=None): def _check_unique(base_id, serial_number, batch_number, exclude_id=None):
from app.models.inbound.semi import StockSemi from app.models.inbound.semi import StockSemi
@ -34,9 +32,6 @@ class SemiInboundService:
if query.first(): if query.first():
raise ValueError(f"该物料已存在批号【{batch_number}】,请勿重复建单,建议在原批次上追加库存。") raise ValueError(f"该物料已存在批号【{batch_number}】,请勿重复建单,建议在原批次上追加库存。")
# ============================================================
# 1. 基础物料搜索 (已修改支持分页)
# ============================================================
@staticmethod @staticmethod
def search_base_material(keyword, page=1, limit=50): def search_base_material(keyword, page=1, limit=50):
try: try:
@ -47,7 +42,7 @@ class SemiInboundService:
or_( or_(
MaterialBase.name.ilike(kw), MaterialBase.name.ilike(kw),
MaterialBase.spec_model.ilike(kw), MaterialBase.spec_model.ilike(kw),
MaterialBase.company_name.ilike(kw) # [新增] 支持搜公司 MaterialBase.company_name.ilike(kw)
) )
) )
query = query.order_by(MaterialBase.id.desc()) query = query.order_by(MaterialBase.id.desc())
@ -56,7 +51,7 @@ class SemiInboundService:
for item in pagination.items: for item in pagination.items:
results.append({ results.append({
'id': item.id, 'id': item.id,
'company_name': item.company_name, # [新增] 'company_name': item.company_name,
'name': item.name, 'name': item.name,
'spec': item.spec_model, 'spec': item.spec_model,
'category': item.category, 'category': item.category,
@ -64,19 +59,11 @@ class SemiInboundService:
'type': item.material_type, 'type': item.material_type,
'status': '启用' 'status': '启用'
}) })
return { return {"items": results, "total": pagination.total, "page": page, "has_next": pagination.has_next}
"items": results,
"total": pagination.total,
"page": page,
"has_next": pagination.has_next
}
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
return {"items": [], "total": 0, "page": 1, "has_next": False} return {"items": [], "total": 0, "page": 1, "has_next": False}
# ============================================================
# 1.5 BOM 搜索逻辑
# ============================================================
@staticmethod @staticmethod
def search_bom_options(keyword): def search_bom_options(keyword):
from app.models.bom import BomTable from app.models.bom import BomTable
@ -113,9 +100,6 @@ class SemiInboundService:
traceback.print_exc() traceback.print_exc()
return [] return []
# ============================================================
# 2. 新增入库逻辑
# ============================================================
@staticmethod @staticmethod
def handle_inbound(data): def handle_inbound(data):
from app.models.inbound.semi import StockSemi from app.models.inbound.semi import StockSemi
@ -172,9 +156,9 @@ class SemiInboundService:
in_qty = float(data.get('in_quantity') or 0) in_qty = float(data.get('in_quantity') or 0)
raw_cost = float(data.get('raw_material_cost') or 0) raw_cost = float(data.get('raw_material_cost') or 0)
manual_cost = float(data.get('manual_cost') or 0) # 【重要修改】:把前端的 unit_total_cost单件成本存入原数据库的 manual_cost 字段中
unit_total_cost = raw_cost + manual_cost unit_cost = float(data.get('unit_total_cost') or raw_cost)
total_value = unit_total_cost * in_qty total_value = unit_cost * in_qty
next_global_id = 0 next_global_id = 0
try: try:
@ -220,7 +204,7 @@ class SemiInboundService:
production_end_time=p_end, production_end_time=p_end,
production_time_range=time_range_str, production_time_range=time_range_str,
raw_material_cost=raw_cost, raw_material_cost=raw_cost,
manual_cost=manual_cost, manual_cost=unit_cost, # 映射到 manual_cost 物理字段
total_price=total_value, total_price=total_value,
arrival_photo=json.dumps(arrival_list), arrival_photo=json.dumps(arrival_list),
quality_report_link=json.dumps(quality_report_list), quality_report_link=json.dumps(quality_report_list),
@ -236,9 +220,6 @@ class SemiInboundService:
traceback.print_exc() traceback.print_exc()
raise e raise e
# ============================================================
# 3. 更新逻辑
# ============================================================
@staticmethod @staticmethod
def update_inbound(stock_id, data): def update_inbound(stock_id, data):
from app.models.inbound.semi import StockSemi from app.models.inbound.semi import StockSemi
@ -312,7 +293,6 @@ class SemiInboundService:
stock.production_time_range = raw_range stock.production_time_range = raw_range
qty_changed = False qty_changed = False
cost_changed = False
if 'in_quantity' in data: if 'in_quantity' in data:
new_qty = float(data['in_quantity']) new_qty = float(data['in_quantity'])
diff = new_qty - float(stock.in_quantity) diff = new_qty - float(stock.in_quantity)
@ -321,15 +301,16 @@ class SemiInboundService:
stock.stock_quantity = float(stock.stock_quantity) + diff stock.stock_quantity = float(stock.stock_quantity) + diff
stock.available_quantity = float(stock.available_quantity) + diff stock.available_quantity = float(stock.available_quantity) + diff
qty_changed = True qty_changed = True
if 'raw_material_cost' in data: if 'raw_material_cost' in data:
stock.raw_material_cost = float(data['raw_material_cost']) stock.raw_material_cost = float(data['raw_material_cost'])
cost_changed = True if 'unit_total_cost' in data:
if 'manual_cost' in data: stock.manual_cost = float(data['unit_total_cost']) # 映射到 manual_cost 物理字段
stock.manual_cost = float(data['manual_cost'])
cost_changed = True if 'unit_total_cost' in data or qty_changed:
if cost_changed or qty_changed: qty = float(stock.in_quantity or 1)
unit_total = float(stock.raw_material_cost) + float(stock.manual_cost) # 使用存入 manual_cost 的单价计算总价
stock.total_price = float(stock.in_quantity) * unit_total stock.total_price = float(stock.manual_cost or 0) * qty
db.session.commit() db.session.commit()
return stock return stock
@ -337,9 +318,6 @@ class SemiInboundService:
db.session.rollback() db.session.rollback()
raise e raise e
# ============================================================
# 4. 删除逻辑
# ============================================================
@staticmethod @staticmethod
def delete_inbound(stock_id): def delete_inbound(stock_id):
from app.models.inbound.semi import StockSemi from app.models.inbound.semi import StockSemi
@ -354,9 +332,6 @@ class SemiInboundService:
db.session.rollback() db.session.rollback()
raise e raise e
# ============================================================
# 5. 出库历史
# ============================================================
@staticmethod @staticmethod
def get_outbound_history(stock_id): def get_outbound_history(stock_id):
try: try:
@ -367,9 +342,6 @@ class SemiInboundService:
except: except:
return [] return []
# ============================================================
# 6. 获取列表
# ============================================================
@staticmethod @staticmethod
def get_list(page, limit, keyword=None, statuses=None, category=None, material_type=None, company=None): def get_list(page, limit, keyword=None, statuses=None, category=None, material_type=None, company=None):
from app.models.inbound.semi import StockSemi from app.models.inbound.semi import StockSemi
@ -381,7 +353,7 @@ class SemiInboundService:
or_( or_(
MaterialBase.name.ilike(kw), MaterialBase.name.ilike(kw),
MaterialBase.spec_model.ilike(kw), MaterialBase.spec_model.ilike(kw),
MaterialBase.company_name.ilike(kw), # [新增] MaterialBase.company_name.ilike(kw),
StockSemi.batch_number.ilike(kw), StockSemi.batch_number.ilike(kw),
StockSemi.serial_number.ilike(kw), StockSemi.serial_number.ilike(kw),
StockSemi.sku.ilike(kw), StockSemi.sku.ilike(kw),
@ -394,7 +366,6 @@ class SemiInboundService:
if material_type and material_type.strip(): if material_type and material_type.strip():
query = query.filter(MaterialBase.material_type == material_type.strip()) query = query.filter(MaterialBase.material_type == material_type.strip())
# [新增] 公司筛选
if company and company.strip(): if company and company.strip():
query = query.filter(MaterialBase.company_name == company.strip()) query = query.filter(MaterialBase.company_name == company.strip())
@ -411,18 +382,12 @@ class SemiInboundService:
) )
pagination = query.order_by(StockSemi.production_date.desc()).paginate(page=page, per_page=limit, pagination = query.order_by(StockSemi.production_date.desc()).paginate(page=page, per_page=limit,
error_out=False) error_out=False)
current_items = pagination.items
def parse_img(json_str):
if not json_str: return []
try:
return json.loads(json_str) if json_str.startswith('[') else [json_str]
except:
return []
items = [] items = []
for item in current_items: for item in pagination.items:
items.append(item.to_dict()) # 直接使用 Model 的 to_dict (已包含 company_name) # 把 manual_cost 伪装成 unit_total_cost 返回给前端
item_dict = item.to_dict()
item_dict['unit_total_cost'] = float(item.manual_cost or 0)
items.append(item_dict)
return {"total": pagination.total, "items": items} return {"total": pagination.total, "items": items}
except Exception as e: except Exception as e:
print(f"List Error: {e}") print(f"List Error: {e}")
@ -451,29 +416,18 @@ class SemiInboundService:
except Exception: except Exception:
return [] return []
# ============================================================
# 7. 获取筛选项 (排序)
# ============================================================
@staticmethod @staticmethod
def get_filter_options(): def get_filter_options():
try: try:
from app.models.base import MaterialBase from app.models.base import MaterialBase
# 类别 categories = db.session.query(MaterialBase.category).filter(MaterialBase.category != None,
categories = db.session.query(MaterialBase.category) \ MaterialBase.category != '').distinct().all()
.filter(MaterialBase.category != None, MaterialBase.category != '') \
.distinct().all()
sorted_categories = sorted([r[0] for r in categories]) sorted_categories = sorted([r[0] for r in categories])
types = db.session.query(MaterialBase.material_type).filter(MaterialBase.material_type != None,
# 类型 MaterialBase.material_type != '').distinct().all()
types = db.session.query(MaterialBase.material_type) \
.filter(MaterialBase.material_type != None, MaterialBase.material_type != '') \
.distinct().all()
sorted_types = sorted([r[0] for r in types]) sorted_types = sorted([r[0] for r in types])
companies = db.session.query(MaterialBase.company_name).filter(MaterialBase.company_name != None,
# [新增] 公司 MaterialBase.company_name != '').distinct().all()
companies = db.session.query(MaterialBase.company_name) \
.filter(MaterialBase.company_name != None, MaterialBase.company_name != '') \
.distinct().all()
sorted_companies = sorted([r[0] for r in companies]) sorted_companies = sorted([r[0] for r in companies])
return { return {
@ -482,13 +436,9 @@ class SemiInboundService:
"companies": sorted_companies "companies": sorted_companies
} }
except Exception: except Exception:
import traceback
traceback.print_exc() traceback.print_exc()
return {"categories": [], "types": [], "companies": []} return {"categories": [], "types": [], "companies": []}
# ============================================================
# 8. 获取历史生产负责人 (修改为全局查询)
# ============================================================
@staticmethod @staticmethod
def get_history_managers(keyword=None): def get_history_managers(keyword=None):
from app.models.inbound.semi import StockSemi from app.models.inbound.semi import StockSemi
@ -503,4 +453,52 @@ class SemiInboundService:
return [r[0] for r in records if r[0]] return [r[0] for r in records if r[0]]
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
return [] return []
@staticmethod
def calculate_bom_cost(bom_no, bom_version):
"""
根据 BOM 编号和版本计算原材料总成本
遍历 BOM 子件,使用原生 SQL 查物理表 bom_table取每个子件在采购、半成品、成品三个表中的最高单价乘以用量后累加
"""
from app.models.inbound.buy import StockBuy
from app.models.inbound.semi import StockSemi
from app.models.inbound.product import StockProduct
from sqlalchemy import func, text
try:
# 使用原生 SQL 精准查询 bom_table避免模型映射错误
sql = text("""
SELECT child_id, dosage
FROM bom_table
WHERE bom_no = :bom_no AND version = :version
""")
bom_lines = db.session.execute(sql, {'bom_no': bom_no, 'version': bom_version}).fetchall()
total_cost = 0.0
for line in bom_lines:
component_base_id = line[0] # child_id
usage_qty = float(line[1] or 1.0) # dosage
# 1. 查采购表最高价 (不含税)
buy_price = db.session.query(func.max(StockBuy.pre_tax_unit_price)).filter(
StockBuy.base_id == component_base_id
).scalar() or 0.0
# 2. 查半成品表最高价 (单件成本映射存在 manual_cost 里了)
semi_price = db.session.query(func.max(StockSemi.manual_cost)).filter(
StockSemi.base_id == component_base_id
).scalar() or 0.0
# 3. 查成品表最高价 (同样存储在 manual_cost 字段里)
product_price = db.session.query(func.max(StockProduct.manual_cost)).filter(
StockProduct.base_id == component_base_id
).scalar() or 0.0
# 4. 取三个表中的最大值,乘以用量 (dosage)
max_price = max(float(buy_price), float(semi_price), float(product_price))
total_cost += max_price * usage_qty
return round(total_cost, 2)
except Exception as e:
traceback.print_exc()
raise e

View File

@ -156,7 +156,7 @@
</el-link> </el-link>
</template> </template>
<template #default="scope" v-else-if="['sale_price', 'raw_material_cost', 'manual_cost'].includes(col.prop)"> <template #default="scope" v-else-if="['sale_price', 'raw_material_cost', 'unit_total_cost', 'total_price'].includes(col.prop)">
<span class="money-text">{{ formatMoney(scope.row[col.prop]) }}</span> <span class="money-text">{{ formatMoney(scope.row[col.prop]) }}</span>
</template> </template>
@ -392,14 +392,21 @@
<el-date-picker v-model="form.production_time_range" type="datetimerange" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" /> <el-date-picker v-model="form.production_time_range" type="datetimerange" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="6"> </el-row>
<el-row :gutter="24">
<el-col :span="8">
<el-form-item label="原料成本"> <el-form-item label="原料成本">
<el-input-number v-model="form.raw_material_cost" :precision="2" :controls="false" style="width:100%" placeholder="请输入"/> <el-input-number v-model="form.raw_material_cost" :precision="2" :controls="false" style="width:100%" placeholder="请输入"/>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="8">
<el-form-item label="人工成本"> <el-form-item label="单件成本">
<el-input-number v-model="form.manual_cost" :precision="2" :controls="false" style="width:100%" placeholder="请输入"/> <el-input-number v-model="form.unit_total_cost" :precision="2" :controls="false" style="width:100%" placeholder="请输入"/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="总成本">
<el-input-number v-model="form.total_price" :precision="2" :controls="false" style="width:100%" placeholder="自动计算" disabled/>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
@ -467,7 +474,8 @@ import {
searchMaterialBase, searchMaterialBase,
searchBom, searchBom,
getFilterOptions, getFilterOptions,
getManagerHistory // [新增] getManagerHistory, // [新增]
calculateBomCost
} from '@/api/inbound/product' } from '@/api/inbound/product'
import { uploadFile, deleteFile } from '@/api/inbound/buy' import { uploadFile, deleteFile } from '@/api/inbound/buy'
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue' import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
@ -561,7 +569,8 @@ const allColumns = [
{ prop: 'bom_code', label: 'BOM', minWidth: '100' }, { prop: 'bom_code', label: 'BOM', minWidth: '100' },
{ prop: 'production_manager', label: '负责人', minWidth: '100' }, { prop: 'production_manager', label: '负责人', minWidth: '100' },
{ prop: 'raw_material_cost', label: '原料成本', minWidth: '100' }, { prop: 'raw_material_cost', label: '原料成本', minWidth: '100' },
{ prop: 'manual_cost', label: '人工成本', minWidth: '100' }, { prop: 'unit_total_cost', label: '单件成本', minWidth: '100' },
{ prop: 'total_price', label: '总成本', minWidth: '100' },
{ prop: 'inbound_date', label: '生产日期', minWidth: '120' }, { prop: 'inbound_date', label: '生产日期', minWidth: '120' },
{ prop: 'detail_link', label: '详情', minWidth: '100' } { prop: 'detail_link', label: '详情', minWidth: '100' }
] ]
@ -586,7 +595,8 @@ const permissionMap: Record<string, string> = {
bom_code: 'inbound_product:bom_code', bom_code: 'inbound_product:bom_code',
production_manager: 'inbound_product:production_manager', production_manager: 'inbound_product:production_manager',
raw_material_cost: 'inbound_product:raw_material_cost', raw_material_cost: 'inbound_product:raw_material_cost',
manual_cost: 'inbound_product:manual_cost', unit_total_cost: 'inbound_product:unit_total_cost',
total_price: 'inbound_product:total_price',
inbound_date: 'inbound_product:inbound_date', inbound_date: 'inbound_product:inbound_date',
detail_link: 'inbound_product:detail_link', detail_link: 'inbound_product:detail_link',
} }
@ -642,7 +652,8 @@ const form = reactive({
bom_code: '', bom_version: '', work_order_code: '', order_id: '', bom_code: '', bom_version: '', work_order_code: '', order_id: '',
production_manager: '', production_time_range: [] as string[], production_manager: '', production_time_range: [] as string[],
raw_material_cost: undefined as number | undefined, raw_material_cost: undefined as number | undefined,
manual_cost: undefined as number | undefined, unit_total_cost: undefined as number | undefined,
total_price: undefined as number | undefined,
sale_price: undefined as number | undefined, sale_price: undefined as number | undefined,
quality_report_link: [] as string[], inspection_report_link: [] as string[], product_photo: [] as string[], detail_link: '' quality_report_link: [] as string[], inspection_report_link: [] as string[], product_photo: [] as string[], detail_link: ''
}) })
@ -657,7 +668,7 @@ const handleSearchBom = async (query: string) => {
bomOptions.value = res.data || [] bomOptions.value = res.data || []
} finally { bomSearchLoading.value = false } } finally { bomSearchLoading.value = false }
} }
const handleBomSelect = (val: string) => { const handleBomSelect = async (val: string) => {
if (!val) { if (!val) {
form.bom_code = '' form.bom_code = ''
form.bom_version = '' form.bom_version = ''
@ -666,6 +677,17 @@ const handleBomSelect = (val: string) => {
const [code, version] = val.split('###') const [code, version] = val.split('###')
form.bom_code = code form.bom_code = code
form.bom_version = version form.bom_version = version
// 自动计算 BOM 成本并填入 raw_material_cost 和 unit_total_cost
try {
const res: any = await calculateBomCost({ bom_code: code, bom_version: version })
if (res.code === 200 && typeof res.data === 'number') {
form.raw_material_cost = res.data
form.unit_total_cost = res.data
}
} catch (e) {
// 计算失败不影响现有输入
console.warn('BOM 成本计算失败', e)
}
} }
// ------------------------------------ // ------------------------------------
@ -860,9 +882,13 @@ const handleUpdate = (row: any) => {
inspection_report_link: row.inspection_report_link || [], inspection_report_link: row.inspection_report_link || [],
in_quantity: Number(row.qty_inbound), in_quantity: Number(row.qty_inbound),
raw_material_cost: (row.raw_material_cost !== null && row.raw_material_cost !== undefined) ? Number(row.raw_material_cost) : undefined, raw_material_cost: (row.raw_material_cost !== null && row.raw_material_cost !== undefined) ? Number(row.raw_material_cost) : undefined,
manual_cost: (row.manual_cost !== null && row.manual_cost !== undefined) ? Number(row.manual_cost) : undefined, unit_total_cost: (row.unit_total_cost !== null && row.unit_total_cost !== undefined) ? Number(row.unit_total_cost) : undefined,
sale_price: (row.sale_price !== null && row.sale_price !== undefined) ? Number(row.sale_price) : undefined sale_price: (row.sale_price !== null && row.sale_price !== undefined) ? Number(row.sale_price) : undefined
}) })
// 计算总成本
const u = Number(form.unit_total_cost || 0)
const q = Number(form.in_quantity || 1)
form.total_price = Number((u * q).toFixed(2))
if(row.production_start_time && row.production_end_time) { form.production_time_range = [row.production_start_time, row.production_end_time] } else { form.production_time_range = [] } if(row.production_start_time && row.production_end_time) { form.production_time_range = [row.production_start_time, row.production_end_time] } else { form.production_time_range = [] }
productPhotoList.value = form.product_photo.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) })) productPhotoList.value = form.product_photo.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
const qReports = form.quality_report_link || [] const qReports = form.quality_report_link || []
@ -970,7 +996,8 @@ const submitForm = async () => {
quality_report_link: qImages, quality_report_link: qImages,
inspection_report_link: iImages, inspection_report_link: iImages,
raw_material_cost: Number(form.raw_material_cost || 0), raw_material_cost: Number(form.raw_material_cost || 0),
manual_cost: Number(form.manual_cost || 0), unit_total_cost: Number(form.unit_total_cost || 0),
total_price: Number(form.total_price || 0),
sale_price: Number(form.sale_price || 0), sale_price: Number(form.sale_price || 0),
production_start_time: form.production_time_range?.[0], production_start_time: form.production_time_range?.[0],
production_end_time: form.production_time_range?.[1] production_end_time: form.production_time_range?.[1]
@ -1000,7 +1027,7 @@ const handlePrint = async (row: any) => {
const confirmPrint = async () => { printing.value = true; try { await executePrint(currentPrintData.value); ElMessage.success('已发送'); printVisible.value = false } catch (e: any) { ElMessage.error('打印失败') } finally { printing.value = false } } const confirmPrint = async () => { printing.value = true; try { await executePrint(currentPrintData.value); ElMessage.success('已发送'); printVisible.value = false } catch (e: any) { ElMessage.error('打印失败') } finally { printing.value = false } }
const resetForm = () => { const resetForm = () => {
materialOptions.value = []; bomOptions.value = []; productPhotoList.value = []; qualityFileList.value = []; inspectionFileList.value = []; quality_url.value = ''; inspection_url.value = '' materialOptions.value = []; bomOptions.value = []; productPhotoList.value = []; qualityFileList.value = []; inspectionFileList.value = []; quality_url.value = ''; inspection_url.value = ''
Object.assign(form, { id: undefined, base_id: undefined, material_name: '', spec_model: '', material_type: '', category: '', unit: '', sku: '', barcode: '', serial_number: '', in_date: '', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', status: '在库', quality_status: '合格', bom_code: '', bom_version: '', work_order_code: '', order_id: '', production_manager: '', production_time_range: [], raw_material_cost: undefined, manual_cost: undefined, sale_price: undefined, quality_report_link: [], inspection_report_link: [], product_photo: [], detail_link: '' }) Object.assign(form, { id: undefined, base_id: undefined, material_name: '', spec_model: '', material_type: '', category: '', unit: '', sku: '', barcode: '', serial_number: '', in_date: '', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', status: '在库', quality_status: '合格', bom_code: '', bom_version: '', work_order_code: '', order_id: '', production_manager: '', production_time_range: [], raw_material_cost: undefined, unit_total_cost: undefined, total_price: undefined, sale_price: undefined, quality_report_link: [], inspection_report_link: [], product_photo: [], detail_link: '' })
} }
const getStatusType = (s:string) => ({'在库':'success','出库':'info','借库':'warning','损耗':'danger'}[s]||'warning') const getStatusType = (s:string) => ({'在库':'success','出库':'info','借库':'warning','损耗':'danger'}[s]||'warning')
const getQualityType = (s:string) => ({'合格':'success','不合格':'danger','待检':'info'}[s]||'info') const getQualityType = (s:string) => ({'合格':'success','不合格':'danger','待检':'info'}[s]||'info')
@ -1011,6 +1038,13 @@ onMounted(() => {
fetchData() fetchData()
fetchOptions() fetchOptions()
}) })
// 成本计算监听
watch([() => form.unit_total_cost, () => form.in_quantity], ([unit, qty]) => {
const unitNum = Number(unit || 0)
const qtyNum = Number(qty || 1)
form.total_price = Number((unitNum * qtyNum).toFixed(2))
})
</script> </script>
<style scoped> <style scoped>
@ -1087,4 +1121,4 @@ onMounted(() => {
.product-dropdown { width: 580px !important; } .product-dropdown { width: 580px !important; }
.product-dropdown .el-select-dropdown__wrap { max-height: 320px !important; } .product-dropdown .el-select-dropdown__wrap { max-height: 320px !important; }
.product-dropdown .el-input__suffix { z-index: 10; } .product-dropdown .el-input__suffix { z-index: 10; }
</style> </style>

View File

@ -182,7 +182,7 @@
</el-link> </el-link>
</template> </template>
<template #default="scope" v-else-if="['raw_material_cost', 'manual_cost', 'unit_total_cost'].includes(col.prop)"> <template #default="scope" v-else-if="['raw_material_cost', 'unit_total_cost', 'total_price'].includes(col.prop)">
<span class="money-text">{{ formatMoney(scope.row[col.prop]) }}</span> <span class="money-text">{{ formatMoney(scope.row[col.prop]) }}</span>
</template> </template>
</el-table-column> </el-table-column>
@ -473,13 +473,13 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="8"> <el-col :span="8">
<el-form-item label="单件成本"> <el-form-item label="单件成本">
<el-input-number v-model="form.manual_cost" :precision="2" :controls="false" style="width:100%" placeholder="请输入"/> <el-input-number v-model="form.unit_total_cost" :precision="2" :controls="false" style="width:100%" placeholder="请输入"/>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="8"> <el-col :span="8">
<el-form-item label="总成本"> <el-form-item label="总成本">
<el-input-number v-model="form.unit_total_cost" :precision="2" :controls="false" style="width:100%" placeholder="自动计算" disabled/> <el-input-number v-model="form.total_price" :precision="2" :controls="false" style="width:100%" placeholder="自动计算" disabled/>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
@ -539,7 +539,8 @@ import {
searchMaterialBase, searchMaterialBase,
searchBom, searchBom,
getFilterOptions, getFilterOptions,
getManagerHistory // [新增] getManagerHistory, // [新增]
calculateBomCost
} from '@/api/inbound/semi' } from '@/api/inbound/semi'
import { uploadFile, deleteFile } from '@/api/inbound/buy' import { uploadFile, deleteFile } from '@/api/inbound/buy'
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue' import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
@ -642,8 +643,8 @@ const stockColumns = [
{prop: 'bom_version', label: 'BOM版本', minWidth: '90'}, {prop: 'bom_version', label: 'BOM版本', minWidth: '90'},
{prop: 'work_order_code', label: '工单号', minWidth: '120'}, {prop: 'work_order_code', label: '工单号', minWidth: '120'},
{prop: 'raw_material_cost', label: '原料成本', minWidth: '100'}, {prop: 'raw_material_cost', label: '原料成本', minWidth: '100'},
{prop: 'manual_cost', label: '单件成本', minWidth: '100'}, // 原人工成本,现为单件成本 {prop: 'unit_total_cost', label: '单件成本', minWidth: '100'},
{prop: 'unit_total_cost', label: '总成本', minWidth: '100'}, // 原单件总本,现为总成本 {prop: 'total_price', label: '总成本', minWidth: '100'},
{prop: 'production_manager', label: '生产负责人', minWidth: '100'}, {prop: 'production_manager', label: '生产负责人', minWidth: '100'},
{prop: 'production_start_time', label: '生产开始', minWidth: '160'}, {prop: 'production_start_time', label: '生产开始', minWidth: '160'},
{prop: 'production_end_time', label: '生产结束', minWidth: '160'}, {prop: 'production_end_time', label: '生产结束', minWidth: '160'},
@ -677,8 +678,8 @@ const permissionMap: Record<string, string> = {
bom_version: 'inbound_semi:bom_version', bom_version: 'inbound_semi:bom_version',
work_order_code: 'inbound_semi:work_order_code', work_order_code: 'inbound_semi:work_order_code',
raw_material_cost: 'inbound_semi:raw_material_cost', raw_material_cost: 'inbound_semi:raw_material_cost',
manual_cost: 'inbound_semi:manual_cost',
unit_total_cost: 'inbound_semi:unit_total_cost', unit_total_cost: 'inbound_semi:unit_total_cost',
total_price: 'inbound_semi:total_price',
production_manager: 'inbound_semi:production_manager', production_manager: 'inbound_semi:production_manager',
production_start_time: 'inbound_semi:production_start_time', production_start_time: 'inbound_semi:production_start_time',
production_end_time: 'inbound_semi:production_end_time', production_end_time: 'inbound_semi:production_end_time',
@ -726,18 +727,16 @@ const form = reactive({
company_name: '', company_name: '',
material_name: '', spec_model: '', category: '', unit: '', material_type: '', sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', quality_status: '合格', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', bom_code: '', bom_version: '', work_order_code: '', material_name: '', spec_model: '', category: '', unit: '', material_type: '', sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', quality_status: '合格', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', bom_code: '', bom_version: '', work_order_code: '',
raw_material_cost: undefined as number | undefined, raw_material_cost: undefined as number | undefined,
manual_cost: undefined as number | undefined,
unit_total_cost: undefined as number | undefined, unit_total_cost: undefined as number | undefined,
total_price: undefined as number | undefined,
production_manager: '', production_time_range: [] as string[], arrival_photo: [] as string[], quality_report_link: [] as string[], detail_link: '' production_manager: '', production_time_range: [] as string[], arrival_photo: [] as string[], quality_report_link: [] as string[], detail_link: ''
}) })
// === 新增:监听计算总成本 === // === 监听计算总成本 ===
watch([() => form.in_quantity, () => form.manual_cost], ([qty, manual_cost]) => { watch([() => form.unit_total_cost, () => form.in_quantity], ([unit, qty]) => {
if (manual_cost !== undefined && manual_cost !== null) { const unitNum = Number(unit || 0)
form.unit_total_cost = Number((qty * manual_cost).toFixed(2)) const qtyNum = Number(qty || 1)
} else { form.total_price = Number((unitNum * qtyNum).toFixed(2))
form.unit_total_cost = undefined
}
}) })
// ------------------------------------ // ------------------------------------
@ -750,7 +749,7 @@ const handleSearchBom = async (query: string) => {
bomOptions.value = res.data || [] bomOptions.value = res.data || []
} finally { bomSearchLoading.value = false } } finally { bomSearchLoading.value = false }
} }
const handleBomSelect = (val: string) => { const handleBomSelect = async (val: string) => {
// val 格式为 bom_no###version // val 格式为 bom_no###version
if (!val) { if (!val) {
form.bom_code = '' form.bom_code = ''
@ -760,6 +759,17 @@ const handleBomSelect = (val: string) => {
const [code, version] = val.split('###') const [code, version] = val.split('###')
form.bom_code = code form.bom_code = code
form.bom_version = version form.bom_version = version
// 自动计算 BOM 成本并填入 raw_material_cost 和 unit_total_cost
try {
const res: any = await calculateBomCost({ bom_code: code, bom_version: version })
if (res.code === 200 && typeof res.data === 'number') {
form.raw_material_cost = res.data
form.unit_total_cost = res.data
}
} catch (e) {
// 计算失败不影响现有输入
console.warn('BOM 成本计算失败', e)
}
} }
// ------------------------------------ // ------------------------------------
@ -989,13 +999,16 @@ const handleUpdate = (row: any) => {
in_quantity: Number(row.qty_inbound), stock_quantity: Number(row.qty_stock), available_quantity: Number(row.qty_available), in_quantity: Number(row.qty_inbound), stock_quantity: Number(row.qty_stock), available_quantity: Number(row.qty_available),
bom_code: row.bom_code, bom_version: row.bom_version, work_order_code: row.work_order_code, bom_code: row.bom_code, bom_version: row.bom_version, work_order_code: row.work_order_code,
raw_material_cost: (row.raw_material_cost !== null && row.raw_material_cost !== undefined) ? Number(row.raw_material_cost) : undefined, raw_material_cost: (row.raw_material_cost !== null && row.raw_material_cost !== undefined) ? Number(row.raw_material_cost) : undefined,
manual_cost: (row.manual_cost !== null && row.manual_cost !== undefined) ? Number(row.manual_cost) : undefined,
unit_total_cost: (row.unit_total_cost !== null && row.unit_total_cost !== undefined) ? Number(row.unit_total_cost) : undefined, unit_total_cost: (row.unit_total_cost !== null && row.unit_total_cost !== undefined) ? Number(row.unit_total_cost) : undefined,
production_manager: row.production_manager, production_manager: row.production_manager,
production_time_range: (row.production_start_time && row.production_end_time) ? [row.production_start_time, row.production_end_time] : [], production_time_range: (row.production_start_time && row.production_end_time) ? [row.production_start_time, row.production_end_time] : [],
detail_link: row.detail_link, detail_link: row.detail_link,
arrival_photo: row.arrival_photo || [], quality_report_link: row.quality_report_link || [] arrival_photo: row.arrival_photo || [], quality_report_link: row.quality_report_link || []
}) })
// 计算总成本
const u = Number(form.unit_total_cost || 0)
const q = Number(form.in_quantity || 1)
form.total_price = Number((u * q).toFixed(2))
arrivalFileList.value = form.arrival_photo.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) })) arrivalFileList.value = form.arrival_photo.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
const reports = form.quality_report_link || [] const reports = form.quality_report_link || []
const reportImgs = reports.filter(r => !isExternalLink(r)) const reportImgs = reports.filter(r => !isExternalLink(r))
@ -1093,8 +1106,8 @@ const submitForm = async () => {
quality_report_link: onlyImages, quality_report_link: onlyImages,
in_quantity: Number(form.in_quantity), in_quantity: Number(form.in_quantity),
raw_material_cost: Number(form.raw_material_cost || 0), raw_material_cost: Number(form.raw_material_cost || 0),
manual_cost: Number(form.manual_cost || 0),
unit_total_cost: Number(form.unit_total_cost || 0), unit_total_cost: Number(form.unit_total_cost || 0),
total_price: Number(form.total_price || 0),
production_start_time: form.production_time_range?.[0] || null, production_start_time: form.production_time_range?.[0] || null,
production_end_time: form.production_time_range?.[1] || null production_end_time: form.production_time_range?.[1] || null
} }
@ -1131,7 +1144,7 @@ const resetForm = () => {
id: undefined, base_id: undefined, id: undefined, base_id: undefined,
company_name: '', // [新增] company_name: '', // [新增]
material_name: '', spec_model: '', category: '', unit: '', material_type: '', sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', quality_status: '合格', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', bom_code: '', bom_version: '', work_order_code: '', material_name: '', spec_model: '', category: '', unit: '', material_type: '', sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', quality_status: '合格', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', bom_code: '', bom_version: '', work_order_code: '',
raw_material_cost: undefined, manual_cost: undefined, unit_total_cost: undefined, raw_material_cost: undefined, unit_total_cost: undefined, total_price: undefined,
production_manager: '', production_time_range: [], arrival_photo: [], quality_report_link: [], detail_link: '' }) production_manager: '', production_time_range: [], arrival_photo: [], quality_report_link: [], detail_link: '' })
} }
const getStatusType = (status: string) => { const map: any = { '在库': 'success', '出库': 'info', '借库': 'warning', '损耗': 'danger' }; return map[status] || 'warning' } const getStatusType = (status: string) => { const map: any = { '在库': 'success', '出库': 'info', '借库': 'warning', '损耗': 'danger' }; return map[status] || 'warning' }
@ -1221,4 +1234,4 @@ onMounted(() => {
.long-dropdown { width: 580px !important; } .long-dropdown { width: 580px !important; }
.long-dropdown .el-select-dropdown__wrap { max-height: 320px !important; } .long-dropdown .el-select-dropdown__wrap { max-height: 320px !important; }
.long-dropdown .el-input__suffix { z-index: 10; } .long-dropdown .el-input__suffix { z-index: 10; }
</style> </style>