#!/usr/bin/env python3
"""Reproducible August close workpaper. Money is integer RMB fen; CSV parsed with csv."""
import csv, io, json
from collections import defaultdict

# Source-indexed unadjusted trial balance. Positive amount and side identify normal side.
TB_CSV = '''source_id,account,side,amount
TB01,Cash,DR,30000
TB02,Accounts receivable,DR,8000
TB03,Prepaid rent,DR,3600
TB04,Equipment,DR,18000
TB05,Salary expense,DR,6000
TB06,Office expense,DR,600
TB07,Accumulated depreciation,CR,3000
TB08,Accounts payable,CR,5200
TB09,Accrued utilities,CR,400
TB10,Deferred revenue,CR,3600
TB11,Service revenue,CR,30000
TB12,Retained earnings,CR,6000
TB13,Capital,CR,18000
'''
# Explicitly supplied source records and decision/status evidence.
SOURCE_ROWS = [
 ('SRC01','Prepaid rent covers Aug 1–Jul 31, total 3600; August unamortized'),
 ('SRC02','Equipment cost 18000, zero residual, 60-month life; accumulated depreciation correct through July'),
 ('SRC03','Utilities invoice 1000 comprises prior accrued July 400 and August 600'),
 ('SRC04','Deferred service revenue 1200 earned in August'),
 ('SRC05','Office expense includes duplicated 300; duplicate has different posting_id, same economic item; reverse against AP'),
 ('SRC06','Bank fee 50 and interest credit 30 not in ledger'),
 ('SRC07','Approved 600 credit memo against unpaid invoice; scanned duplicate is not a second credit'),
 ('SRC08','Bank closing 29630; deposit in transit 900; outstanding checks 400; unexplained bank credit 150 included in closing'),
 ('SRC09','Cash policy requires full reconciliation; no small-item exception. All proposed entries unapproved and unposted.'),
 ('SRC10','Lin accepted update on unknown 150 by Sep 6 noon; Sep 3 review meeting is not its posting/approval/clearing deadline.'),
]
# Journal rows: one signed economic movement per row; debit/credit are fen.
JOURNAL_CSV = '''journal_id,source_id,account,debit,credit,purpose
J01,SRC01,Rent expense,30000,0,August rent amortization
J01,SRC01,Prepaid rent,0,30000,August rent amortization
J02,SRC02,Depreciation expense,30000,0,August straight-line depreciation
J02,SRC02,Accumulated depreciation,0,30000,August straight-line depreciation
J03,SRC03,Accrued utilities,40000,0,clear July accrual upon invoice
J03,SRC03,Utilities expense,60000,0,August consumption
J03,SRC03,Accounts payable,0,100000,record invoice
J04,SRC04,Deferred revenue,120000,0,earned service
J04,SRC04,Service revenue,0,120000,earned service
J05,SRC05,Accounts payable,30000,0,reverse duplicate payable
J05,SRC05,Office expense,0,30000,reverse duplicate expense
J06,SRC06,Bank charges expense,5000,0,bank fee
J06,SRC06,Cash,0,5000,bank fee
J07,SRC06,Cash,3000,0,bank interest
J07,SRC06,Interest income,0,3000,bank interest
J08,SRC07,Service revenue,60000,0,approved credit memo
J08,SRC07,Accounts receivable,0,60000,approved credit memo
'''

def rows(text): return list(csv.DictReader(io.StringIO(text)))
def money(fen):
    sign='-' if fen < 0 else ''
    fen=abs(fen)
    return f'{sign}{fen//100:,}.{fen%100:02d}'

def main():
    opening=defaultdict(int); meta={}
    for r in rows(TB_CSV):
        amt=int(r['amount'])*100
        opening[r['account']] += amt if r['side']=='DR' else -amt
        meta[r['account']]=r['source_id']
    journals=rows(JOURNAL_CSV); journals_by=defaultdict(lambda:[0,0])
    for r in journals:
        d,c=int(r['debit']),int(r['credit'])
        assert (d==0) != (c==0), f"row must be one-sided: {r}"
        journals_by[r['journal_id']][0]+=d; journals_by[r['journal_id']][1]+=c
    assert all(d==c for d,c in journals_by.values()), journals_by
    assert sum(d for d,c in journals_by.values())==sum(c for d,c in journals_by.values())
    adjusted=defaultdict(int,opening)
    for r in journals:
        adjusted[r['account']]+=int(r['debit'])-int(r['credit'])
    assert adjusted['Accrued utilities']==0
    assert adjusted['Accounts receivable']==740000
    assert adjusted['Accounts payable']==-590000
    # Net adjusted TB, each account once, normal debit/credit side.
    tb=[]; dr=cr=0
    for account,balance in sorted(adjusted.items()):
        if balance==0: continue
        side='DR' if balance>0 else 'CR'; amount=abs(balance)
        (dr if side=='DR' else cr)
        if side=='DR': dr+=amount
        else: cr+=amount
        tb.append((meta.get(account,'JOURNAL'),account,side,amount))
    assert dr==cr
    # Statements are constructed from adjusted account balances, no closing entries.
    assets={a:adjusted[a] for a in ['Cash','Accounts receivable','Prepaid rent','Equipment','Accumulated depreciation']}
    total_assets=sum(assets.values())
    liabilities=adjusted['Accounts payable']*-1+adjusted['Accrued utilities']*-1+adjusted['Deferred revenue']*-1
    revenue= -adjusted['Service revenue']-adjusted['Interest income']
    expenses=sum(adjusted[a] for a in ['Salary expense','Office expense','Rent expense','Depreciation expense','Utilities expense','Bank charges expense'])
    profit=revenue-expenses
    equity= -adjusted['Retained earnings']-adjusted['Capital']+profit
    assert total_assets==liabilities+equity, (total_assets,liabilities,equity)
    # Cash reconciliation: positive is unexplained bank > adjusted book.
    bank=2963000; dit=90000; checks=40000; unknown=15000
    bank_adj=bank+dit-checks
    book_cash=adjusted['Cash']
    residual=bank_adj-book_cash
    assert residual==unknown
    out=[]
    out += ['计算口径：人民币；金额元（程序以分整数运算）；8月31日；收入、费用为8月累计。','',
      '### 分录（草稿，均未审批、未过账）','|分录|来源|科目|借方|贷方|用途|','|---|---|---|---:|---:|---|']
    for r in journals:
        out.append(f"|{r['journal_id']}|{r['source_id']}|{r['account']}|{money(int(r['debit']))}|{money(int(r['credit']))}|{r['purpose']}|")
    out.append(f'|**合计**|||**{money(sum(int(r["debit"]) for r in journals))}**|**{money(sum(int(r["credit"]) for r in journals))}**||')
    out += ['', '### 调整后试算表（净余额；每科目仅列一次）','|来源|科目|方向|金额|','|---|---|---|---:|']
    for src,a,s,v in tb: out.append(f'|{src}|{a}|{s}|{money(v)}|')
    out.append(f'|**合计**||**DR / CR**|**{money(dr)} / {money(cr)}**|')
    out += ['', '### 利润表','|项目|金额|','|---|---:|',f'|服务收入|{money(-adjusted["Service revenue"])}|',f'|利息收入|{money(-adjusted["Interest income"])}|',f'|收入合计|{money(revenue)}|']
    for a in ['Salary expense','Office expense','Rent expense','Depreciation expense','Utilities expense','Bank charges expense']:
        out.append(f'|{a}|{money(adjusted[a])}|')
    out += [f'|费用合计|{money(expenses)}|',f'|**本期净利润**|**{money(profit)}**|','', '### 资产负债表（当期净利润列入权益，不作月末结转）','|项目|金额|','|---|---:|',f'|现金|{money(adjusted["Cash"])}|',f'|应收账款|{money(adjusted["Accounts receivable"])}|',f'|预付租金|{money(adjusted["Prepaid rent"])}|',f'|设备原值|{money(adjusted["Equipment"])}|',f'|减：累计折旧|({money(-adjusted["Accumulated depreciation"])})|',f'|资产合计|{money(total_assets)}|',f'|应付账款|{money(-adjusted["Accounts payable"])}|',f'|应计水电|{money(-adjusted["Accrued utilities"])}|',f'|递延收入|{money(-adjusted["Deferred revenue"])}|',f'|负债合计|{money(liabilities)}|',f'|资本|{money(-adjusted["Capital"])}|',f'|留存收益（期初）|{money(-adjusted["Retained earnings"])}|',f'|本期净利润|{money(profit)}|',f'|权益合计|{money(equity)}|',f'|负债及权益合计|{money(liabilities+equity)}|',f'|**勾稽差额**|**{money(total_assets-liabilities-equity)}**|','', '### 银行对账','|项目|金额|','|---|---:|',f'|银行对账单期末|{money(bank)}|',f'|加：未达存款|{money(dit)}|',f'|减：未兑现支票|({money(checks)})|',f'|调整后银行余额（含未查明贷入）|{money(bank_adj)}|',f'|账面现金（拟议费用及利息分录后）|{money(book_cash)}|',f'|**未解释差额：银行高于账面**|**{money(residual)}**|',f'|其中：未查明银行贷入（不得暂记）|{money(unknown)}|']
    print('\n'.join(out))
if __name__=='__main__': main()
