"""R04原创构造预算复算;标准库,任意cwd运行,结果写脚本同目录。""" from calendar import monthrange from datetime import date from decimal import Decimal, ROUND_HALF_UP from pathlib import Path import json D = Decimal ZERO = D('0.00') def money(value): return D(str(value)).quantize(D('.01'), rounding=ROUND_HALF_UP) def months(start, n): year, month = map(int, start.split('-')) for i in range(n): y, m = divmod(year * 12 + month - 1 + i, 12) yield date(y, m + 1, monthrange(y, m + 1)[1]).isoformat() def monthly(opening, start, incomes, expenses, floor=0): assert len(incomes) == len(expenses) cash = money(opening) rows = [] for when, incoming, outgoing in zip(months(start, len(incomes)), incomes, expenses): inc, out = money(incoming), money(outgoing) before = cash cash += inc - out rows.append(dict(date=when, opening=before, income=inc, expense=out, net=inc-out, closing=cash, floor_gap=max(ZERO, money(floor)-cash))) return summary(opening, rows, floor) def events(opening, items, floor=0): cash = money(opening) rows = [] last = None for when, label, change in sorted(items, key=lambda x: x[0]): date.fromisoformat(when) assert last is None or when >= last last = when change = money(change) before = cash cash += change rows.append(dict(date=when, label=label, opening=before, income=max(ZERO, change), expense=max(ZERO, -change), net=change, closing=cash, floor_gap=max(ZERO, money(floor)-cash))) return summary(opening, rows, floor) def summary(opening, rows, floor): total_in = sum((r['income'] for r in rows), ZERO) total_out = sum((r['expense'] for r in rows), ZERO) minimum = min([money(opening)] + [r['closing'] for r in rows]) assert money(opening) + total_in - total_out == rows[-1]['closing'] return dict(opening=money(opening), floor=money(floor), rows=rows, total_income=total_in, total_expense=total_out, closing=rows[-1]['closing'], minimum_including_opening=minimum, minimum_closing=min(r['closing'] for r in rows), extra_to_zero=max(ZERO, -minimum), extra_to_floor=max(ZERO, money(floor)-minimum), first_negative=next((r['date'] for r in rows if r['closing'] < 0), None)) def build(): result = {} late = [('2026-11-03','住房付款',-4000),('2026-11-10','日常付款',-1800), ('2026-11-17','必要付款',-1200),('2026-11-25','工资到账',12000),('2026-11-28','日常付款',-2000)] early = [(('2026-11-02' if label=='工资到账' else day),label,amount) for day,label,amount in late] result['DJ2-028'] = dict(late=events(3500,late,1000), early=events(3500,early,1000), week_ends=['2026-11-07','2026-11-14','2026-11-21','2026-11-30'], late_weekly=monthly(3500,'2026-11',[0,0,0,12000],[4000,1800,1200,2000],1000), early_weekly=monthly(3500,'2026-11',[12000,0,0,0],[4000,1800,1200,2000],1000)) # 周表仅借用加减递推;用真实周末日期替换月表日期,避免误称四个月。 for name in ['late_weekly','early_weekly']: part=result['DJ2-028'][name] for row,when in zip(part['rows'],result['DJ2-028']['week_ends']): row['date']=when part['first_negative']=next((r['date'] for r in part['rows'] if r['closing']<0),None) deposits=[('2026-11-30','预留',2000),('2026-12-31','预留',2000),('2027-01-31','预留',2000),('2027-02-05','春季缴费',-8000)] for i,when in enumerate(months('2027-02',7)): deposits.append((when,'第二学期预留', '1428.58' if i==6 else '1428.57')) deposits.append(('2027-09-05','秋季缴费',-10000)) result['DJ2-029']=dict(schedule=events(2000,deposits), first_fee=money(8000),second_fee=money(10000), start_options=[dict(start=start,available_deposits=n,monthly_needed=money((D(8000)-D(2000))/n)) for start,n in [('2026-11',3),('2026-12',2),('2027-01',1)]], second_regular=money(D(10000)/D(7)),second_last=money(10000)-money('1428.57')*6, flat_monthly=money(D(18000)/D(12)),flat_first_available=money(2000+1500*3),flat_first_gap=money(8000-6500)) result['DJ2-030']=dict(actual=monthly(6000,'2026-11',[0,18000,0,18000,0,18000],[8000]*6,2000), averaged=monthly(6000,'2026-11',[9000]*6,[8000]*6,2000), early=monthly(6000,'2026-11',[18000,0,18000,0,18000,0],[8000]*6,2000), next_month_reserved=money(10000),cycle_net=money(18000-8000*2)) move=[('2026-11-01','旧房租金',-3000),('2026-11-03','新房押金',-6000),('2026-11-05','新房租金',-3600), ('2026-11-08','搬运',-1200),('2026-11-10','前段生活费',-1500),('2026-11-20','工资',10000), ('2026-11-25','旧押金退回',5000),('2026-11-28','后段生活费',-1500)] result['DJ2-031']=dict(base=events(15000,move,3000), early_refund=events(15000,[(('2026-11-02' if label=='旧押金退回' else day),label,a) for day,label,a in move],3000), delayed_refund=events(15000,[(('2026-12-05' if label=='旧押金退回' else day),label,a) for day,label,a in move],3000), partial_refund=events(15000,[(day,label,4200 if label=='旧押金退回' else a) for day,label,a in move],3000), consumed=money(3000+3600+1200+1500*2),new_locked=money(6000),old_released=money(5000), november_without_refund=money(8200),partial_difference=money(800)) payments=[('2026-11-05','定金',-12000),('2026-12-10','进度款',-30000),('2027-01-15','验收款',-12000),('2027-02-10','尾款',-6000)] savings=[(f'{when[:7]}-25','常规收支后结余',8000) for when in months('2026-11',4)] revised=[(day if label=='定金' else f'{day[:7]}-26',label,a) for day,label,a in payments] result['DJ2-032']=dict(original=events(30000,payments+savings,5000),later=events(30000,revised+savings,5000), monthly_view=monthly(30000,'2026-11',[8000]*4,[12000,30000,12000,6000],5000), contract_total=money(60000),requirements=[dict(date=day,cumulative_payment=money(paid), available_surplus=money(saved),minimum_opening=money(paid+5000-saved)) for day,paid,saved in [('2026-11-05',12000,0),('2026-12-10',42000,8000),('2027-01-15',54000,16000),('2027-02-10',60000,24000)]]) holiday_payments=[('2026-12-15','出行付款',-3000),('2027-01-20','礼物付款',-4500),('2027-02-01','其他节日付款',-4500)] scenarios={} for start,n in [('2026-08',6),('2026-10',4),('2026-11',3),('2026-12',2)]: scenarios[start]=events(2000,holiday_payments+[(when,'每月预留',2000) for when in months(start,n)]) result['DJ2-033']=dict(scenarios=scenarios,november_topups=[dict(date='2027-01-20',amount=money(1500)),dict(date='2027-02-01',amount=money(2500))], total_target=money(12000),november_total_prepared=money(8000)) concentrated=[0,3600]+[0]*10 staggered=[1200,0,0,0,1800,0,0,0,600,0,0,0] result['DJ2-034']=dict(concentrated=monthly(2500,'2026-11',[700]*12,concentrated,1000), staggered=monthly(2500,'2026-11',[700]*12,staggered,1000), averaged=monthly(2500,'2026-11',[700]*12,[300]*12,1000), subscription_amounts=[money(x) for x in [1200,1800,600]],annual_total=money(3600),monthly_average=money(300)) original=monthly(10000,'2027-01',[12000]*12,[10000]*12) actual=monthly(10000,'2027-01',[12000]*3,[10300,10800,10100]) actual_start=actual['closing'] updated=monthly(actual_start,'2027-04',[12400]*9,[10800,10800,10800,10800,13800,10800,10800,10800,10800]) carried=monthly(actual_start,'2027-04',[12000]*9,[10000]*9) result['DJ2-035']=dict(original=original,actual=actual,updated=updated,only_opening_changed=carried, q1_variance=actual_start-original['rows'][2]['closing'], future_monthly_variance=money(400),future_regular_variance=money(400*9),new_once=money(3000), year_end_variance=updated['closing']-original['closing']) quotes={'low':[6000,4000,5000],'middle':[9000,6000,7000],'high':[12000,8000,10000],'mixed':[12000,4000,10000]} result['DJ2-036']={name:monthly(20000,'2026-11',[3000]*6,[0,rep,app,0,care,0],8000) for name,(rep,app,care) in quotes.items()} result['DJ2-036']['quotes']={key:[money(x) for x in value] for key,value in quotes.items()} result['DJ2-036']['march_threshold']=dict(before_project=money(12000+3000),floor=money(8000), maximum_project_without_floor_breach=money(15000-8000),high_project=money(10000),gap=money(3000)) assert len(result)==9 assert result['DJ2-028']['late']['closing']==result['DJ2-028']['early']['closing']==money(6500) assert result['DJ2-029']['schedule']['closing']==ZERO assert result['DJ2-030']['actual']['closing']==result['DJ2-030']['averaged']['closing'] assert result['DJ2-032']['original']['extra_to_floor']==money(13000) assert result['DJ2-034']['concentrated']['closing']==result['DJ2-034']['staggered']['closing']==result['DJ2-034']['averaged']['closing'] assert result['DJ2-035']['year_end_variance']==money(-7800) assert result['DJ2-036']['high']['extra_to_floor']==money(3000) return result def encode(value): if isinstance(value,D): return format(value,'.2f') raise TypeError(type(value).__name__) if __name__=='__main__': output=Path(__file__).resolve().with_suffix('.json') output.write_text(json.dumps(build(),ensure_ascii=False,indent=2,default=encode)+'\n',encoding='utf-8') print(f'R04九篇复算通过,输出 {output}')