youhua
This commit is contained in:
@@ -178,7 +178,7 @@ def fetch_receipt_details_full():
|
|||||||
"转换单位": item.get("convertUnitName"),
|
"转换单位": item.get("convertUnitName"),
|
||||||
"收货单价": item.get("receivePrice"),
|
"收货单价": item.get("receivePrice"),
|
||||||
"收货时间": item.get("receiptTime"),
|
"收货时间": 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("convertGoodsQuantity") if item.get("convertGoodsQuantity") is not None else item.get("goodsQuantity"),
|
||||||
"收货总金额": item.get("receiveAmount")
|
"收货总金额": item.get("receiveAmount")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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))
|
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()
|
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")
|
r_qty = item.get("convertGoodsQuantity") if item.get("convertGoodsQuantity") is not None else item.get("goodsQuantity")
|
||||||
|
|
||||||
if existing_record:
|
if existing_record:
|
||||||
|
|||||||
@@ -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_supplier_name ON receipt_details(supplier_name)')
|
||||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_receipt_time ON receipt_details(receipt_time)')
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_receipt_time ON receipt_details(receipt_time)')
|
||||||
|
|
||||||
# 创建 BOM 成本表(父件表)
|
# 注意:为了在打包部署时不丢失用户已抓取的数据,改为 IF NOT EXISTS
|
||||||
cursor.execute('DROP TABLE IF EXISTS bom_child')
|
|
||||||
cursor.execute('DROP TABLE IF EXISTS bom_parent')
|
|
||||||
|
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
CREATE TABLE bom_parent (
|
CREATE TABLE IF NOT EXISTS bom_parent (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
parent_material_code TEXT UNIQUE,
|
parent_material_code TEXT UNIQUE,
|
||||||
parent_material_name TEXT
|
parent_material_name TEXT
|
||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
|
|
||||||
# 创建 BOM 成本表(子件明细表)
|
|
||||||
# 由于是树状结构,我们采用“邻接表”模型,记录每个节点的 parent_id
|
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
CREATE TABLE bom_child (
|
CREATE TABLE IF NOT EXISTS bom_child (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
parent_material_code TEXT, -- 归属的最顶层父件
|
parent_material_code TEXT, -- 归属的最顶层父件
|
||||||
node_material_code TEXT,
|
node_material_code TEXT,
|
||||||
|
|||||||
109
web_ui/app.py
109
web_ui/app.py
@@ -40,6 +40,20 @@ DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|||||||
# 抓取脚本目录 (属于代码文件,从 BASE_DIR 加载)
|
# 抓取脚本目录 (属于代码文件,从 BASE_DIR 加载)
|
||||||
BROWSER_LOGIN_DIR = BASE_DIR / "browser_login"
|
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():
|
def background_sync_job():
|
||||||
"""APScheduler 后台定时任务执行增量抓取"""
|
"""APScheduler 后台定时任务执行增量抓取"""
|
||||||
print("[定时任务] 正在执行后台增量数据同步...")
|
print("[定时任务] 正在执行后台增量数据同步...")
|
||||||
@@ -284,15 +298,19 @@ def get_bom_tree(parent_code):
|
|||||||
if material_codes:
|
if material_codes:
|
||||||
# 使用 IN 语句批量查询,并按物料分组取最新时间的价格
|
# 使用 IN 语句批量查询,并按物料分组取最新时间的价格
|
||||||
placeholders = ','.join(['?'] * len(material_codes))
|
placeholders = ','.join(['?'] * len(material_codes))
|
||||||
# SQLite 分组取最新记录的经典写法
|
# SQLite 分组取最新记录的安全写法(使用窗口函数 ROW_NUMBER)
|
||||||
price_records = conn.execute(f'''
|
price_records = conn.execute(f'''
|
||||||
SELECT material_code,
|
WITH RankedRecords AS (
|
||||||
ROUND(COALESCE(total_amount / NULLIF(receive_qty, 0), receive_price, 0), 2) AS receive_price,
|
SELECT material_code,
|
||||||
receipt_time, purchase_order_code, supplier_name
|
ROUND(COALESCE(total_amount / NULLIF(receive_qty, 0), receive_price, 0), 2) AS receive_price,
|
||||||
FROM receipt_details
|
receipt_time, purchase_order_code, supplier_name,
|
||||||
WHERE material_code IN ({placeholders})
|
ROW_NUMBER() OVER(PARTITION BY material_code ORDER BY receipt_time DESC, id DESC) as rn
|
||||||
GROUP BY material_code
|
FROM receipt_details
|
||||||
HAVING receipt_time = MAX(receipt_time)
|
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()
|
''', material_codes).fetchall()
|
||||||
|
|
||||||
for r in price_records:
|
for r in price_records:
|
||||||
@@ -478,15 +496,19 @@ def get_bom_tree_compare(parent_code):
|
|||||||
|
|
||||||
# --- A. 查询历史最新价格及铸件重量/单位 ---
|
# --- A. 查询历史最新价格及铸件重量/单位 ---
|
||||||
latest_records = conn.execute(f'''
|
latest_records = conn.execute(f'''
|
||||||
SELECT material_code,
|
WITH RankedRecords AS (
|
||||||
unit_name,
|
SELECT material_code,
|
||||||
purchase_qty,
|
unit_name,
|
||||||
receive_qty,
|
purchase_qty,
|
||||||
ROUND(COALESCE(total_amount / NULLIF(receive_qty, 0), receive_price, 0), 2) AS receive_price
|
receive_qty,
|
||||||
FROM receipt_details
|
ROUND(COALESCE(total_amount / NULLIF(receive_qty, 0), receive_price, 0), 2) AS receive_price,
|
||||||
WHERE material_code IN ({placeholders})
|
ROW_NUMBER() OVER(PARTITION BY material_code ORDER BY receipt_time DESC, id DESC) as rn
|
||||||
GROUP BY material_code
|
FROM receipt_details
|
||||||
HAVING receipt_time = MAX(receipt_time)
|
WHERE material_code IN ({placeholders})
|
||||||
|
)
|
||||||
|
SELECT material_code, unit_name, purchase_qty, receive_qty, receive_price
|
||||||
|
FROM RankedRecords
|
||||||
|
WHERE rn = 1
|
||||||
''', unique_codes).fetchall()
|
''', unique_codes).fetchall()
|
||||||
|
|
||||||
for r in latest_records:
|
for r in latest_records:
|
||||||
@@ -510,13 +532,17 @@ def get_bom_tree_compare(parent_code):
|
|||||||
# 1. 先查询该期间内的最新收货价
|
# 1. 先查询该期间内的最新收货价
|
||||||
params = unique_codes + [start_date, end_date_full]
|
params = unique_codes + [start_date, end_date_full]
|
||||||
period_latest_records = conn.execute(f'''
|
period_latest_records = conn.execute(f'''
|
||||||
SELECT material_code,
|
WITH RankedRecords AS (
|
||||||
ROUND(COALESCE(total_amount / NULLIF(receive_qty, 0), receive_price, 0), 2) AS receive_price
|
SELECT material_code,
|
||||||
FROM receipt_details
|
ROUND(COALESCE(total_amount / NULLIF(receive_qty, 0), receive_price, 0), 2) AS receive_price,
|
||||||
WHERE material_code IN ({placeholders})
|
ROW_NUMBER() OVER(PARTITION BY material_code ORDER BY receipt_time DESC, id DESC) as rn
|
||||||
AND receipt_time >= ? AND receipt_time <= ?
|
FROM receipt_details
|
||||||
GROUP BY material_code
|
WHERE material_code IN ({placeholders})
|
||||||
HAVING receipt_time = MAX(receipt_time)
|
AND receipt_time >= ? AND receipt_time <= ?
|
||||||
|
)
|
||||||
|
SELECT material_code, receive_price
|
||||||
|
FROM RankedRecords
|
||||||
|
WHERE rn = 1
|
||||||
''', params).fetchall()
|
''', params).fetchall()
|
||||||
|
|
||||||
found_codes = set()
|
found_codes = set()
|
||||||
@@ -532,13 +558,17 @@ def get_bom_tree_compare(parent_code):
|
|||||||
missing_ph = ','.join(['?'] * len(missing_codes))
|
missing_ph = ','.join(['?'] * len(missing_codes))
|
||||||
fallback_params = missing_codes + [start_date]
|
fallback_params = missing_codes + [start_date]
|
||||||
fallback_records = conn.execute(f'''
|
fallback_records = conn.execute(f'''
|
||||||
SELECT material_code,
|
WITH RankedRecords AS (
|
||||||
ROUND(COALESCE(total_amount / NULLIF(receive_qty, 0), receive_price, 0), 2) AS receive_price
|
SELECT material_code,
|
||||||
FROM receipt_details
|
ROUND(COALESCE(total_amount / NULLIF(receive_qty, 0), receive_price, 0), 2) AS receive_price,
|
||||||
WHERE material_code IN ({missing_ph})
|
ROW_NUMBER() OVER(PARTITION BY material_code ORDER BY receipt_time DESC, id DESC) as rn
|
||||||
AND receipt_time < ?
|
FROM receipt_details
|
||||||
GROUP BY material_code
|
WHERE material_code IN ({missing_ph})
|
||||||
HAVING receipt_time = MAX(receipt_time)
|
AND receipt_time < ?
|
||||||
|
)
|
||||||
|
SELECT material_code, receive_price
|
||||||
|
FROM RankedRecords
|
||||||
|
WHERE rn = 1
|
||||||
''', fallback_params).fetchall()
|
''', fallback_params).fetchall()
|
||||||
|
|
||||||
fallback_found = set()
|
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')
|
is_real_purchased_leaf = (not has_children) and (node['latestUnitPrice'] > 0 or node['periodAStatus'] != 'missing' or node['periodBStatus'] != 'missing')
|
||||||
|
|
||||||
if not has_children:
|
if not has_children:
|
||||||
# 叶子节点,总成本 = 单价 * 耗用量
|
# 叶子节点,核心逻辑修改:区分是否是 1PZJ 铸件
|
||||||
node['totalLatest'] = node['latestUnitPrice'] * usage_qty
|
if code and code.startswith('1PZJ') and node.get('castingWeight'):
|
||||||
node['totalPeriodA'] = node['periodAUnitPrice'] * usage_qty
|
# 铸件:总成本 = BOM 用量(件) * 单件重量(KG/件) * 采购单价(元/KG)
|
||||||
node['totalPeriodB'] = node['periodBUnitPrice'] * usage_qty
|
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),那就大大方方给它标红点
|
# 如果它真的是虚拟件(基准价是 0,且 A B 都是 missing),那就大大方方给它标红点
|
||||||
|
|||||||
@@ -320,45 +320,76 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column label="历史最新基准价 (¥)" align="center" min-width="120">
|
<!-- 单价列 (并排显示) -->
|
||||||
|
<el-table-column label="历史基准单价(¥)" align="center" min-width="120">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<span v-if="scope.row.totalLatest > 0" v-text="scope.row.totalLatest.toFixed(2)"></span>
|
<span v-if="scope.row.latestUnitPrice > 0" style="color: #606266;">
|
||||||
|
¥<span v-text="scope.row.latestUnitPrice.toFixed(2)"></span>
|
||||||
|
</span>
|
||||||
<span v-else style="color: #C0C4CC;">-</span>
|
<span v-else style="color: #C0C4CC;">-</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column label="期间 A 最新价 (¥)" align="center" min-width="140">
|
<el-table-column label="期间 A 单价(¥)" align="center" min-width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span v-if="scope.row.periodAUnitPrice > 0" style="color: #606266;">
|
||||||
|
¥<span v-text="scope.row.periodAUnitPrice.toFixed(2)"></span>
|
||||||
|
</span>
|
||||||
|
<span v-else style="color: #C0C4CC;">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="期间 B 单价(¥)" align="center" min-width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span v-if="scope.row.periodBUnitPrice > 0" style="color: #606266;">
|
||||||
|
¥<span v-text="scope.row.periodBUnitPrice.toFixed(2)"></span>
|
||||||
|
</span>
|
||||||
|
<span v-else style="color: #C0C4CC;">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 总成本列 -->
|
||||||
|
<el-table-column label="历史基准总成本(¥)" align="center" min-width="130">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span v-if="scope.row.totalLatest > 0">
|
||||||
|
<strong v-text="scope.row.totalLatest.toFixed(2)"></strong>
|
||||||
|
</span>
|
||||||
|
<span v-else style="color: #C0C4CC;">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="期间 A 总成本(¥)" align="center" min-width="150">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-tooltip effect="dark" :content="getStatusText(scope.row.showAStatus)" placement="top" :disabled="scope.row.showAStatus !== 'fallback'">
|
<el-tooltip effect="dark" :content="getStatusText(scope.row.showAStatus)" placement="top" :disabled="scope.row.showAStatus !== 'fallback'">
|
||||||
<span style="display: flex; justify-content: center; align-items: center;">
|
<span style="display: flex; justify-content: center; align-items: center;">
|
||||||
<span class="status-dot" :class="'dot-' + scope.row.showAStatus"></span>
|
<span class="status-dot" :class="'dot-' + scope.row.showAStatus"></span>
|
||||||
<span v-if="scope.row.totalPeriodA > 0" v-text="scope.row.totalPeriodA.toFixed(2)"></span>
|
<strong v-if="scope.row.totalPeriodA > 0" v-text="scope.row.totalPeriodA.toFixed(2)"></strong>
|
||||||
<span v-else style="color: #C0C4CC; margin-left: 2px;">-</span>
|
<span v-else style="color: #C0C4CC; margin-left: 2px;">-</span>
|
||||||
</span>
|
</span>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column label="期间 B 最新价 (¥)" align="center" min-width="140">
|
<el-table-column label="期间 B 总成本(¥)" align="center" min-width="150">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-tooltip effect="dark" :content="getStatusText(scope.row.showBStatus)" placement="top" :disabled="scope.row.showBStatus !== 'fallback'">
|
<el-tooltip effect="dark" :content="getStatusText(scope.row.showBStatus)" placement="top" :disabled="scope.row.showBStatus !== 'fallback'">
|
||||||
<span style="display: flex; justify-content: center; align-items: center;">
|
<span style="display: flex; justify-content: center; align-items: center;">
|
||||||
<span class="status-dot" :class="'dot-' + scope.row.showBStatus"></span>
|
<span class="status-dot" :class="'dot-' + scope.row.showBStatus"></span>
|
||||||
<span v-if="scope.row.totalPeriodB > 0" v-text="scope.row.totalPeriodB.toFixed(2)"></span>
|
<strong v-if="scope.row.totalPeriodB > 0" v-text="scope.row.totalPeriodB.toFixed(2)"></strong>
|
||||||
<span v-else style="color: #C0C4CC; margin-left: 2px;">-</span>
|
<span v-else style="color: #C0C4CC; margin-left: 2px;">-</span>
|
||||||
</span>
|
</span>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column label="差异对比 (B - A)" align="center" min-width="120">
|
<el-table-column label="差异对比 (总成本 B - A)" align="center" min-width="140">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<span v-if="scope.row.totalPeriodA > 0 && scope.row.totalPeriodB > 0">
|
<span v-if="scope.row.totalPeriodB > 0">
|
||||||
<span v-if="scope.row.totalPeriodB > scope.row.totalPeriodA" class="price-up">
|
<span v-if="scope.row.totalPeriodB > (scope.row.totalPeriodA || 0)" class="price-up">
|
||||||
<i class="el-icon-top"></i> <span v-text="(scope.row.totalPeriodB - scope.row.totalPeriodA).toFixed(2)"></span>
|
<i class="el-icon-top"></i> <span v-text="(scope.row.totalPeriodB - (scope.row.totalPeriodA || 0)).toFixed(2)"></span>
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="scope.row.totalPeriodB < scope.row.totalPeriodA" class="price-down">
|
<span v-else-if="scope.row.totalPeriodB < (scope.row.totalPeriodA || 0)" class="price-down">
|
||||||
<i class="el-icon-bottom"></i> <span v-text="(scope.row.totalPeriodA - scope.row.totalPeriodB).toFixed(2)"></span>
|
<i class="el-icon-bottom"></i> <span v-text="((scope.row.totalPeriodA || 0) - scope.row.totalPeriodB).toFixed(2)"></span>
|
||||||
</span>
|
</span>
|
||||||
<span v-else style="color: #909399;">持平</span>
|
<span v-else style="color: #909399;">持平</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user