feat(frontend): 新增廠商缺貨匯入 V2
All checks were successful
CD Pipeline / deploy (push) Successful in 1m42s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m42s
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
> 本文件定義專案開發的核心準則與不可違反的規範
|
||||
> **建立日期**: 2026-01-12
|
||||
> **當前版本**: V10.38 (Vendor stockout list v2 feature flag)
|
||||
> **當前版本**: V10.39 (Vendor stockout import v2 feature flag)
|
||||
> **最後更新**: 2026-05-01
|
||||
|
||||
---
|
||||
|
||||
4
app.py
4
app.py
@@ -95,8 +95,8 @@ except Exception as e:
|
||||
sys_log.error(f"無法檢測磁碟空間: {e}")
|
||||
|
||||
# 🚩 系統版本定義 (備份與顯示用)
|
||||
# 🚩 2026-05-01 V10.38: Vendor stockout list v2 feature flag
|
||||
SYSTEM_VERSION = "V10.38"
|
||||
# 🚩 2026-05-01 V10.39: Vendor stockout import v2 feature flag
|
||||
SYSTEM_VERSION = "V10.39"
|
||||
|
||||
# ==========================================
|
||||
# 🔒 SQL Injection 防護函數
|
||||
|
||||
@@ -211,6 +211,11 @@ def index():
|
||||
def import_page():
|
||||
"""Excel 匯入頁面"""
|
||||
sys_log.info("[VendorStockout] 進入匯入頁面")
|
||||
if request.args.get('ui') == 'v2':
|
||||
return render_template(
|
||||
'vendor_stockout_import_v2.html',
|
||||
active_page='vendor_stockout'
|
||||
)
|
||||
return render_template('vendor_stockout/import.html')
|
||||
|
||||
|
||||
@@ -428,28 +433,12 @@ def api_import_excel():
|
||||
def api_import_template():
|
||||
"""下載 Excel 匯入範本(使用實際欄位名稱)"""
|
||||
try:
|
||||
# 建立範本 DataFrame(使用實際的欄位名稱)
|
||||
template_data = {
|
||||
'當前日期': ['2026-01-12'],
|
||||
'處別': ['範例處別'],
|
||||
'科別': ['範例科別'],
|
||||
'PM姓名': ['王小明'],
|
||||
'區ID': ['A01'],
|
||||
'區名稱': ['範例區域'],
|
||||
'商品ID': ['123456789'],
|
||||
'商品名稱': ['範例商品名稱'],
|
||||
'單品/組合商品': ['單品'],
|
||||
'借採轉': ['否'],
|
||||
'來源供應商編號': ['V001'],
|
||||
'來源供應商名稱': ['範例供應商股份有限公司'],
|
||||
'商品可賣量': [50],
|
||||
'缺貨日期': ['2026-01-10'],
|
||||
'缺貨天數': [2],
|
||||
'缺貨商品前30天業績': [150000],
|
||||
'最近30天銷售量': [300],
|
||||
'庫存水位': ['低']
|
||||
}
|
||||
df = pd.DataFrame(template_data)
|
||||
template_columns = [
|
||||
'當前日期', '處別', '科別', 'PM姓名', '區ID', '區名稱', '商品ID', '商品名稱',
|
||||
'單品/組合商品', '借採轉', '來源供應商編號', '來源供應商名稱', '商品可賣量',
|
||||
'缺貨日期', '缺貨天數', '缺貨商品前30天業績', '最近30天銷售量', '庫存水位'
|
||||
]
|
||||
df = pd.DataFrame(columns=template_columns)
|
||||
|
||||
# 輸出到記憶體
|
||||
output = io.BytesIO()
|
||||
|
||||
574
templates/vendor_stockout_import_v2.html
Normal file
574
templates/vendor_stockout_import_v2.html
Normal file
@@ -0,0 +1,574 @@
|
||||
{% extends 'ewoooc_base.html' %}
|
||||
|
||||
{% block title %}EwoooC 缺貨匯入{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.stockout-import-stack {
|
||||
display: grid;
|
||||
gap: 22px;
|
||||
}
|
||||
|
||||
.stockout-import-header,
|
||||
.stockout-upload-panel,
|
||||
.stockout-import-card {
|
||||
background: var(--momo-bg-surface);
|
||||
border: 1px solid var(--momo-border-light);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.stockout-import-header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.5fr) minmax(260px, 0.7fr);
|
||||
gap: 18px;
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.stockout-import-eyebrow {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 10px;
|
||||
color: var(--momo-accent-strong);
|
||||
background: var(--momo-accent-soft);
|
||||
border-radius: var(--momo-radius-pill);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.stockout-import-title {
|
||||
margin: 0;
|
||||
color: var(--momo-text-primary);
|
||||
font-size: 30px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.stockout-import-subtitle {
|
||||
max-width: 760px;
|
||||
margin: 8px 0 0;
|
||||
color: var(--momo-text-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.stockout-import-actions,
|
||||
.stockout-result-grid {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stockout-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
min-height: 36px;
|
||||
padding: 8px 14px;
|
||||
color: var(--momo-text-primary);
|
||||
background: var(--momo-bg-paper);
|
||||
border: 1px solid var(--momo-border-light);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
text-decoration: none;
|
||||
transition: var(--momo-transition-base);
|
||||
}
|
||||
|
||||
.stockout-action:hover {
|
||||
color: var(--momo-text-primary);
|
||||
border-color: var(--momo-border-strong);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.stockout-action.is-primary {
|
||||
color: var(--momo-text-inverse);
|
||||
background: var(--momo-ink);
|
||||
border-color: var(--momo-ink);
|
||||
}
|
||||
|
||||
.stockout-import-note {
|
||||
display: grid;
|
||||
align-content: center;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
color: var(--momo-text-inverse);
|
||||
background: var(--momo-ink);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.stockout-import-note-label {
|
||||
color: rgba(250, 247, 240, 0.62);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.stockout-import-note-value {
|
||||
color: var(--momo-text-inverse);
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.stockout-upload-panel {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.35fr) minmax(260px, 0.65fr);
|
||||
gap: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stockout-dropzone {
|
||||
display: grid;
|
||||
min-height: 330px;
|
||||
cursor: pointer;
|
||||
place-items: center;
|
||||
padding: 30px;
|
||||
background: var(--momo-bg-surface);
|
||||
border: 0;
|
||||
border-right: 1px solid var(--momo-border-light);
|
||||
transition: var(--momo-transition-base);
|
||||
}
|
||||
|
||||
.stockout-dropzone:hover,
|
||||
.stockout-dropzone.is-dragover {
|
||||
background: var(--momo-bg-paper);
|
||||
}
|
||||
|
||||
.stockout-dropzone-inner {
|
||||
max-width: 520px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stockout-upload-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
color: var(--momo-accent-strong);
|
||||
background: var(--momo-accent-soft);
|
||||
border-radius: 8px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.stockout-dropzone-title {
|
||||
margin: 18px 0 8px;
|
||||
color: var(--momo-text-primary);
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.stockout-dropzone-subtitle {
|
||||
margin: 0;
|
||||
color: var(--momo-text-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.stockout-import-side {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
align-content: start;
|
||||
padding: 18px;
|
||||
background: var(--momo-bg-paper);
|
||||
}
|
||||
|
||||
.stockout-import-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stockout-card-label {
|
||||
color: var(--momo-text-secondary);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.stockout-card-title {
|
||||
margin-top: 8px;
|
||||
color: var(--momo-text-primary);
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.stockout-card-meta {
|
||||
margin-top: 8px;
|
||||
color: var(--momo-text-tertiary);
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.stockout-file-panel,
|
||||
.stockout-progress-panel,
|
||||
.stockout-result-panel,
|
||||
.stockout-error-panel {
|
||||
display: none;
|
||||
padding: 18px;
|
||||
background: var(--momo-bg-surface);
|
||||
border: 1px solid var(--momo-border-light);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.stockout-file-panel.is-visible,
|
||||
.stockout-progress-panel.is-visible,
|
||||
.stockout-result-panel.is-visible,
|
||||
.stockout-error-panel.is-visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stockout-file-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.stockout-file-name {
|
||||
color: var(--momo-text-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.stockout-file-size {
|
||||
color: var(--momo-text-tertiary);
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.stockout-progress-track {
|
||||
height: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--momo-bg-paper);
|
||||
border: 1px solid var(--momo-border-light);
|
||||
border-radius: var(--momo-radius-pill);
|
||||
}
|
||||
|
||||
.stockout-progress-bar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--momo-accent);
|
||||
animation: stockout-pulse 1.1s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.stockout-result-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.stockout-result-cell {
|
||||
padding: 14px;
|
||||
background: var(--momo-bg-paper);
|
||||
border: 1px solid var(--momo-border-light);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.stockout-result-value {
|
||||
color: var(--momo-text-primary);
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stockout-result-label {
|
||||
margin-top: 8px;
|
||||
color: var(--momo-text-secondary);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.stockout-batch-id {
|
||||
margin-top: 14px;
|
||||
color: var(--momo-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stockout-error-panel {
|
||||
color: var(--momo-danger);
|
||||
background: rgba(214, 83, 68, 0.08);
|
||||
border-color: rgba(214, 83, 68, 0.22);
|
||||
}
|
||||
|
||||
@keyframes stockout-pulse {
|
||||
from { opacity: 0.48; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.stockout-import-header,
|
||||
.stockout-upload-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stockout-dropzone {
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--momo-border-light);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.stockout-import-title {
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.stockout-file-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.stockout-result-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block ewooo_content %}
|
||||
<div class="stockout-import-stack">
|
||||
<section class="stockout-import-header">
|
||||
<div>
|
||||
<div class="stockout-import-eyebrow momo-mono">
|
||||
<i class="fas fa-file-import"></i>
|
||||
STOCKOUT IMPORT
|
||||
</div>
|
||||
<h1 class="stockout-import-title">Excel 匯入</h1>
|
||||
<p class="stockout-import-subtitle">
|
||||
上傳正式缺貨 Excel 後,系統會使用既有匯入 API 寫入 vendor_stockout,完成後直接回傳批次編號與成功、重複、失敗筆數。
|
||||
</p>
|
||||
<div class="stockout-import-actions mt-3">
|
||||
<a class="stockout-action" href="{{ url_for('vendor.index', ui='v2') }}">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
回總覽
|
||||
</a>
|
||||
<a class="stockout-action" href="{{ url_for('vendor.list_page', ui='v2') }}">
|
||||
<i class="fas fa-table-list"></i>
|
||||
缺貨清單
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<aside class="stockout-import-note">
|
||||
<div class="stockout-import-note-label momo-mono">資料原則</div>
|
||||
<div class="stockout-import-note-value">只匯入使用者上傳的正式 Excel,不預填任何資料列。</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section class="stockout-upload-panel" aria-label="Excel 匯入">
|
||||
<button class="stockout-dropzone" id="stockoutDropzone" type="button">
|
||||
<input id="stockoutFileInput" type="file" accept=".xlsx,.xls" hidden>
|
||||
<span class="stockout-dropzone-inner">
|
||||
<span class="stockout-upload-icon">
|
||||
<i class="fas fa-cloud-arrow-up"></i>
|
||||
</span>
|
||||
<span class="stockout-dropzone-title d-block">選擇或拖曳 Excel 檔案</span>
|
||||
<span class="stockout-dropzone-subtitle d-block">
|
||||
支援 .xlsx / .xls。檔案送出後會寫入正式缺貨資料表,請確認內容是可匯入的正式清單。
|
||||
</span>
|
||||
<span class="stockout-action is-primary mt-3">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
選擇檔案
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<aside class="stockout-import-side">
|
||||
<div class="stockout-import-card">
|
||||
<div class="stockout-card-label momo-mono">必要欄位</div>
|
||||
<div class="stockout-card-title">來源供應商編號、來源供應商名稱、商品ID、商品名稱</div>
|
||||
<div class="stockout-card-meta momo-mono">缺少必要欄位時,API 會拒絕匯入並回傳錯誤原因。</div>
|
||||
</div>
|
||||
<div class="stockout-import-card">
|
||||
<div class="stockout-card-label momo-mono">欄位範本</div>
|
||||
<div class="stockout-card-title">下載只有正式欄位標題的空白範本</div>
|
||||
<div class="stockout-card-meta">
|
||||
<a class="stockout-action" href="{{ url_for('vendor.api_import_template') }}">
|
||||
<i class="fas fa-download"></i>
|
||||
下載範本
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section class="stockout-file-panel" id="stockoutFilePanel">
|
||||
<div class="stockout-file-row">
|
||||
<div>
|
||||
<div class="stockout-card-label momo-mono">已選擇檔案</div>
|
||||
<div class="stockout-file-name" id="stockoutFileName"></div>
|
||||
<div class="stockout-file-size momo-mono" id="stockoutFileSize"></div>
|
||||
</div>
|
||||
<button class="stockout-action is-primary" id="stockoutUploadButton" type="button">
|
||||
<i class="fas fa-upload"></i>
|
||||
開始匯入
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stockout-progress-panel" id="stockoutProgressPanel">
|
||||
<div class="stockout-card-label momo-mono mb-2">匯入中</div>
|
||||
<div class="stockout-progress-track">
|
||||
<div class="stockout-progress-bar"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stockout-result-panel" id="stockoutResultPanel">
|
||||
<div class="stockout-card-label momo-mono">匯入結果</div>
|
||||
<div class="stockout-result-grid">
|
||||
<div class="stockout-result-cell">
|
||||
<div class="stockout-result-value momo-mono" id="stockoutTotalCount">0</div>
|
||||
<div class="stockout-result-label">總筆數</div>
|
||||
</div>
|
||||
<div class="stockout-result-cell">
|
||||
<div class="stockout-result-value momo-mono" id="stockoutSuccessCount">0</div>
|
||||
<div class="stockout-result-label">成功匯入</div>
|
||||
</div>
|
||||
<div class="stockout-result-cell">
|
||||
<div class="stockout-result-value momo-mono" id="stockoutDuplicateCount">0</div>
|
||||
<div class="stockout-result-label">重複項目</div>
|
||||
</div>
|
||||
<div class="stockout-result-cell">
|
||||
<div class="stockout-result-value momo-mono" id="stockoutFailedCount">0</div>
|
||||
<div class="stockout-result-label">失敗</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stockout-batch-id momo-mono">批次編號 <span id="stockoutBatchId"></span></div>
|
||||
<div class="stockout-import-actions mt-3">
|
||||
<a class="stockout-action is-primary" href="{{ url_for('vendor.list_page', ui='v2') }}">
|
||||
<i class="fas fa-table-list"></i>
|
||||
查看缺貨清單
|
||||
</a>
|
||||
<button class="stockout-action" id="stockoutImportAgainButton" type="button">
|
||||
<i class="fas fa-rotate-right"></i>
|
||||
再次匯入
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stockout-error-panel" id="stockoutErrorPanel">
|
||||
<div class="stockout-card-label momo-mono">匯入失敗</div>
|
||||
<div class="stockout-card-title" id="stockoutErrorMessage"></div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
(function () {
|
||||
const dropzone = document.getElementById('stockoutDropzone');
|
||||
const fileInput = document.getElementById('stockoutFileInput');
|
||||
const filePanel = document.getElementById('stockoutFilePanel');
|
||||
const fileName = document.getElementById('stockoutFileName');
|
||||
const fileSize = document.getElementById('stockoutFileSize');
|
||||
const uploadButton = document.getElementById('stockoutUploadButton');
|
||||
const progressPanel = document.getElementById('stockoutProgressPanel');
|
||||
const resultPanel = document.getElementById('stockoutResultPanel');
|
||||
const errorPanel = document.getElementById('stockoutErrorPanel');
|
||||
const errorMessage = document.getElementById('stockoutErrorMessage');
|
||||
const importAgainButton = document.getElementById('stockoutImportAgainButton');
|
||||
let selectedFile = null;
|
||||
|
||||
function show(panel) {
|
||||
panel.classList.add('is-visible');
|
||||
}
|
||||
|
||||
function hide(panel) {
|
||||
panel.classList.remove('is-visible');
|
||||
}
|
||||
|
||||
function resetTransientPanels() {
|
||||
hide(progressPanel);
|
||||
hide(resultPanel);
|
||||
hide(errorPanel);
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
function handleFile(file) {
|
||||
if (!file) return;
|
||||
if (!file.name.match(/\.(xlsx|xls)$/i)) {
|
||||
errorMessage.textContent = '請選擇 Excel 檔案 (.xlsx 或 .xls)';
|
||||
show(errorPanel);
|
||||
return;
|
||||
}
|
||||
selectedFile = file;
|
||||
fileName.textContent = file.name;
|
||||
fileSize.textContent = formatFileSize(file.size);
|
||||
resetTransientPanels();
|
||||
show(filePanel);
|
||||
}
|
||||
|
||||
dropzone.addEventListener('click', function () {
|
||||
fileInput.click();
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', function (event) {
|
||||
handleFile(event.target.files[0]);
|
||||
});
|
||||
|
||||
dropzone.addEventListener('dragover', function (event) {
|
||||
event.preventDefault();
|
||||
dropzone.classList.add('is-dragover');
|
||||
});
|
||||
|
||||
dropzone.addEventListener('dragleave', function () {
|
||||
dropzone.classList.remove('is-dragover');
|
||||
});
|
||||
|
||||
dropzone.addEventListener('drop', function (event) {
|
||||
event.preventDefault();
|
||||
dropzone.classList.remove('is-dragover');
|
||||
handleFile(event.dataTransfer.files[0]);
|
||||
});
|
||||
|
||||
uploadButton.addEventListener('click', async function () {
|
||||
if (!selectedFile) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', selectedFile);
|
||||
hide(filePanel);
|
||||
hide(resultPanel);
|
||||
hide(errorPanel);
|
||||
show(progressPanel);
|
||||
|
||||
try {
|
||||
const response = await fetchWithCSRF('/vendor-stockout/api/import/excel', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
const result = await response.json();
|
||||
hide(progressPanel);
|
||||
|
||||
if (result.success) {
|
||||
document.getElementById('stockoutTotalCount').textContent = result.data.total_count;
|
||||
document.getElementById('stockoutSuccessCount').textContent = result.data.success_count;
|
||||
document.getElementById('stockoutDuplicateCount').textContent = result.data.duplicate_count;
|
||||
document.getElementById('stockoutFailedCount').textContent = result.data.failed_count;
|
||||
document.getElementById('stockoutBatchId').textContent = result.data.batch_id;
|
||||
show(resultPanel);
|
||||
} else {
|
||||
errorMessage.textContent = result.message || '匯入失敗';
|
||||
show(errorPanel);
|
||||
}
|
||||
} catch (error) {
|
||||
hide(progressPanel);
|
||||
errorMessage.textContent = '網路錯誤:' + error.message;
|
||||
show(errorPanel);
|
||||
}
|
||||
});
|
||||
|
||||
importAgainButton.addEventListener('click', function () {
|
||||
selectedFile = null;
|
||||
fileInput.value = '';
|
||||
hide(filePanel);
|
||||
resetTransientPanels();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -341,7 +341,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="vendor-actions">
|
||||
<a class="vendor-action is-primary" href="{{ url_for('vendor.import_page') }}">
|
||||
<a class="vendor-action is-primary" href="{{ url_for('vendor.import_page', ui='v2') }}">
|
||||
<i class="fas fa-file-import"></i>
|
||||
匯入 Excel
|
||||
</a>
|
||||
@@ -408,7 +408,7 @@
|
||||
<span class="title">工作入口</span>
|
||||
</div>
|
||||
<div class="vendor-flow-grid">
|
||||
<a class="vendor-flow-card" href="{{ url_for('vendor.import_page') }}">
|
||||
<a class="vendor-flow-card" href="{{ url_for('vendor.import_page', ui='v2') }}">
|
||||
<span class="vendor-flow-icon"><i class="fas fa-file-import"></i></span>
|
||||
<span>
|
||||
<span class="vendor-flow-title">Excel 匯入</span>
|
||||
|
||||
@@ -334,7 +334,7 @@
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
回總覽
|
||||
</a>
|
||||
<a class="stockout-action is-primary" href="{{ url_for('vendor.import_page') }}">
|
||||
<a class="stockout-action is-primary" href="{{ url_for('vendor.import_page', ui='v2') }}">
|
||||
<i class="fas fa-file-import"></i>
|
||||
匯入 Excel
|
||||
</a>
|
||||
|
||||
@@ -99,3 +99,18 @@ def test_vendor_stockout_list_v2_is_feature_flagged_and_uses_real_stockout_rows(
|
||||
assert "record.vendor_name" in template
|
||||
assert "mock" not in template.lower()
|
||||
assert "假" not in template
|
||||
|
||||
|
||||
def test_vendor_stockout_import_v2_is_feature_flagged_and_does_not_ship_sample_rows():
|
||||
route_source = (ROOT / "routes/vendor_routes.py").read_text(encoding="utf-8")
|
||||
template = (ROOT / "templates/vendor_stockout_import_v2.html").read_text(encoding="utf-8")
|
||||
template_function = route_source.split("def api_import_template():", 1)[1].split("@vendor_bp.route('/api/stockout/list'", 1)[0]
|
||||
|
||||
assert "vendor_stockout_import_v2.html" in route_source
|
||||
assert "template_columns = [" in template_function
|
||||
assert "pd.DataFrame(columns=template_columns)" in template_function
|
||||
assert "fetchWithCSRF('/vendor-stockout/api/import/excel'" in template
|
||||
assert "vendor_stockout" in template
|
||||
assert "範例" not in template_function
|
||||
assert "mock" not in template.lower()
|
||||
assert "假" not in template
|
||||
|
||||
Reference in New Issue
Block a user