不良品数量相关优化

This commit is contained in:
Codex 2026-10-29 11:09:20 +08:00
parent c088fdb823
commit 1eb96ece68
3 changed files with 26 additions and 37 deletions

View File

@ -2580,16 +2580,11 @@ def _optional_int(data, key, label):
def _arrival_batch_quantities(data, order_type='domestic'): def _arrival_batch_quantities(data, order_type='domestic'):
"""Parse quantities; packaging defective quantity may be any integer.""" """Parse quantities; defective quantity may be any integer for all order types."""
arrival_qty, arrival_qty_filled = _optional_int(data, 'arrival_qty', '到货数量') arrival_qty, arrival_qty_filled = _optional_int(data, 'arrival_qty', '到货数量')
defective_qty, defective_qty_filled = _optional_int(data, 'defective_qty', '不良品数量') defective_qty, defective_qty_filled = _optional_int(data, 'defective_qty', '不良品数量')
if arrival_qty_filled and arrival_qty < 0: if arrival_qty_filled and arrival_qty < 0:
raise ValueError('到货数量不能为负数') raise ValueError('到货数量不能为负数')
if order_type != 'packaging' and defective_qty_filled:
if defective_qty < 0:
raise ValueError('不良品数量不能为负数')
if defective_qty > arrival_qty:
raise ValueError('不良品数量不能大于到货数量')
return arrival_qty, defective_qty, arrival_qty_filled, defective_qty_filled return arrival_qty, defective_qty, arrival_qty_filled, defective_qty_filled
@ -2817,18 +2812,10 @@ def domestic_orders_update_batch():
requested_defective = int(data.get('defective_qty')) requested_defective = int(data.get('defective_qty'))
except (TypeError, ValueError): except (TypeError, ValueError):
return jsonify({"code": 400, "msg": "不良品数量必须为整数"}), 400 return jsonify({"code": 400, "msg": "不良品数量必须为整数"}), 400
if order_type != 'packaging' and requested_defective < 0: defective_locked = current.get('defective_qty_filled')
return jsonify({"code": 400, "msg": "不良品数量不能为负数"}), 400
effective_arrival_qty = (
requested_qty if 'requested_qty' in locals() and data.get('arrival_qty') not in (None, '')
else int(current.get('arrival_qty') or 0)
)
if order_type != 'packaging' and requested_defective > effective_arrival_qty:
return jsonify({"code": 400, "msg": "不良品数量不能大于到货数量"}), 400
defective_locked = order_type != 'packaging' or current.get('defective_qty_filled')
if defective_locked and requested_defective != int(current.get('defective_qty') or 0): if defective_locked and requested_defective != int(current.get('defective_qty') or 0):
return jsonify({"code": 400, "msg": "不良品数量保存后不允许修改"}), 400 return jsonify({"code": 400, "msg": "不良品数量保存后不允许修改"}), 400
if order_type == 'packaging' and not current.get('defective_qty_filled'): if not current.get('defective_qty_filled'):
update_fields.extend(['defective_qty=%s', 'defective_qty_filled=1']) update_fields.extend(['defective_qty=%s', 'defective_qty_filled=1'])
update_params.append(requested_defective) update_params.append(requested_defective)
supplemented.append(f'不良品数量={requested_defective}') supplemented.append(f'不良品数量={requested_defective}')

View File

@ -21755,7 +21755,7 @@ function showOrderDetail(encodedStr, title) {
html += '<div style="display:flex;align-items:center;margin:0 0 8px 0">'; html += '<div style="display:flex;align-items:center;margin:0 0 8px 0">';
html += '<div style="margin-left:auto;display:flex;align-items:center;gap:6px"><button id="addMultiBatchBtn" class="btn btn-sm btn-outline" onclick="promptAddArrivalBatchRows(this)" data-order-id="' + row.id + '" data-order-type="' + orderType + '" style="font-size:12px">➕ 添加多行</button><button id="addBatchBtn" class="btn btn-sm btn-outline" onclick="addArrivalBatchRow(this)" data-order-id="' + row.id + '" data-order-type="' + orderType + '" style="font-size:12px">➕ 添加一行</button></div>'; html += '<div style="margin-left:auto;display:flex;align-items:center;gap:6px"><button id="addMultiBatchBtn" class="btn btn-sm btn-outline" onclick="promptAddArrivalBatchRows(this)" data-order-id="' + row.id + '" data-order-type="' + orderType + '" style="font-size:12px">➕ 添加多行</button><button id="addBatchBtn" class="btn btn-sm btn-outline" onclick="addArrivalBatchRow(this)" data-order-id="' + row.id + '" data-order-type="' + orderType + '" style="font-size:12px">➕ 添加一行</button></div>';
html += '</div>'; html += '</div>';
var showDefectiveQty = orderType === 'packaging'; var showDefectiveQty = true;
html += '<table id="arrivalBatchTable" style="width:100%;border-collapse:collapse;font-size:13px;table-layout:auto" data-order-qty="' + (parseInt(row['数量']) || 0) + '"><thead><tr>'; html += '<table id="arrivalBatchTable" style="width:100%;border-collapse:collapse;font-size:13px;table-layout:auto" data-order-qty="' + (parseInt(row['数量']) || 0) + '"><thead><tr>';
html += '<th style="border:1.5px solid #b0b0b0;padding:6px 8px;background:#f5f5f5;color:#555;font-weight:600;text-align:center;white-space:nowrap;width:1%">批次编号</th>'; html += '<th style="border:1.5px solid #b0b0b0;padding:6px 8px;background:#f5f5f5;color:#555;font-weight:600;text-align:center;white-space:nowrap;width:1%">批次编号</th>';
html += '<th style="border:1.5px solid #b0b0b0;padding:6px 8px;background:#f5f5f5;color:#555;font-weight:600;text-align:center;white-space:nowrap;width:1%">操作人</th>'; html += '<th style="border:1.5px solid #b0b0b0;padding:6px 8px;background:#f5f5f5;color:#555;font-weight:600;text-align:center;white-space:nowrap;width:1%">操作人</th>';
@ -21859,7 +21859,7 @@ function showOrderDetail(encodedStr, title) {
.then(function(res) { .then(function(res) {
if (res.code !== 0) { if (res.code !== 0) {
var abBody = overlay.querySelector('#arrivalBatchBody'); var abBody = overlay.querySelector('#arrivalBatchBody');
if (abBody) abBody.innerHTML = '<tr><td colspan="' + (orderType === 'packaging' ? 8 : 7) + '" style="border:1.5px solid #b0b0b0;padding:20px;text-align:center;color:#bbb">暂无数据</td></tr>'; if (abBody) abBody.innerHTML = '<tr><td colspan="8" style="border:1.5px solid #b0b0b0;padding:20px;text-align:center;color:#bbb">暂无数据</td></tr>';
var pmBody = overlay.querySelector('#packagingMaterialBody'); var pmBody = overlay.querySelector('#packagingMaterialBody');
if (pmBody) pmBody.innerHTML = '<tr><td colspan="7" style="border:1.5px solid #b0b0b0;padding:20px;text-align:center;color:#bbb">暂无数据</td></tr>'; if (pmBody) pmBody.innerHTML = '<tr><td colspan="7" style="border:1.5px solid #b0b0b0;padding:20px;text-align:center;color:#bbb">暂无数据</td></tr>';
return; return;
@ -21872,7 +21872,7 @@ function showOrderDetail(encodedStr, title) {
var abHtml = ''; var abHtml = '';
var totalQty = 0; var totalQty = 0;
var totalDefectiveQty = 0; var totalDefectiveQty = 0;
var showDefectiveQty = orderType === 'packaging'; var showDefectiveQty = true;
var batchCount = data.arrival_batches ? data.arrival_batches.length : 0; var batchCount = data.arrival_batches ? data.arrival_batches.length : 0;
// 数据行 // 数据行
data.arrival_batches.forEach(function(b, idx) { data.arrival_batches.forEach(function(b, idx) {
@ -21892,7 +21892,7 @@ function showOrderDetail(encodedStr, title) {
abHtml += '<td class="batch-operated-at" style="border:1.5px solid #b0b0b0;padding:6px 8px;text-align:center;white-space:nowrap">' + escHtml(b.operated_at || '-') + '</td>'; abHtml += '<td class="batch-operated-at" style="border:1.5px solid #b0b0b0;padding:6px 8px;text-align:center;white-space:nowrap">' + escHtml(b.operated_at || '-') + '</td>';
abHtml += renderArrivalBatchStoredCell('date', 'batch-date', dateVal, dateFilled, allowBlankFill, '到货日期') + abHtml += renderArrivalBatchStoredCell('date', 'batch-date', dateVal, dateFilled, allowBlankFill, '到货日期') +
renderArrivalBatchStoredCell('number', 'batch-qty', qtyFilled ? qty : '', qtyFilled, allowBlankFill, '到货数量', true) + renderArrivalBatchStoredCell('number', 'batch-qty', qtyFilled ? qty : '', qtyFilled, allowBlankFill, '到货数量', true) +
(showDefectiveQty ? renderArrivalBatchStoredCell('number', 'batch-defective-qty', defectiveFilled ? defectiveQty : '', defectiveFilled, true, '不良品数量', false) : '') + renderArrivalBatchStoredCell('number', 'batch-defective-qty', defectiveFilled ? defectiveQty : '', defectiveFilled, true, '不良品数量', false) +
'<td class="batch-unfinished" style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center">0</td>' + '<td class="batch-unfinished" style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center">0</td>' +
'<td style="border:1.5px solid #b0b0b0;padding:4px 6px"><div style="display:flex;gap:2px;align-items:center;justify-content:center">' + '<td style="border:1.5px solid #b0b0b0;padding:4px 6px"><div style="display:flex;gap:2px;align-items:center;justify-content:center">' +
'<span class="arrival-remark-field" style="position:relative;display:block;flex:1;min-width:120px;max-width:220px"><textarea rows="2" class="batch-remark" data-orig="' + escHtml(b.remark || '') + '" style="width:100%;box-sizing:border-box;font-size:12px;text-align:left;border:1px solid #d9d9d9;border-radius:3px;padding:4px 27px 4px 6px;resize:vertical">' + escHtml(b.remark || '') + '</textarea><button type="button" class="arrival-remark-expand" title="展开编辑备注" aria-label="展开编辑备注" onclick="openArrivalRemarkViewer(this)" style="position:absolute;right:3px;bottom:3px;width:20px;height:20px;padding:0;border:0;border-radius:4px;background:#e6f7ff;color:#1677ff;line-height:20px;font-size:13px;cursor:pointer">⛶</button></span>' + '<span class="arrival-remark-field" style="position:relative;display:block;flex:1;min-width:120px;max-width:220px"><textarea rows="2" class="batch-remark" data-orig="' + escHtml(b.remark || '') + '" style="width:100%;box-sizing:border-box;font-size:12px;text-align:left;border:1px solid #d9d9d9;border-radius:3px;padding:4px 27px 4px 6px;resize:vertical">' + escHtml(b.remark || '') + '</textarea><button type="button" class="arrival-remark-expand" title="展开编辑备注" aria-label="展开编辑备注" onclick="openArrivalRemarkViewer(this)" style="position:absolute;right:3px;bottom:3px;width:20px;height:20px;padding:0;border:0;border-radius:4px;background:#e6f7ff;color:#1677ff;line-height:20px;font-size:13px;cursor:pointer">⛶</button></span>' +
@ -21909,7 +21909,7 @@ function showOrderDetail(encodedStr, title) {
abHtml += '<td class="batch-operated-at" style="border:1.5px solid #b0b0b0;padding:6px 8px;text-align:center;color:#ccc">-</td>'; abHtml += '<td class="batch-operated-at" style="border:1.5px solid #b0b0b0;padding:6px 8px;text-align:center;color:#ccc">-</td>';
abHtml += '<td style="border:1.5px solid #b0b0b0;padding:4px 6px;text-align:center"><input type="date" class="batch-date" style="width:66%;box-sizing:border-box;font-size:12px;color:#999;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px"></td>'; abHtml += '<td style="border:1.5px solid #b0b0b0;padding:4px 6px;text-align:center"><input type="date" class="batch-date" style="width:66%;box-sizing:border-box;font-size:12px;color:#999;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px"></td>';
abHtml += '<td style="border:1.5px solid #b0b0b0;padding:4px 6px;text-align:center"><input type="number" min="0" class="batch-qty" placeholder="数量" oninput="recalcArrivalUnfinished()" onkeydown="batchQtyKeydown(event)" style="width:66%;box-sizing:border-box;font-size:12px;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px"></td>'; abHtml += '<td style="border:1.5px solid #b0b0b0;padding:4px 6px;text-align:center"><input type="number" min="0" class="batch-qty" placeholder="数量" oninput="recalcArrivalUnfinished()" onkeydown="batchQtyKeydown(event)" style="width:66%;box-sizing:border-box;font-size:12px;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px"></td>';
if (showDefectiveQty) abHtml += '<td style="border:1.5px solid #b0b0b0;padding:4px 6px;text-align:center"><input type="number" class="batch-defective-qty" placeholder="不良品" oninput="recalcArrivalUnfinished()" onkeydown="batchQtyKeydown(event)" style="width:66%;box-sizing:border-box;font-size:12px;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px"></td>'; abHtml += '<td style="border:1.5px solid #b0b0b0;padding:4px 6px;text-align:center"><input type="number" class="batch-defective-qty" placeholder="不良品" oninput="recalcArrivalUnfinished()" onkeydown="batchQtyKeydown(event)" style="width:66%;box-sizing:border-box;font-size:12px;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px"></td>';
abHtml += '<td class="batch-unfinished" style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center;color:#ccc">-</td>'; abHtml += '<td class="batch-unfinished" style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center;color:#ccc">-</td>';
abHtml += '<td style="border:1.5px solid #b0b0b0;padding:4px 6px"><div style="display:flex;gap:2px;align-items:center;justify-content:center"><span class="arrival-remark-field" style="position:relative;display:block;flex:1;min-width:120px;max-width:220px"><textarea rows="2" class="batch-remark" placeholder="备注" style="width:100%;box-sizing:border-box;font-size:12px;text-align:left;border:1px solid #d9d9d9;border-radius:3px;padding:4px 27px 4px 6px;resize:vertical"></textarea><button type="button" class="arrival-remark-expand" title="展开编辑备注" aria-label="展开编辑备注" onclick="openArrivalRemarkViewer(this)" style="position:absolute;right:3px;bottom:3px;width:20px;height:20px;padding:0;border:0;border-radius:4px;background:#e6f7ff;color:#1677ff;line-height:20px;font-size:13px;cursor:pointer">⛶</button></span><button onclick="saveNewArrivalBatch(this,' + orderId + ',\'' + orderType + '\')" title="保存" style="padding:2px 6px;font-size:11px;border:1px solid #1890ff;background:#e6f7ff;color:#1890ff;border-radius:3px;cursor:pointer;white-space:nowrap">✅</button></div></td>'; abHtml += '<td style="border:1.5px solid #b0b0b0;padding:4px 6px"><div style="display:flex;gap:2px;align-items:center;justify-content:center"><span class="arrival-remark-field" style="position:relative;display:block;flex:1;min-width:120px;max-width:220px"><textarea rows="2" class="batch-remark" placeholder="备注" style="width:100%;box-sizing:border-box;font-size:12px;text-align:left;border:1px solid #d9d9d9;border-radius:3px;padding:4px 27px 4px 6px;resize:vertical"></textarea><button type="button" class="arrival-remark-expand" title="展开编辑备注" aria-label="展开编辑备注" onclick="openArrivalRemarkViewer(this)" style="position:absolute;right:3px;bottom:3px;width:20px;height:20px;padding:0;border:0;border-radius:4px;background:#e6f7ff;color:#1677ff;line-height:20px;font-size:13px;cursor:pointer">⛶</button></span><button onclick="saveNewArrivalBatch(this,' + orderId + ',\'' + orderType + '\')" title="保存" style="padding:2px 6px;font-size:11px;border:1px solid #1890ff;background:#e6f7ff;color:#1890ff;border-radius:3px;cursor:pointer;white-space:nowrap">✅</button></div></td>';
abHtml += '</tr>'; abHtml += '</tr>';
@ -21946,7 +21946,7 @@ function showOrderDetail(encodedStr, title) {
.catch(function(err) { .catch(function(err) {
console.error('加载详情额外数据失败:', err); console.error('加载详情额外数据失败:', err);
var abBody = overlay.querySelector('#arrivalBatchBody'); var abBody = overlay.querySelector('#arrivalBatchBody');
if (abBody) abBody.innerHTML = '<tr><td colspan="' + (orderType === 'packaging' ? 8 : 7) + '" style="border:1.5px solid #b0b0b0;padding:20px;text-align:center;color:#bbb">加载失败</td></tr>'; if (abBody) abBody.innerHTML = '<tr><td colspan="8" style="border:1.5px solid #b0b0b0;padding:20px;text-align:center;color:#bbb">加载失败</td></tr>';
var pmBody = overlay.querySelector('#packagingMaterialBody'); var pmBody = overlay.querySelector('#packagingMaterialBody');
if (pmBody) pmBody.innerHTML = '<tr><td colspan="' + (orderType === 'domestic' ? 8 : 7) + '" style="border:1.5px solid #b0b0b0;padding:20px;text-align:center;color:#bbb">加载失败</td></tr>'; if (pmBody) pmBody.innerHTML = '<tr><td colspan="' + (orderType === 'domestic' ? 8 : 7) + '" style="border:1.5px solid #b0b0b0;padding:20px;text-align:center;color:#bbb">加载失败</td></tr>';
}); });
@ -21996,7 +21996,7 @@ function appendArrivalBatchEditingRow(btn) {
'<td class="batch-operated-at" style="border:1.5px solid #b0b0b0;padding:6px 8px;text-align:center;color:#ccc">-</td>' + '<td class="batch-operated-at" style="border:1.5px solid #b0b0b0;padding:6px 8px;text-align:center;color:#ccc">-</td>' +
'<td style="border:1.5px solid #b0b0b0;padding:4px 6px;text-align:center"><input type="date" class="batch-date" style="width:66%;box-sizing:border-box;font-size:12px;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px"></td>' + '<td style="border:1.5px solid #b0b0b0;padding:4px 6px;text-align:center"><input type="date" class="batch-date" style="width:66%;box-sizing:border-box;font-size:12px;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px"></td>' +
'<td style="border:1.5px solid #b0b0b0;padding:4px 6px;text-align:center"><input type="number" min="0" class="batch-qty" oninput="recalcArrivalUnfinished()" onkeydown="batchQtyKeydown(event)" style="width:66%;box-sizing:border-box;font-size:12px;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px" placeholder="数量"></td>' + '<td style="border:1.5px solid #b0b0b0;padding:4px 6px;text-align:center"><input type="number" min="0" class="batch-qty" oninput="recalcArrivalUnfinished()" onkeydown="batchQtyKeydown(event)" style="width:66%;box-sizing:border-box;font-size:12px;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px" placeholder="数量"></td>' +
(orderType === 'packaging' ? '<td style="border:1.5px solid #b0b0b0;padding:4px 6px;text-align:center"><input type="number" class="batch-defective-qty" oninput="recalcArrivalUnfinished()" onkeydown="batchQtyKeydown(event)" style="width:66%;box-sizing:border-box;font-size:12px;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px" placeholder="不良品"></td>' : '') + '<td style="border:1.5px solid #b0b0b0;padding:4px 6px;text-align:center"><input type="number" class="batch-defective-qty" oninput="recalcArrivalUnfinished()" onkeydown="batchQtyKeydown(event)" style="width:66%;box-sizing:border-box;font-size:12px;text-align:center;border:1px solid #d9d9d9;border-radius:3px;padding:4px 8px" placeholder="不良品"></td>' +
'<td class="batch-unfinished" style="border:1.5px solid #b0b0b0;padding:4px 6px"></td>' + '<td class="batch-unfinished" style="border:1.5px solid #b0b0b0;padding:4px 6px"></td>' +
'<td style="border:1.5px solid #b0b0b0;padding:4px 6px"><div style="display:flex;gap:2px;align-items:center;justify-content:center">' + '<td style="border:1.5px solid #b0b0b0;padding:4px 6px"><div style="display:flex;gap:2px;align-items:center;justify-content:center">' +
'<span class="arrival-remark-field" style="position:relative;display:block;flex:1;min-width:120px;max-width:220px"><textarea rows="2" class="batch-remark" style="width:100%;box-sizing:border-box;font-size:12px;text-align:left;border:1px solid #d9d9d9;border-radius:3px;padding:4px 27px 4px 6px;resize:vertical" placeholder="备注"></textarea><button type="button" class="arrival-remark-expand" title="展开编辑备注" aria-label="展开编辑备注" onclick="openArrivalRemarkViewer(this)" style="position:absolute;right:3px;bottom:3px;width:20px;height:20px;padding:0;border:0;border-radius:4px;background:#e6f7ff;color:#1677ff;line-height:20px;font-size:13px;cursor:pointer">⛶</button></span>' + '<span class="arrival-remark-field" style="position:relative;display:block;flex:1;min-width:120px;max-width:220px"><textarea rows="2" class="batch-remark" style="width:100%;box-sizing:border-box;font-size:12px;text-align:left;border:1px solid #d9d9d9;border-radius:3px;padding:4px 27px 4px 6px;resize:vertical" placeholder="备注"></textarea><button type="button" class="arrival-remark-expand" title="展开编辑备注" aria-label="展开编辑备注" onclick="openArrivalRemarkViewer(this)" style="position:absolute;right:3px;bottom:3px;width:20px;height:20px;padding:0;border:0;border-radius:4px;background:#e6f7ff;color:#1677ff;line-height:20px;font-size:13px;cursor:pointer">⛶</button></span>' +
@ -22102,7 +22102,6 @@ function saveNewArrivalBatch(btn, orderId, orderType) {
if (!date && qtyRaw === '') { alert('请输入到货日期或数量'); return; } if (!date && qtyRaw === '') { alert('请输入到货日期或数量'); return; }
if (qtyRaw !== '' && (!Number.isInteger(qty) || qty < 0)) { alert('到货数量必须为非负整数'); return; } if (qtyRaw !== '' && (!Number.isInteger(qty) || qty < 0)) { alert('到货数量必须为非负整数'); return; }
if (defectiveRaw !== '' && !Number.isInteger(defectiveQty)) { alert('不良品数量必须为整数'); return; } if (defectiveRaw !== '' && !Number.isInteger(defectiveQty)) { alert('不良品数量必须为整数'); return; }
if (orderType !== 'packaging' && defectiveRaw !== '' && (defectiveQty < 0 || defectiveQty > (qty || 0))) { alert('不良品数量不能为负数或大于到货数量'); return; }
btn.disabled = true; btn.disabled = true;
btn.textContent = '⋯'; btn.textContent = '⋯';
fetch('/api/domestic/orders/add_batch', { fetch('/api/domestic/orders/add_batch', {
@ -22123,7 +22122,7 @@ function saveNewArrivalBatch(btn, orderId, orderType) {
'<td class="batch-operated-at" style="border:1.5px solid #b0b0b0;padding:6px 8px;text-align:center;white-space:nowrap">' + escHtml(res.data && res.data.operated_at || '-') + '</td>' + '<td class="batch-operated-at" style="border:1.5px solid #b0b0b0;padding:6px 8px;text-align:center;white-space:nowrap">' + escHtml(res.data && res.data.operated_at || '-') + '</td>' +
renderArrivalBatchStoredCell('date', 'batch-date', date, !!(res.data && res.data.arrival_date_filled), orderType === 'packaging', '到货日期') + renderArrivalBatchStoredCell('date', 'batch-date', date, !!(res.data && res.data.arrival_date_filled), orderType === 'packaging', '到货日期') +
renderArrivalBatchStoredCell('number', 'batch-qty', qty, !!(res.data && res.data.arrival_qty_filled), orderType === 'packaging', '到货数量', true) + renderArrivalBatchStoredCell('number', 'batch-qty', qty, !!(res.data && res.data.arrival_qty_filled), orderType === 'packaging', '到货数量', true) +
(orderType === 'packaging' ? renderArrivalBatchStoredCell('number', 'batch-defective-qty', defectiveQty, !!(res.data && res.data.defective_qty_filled), true, '不良品数量', false) : '') + renderArrivalBatchStoredCell('number', 'batch-defective-qty', defectiveQty, !!(res.data && res.data.defective_qty_filled), true, '不良品数量', false) +
'<td class="batch-unfinished" style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center">0</td>' + '<td class="batch-unfinished" style="border:1.5px solid #b0b0b0;padding:6px 10px;text-align:center">0</td>' +
'<td style="border:1.5px solid #b0b0b0;padding:4px 6px"><div style="display:flex;gap:2px;align-items:center;justify-content:center"><span class="arrival-remark-field" style="position:relative;display:block;flex:1;min-width:120px;max-width:220px"><textarea rows="2" class="batch-remark" data-orig="' + escHtml(remark) + '" style="width:100%;box-sizing:border-box;font-size:12px;text-align:left;border:1px solid #d9d9d9;border-radius:3px;padding:4px 27px 4px 6px;resize:vertical">' + escHtml(remark) + '</textarea><button type="button" class="arrival-remark-expand" title="展开编辑备注" aria-label="展开编辑备注" onclick="openArrivalRemarkViewer(this)" style="position:absolute;right:3px;bottom:3px;width:20px;height:20px;padding:0;border:0;border-radius:4px;background:#e6f7ff;color:#1677ff;line-height:20px;font-size:13px;cursor:pointer">⛶</button></span><button onclick="saveArrivalBatch(this,' + batchId + ',' + orderId + ',\'' + orderType + '\')" title="保存" style="padding:2px 6px;font-size:11px;border:1px solid #1890ff;background:#e6f7ff;color:#1890ff;border-radius:3px;cursor:pointer;white-space:nowrap">✅</button><button onclick="deleteArrivalBatch(this,' + batchId + ',\'' + orderType + '\')" title="删除" style="padding:2px 6px;font-size:11px;border:1px solid #ff4d4f;background:#fff1f0;color:#ff4d4f;border-radius:3px;cursor:pointer;margin-left:2px;white-space:nowrap">✕</button></div></td>'; '<td style="border:1.5px solid #b0b0b0;padding:4px 6px"><div style="display:flex;gap:2px;align-items:center;justify-content:center"><span class="arrival-remark-field" style="position:relative;display:block;flex:1;min-width:120px;max-width:220px"><textarea rows="2" class="batch-remark" data-orig="' + escHtml(remark) + '" style="width:100%;box-sizing:border-box;font-size:12px;text-align:left;border:1px solid #d9d9d9;border-radius:3px;padding:4px 27px 4px 6px;resize:vertical">' + escHtml(remark) + '</textarea><button type="button" class="arrival-remark-expand" title="展开编辑备注" aria-label="展开编辑备注" onclick="openArrivalRemarkViewer(this)" style="position:absolute;right:3px;bottom:3px;width:20px;height:20px;padding:0;border:0;border-radius:4px;background:#e6f7ff;color:#1677ff;line-height:20px;font-size:13px;cursor:pointer">⛶</button></span><button onclick="saveArrivalBatch(this,' + batchId + ',' + orderId + ',\'' + orderType + '\')" title="保存" style="padding:2px 6px;font-size:11px;border:1px solid #1890ff;background:#e6f7ff;color:#1890ff;border-radius:3px;cursor:pointer;white-space:nowrap">✅</button><button onclick="deleteArrivalBatch(this,' + batchId + ',\'' + orderType + '\')" title="删除" style="padding:2px 6px;font-size:11px;border:1px solid #ff4d4f;background:#fff1f0;color:#ff4d4f;border-radius:3px;cursor:pointer;margin-left:2px;white-space:nowrap">✕</button></div></td>';
recalcArrivalUnfinished(); recalcArrivalUnfinished();
@ -22153,7 +22152,6 @@ function saveArrivalBatch(btn, batchId, orderId, orderType) {
var remark = tr.querySelector('.batch-remark')?.value || ''; var remark = tr.querySelector('.batch-remark')?.value || '';
if (qtyRaw !== '' && (!Number.isInteger(qty) || qty < 0)) { alert('到货数量必须为非负整数'); return; } if (qtyRaw !== '' && (!Number.isInteger(qty) || qty < 0)) { alert('到货数量必须为非负整数'); return; }
if (defectiveRaw !== '' && !Number.isInteger(defectiveQty)) { alert('不良品数量必须为整数'); return; } if (defectiveRaw !== '' && !Number.isInteger(defectiveQty)) { alert('不良品数量必须为整数'); return; }
if (orderType !== 'packaging' && defectiveRaw !== '' && (defectiveQty < 0 || defectiveQty > (qty || 0))) { alert('不良品数量不能为负数或大于到货数量'); return; }
btn.disabled = true; btn.disabled = true;
btn.textContent = '⋯'; btn.textContent = '⋯';
fetch('/api/domestic/orders/update_batch', { fetch('/api/domestic/orders/update_batch', {
@ -22182,11 +22180,11 @@ function saveArrivalBatch(btn, batchId, orderId, orderType) {
if (orderType === 'packaging') { if (orderType === 'packaging') {
var dateCell = dd && dd.closest('td'); var dateCell = dd && dd.closest('td');
var qtyCell = dq && dq.closest('td'); var qtyCell = dq && dq.closest('td');
var defectiveCell = df && df.closest('td');
if (dateCell && res.data && res.data.arrival_date_filled) dateCell.outerHTML = renderArrivalBatchStoredCell('date', 'batch-date', date, true, false, '到货日期'); if (dateCell && res.data && res.data.arrival_date_filled) dateCell.outerHTML = renderArrivalBatchStoredCell('date', 'batch-date', date, true, false, '到货日期');
if (qtyCell && res.data && res.data.arrival_qty_filled) qtyCell.outerHTML = renderArrivalBatchStoredCell('number', 'batch-qty', qty, true, false, '到货数量', true); if (qtyCell && res.data && res.data.arrival_qty_filled) qtyCell.outerHTML = renderArrivalBatchStoredCell('number', 'batch-qty', qty, true, false, '到货数量', true);
if (defectiveCell && res.data && res.data.defective_qty_filled) defectiveCell.outerHTML = renderArrivalBatchStoredCell('number', 'batch-defective-qty', defectiveQty, true, false, '不良品数量', false);
} }
var defectiveCell = df && df.closest('td');
if (defectiveCell && res.data && res.data.defective_qty_filled) defectiveCell.outerHTML = renderArrivalBatchStoredCell('number', 'batch-defective-qty', defectiveQty, true, false, '不良品数量', false);
recalcArrivalUnfinished(); recalcArrivalUnfinished();
refreshOrderListFromArrivalModal(btn); refreshOrderListFromArrivalModal(btn);
setTimeout(function() { btn.textContent = '✅'; }, 500); setTimeout(function() { btn.textContent = '✅'; }, 500);
@ -22370,11 +22368,10 @@ function saveDetailChanges(btn) {
var invalidBatch = batchAdds.concat(batchUpdates).some(function(b) { var invalidBatch = batchAdds.concat(batchUpdates).some(function(b) {
if (b.arrival_qty !== '' && (!Number.isInteger(b.arrival_qty) || b.arrival_qty < 0)) return true; if (b.arrival_qty !== '' && (!Number.isInteger(b.arrival_qty) || b.arrival_qty < 0)) return true;
if (b.defective_qty !== '' && !Number.isInteger(b.defective_qty)) return true; if (b.defective_qty !== '' && !Number.isInteger(b.defective_qty)) return true;
return orderType !== 'packaging' && b.defective_qty !== '' && return false;
(b.defective_qty < 0 || b.defective_qty > (b.arrival_qty || 0));
}); });
if (invalidBatch) { if (invalidBatch) {
showCenteredAlert('提示', orderType === 'packaging' ? '到货数量必须为非负整数,不良品数量必须为整数' : '数量格式错误,且不良品数量不能为负数或大于到货数量'); showCenteredAlert('提示', '到货数量必须为非负整数,不良品数量必须为整数');
return; return;
} }

View File

@ -18,21 +18,26 @@ class PackagingArrivalDefectiveTest(unittest.TestCase):
self.assertEqual((10, 0, True, False), _arrival_batch_quantities( self.assertEqual((10, 0, True, False), _arrival_batch_quantities(
{'arrival_qty': '10', 'defective_qty': ''}, 'packaging')) {'arrival_qty': '10', 'defective_qty': ''}, 'packaging'))
def test_other_order_types_keep_existing_validation(self): def test_domestic_and_overseas_use_same_defective_rules(self):
from routes.purchase import _arrival_batch_quantities from routes.purchase import _arrival_batch_quantities
with self.assertRaisesRegex(ValueError, '不能为负数'): self.assertEqual((10, -1, True, True), _arrival_batch_quantities(
_arrival_batch_quantities({'arrival_qty': '10', 'defective_qty': '-1'}, 'domestic') {'arrival_qty': '10', 'defective_qty': '-1'}, 'domestic'))
with self.assertRaisesRegex(ValueError, '不能大于到货数量'): self.assertEqual((10, 11, True, True), _arrival_batch_quantities(
_arrival_batch_quantities({'arrival_qty': '10', 'defective_qty': '11'}, 'overseas') {'arrival_qty': '10', 'defective_qty': '11'}, 'overseas'))
def test_frontend_keeps_blank_and_only_validates_non_packaging_range(self): def test_frontend_keeps_blank_and_only_validates_non_packaging_range(self):
source = Path(__file__).resolve().parents[1].joinpath('static/js/app.js').read_text(encoding='utf-8') source = Path(__file__).resolve().parents[1].joinpath('static/js/app.js').read_text(encoding='utf-8')
self.assertIn("defectiveRaw === '' ? ''", source) self.assertIn("defectiveRaw === '' ? ''", source)
self.assertIn("orderType !== 'packaging' && defectiveRaw !== ''", source) self.assertNotIn("orderType !== 'packaging' && defectiveRaw !== ''", source)
self.assertIn("renderArrivalBatchStoredCell('number', 'batch-defective-qty'", source) self.assertIn("renderArrivalBatchStoredCell('number', 'batch-defective-qty'", source)
self.assertNotIn('min="0" class="batch-defective-qty"', source) self.assertNotIn('min="0" class="batch-defective-qty"', source)
def test_defective_column_is_visible_for_all_three_order_types(self):
source = Path(__file__).resolve().parents[1].joinpath('static/js/app.js').read_text(encoding='utf-8')
self.assertIn('var showDefectiveQty = true;', source)
self.assertIn("renderArrivalBatchStoredCell('number', 'batch-defective-qty'", source)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()