"""R08构造底稿:标准库Decimal分段计税,不调用站点引擎。 从任意目录运行都会写脚本旁calc_r08.json。--verify-articles核对正文全部 数据表与独立底稿生成的表,另核对各篇指定的关键数字句;不冒充政策资格审查。 """ from decimal import Decimal, ROUND_HALF_UP, getcontext from pathlib import Path from datetime import date import argparse import json import re getcontext().prec = 40 HERE = Path(__file__).resolve().parent D = Decimal ANNUAL = [(D('36000'), D('.03')), (D('144000'), D('.10')), (D('300000'), D('.20')), (D('420000'), D('.25')), (D('660000'), D('.30')), (D('960000'), D('.35')), (D('Infinity'), D('.45'))] BONUS = [(D('36000'), D('.03'), D('0')), (D('144000'), D('.10'), D('210')), (D('300000'), D('.20'), D('1410')), (D('420000'), D('.25'), D('2660')), (D('660000'), D('.30'), D('4410')), (D('960000'), D('.35'), D('7160')), (D('Infinity'), D('.45'), D('15160'))] def money(value): return D(value).quantize(D('.01'), rounding=ROUND_HALF_UP) def annual(base): """按各级区间的实际宽度累加,与全额乘税率减速算数独立。""" base = max(D(0), D(base)); low = D(0); tax = D(0) for high, rate in ANNUAL: width = max(D(0), min(base, high) - low) tax += width * rate if base <= high: break low = high return money(tax) def bonus(gross): gross = D(gross) for high, rate, quick in BONUS: if gross <= high: return money(gross * rate - quick) raise AssertionError('奖金档位缺失') def net_candidates(net): """逐档求代数候选,再按税前所属档和到分回代筛选;不用全域二分。""" out = []; low = D(0) for high, rate, quick in BONUS: exact = (D(net) - quick) / (1 - rate) gross = money(exact) valid = low < exact <= high if valid: tax = bonus(gross) assert gross - tax == money(net) pennies=[] for delta in (D('-.02'),D('-.01'),D(0),D('.01'),D('.02')): nearby=gross+delta if low < nearby <= high and nearby-bonus(nearby)==money(net): pennies.append(nearby) out.append({'gross_exact': str(exact), 'gross': gross, 'tax': tax, 'net': gross-tax, 'rate': rate, 'quick': quick, 'nearby_penny_solutions':pennies}) low = high return out def fmt(value): return f'{value:,.2f}' if isinstance(value, Decimal) else str(value) def table(headers, rows): return '\n'.join(['| '+' | '.join(headers)+' |', '| '+' | '.join(['---']*len(headers))+' |']+ ['| '+' | '.join(fmt(x) for x in row)+' |' for row in rows]) def record(slug, inputs, outputs, tables, claims): return {'slug':slug, 'inputs':inputs, 'outputs':outputs, 'tables':tables, 'verified_numeric_claims':claims} def build(): out = {} low_base = D(90000)-60000-18000-24000 high_base = D(180000)-60000-18000-24000 choice = [] for salary, base in [(D(90000),low_base),(D(180000),high_base)]: split = annual(base)+bonus(36000); merged = annual(base+36000) choice.append([salary, base, max(D(0),base),annual(base),bonus(36000),split, max(D(0),base+36000),merged,merged-split]) out['DJ2-064'] = record('tax-bonus-choice-deduction-room', {'year':2025,'salary':D(90000),'social':D(18000),'additional':D(24000),'bonus':D(36000)}, {'scenarios':choice}, [table(['工资/元','未归零余额/元','单独时综合税基/元','综合税/元','奖金税/元','单独总税/元','并入税基/元','并入总税/元','并入减单独/元'],choice)], ['并入少缴360.00元','单独少缴2,520.00元']) base = D(180000)-60000-36000-24000 a = annual(base+60000)+bonus(30000) b = annual(base+30000)+bonus(60000) merged = annual(base+90000) selection = [['A单独、B并入',D(120000),annual(base+60000),bonus(30000),a], ['B单独、A并入',D(90000),annual(base+30000),bonus(60000),b], ['两笔全部并入',D(150000),merged,D(0),merged]] out['DJ2-065'] = record('tax-bonus-selection-once-year', {'year':2025,'salary':D(180000),'social':D(36000),'additional':D(24000), 'bonuses':[{'date':'2025-06-30','gross':D(30000)},{'date':'2025-12-20','gross':D(60000)}]}, {'valid_qualified_scenarios':selection,'invalid_both_separate_total':annual(base)+bonus(30000)+bonus(60000)}, [table(['资格均已确认时的算术情景','综合税基/元','综合税/元','单独奖金税/元','合计/元'],selection)], ['差额1,890.00元','违规拼接数10,170.00元']) months=[] for unit,start in [('A',1),('B',7)]: paid=D(0) for local in range(1,7): taxable=D(10000)*local; total=annual(taxable) months.append([f'2025-{start+local-1:02d}',unit,str(local),taxable,total,total-paid]) paid=total reconciliation=[] for label,worked,prepaid in [('两单位各任职半年',12,sum(r[5] for r in months)), ('同一单位连续全年',12,annual(D(10000)*12)), ('仅A单位任职半年',6,annual(D(10000)*6))]: salary=D(20000)*worked; social=D(3000)*worked; additional=D(2000)*worked base=salary-60000-social-additional;final=annual(base) reconciliation.append([label,salary,social,additional,base,final,prepaid,final-prepaid]) out['DJ2-066'] = record('tax-job-change-withholding-vs-annual', {'year':2025,'monthly_salary':D(20000),'monthly_social':D(3000),'monthly_additional':D(2000), 'A_employment':['2025-01-01','2025-06-30'],'B_employment':['2025-07-01','2025-12-31']}, {'months':months,'reconciliation':reconciliation}, [table(['月份','单位','本单位累计月数','累计税基/元','累计预扣/元','当月预扣/元'],months), table(['情景','工资/元','专项扣除/元','专项附加/元','年度税基/元','年度税/元','已预扣/元','应纳减预扣/元'],reconciliation)], ['预扣合计6,960.00元','年度应纳9,480.00元','需要补税2,520.00元']) labor=[] for day,gross in [('2025-04-18',D(3000)),('2025-08-22',D(20000)),('2025-11-14',D(60000))]: date.fromisoformat(day) prebase=max(D(0),gross-800) if gross<=4000 else gross*D('.8') pre=money(prebase*D('.2')) if prebase<=20000 else money(prebase*D('.3')-2000) if prebase<=50000 else money(prebase*D('.4')-7000) labor.append([day,gross,prebase,pre,gross-pre,gross*D('.8')]) gross=sum(r[1] for r in labor); income=sum(r[5] for r in labor); pre=sum(r[3] for r in labor) scenarios=[] for salary in (D(180000),D(600000)): base=salary-60000-36000-24000 salpre=annual(base); final=annual(base+income) scenarios.append([salary,income,base+income,final,salpre,pre,final-salpre-pre]) out['DJ2-067']=record('tax-labor-pay-annual-credit', {'year':2025,'salary':D(180000),'social':D(36000),'additional':D(24000),'labor_gross':gross}, {'labor':labor,'labor_income':income,'labor_prepaid':pre,'scenarios':scenarios}, [table(['取得日期','劳务毛额/元','预扣税基/元','预扣税/元','实收/元','年度收入额/元'],labor), table(['工资毛额/元','劳务年度收入额/元','全年税基/元','全年税/元','工资预扣/元','劳务预扣/元','应纳减预扣/元'],scenarios)], ['劳务毛额83,000.00元','年度折算66,400.00元','可以申请退税9,400.00元','补税3,880.00元']) effective=[] for m in range(1,13): aa=D(1000) if 3<=m<=8 else D(0) bb=D(2000) if 9<=m<=12 else D(0) effective.append([f'2025-{m:02d}',aa,bb,aa+bb]) scenarios=[['未计专项附加',D(0),D(120000),annual(120000)], ['逐月资格底稿',D(14000),D(106000),annual(106000)], ['误把两项目均乘全年',D(36000),D(84000),annual(84000)]] out['DJ2-068']=record('tax-special-deduction-effective-months', {'year':2025,'salary':D(216000),'social':D(36000),'confirmed_A_months':[3,4,5,6,7,8], 'confirmed_A_monthly':D(1000),'confirmed_B_months':[9,10,11,12],'confirmed_B_monthly':D(2000)}, {'months':effective,'total':sum(r[3] for r in effective),'scenarios':scenarios}, [table(['所属月','甲项目/元','乙项目/元','本月合计/元'],effective), table(['口径','专项附加/元','年度税基/元','年度税/元'],scenarios)], ['正确合计14,000.00元','税款低估2,200.00元','A单位4,000.00元、B单位10,000.00元']) payroll=[['基本养老个人款',D(1200),D(14400),'已核对专项扣除'], ['基本医疗个人款',D(300),D(3600),'已核对专项扣除'], ['失业保险个人款',D(30),D(360),'已核对专项扣除'], ['住房公积金个人款',D(1000),D(12000),'已核对专项扣除'], ['普通团体商业保险',D(200),D(2400),'本例无税前扣除依据'], ['食堂餐费',D(350),D(4200),'本例税后消费'], ['工会会费',D(40),D(480),'本例无税前扣除依据'], ['归还单位借款',D(600),D(7200),'本例税后还款']] good=D(2530)*12; bad=D(3720)*12 scenarios=[] for label,amount in [('凭证已确认口径',good),('误把全部代扣作专项',bad),('公积金每月仅确认800元',good-D(2400))]: base=D(288000)-60000-24000-amount scenarios.append([label,amount,base,annual(base)]) out['DJ2-069']=record('tax-contribution-deduction-proof', {'year':2025,'salary':D(288000),'additional':D(24000),'monthly_deductions':payroll, 'employer_fund_annual_excluded':D(12000)}, {'scenarios':scenarios,'all_cash_deductions':bad,'after_tax_cash':D(288000)-bad-annual(173640)}, [table(['工资单科目','每月代扣/元','全年代扣/元','核验状态'],payroll), table(['口径','专项扣除/元','年度税基/元','年度税/元'],scenarios)], ['专项扣除30,360.00元','全年实际现金225,552.00元','少算税款2,856.00元','年度税增加480.00元']) candidates=net_candidates(34000) inverse=[[f'候选{i+1}',c['gross'],f'{c["rate"]*100:.0f}%',c['quick'],c['tax'],c['net']] for i,c in enumerate(candidates)] checks=[] for gross in (D(34000),D(35020),D(36000),D('36000.01')): checks.append([gross,bonus(gross),gross-bonus(gross)]) mergedgross=money(D(34000)/D('.9')) incremental=annual(D(60000)+mergedgross)-annual(60000) out['DJ2-070']=record('tax-gross-net-bonus-reconciliation', {'year':2025,'target_net':D(34000),'base_before_bonus':D(60000),'penny_rounding':'ROUND_HALF_UP'}, {'candidates':candidates,'boundary_checks':checks,'merged_gross':mergedgross,'incremental_tax':incremental, 'naive_shortfall':D(34000)-(D(35020)-bonus(35020))}, [table(['逐档有效候选','税前/元','税率','速算扣除/元','税额/元','回代净额/元'],inverse), table(['税前/元','税额/元','净额/元'],checks), table(['构造合同条件','税前奖金/元','本奖金对应税额/元','净额/元'], [['单独计税候选一',candidates[0]['gross'],candidates[0]['tax'],candidates[0]['net']], ['单独计税候选二',candidates[1]['gross'],candidates[1]['tax'],candidates[1]['net']], ['奖金并入前年度税基60000元',mergedgross,incremental,mergedgross-incremental]])], ['同一净额存在两个有效候选','仍差30.60元','合并后全年税额7,257.78元']) pension=[] for salary in (D(72000),D(77000),D(112000),D(232000)): base=salary-60000-12000 post=max(D(0),base-12000) pension.append([salary,base,annual(base),D(12000),post,annual(post),annual(base)-annual(post)]) marginal=[] for paid in (D(0),D(6000),D(12000)): post=max(D(0),D(40000)-paid) marginal.append([paid,post,annual(post),annual(40000)-annual(post)]) out['DJ2-071']=record('tax-personal-pension-unused-deduction', {'year':2025,'social':D(12000),'additional':D(0),'confirmed_pension_paid':D(12000),'other_income':D(0)}, {'scenarios':pension,'cross_bracket':marginal}, [table(['工资/元','缴费前税基/元','缴费前税/元','合格缴费/元','缴费后税基/元','缴费后税/元','本年度少税/元'],pension), table(['据实扣除/元','扣除后税基/元','扣除后税/元','累计少税/元'],marginal)], ['缴费一万二千元,本年度少税为零','本年度只少税150.00元','前六千元少税460.00元','后六千元少税180.00元']) cases=[] for label,salary,additional in [('基准',D(360000),D(36000)),('收入增加30000元',D(390000),D(36000)),('扣除减少12000元',D(360000),D(24000))]: base=salary-60000-60000-additional tax=annual(base); supplement=tax-18000 cases.append([label,salary,additional,base,tax,D(18000),supplement]) cash=[]; reserve=D(1800); free=D(10000) for m in range(1,5): day=date(2026,m,15).isoformat();reserve+=1020;free+=2000-1020 cash.append([day,D(2000),D(1020),D(0),reserve,free]) reserve-=5880;free+=2000 cash.append(['2026-05-20',D(2000),D(0),D(5880),reserve,free]) out['DJ2-072']=record('tax-settlement-cash-reserve', {'tax_year':2025,'settlement_year':2026,'initial_reserve':D(1800),'initial_free_cash':D(10000), 'monthly_surplus':D(2000),'reserve_contributions':4,'chosen_payment_day':'2026-05-20', 'emergency_cash_excluded':D(20000),'unconfirmed_refund_excluded':D(0)}, {'tax_scenarios':cases,'cash':cash,'missing_initial':D(5880)-1800,'monthly_reserve':D(1020), 'stress_monthly_income_case':(cases[1][6]-1800)/4, 'stress_monthly_deduction_case':(cases[2][6]-1800)/4, 'stress_monthly_shortfall':(cases[1][6]-1800)/4-2000, 'date_delay_missing':D(5880)-(D(1800)+D(1020)*3), 'total_cash_check':D(10000)+1800+D(2000)*5-D(5880)}, [table(['估计情景','工资/元','专项附加/元','年度税基/元','年度税/元','已预扣/元','预计补税/元'],cases), table(['预算日期','当月常规结余/元','转入税款专户/元','缴款/元','税款专户余额/元','日常自由现金/元'],cash)], ['预计补税5,880.00元','尚缺4,080.00元','每次预留1,020.00元','日常自由现金15,920.00元', '每次要2,520.00元','每次缺520.00元','四次每次需1,620.00元']) assert len(out)==9 assert len(candidates)==2 assert annual(144000)==D(11880) and annual(144001)==D('11880.20') assert bonus(36000)==D(1080) and bonus(D('36000.01'))==D(3390) assert sum(r[5] for r in months)==6960 assert sum(r[3] for r in labor)==16040 assert sum(r[3] for r in effective)==14000 assert cash[-1][4]==0 and cash[-1][5]==15920 return out def serialize(value): if isinstance(value,Decimal): return format(value,'f') raise TypeError(type(value).__name__) def verify_articles(data): ROOT = HERE.parents[4] # 仅仓库内正文核对需要,不让默认下载运行依赖目录深度。 report=[] for aid,item in data.items(): path=ROOT/'sites/daojijun.com/src/content/article'/f'{item["slug"]}.md' body=path.read_text('utf-8').split('---',2)[2].split('')[0] found=re.findall(r'(?m)^\|[^\n]+\|\n(?:\|[^\n]+\|\n?)+',body) normalize=lambda value:re.sub(r'\s+','',value).replace(',','') expected=item['tables'] assert len(found)==len(expected),(aid,'表数不符',len(found),len(expected)) for i,(actual,wanted) in enumerate(zip(found,expected),1): assert normalize(actual)==normalize(wanted),(aid,f'第{i}张表与独立底稿不符') for claim in item['verified_numeric_claims']: assert claim in body,(aid,'关键数字句不符',claim) report.append({'id':aid,'tables_checked':len(expected),'numeric_claims_checked':len(item['verified_numeric_claims'])}) print(json.dumps({'passed':True,'articles':report},ensure_ascii=False,indent=2)) if __name__=='__main__': parser=argparse.ArgumentParser();parser.add_argument('--verify-articles',action='store_true');args=parser.parse_args() data=build() (HERE/'calc_r08.json').write_text(json.dumps(data,ensure_ascii=False,indent=2,default=serialize)+'\n','utf-8') if args.verify_articles: verify_articles(data) else: print(json.dumps({aid:value['outputs'] for aid,value in data.items()},ensure_ascii=False,indent=2,default=serialize))