diff --git a/.superpowers/prototype/screens/stock-in.html b/.superpowers/prototype/screens/stock-in.html
index 42051f6..f6905c7 100644
--- a/.superpowers/prototype/screens/stock-in.html
+++ b/.superpowers/prototype/screens/stock-in.html
@@ -170,6 +170,7 @@
diff --git a/.superpowers/prototype/screens/stock-in.js b/.superpowers/prototype/screens/stock-in.js
index 726573e..f523443 100644
--- a/.superpowers/prototype/screens/stock-in.js
+++ b/.superpowers/prototype/screens/stock-in.js
@@ -54,7 +54,7 @@ const MODES = {
in:{ title:'入库单', partnerLabel:'供应商', partnerDict:'suppliers', handler:'入库员',
cols:['idx','name','series','spec','prodDate','batch','qty','price','sale','amount','act'] },
out:{ title:'出库单', partnerLabel:'客户', partnerDict:'customers', handler:'出库员',
- cols:['idx','name','series','spec','avail','qty','price','sale','amount','act'] },
+ cols:['idx','name','series','spec','avail','qty','price','sale','profit','act'] },
};
const COLDEF = {
idx:{type:'idx',w:38},
@@ -65,9 +65,10 @@ const COLDEF = {
batch:{type:'text',label:'批次号',req:true,w:108,ph:'必填'},
avail:{type:'avail',label:'可用库存',w:92,num:true},
qty:{type:'num',label:'数量',req:true,w:78,num:true},
- price:{type:'num',label:'单价',req:true,w:96,num:true,money:true},
- sale:{type:'num',label:'出售价格',req:true,w:104,num:true,money:true},
- amount:{type:'amount',label:'金额',w:104,num:true},
+ price:{type:'num',label:'进价',req:true,w:96,num:true,money:true},
+ sale:{type:'num',label:'售价',req:true,w:104,num:true,money:true},
+ amount:{type:'amount',label:'总进价',w:104,num:true},
+ profit:{type:'profit',label:'利润',w:104,num:true},
act:{type:'act',w:62},
};
// 录单可聚焦字段顺序(出库的 series/spec/avail 只读,不进流)
@@ -83,8 +84,12 @@ function newRow(){ return {id:'r'+(++SEQ), name:null,series:null,spec:null,prodD
/* ---------------- 工具 ---------------- */
function idName(dictKey,id){ if(id==null) return ''; const o=(DICT[dictKey]||[]).find(x=>x.id===id); return o?o.name:''; }
function fmt(n){ return '¥'+(Number(n)||0).toLocaleString('zh-CN',{minimumFractionDigits:2,maximumFractionDigits:2}); }
-function rowAmount(r){ return (parseFloat(r.qty)||0)*(parseFloat(r.price)||0); }
+function rowAmount(r){ const unit=mode==='out'?(parseFloat(r.sale)||0):(parseFloat(r.price)||0); return (parseFloat(r.qty)||0)*unit; }
function grandTotal(){ return state.rows.reduce((s,r)=>s+rowAmount(r),0); }
+// 出库行利润 =(售价 − 进价)× 数量;售价未填不计
+function rowProfit(r){ const sale=parseFloat(r.sale)||0; if(sale<=0) return 0; return (sale-(parseFloat(r.price)||0))*(parseFloat(r.qty)||0); }
+function grandProfit(){ return state.rows.reduce((s,r)=>s+rowProfit(r),0); }
+function profitHTML(r){ const sale=parseFloat(r.sale)||0; if(sale<=0) return '—'; const v=rowProfit(r); return `${fmt(v)}`; }
// 入库进价留空/0 = 待定价(暂估),金额显示「待定价」橙色;确认进价后回填
function isPending(r){ return mode==='in' && (!r.price || (parseFloat(r.price)||0)<=0); }
function amtHTML(r){ return isPending(r) ? '待定价' : fmt(rowAmount(r)); }
@@ -198,7 +203,7 @@ function pickCombo(combo,item){
r[field]=id; r._err.delete(field);
if(field==='name'){ const nm=NAMES.find(n=>n.id===id);
if(mode==='in'){ if(!r.series)r.series=nm.dSeries; if(!r.spec)r.spec=nm.dSpec; if(!r.price)r.price=String(nm.cost); if(!r.sale)r.sale=String(nm.sale); }
- else { r.series=nm.dSeries; r.spec=nm.dSpec; r.price=String(nm.sale); r.sale=String(nm.sale); r.avail=nm.stock; }
+ else { r.series=nm.dSeries; r.spec=nm.dSpec; r.price=String(nm.cost); r.sale=String(nm.sale); r.avail=nm.stock; }
}
closePop();
renderDetail(); updateTotals(); draftSave();
@@ -268,20 +273,21 @@ function renderDetailHead(){ const el=document.getElementById('dhAct'); if(!el)
function toggleExpand(i){ state.rows[i].expanded=!state.rows[i].expanded; renderDetail(); }
function gridHTML(){
const cols=MODES[mode].cols; const ro=readonlyMode();
- let head=''+cols.map(k=>{ const d=COLDEF[k]; if(k==='idx')return '| # | '; if(k==='act')return ' | '; const req=d.req&&!(mode==='in'&&k==='price'); const cls=(d.num?'num ':'')+(req?'req':''); const style=d.w?`style="width:${d.w}px"`:''; const lbl=(mode==='in'&&k==='price')?'单价(可待定)':(d.label||''); return `${lbl} | `; }).join('')+'
';
+ let head=''+cols.map(k=>{ const d=COLDEF[k]; if(k==='idx')return '| # | '; if(k==='act')return ' | '; const req=d.req&&!(mode==='in'&&k==='price'); const cls=(d.num?'num ':'')+(req?'req':''); const style=d.w?`style="width:${d.w}px"`:''; const IN_LBL={price:'进价(单瓶)',sale:'参考售价'}; const lbl=(mode==='in'&&k==='price')?'进价(单瓶)(可待定)':((mode==='in'&&IN_LBL[k])||d.label||''); return `${lbl} | `; }).join('')+'
';
let body=state.rows.map((r,i)=>{
const rerr=r._err.size?'error':'';
const tds=cols.map(k=>{ const d=COLDEF[k];
if(k==='idx') return `${i+1} | `;
- if(k==='name') return `${comboCellHTML(i,'name',true)} | `;
+ if(k==='name'){ const nm=(DICT.names||[]).find(x=>x.id===r.name); const code=(mode==='out'&&nm&&nm.code)?`${esc(nm.code)}
`:''; return `${comboCellHTML(i,'name',true)}${code} | `; }
if(k==='series'||k==='spec'){ if(mode==='out') return `${esc(idName(d.dict,r[k]))||'—'} | `; return `${comboCellHTML(i,k,true)} | `; }
if(k==='avail'){ const v=r.avail; const c=v==null?'color:var(--faint)':(v<=0?'color:var(--danger)':''); return `${v==null?'—':v} | `; }
if(k==='prodDate'){ const ph=r.prodDate?'':' ph'; return `${r.prodDate?esc(r.prodDate):'选择日期'} | `; }
if(k==='batch') return ` | `;
if(k==='qty') return ` | `;
- if(k==='price') return ` | `;
+ if(k==='price'){ if(mode==='out') return `${r.price?fmt(parseFloat(r.price)||0):'—'} | `; return ` | `; }
if(k==='sale') return ` | `;
if(k==='amount') return `${amtHTML(r)} | `;
+ if(k==='profit') return `${profitHTML(r)} | `;
if(k==='act'){ const exp = mode==='in'?``:''; return ` | `; }
return ' | ';
}).join('');
@@ -308,9 +314,9 @@ function cardsHTML(){
rows+=mrow('可用库存',false,`${r.avail==null?'—':r.avail}`);
}
rows+=mrow('数量',true,``);
- rows+=mrow(mode==='in'?'单价(可待定)':'单价',mode!=='in',``);
- rows+=mrow('出售价格',true,``);
- rows+=``;
+ rows+=mrow(mode==='in'?'进价·单瓶(可待定)':'进价',false,``);
+ rows+=mrow(mode==='in'?'参考售价':'售价',true,``);
+ rows+= mode==='out' ? `` : ``;
const act=ro?'':``;
return `商品 ${i+1}${sub?`${esc(sub)}`:''}${act}
${rows}
`;
}).join('');
@@ -320,8 +326,9 @@ function cardsHTML(){
function mrow(label,req,inner){ return ``; }
function updateTotals(){
- state.rows.forEach((r,i)=>{ const el=document.querySelector(`[data-amt="${i}"]`); if(el) el.innerHTML=amtHTML(r); });
+ state.rows.forEach((r,i)=>{ const el=document.querySelector(`[data-amt="${i}"]`); if(el) el.innerHTML=amtHTML(r); const pl=document.querySelector(`[data-profit="${i}"]`); if(pl) pl.innerHTML=profitHTML(r); });
document.getElementById('grandTotal').textContent=fmt(grandTotal());
+ const pw=document.getElementById('profitWrap'); if(pw){ pw.style.display=mode==='out'?'':'none'; const gp=document.getElementById('grandProfit'); if(gp){ const v=grandProfit(); gp.textContent=fmt(v); gp.style.color=v<0?'var(--danger)':'var(--success)'; } }
document.getElementById('rowsN').textContent='明细 '+state.rows.length+' 行';
}
@@ -356,7 +363,6 @@ function validate(asDraft){
} else {
if(r.avail!=null && (parseFloat(r.qty)||0)>r.avail){ r._err.add('qty'); flag(`第 ${i+1} 行数量超出可用库存(${r.avail})`,()=>focusCell(i,'qty')); }
}
- if(mode!=='in' && (!r.price || (parseFloat(r.price)||0)<=0)){ r._err.add('price'); flag(`第 ${i+1} 行请填写单价`,()=>focusCell(i,'price')); }
if(!r.sale || (parseFloat(r.sale)||0)<=0){ r._err.add('sale'); flag(`第 ${i+1} 行请填写出售价格`,()=>focusCell(i,'sale')); }
});
}
diff --git a/.superpowers/prototype/screens/stock-out-list.html b/.superpowers/prototype/screens/stock-out-list.html
index a2bbcf1..67df7d7 100644
--- a/.superpowers/prototype/screens/stock-out-list.html
+++ b/.superpowers/prototype/screens/stock-out-list.html
@@ -295,11 +295,12 @@ function auditBtns(o,idx){
return '';
}
function openDetail(idx){ const o=ORDERS[idx]; document.getElementById('dTitle').textContent=o.no;
- let lh=`商品 · 编码系列 / 规格数量单价金额
`;
- o.lines.forEach(l=>{ const pi=PINFO[l.name]||{}; const pend=(l.price||0)<=0; const priceC=pend?'
待定价':money(l.price); const amtC=pend?'
待定价':money(l.qty*l.price); lh+=`
${l.name}${pi.code||'—'}${pi.series||'—'}${pi.spec||''}${l.qty}${priceC}${amtC}
`; }); lh+='
';
+ const GRID='1fr 108px 36px 60px 60px 64px';
+ let lh=`商品 · 编码系列 / 规格数量成本价售价利润
`;
+ o.lines.forEach(l=>{ const pi=PINFO[l.name]||{}; const cost=l.cost!=null?l.cost:Math.round((l.price||0)*0.85); const pend=(l.price||0)<=0; const saleC=pend?'
待定价':money(l.price); const profit=(l.price-cost)*l.qty; const profitC=pend?'
待定价':`
${money(profit)}`; lh+=`
${l.name}${pi.code||'—'}${pi.series||'—'}${pi.spec||''}${l.qty}${money(cost)}${saleC}${profitC}
`; }); lh+='
';
const pendCount=o.lines.filter(l=>(l.price||0)<=0).length;
const auditor=o.auditor?o.auditor:'—';
- document.getElementById('drawerBody').innerHTML=`单据日期${o.date}
${PARTY_LABEL}${o.party}
仓库${o.wh}
商品数${o.count}
出库员${o.op}
审核员${auditor}
状态${o.status}${o.ret?` ${o.ret}`:''}
${lh}合计金额 ${money(o.amount)}
`+
+ document.getElementById('drawerBody').innerHTML=`单据日期${o.date}
${PARTY_LABEL}${o.party}
仓库${o.wh}
商品数${o.count}
出库员${o.op}
审核员${auditor}
状态${o.status}${o.ret?` ${o.ret}`:''}
${lh}合计金额 ${money(o.amount)}
合计利润 ${money(o.lines.reduce((s,l)=>{const c=l.cost!=null?l.cost:Math.round((l.price||0)*0.85);return s+((l.price||0)>0?((l.price-c)*l.qty):0);},0))}
`+
`${(o.status==='已审核'&&pendCount>0)?`
`:''}
`;
document.getElementById('drawer').classList.add('show'); document.getElementById('drawerMask').classList.add('show'); }
function closeDrawer(){ document.getElementById('drawer').classList.remove('show'); document.getElementById('drawerMask').classList.remove('show'); }
diff --git a/CLAUDE.md b/CLAUDE.md
index 8bb66e8..6d1e731 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -212,6 +212,12 @@ cd client && flutter test
- 基础数据字典(品牌/型号/版本 + 产地/保质期/储存/介绍)在「基础数据」页管理,入库选文本后据此建 product。
- 库存(`inventories`)/出入库明细 = 指向 product 的引用 + **快照列**(`product_code/name/series/spec/...`,导入/审核时拷贝,作历史保真+显示兜底)。显示优先 product、回退快照(`COALESCE(p.*, 快照)` / 前端 `lineOrProduct`)。**快照列保留,勿擅自删**。
+### 定价字段口径(2026-07 消歧后,唯一口径)
+- 明细两侧价格分列:`cost_price`(成本/进价单价)+ `cost_amount`(成本小计);出库另有 `sale_price`(售价)+ `sale_amount`(售价小计,待定价=0)。**旧列 unit_price/total_price/total_amount 已弃用**(值已回填新列,观察一版后 DROP),新代码禁止读写旧列。
+- 单据合计:入库 `cost_total`(应付=Σ总进价);出库 `sale_total`(应收=Σ售价小计)+ `profit_total`(总利润=Σ(售价>0?(售价-成本)×数量:0),建单落库,确认售价/确认进价联动重算,重算函数 `recalcStockOutProfit`)。
+- **成本与利润仅管理员可见**:出库 List/Get 对 operator/readonly 服务端抹零(`stripStockOutCost`),前端同时隐藏对应列——新增出库相关展示时两侧都要遵守。
+- 财务口径不变:应收/应付、退单冲账均按上述合计列;出库退单展示与冲账同为售价口径。
+
### 库存变更
- 入库/出库审核通过时,必须在同一事务中同时完成:
1. 更新 `inventories` 表数量
diff --git a/design/CONTRACT.md b/design/CONTRACT.md
index b12d0f5..d7423ae 100644
--- a/design/CONTRACT.md
+++ b/design/CONTRACT.md
@@ -150,6 +150,13 @@ AppTokens 目前**仅颜色**。原型还驱动:
> - **注册已知差异(用户拍板 2026-07-03:先不加)**:无「门店编号(留空自动分配)」「授权兑换券」两字段——后端 RegisterInput 暂不支持;门店地址必填(后端 binding:required,原型选填);密码下限 6 位(后端 min=6,原型文案 8 位)。表单因此少两行 → **fidelity 暂撤册**(screens.mjs 有注释存根),golden ×3 锁回归;后端补 shop_code/voucher 后恢复入闸。
> - 校验口径 = 原型:提交时逐项 toast(DsToast),无内联错误文案,38 高盒式字段不抖动。
>
+> **出入库定价重设计(2026-07-03 用户拍板,plan 批准)**:
+> - 出库详情明细:单价/金额 两列 → **成本价/售价/利润** 三列(管理员);operator 见 售价/小计。合计区双行:合计金额(总售价)+ 合计利润(管理员)。
+> - 出库建单:金额列(=售价×数量,qty=1 与售价重复)→ **利润**列(实时,负数 danger);商品名称下带编码(字体同系列列);底栏加合计利润;进价/利润列仅管理员。
+> - 入库建单列头:进价 → **进价(单瓶)**、售价 → **参考售价**、金额 → **总进价**。
+> - 原型已同步(stock-in.js 双模式 + stock-out-list.html 详情抽屉),与真实页一致。
+> - 底层字段消歧:unit_price/total_price/total_amount → cost_price/cost_amount/cost_total(入)/sale_amount/sale_total/profit_total(出),见 CLAUDE.md「定价字段口径」。
+>
> **全局对话框统一(2026-07-03 用户要求遍历)**:
> - 主题层钉真相源:`DialogTheme`(surface/r-xl/边框/标题 fs-h2·700)、全局 `InputDecorationTheme`(.input 盒式,禁下划线兜底)、Elevated/FilledButton 主题(.btn.primary 规格)。
> - 全部 AlertDialog 动作行换 `DsButton`(取消=ghost / 确认=primary / 删除·拒绝=danger / 通过=success / 撤回·结清=accent,`DsBtnVariant` 扩 success/accent);表单字段改 `DsField`(label 在上)+ 盒式输入(添加设备等)。
diff --git a/docs/db-schema.html b/docs/db-schema.html
index 55cdfdd..21cb6ee 100644
--- a/docs/db-schema.html
+++ b/docs/db-schema.html
@@ -432,7 +432,7 @@
| reviewer_id | BIGINT UNSIGNED | 是 | NULL | 审核人 |
| status | ENUM('draft','pending','approved','rejected') | 否 | draft | |
| order_date | DATE | 否 | — | |
-| total_amount | DECIMAL(16,2) | 否 | 0 | |
+| cost_total | DECIMAL(16,2) | 否 | 0 | 应付合计=Σ总进价(2026-07 消歧,旧列 total_amount 弃用待删) |
| reviewed_at | DATETIME | 是 | NULL | |
| custom_fields | JSON | 是 | NULL | |
| remark | VARCHAR(500) | 是 | NULL | |
@@ -456,8 +456,8 @@
| series | VARCHAR(100) | 是 | NULL | 系列(历史导入快照) |
| spec | VARCHAR(100) | 是 | NULL | 规格(历史导入快照) |
| quantity | DECIMAL(12,3) | 否 | — | 数量 |
-| unit_price | DECIMAL(16,2) | 否 | 0 | 单价 |
-| total_price | DECIMAL(16,2) | 否 | 0 | |
+| cost_price | DECIMAL(16,2) | 否 | 0 | 进价·单瓶(旧列 unit_price 弃用待删) |
+| cost_amount | DECIMAL(16,2) | 否 | 0 | 总进价=quantity×cost_price(旧列 total_price 弃用待删) |
| batch_no | VARCHAR(50) | 是 | NULL | 批次号 |
| production_date | DATE | 是 | NULL | 生产日期 |
| expire_date | DATE | 是 | NULL | 有效期 |
@@ -482,7 +482,8 @@
| reviewer_id | BIGINT UNSIGNED | 是 | NULL | |
| status | ENUM('draft','pending','approved','rejected') | 否 | draft | |
| order_date | DATE | 否 | — | |
-| total_amount | DECIMAL(16,2) | 否 | 0 | |
+| sale_total | DECIMAL(16,2) | 否 | 0 | 应收合计=Σ售价小计(旧列 total_amount 弃用待删) |
+| profit_total | DECIMAL(16,2) | 否 | 0 | 总利润=Σ(售价>0?(售价-成本)×数量:0),建单落库、确认售价/进价联动重算;仅管理员可见(operator 响应抹零) |
| reviewed_at | DATETIME | 是 | NULL | |
| custom_fields | JSON | 是 | NULL | |
| remark | VARCHAR(500) | 是 | NULL | |
@@ -494,7 +495,7 @@
stock_out_items
-
当前问题相关:出库行快照 batch_no/production_date 在 App 建单时常为空(选货按 product 聚合、未带批次)。打印回退到 products 主记录——若该 product 也没填,就显示空白,但库存页仍能显示(取自入库明细)。unit_price=成本、sale_price=售价。
+
当前问题相关:出库行快照 batch_no/production_date 在 App 建单时常为空(选货按 product 聚合、未带批次)。打印回退到 products 主记录——若该 product 也没填,就显示空白,但库存页仍能显示(取自入库明细)。cost_price=成本、sale_price=售价(2026-07 已消歧改名)。
| 列名 | 类型 | 可空 | 默认 | 说明 |
| id PK | BIGINT UNSIGNED | 否 | AUTO_INC | |
@@ -506,9 +507,10 @@
| series | VARCHAR(100) | 是 | NULL | 系列(历史导入快照) |
| spec | VARCHAR(100) | 是 | NULL | 规格(历史导入快照) |
| quantity | DECIMAL(12,3) | 否 | — | |
-| unit_price | DECIMAL(16,2) | 否 | 0 | 成本单价(入库成本快照) |
+| cost_price | DECIMAL(16,2) | 否 | 0 | 成本单价·入库成本快照(旧列 unit_price 弃用待删;仅管理员可见) |
| sale_price | DECIMAL(16,2) | 否 | 0 | 售价(出库实际销售单价,应收按售价×数量) |
-| total_price | DECIMAL(16,2) | 否 | 0 | 成本小计(unit_price×quantity) |
+| cost_amount | DECIMAL(16,2) | 否 | 0 | 成本小计=quantity×cost_price(旧列弃用待删;仅管理员可见) |
+| sale_amount | DECIMAL(16,2) | 否 | 0 | 售价小计=quantity×sale_price(待定价=0) |
| batch_no | VARCHAR(50) | 是 | NULL | 批次号 |
| production_date | DATE | 是 | NULL | 生产日期 |
| custom_fields | JSON | 是 | NULL | |