workshop_quality_r13_model.py 6.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
  1. """A reusable, blank-input decision model, separate from empirical company data."""
  2. import csv
  3. import json
  4. FIELDS = [
  5. ('R0','原方案净实现收入','元/销售单位','财务/渠道结算'),
  6. ('V0','原方案变动成本','元/销售单位','财务/已核BOM及履约'),
  7. ('Rn','新方案净实现收入','元/销售单位','财务/渠道结算'),
  8. ('Vn','新方案变动成本','元/销售单位','已核配方、包材、制造、装箱、物流和预期报损;不重复计项'),
  9. ('F','一次增量投入','元','报价、打样、必要验证、试产和印版'),
  10. ('Qn','真正新增销量','销售单位/同一分析期','研究或经营估计;不含从旧品转移'),
  11. ('Qc','从原方案转移销量','销售单位/同一分析期','经营估计;与新增互斥'),
  12. ]
  13. SCRIPT = r'''(() => {
  14. const names = ['R0','V0','Rn','Vn','F','Qn','Qc'];
  15. const state = document.querySelector('#model-state');
  16. let origin = 'blank';
  17. const calculate = (v) => {
  18. const u0=v.R0-v.V0, un=v.Rn-v.Vn, du=un-u0;
  19. const value=v.Qn*un+v.Qc*du-v.F;
  20. return {u0,un,du,value,breakEven:du>0 ? Math.ceil(v.F/du) : null};
  21. };
  22. const fmt = n => Number(n).toLocaleString('zh-CN',{maximumFractionDigits:2});
  23. function update() {
  24. const rows=names.map(k=>[k,document.getElementById(k).value.trim()]);
  25. if(rows.some(([k,v])=>v==='')) { state.textContent='输入未齐,不计算;空值不按0处理。';document.querySelector('#sensitivity').innerHTML='';return; }
  26. const v=Object.fromEntries(rows.map(([k,x])=>[k,Number(x)]));
  27. if(Object.values(v).some(x=>!Number.isFinite(x)||x<0)||!Number.isInteger(v.Qn)||!Number.isInteger(v.Qc)) {state.textContent='输入应为非负有限数;销量为非负整数。';document.querySelector('#sensitivity').innerHTML='';return;}
  28. const result=calculate(v);
  29. const badge=origin==='synthetic'?'算术假设示例,非企业参数':'本地输入试算,参数真实性未核验';
  30. state.textContent=badge+'。原单位贡献 '+fmt(result.u0)+';新单位贡献 '+fmt(result.un)+';单位增量 '+fmt(result.du)+' 元。该分析期增量贡献 '+fmt(result.value)+' 元。'+(result.breakEven===null?'同销量替代下,单位增量≤0,不能靠增加替代销量覆盖正的一次投入。':'仅同销量替代情景,覆盖F至少需要 '+fmt(result.breakEven)+' 个销售单位。')+'此模型未含资金时间价值及未声明的固定运营变化,不等于量产或盈利验收。';
  31. const scenarios=[['当前输入',v],['新净收入降低10%',{...v,Rn:v.Rn*.9}],['新变动成本增加10%',{...v,Vn:v.Vn*1.1}],['新增销量减半',{...v,Qn:Math.floor(v.Qn*.5)}],['无真正新增销量',{...v,Qn:0}]];
  32. document.querySelector('#sensitivity').innerHTML='<div class="table-wrap"><table><thead><tr><th>单因素敏感性(其他条件固定)</th><th>新单位贡献/元</th><th>分析期增量贡献/元</th></tr></thead><tbody>'+scenarios.map(([name,x])=>{const z=calculate(x);return '<tr><td>'+name+'</td><td>'+fmt(z.un)+'</td><td>'+fmt(z.value)+'</td></tr>'}).join('')+'</tbody></table></div>';
  33. window.modelResult={...result,origin,inputs:v,empiricalVerified:false};
  34. }
  35. names.forEach(k=>document.getElementById(k).addEventListener('input',()=>{origin='user-unverified';delete window.modelResult;update();}));
  36. document.getElementById('example').onclick=()=>{
  37. const v={R0:20,V0:15,Rn:21,Vn:15.5,F:1000,Qn:100,Qc:1000};
  38. names.forEach(k=>document.getElementById(k).value=v[k]);origin='synthetic';update();
  39. };
  40. document.getElementById('reset').onclick=()=>{names.forEach(k=>document.getElementById(k).value='');origin='blank';delete window.modelResult;update();};
  41. update();
  42. })();'''
  43. def build_model(out,shell):
  44. model={'schema':'ksf.incremental-feasibility.v1','unit':'元/统一销售单位,销量为同一分析期',
  45. 'empiricalCompanyData':False,'fields':{k:{'label':label,'value':None,'unit':unit,'sourceNeeded':source} for k,label,unit,source in FIELDS},
  46. 'formulas':{'u0':'R0-V0','un':'Rn-Vn','deltaUnit':'un-u0','incrementalContribution':'Qn*un+Qc*(un-u0)-F',
  47. 'sameVolumeBreakEven':'ceil(F/(un-u0)) only when un-u0>0; Qn=0'},
  48. 'limitations':['不以零售价代净实现收入','费用口径互斥,不能重复计税/运费/报损','新销量与旧品转移销量分开','仅单期贡献模型,未含资金时间价值和额外固定运营变化']}
  49. (out/'商业回推模型.json').write_text(json.dumps(model,ensure_ascii=False,indent=2)+'\n',encoding='utf8')
  50. with (out/'商业回推输入模板.csv').open('w',encoding='utf-8-sig',newline='') as f:
  51. w=csv.writer(f);w.writerow(['变量','含义','实际值(留空待核)','单位','需要的依据'])
  52. for k,label,unit,source in FIELDS:w.writerow([k,label,'',unit,source])
  53. inputs=''.join(f'<label for="{k}">{label}<br><small>{unit}</small><input id="{k}" type="number" min="0" step="'+('1' if k in ['Qn','Qc'] else 'any')+'" placeholder="待核,不自动填值"></label>' for k,label,unit,_ in FIELDS)
  54. body='''<article><h2>先统一口径,再看增量</h2><p>O1拆配比与装箱,O2拆印版及旧包材消化,O3拆拣货/错发/物流;依报价归入变动成本或一次投入,已含费用不重复加入。所有输入默认为空;本页仅本地计算、不保存、不上传。</p><p>统一使用同一销售单位及分析期。u₀=R₀−V₀,uₙ=Rₙ−Vₙ;增量贡献=Qₙ×uₙ+Q꜀×(uₙ−u₀)−F。若只替代原销量,且单位增量为正,盈亏临界=⌈F/(uₙ−u₀)⌉。</p><p><a href="商业回推输入模板.csv">空白输入模板 CSV</a> · <a href="商业回推模型.json">模型及口径 JSON</a> · <a href="reports/feasibility.html">返回可行性报告</a></p><div class="calcgrid">'''+inputs+'''</div><p><button class="switch" id="example">载入算术假设示例(非企业参数)</button> <button class="switch" id="reset">清空输入</button></p><output id="model-state" aria-live="polite"></output><div id="sensitivity"></div><p class="muted">算术示例专供检验公式:20/15 → 21/15.5,F=1000,新增100、转移1000。不是公司收入、成本、销量预测或售价建议。用户修改参数后的真实性仍需另核。</p></article><script>'''+SCRIPT+'</script>'
  55. (out/'商业回推演算器.html').write_text(shell('商业回推|空值模型与敏感性试算',body),encoding='utf8')