docs: 定价重设计原型同步(stock-in.js 双模式/详情三列)+ CLAUDE.md 定价口径节 + db-schema/CONTRACT 更新
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
@@ -170,6 +170,7 @@
|
||||
|
||||
<div class="form-foot">
|
||||
<span class="rows-n" id="rowsN">明细 0 行</span>
|
||||
<div class="total" id="profitWrap" style="display:none">合计利润 <b id="grandProfit">¥0.00</b></div>
|
||||
<div class="total">合计金额 <b id="grandTotal">¥0.00</b></div>
|
||||
<div class="foot-act" id="footActions"></div>
|
||||
</div>
|
||||
|
||||
+20
-14
@@ -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 '<span style="color:var(--faint)">—</span>'; const v=rowProfit(r); return `<span style="color:${v<0?'var(--danger)':'var(--success)'}">${fmt(v)}</span>`; }
|
||||
// 入库进价留空/0 = 待定价(暂估),金额显示「待定价」橙色;确认进价后回填
|
||||
function isPending(r){ return mode==='in' && (!r.price || (parseFloat(r.price)||0)<=0); }
|
||||
function amtHTML(r){ return isPending(r) ? '<span class="pending-price">待定价</span>' : 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='<tr>'+cols.map(k=>{ const d=COLDEF[k]; if(k==='idx')return '<th class="gidx">#</th>'; if(k==='act')return '<th class="gact"></th>'; 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')?'单价<small style="color:var(--faint);font-weight:400">(可待定)</small>':(d.label||''); return `<th class="${cls}" ${style}>${lbl}</th>`; }).join('')+'</tr>';
|
||||
let head='<tr>'+cols.map(k=>{ const d=COLDEF[k]; if(k==='idx')return '<th class="gidx">#</th>'; if(k==='act')return '<th class="gact"></th>'; 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')?'进价(单瓶)<small style="color:var(--faint);font-weight:400">(可待定)</small>':((mode==='in'&&IN_LBL[k])||d.label||''); return `<th class="${cls}" ${style}>${lbl}</th>`; }).join('')+'</tr>';
|
||||
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 `<td class="gidx">${i+1}</td>`;
|
||||
if(k==='name') return `<td class="cpad">${comboCellHTML(i,'name',true)}</td>`;
|
||||
if(k==='name'){ const nm=(DICT.names||[]).find(x=>x.id===r.name); const code=(mode==='out'&&nm&&nm.code)?`<div style="color:var(--muted);font-size:var(--fs-body)">${esc(nm.code)}</div>`:''; return `<td class="cpad">${comboCellHTML(i,'name',true)}${code}</td>`; }
|
||||
if(k==='series'||k==='spec'){ if(mode==='out') return `<td class="ro">${esc(idName(d.dict,r[k]))||'<span style="color:var(--faint)">—</span>'}</td>`; return `<td class="cpad">${comboCellHTML(i,k,true)}</td>`; }
|
||||
if(k==='avail'){ const v=r.avail; const c=v==null?'color:var(--faint)':(v<=0?'color:var(--danger)':''); return `<td class="ro num" style="${c}">${v==null?'—':v}</td>`; }
|
||||
if(k==='prodDate'){ const ph=r.prodDate?'':' ph'; return `<td class="cpad"><div class="datefield gci-date ${r._err.has('prodDate')?'error':''} ${ro?'disabled':''}" tabindex="0" data-row="${i}" data-field="prodDate" data-val="${esc(r.prodDate)}"><span class="dval${ph}">${r.prodDate?esc(r.prodDate):'选择日期'}</span><svg viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="17" rx="2"/><path d="M3 9h18M8 2v4M16 2v4"/></svg></div></td>`; }
|
||||
if(k==='batch') return `<td class="cpad"><input class="gci ${r._err.has('batch')?'error':''}" data-row="${i}" data-field="batch" value="${esc(r.batch)}" placeholder="批次号" ${ro?'disabled':''}></td>`;
|
||||
if(k==='qty') return `<td class="cpad num"><input class="gci ${r._err.has('qty')?'error':''}" inputmode="decimal" data-row="${i}" data-field="qty" value="${esc(r.qty)}" ${ro?'disabled':''}></td>`;
|
||||
if(k==='price') return `<td class="cpad num"><input class="gci ${r._err.has('price')?'error':''}" inputmode="decimal" data-row="${i}" data-field="price" value="${esc(r.price)}" placeholder="0.00" ${ro?'disabled':''}></td>`;
|
||||
if(k==='price'){ if(mode==='out') return `<td class="ro num">${r.price?fmt(parseFloat(r.price)||0):'—'}</td>`; return `<td class="cpad num"><input class="gci ${r._err.has('price')?'error':''}" inputmode="decimal" data-row="${i}" data-field="price" value="${esc(r.price)}" placeholder="0.00" ${ro?'disabled':''}></td>`; }
|
||||
if(k==='sale') return `<td class="cpad num"><input class="gci ${r._err.has('sale')?'error':''}" inputmode="decimal" data-row="${i}" data-field="sale" value="${esc(r.sale)}" placeholder="0.00" ${ro?'disabled':''}></td>`;
|
||||
if(k==='amount') return `<td class="amt num" data-amt="${i}">${amtHTML(r)}</td>`;
|
||||
if(k==='profit') return `<td class="amt num" data-profit="${i}">${profitHTML(r)}</td>`;
|
||||
if(k==='act'){ const exp = mode==='in'?`<svg class="gexp ${r.expanded?'on':''}" title="选填项(产地/保质期/储存/介绍)" onclick="toggleExpand(${i})" viewBox="0 0 24 24"><path d="M6 9l6 6 6-6"/></svg>`:''; return `<td class="gact"><div class="gact-in">${exp}<svg title="复制本行" onclick="copyRow(${i})" viewBox="0 0 24 24"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 012-2h10"/></svg><svg class="gdel" title="删除" onclick="delRow(${i})" viewBox="0 0 24 24"><path d="M4 7h16M9 7V5a2 2 0 012-2h2a2 2 0 012 2v2M6 7l1 13a2 2 0 002 2h6a2 2 0 002-2l1-13"/></svg></div></td>`; }
|
||||
return '<td></td>';
|
||||
}).join('');
|
||||
@@ -308,9 +314,9 @@ function cardsHTML(){
|
||||
rows+=mrow('可用库存',false,`<span style="color:${r.avail<=0?'var(--danger)':'var(--muted)'}">${r.avail==null?'—':r.avail}</span>`);
|
||||
}
|
||||
rows+=mrow('数量',true,`<input inputmode="decimal" data-row="${i}" data-field="qty" value="${esc(r.qty)}" class="${r._err.has('qty')?'error':''}" ${ro?'disabled':''}>`);
|
||||
rows+=mrow(mode==='in'?'单价(可待定)':'单价',mode!=='in',`<input inputmode="decimal" data-row="${i}" data-field="price" value="${esc(r.price)}" placeholder="${mode==='in'?'留空=待定价':'0.00'}" class="${r._err.has('price')?'error':''}" ${ro?'disabled':''}>`);
|
||||
rows+=mrow('出售价格',true,`<input inputmode="decimal" data-row="${i}" data-field="sale" value="${esc(r.sale)}" placeholder="0.00" class="${r._err.has('sale')?'error':''}" ${ro?'disabled':''}>`);
|
||||
rows+=`<div class="mrow"><label>金额</label><div class="mv amt" data-amt="${i}">${amtHTML(r)}</div></div>`;
|
||||
rows+=mrow(mode==='in'?'进价·单瓶(可待定)':'进价',false,`<input inputmode="decimal" data-row="${i}" data-field="price" value="${esc(r.price)}" placeholder="${mode==='in'?'留空=待定价':'0.00'}" class="${r._err.has('price')?'error':''}" ${ro?'disabled':''}>`);
|
||||
rows+=mrow(mode==='in'?'参考售价':'售价',true,`<input inputmode="decimal" data-row="${i}" data-field="sale" value="${esc(r.sale)}" placeholder="0.00" class="${r._err.has('sale')?'error':''}" ${ro?'disabled':''}>`);
|
||||
rows+= mode==='out' ? `<div class="mrow"><label>利润</label><div class="mv amt" data-profit="${i}">${profitHTML(r)}</div></div>` : `<div class="mrow"><label>总进价</label><div class="mv amt" data-amt="${i}">${amtHTML(r)}</div></div>`;
|
||||
const act=ro?'':`<div class="mact"><svg title="复制" onclick="copyRow(${i})" viewBox="0 0 24 24"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 012-2h10"/></svg><svg class="gdel" title="删除" onclick="delRow(${i})" viewBox="0 0 24 24"><path d="M4 7h16M9 7V5a2 2 0 012-2h2a2 2 0 012 2v2M6 7l1 13a2 2 0 002 2h6a2 2 0 002-2l1-13"/></svg></div>`;
|
||||
return `<div class="mcard ${rerr}"><div class="mcard-head"><span class="mt">商品 ${i+1}</span>${sub?`<span class="msub">${esc(sub)}</span>`:''}${act}</div><div class="mcard-body">${rows}</div></div>`;
|
||||
}).join('');
|
||||
@@ -320,8 +326,9 @@ function cardsHTML(){
|
||||
function mrow(label,req,inner){ return `<div class="mrow"><label class="${req?'req':''}">${label}</label><div class="mv">${inner}</div></div>`; }
|
||||
|
||||
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')); }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -295,11 +295,12 @@ function auditBtns(o,idx){
|
||||
return '';
|
||||
}
|
||||
function openDetail(idx){ const o=ORDERS[idx]; document.getElementById('dTitle').textContent=o.no;
|
||||
let lh=`<div class="lines"><div class="lh" style="grid-template-columns:1fr 124px 40px 64px 78px"><span>商品 · 编码</span><span>系列 / 规格</span><span style="text-align:right">数量</span><span style="text-align:right">单价</span><span style="text-align:right">金额</span></div>`;
|
||||
o.lines.forEach(l=>{ const pi=PINFO[l.name]||{}; const pend=(l.price||0)<=0; const priceC=pend?'<span class="pending-price">待定价</span>':money(l.price); const amtC=pend?'<span class="pending-price">待定价</span>':money(l.qty*l.price); lh+=`<div class="lr" style="grid-template-columns:1fr 124px 40px 64px 78px;align-items:start"><span class="lcell"><span class="ln">${l.name}</span><span class="lcode">${pi.code||'—'}</span></span><span class="lcell"><span class="lser">${pi.series||'—'}</span><span class="lspec">${pi.spec||''}</span></span><span style="text-align:right;font-family:var(--font-mono)">${l.qty}</span><span style="text-align:right;font-family:var(--font-mono)">${priceC}</span><span class="amt">${amtC}</span></div>`; }); lh+='</div>';
|
||||
const GRID='1fr 108px 36px 60px 60px 64px';
|
||||
let lh=`<div class="lines"><div class="lh" style="grid-template-columns:${GRID}"><span>商品 · 编码</span><span>系列 / 规格</span><span style="text-align:right">数量</span><span style="text-align:right">成本价</span><span style="text-align:right">售价</span><span style="text-align:right">利润</span></div>`;
|
||||
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?'<span class="pending-price">待定价</span>':money(l.price); const profit=(l.price-cost)*l.qty; const profitC=pend?'<span class="pending-price">待定价</span>':`<span style="color:${profit<0?'var(--danger)':'var(--success)'}">${money(profit)}</span>`; lh+=`<div class="lr" style="grid-template-columns:${GRID};align-items:start"><span class="lcell"><span class="ln">${l.name}</span><span class="lcode">${pi.code||'—'}</span></span><span class="lcell"><span class="lser">${pi.series||'—'}</span><span class="lspec">${pi.spec||''}</span></span><span style="text-align:right;font-family:var(--font-mono)">${l.qty}</span><span style="text-align:right;font-family:var(--font-mono)">${money(cost)}</span><span style="text-align:right;font-family:var(--font-mono)">${saleC}</span><span class="amt">${profitC}</span></div>`; }); lh+='</div>';
|
||||
const pendCount=o.lines.filter(l=>(l.price||0)<=0).length;
|
||||
const auditor=o.auditor?o.auditor:'<span style="color:var(--faint)">—</span>';
|
||||
document.getElementById('drawerBody').innerHTML=`<div class="drow"><span>单据日期</span><b>${o.date}</b></div><div class="drow"><span>${PARTY_LABEL}</span><b>${o.party}</b></div><div class="drow"><span>仓库</span><b>${o.wh}</b></div><div class="drow"><span>商品数</span><b>${o.count}</b></div><div class="drow"><span>出库员</span><b>${o.op}</b></div><div class="drow"><span>审核员</span><b>${auditor}</b></div><div class="drow"><span>状态</span><b><span class="badge b-${o.status}">${o.status}</span>${o.ret?` <span class="badge b-${o.ret}">${o.ret}</span>`:''}</b></div><label style="display:block;font-size:var(--fs-sm);color:var(--muted);margin:16px 0 8px;">出库明细</label>${lh}<div class="ltotal">合计金额 <b>${money(o.amount)}</b></div>`+
|
||||
document.getElementById('drawerBody').innerHTML=`<div class="drow"><span>单据日期</span><b>${o.date}</b></div><div class="drow"><span>${PARTY_LABEL}</span><b>${o.party}</b></div><div class="drow"><span>仓库</span><b>${o.wh}</b></div><div class="drow"><span>商品数</span><b>${o.count}</b></div><div class="drow"><span>出库员</span><b>${o.op}</b></div><div class="drow"><span>审核员</span><b>${auditor}</b></div><div class="drow"><span>状态</span><b><span class="badge b-${o.status}">${o.status}</span>${o.ret?` <span class="badge b-${o.ret}">${o.ret}</span>`:''}</b></div><label style="display:block;font-size:var(--fs-sm);color:var(--muted);margin:16px 0 8px;">出库明细</label>${lh}<div class="ltotal">合计金额 <b>${money(o.amount)}</b></div><div class="ltotal" style="padding-top:4px">合计利润 <b style="color:var(--success)">${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))}</b></div>`+
|
||||
`<div class="dactions"><div class="dact-group"><span class="dact-label">单据审核</span><div class="dact-row">${auditBtns(o,idx)}</div></div>${(o.status==='已审核'&&pendCount>0)?`<div class="dact-group"><span class="dact-label">售价 · ${pendCount} 项待定价</span><div class="dact-row"><div class="btn primary sm" onclick="openConfirmSale(${idx})"><svg viewBox="0 0 24 24"><use href="#i-edit"/></svg>确认售价</div></div></div>`:''}<div class="dact-group"><span class="dact-label">打印</span><div class="dact-row"><div class="btn ghost sm" onclick="toast('打印标签(原型)')"><svg viewBox="0 0 24 24"><use href="#i-ic49"/></svg>打印标签</div><div class="btn ghost sm" onclick="toast('打印单据(原型)')"><svg viewBox="0 0 24 24"><use href="#i-ic50"/></svg>打印单据</div></div></div></div>`;
|
||||
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'); }
|
||||
|
||||
Reference in New Issue
Block a user