import csv, json
from decimal import Decimal, ROUND_HALF_UP
from datetime import date, timedelta
from pathlib import Path

ROOT = Path(__file__).resolve().parent
D = Decimal
with (ROOT/'source_rows.csv').open(newline='', encoding='utf-8') as f:
    rows = list(csv.DictReader(f))
byid = {r['record_id']: r for r in rows}
assert len(rows) == 9 and len(byid) == 9
po = byid['PO9']; v1=byid['I77v1']; v2=byid['I77v2']
assert v2['source_text'].startswith('Supersedes I77v1')
assert byid['I77v2-DUP']['record_type']=='duplicate_scan'
boxes_per_case = D('8'); unit_rate=D('12'); freight=D('32'); tax_rate=D('0.20')
ordered=D(po['quantity_boxes']); accepted=D(byid['RCPT-WP-20270815']['quantity_boxes']); returned=-D(byid['RET-WP-20270816']['quantity_boxes'])
retained=accepted-returned
backorder=ordered-accepted
replacement_requested=returned
outstanding=backorder+replacement_requested
assert accepted+backorder==ordered
assert accepted-returned==retained
assert retained+outstanding==ordered
# Document-date view: Aug 10 reported cumulative accepted 16; Aug 15 accepted 18.
doc_receipt = {'I77v1':D('16'), 'I77v2':accepted}
assert doc_receipt['I77v1'] <= doc_receipt['I77v2']
# Calculate current superseding invoice only; duplicate scan and superseded version are excluded.
invoice_boxes=D(v2['quantity_boxes']); units=invoice_boxes*boxes_per_case
items=units*unit_rate; taxable=items+freight; vat=(taxable*tax_rate).quantize(D('0.01'), rounding=ROUND_HALF_UP); gross=taxable+vat
credit_gross=D(byid['CN9']['amount']); credit_net=(credit_gross/(D(1)+tax_rate)).quantize(D('0.01'), rounding=ROUND_HALF_UP); credit_vat=credit_gross-credit_net
net_payable=gross-credit_gross
assert (credit_net+credit_vat)==credit_gross
assert net_payable==D('1766.40')
assert invoice_boxes==accepted and invoice_boxes-returned==retained
assert D(v1['quantity_boxes'])==D('20') and doc_receipt['I77v1']==D('16')
term_due=date.fromisoformat('2027-08-15')+timedelta(days=30)
# Lotuses gross invoice remains distinct currency and unknown tax split/rate.
lotus=D(byid['LOTUS-I77']['amount'])
result={
 'chronology':[{'event':'I77v1 issue','date':v1['date'],'accepted_as_of_boxes':str(doc_receipt['I77v1'])}, {'event':'I77v2 issue/supersedes v1','date':v2['date'],'accepted_as_of_boxes':str(doc_receipt['I77v2'])}, {'event':'cumulative accepted receipt','date':'2027-08-15','boxes':str(accepted)}, {'event':'return','date':'2027-08-16','boxes':str(returned)}],
 'fulfillment':{'ordered_boxes':str(ordered),'accepted_boxes':str(accepted),'returned_boxes':str(returned),'net_retained_boxes':str(retained),'never_delivered_backorder_boxes':str(backorder),'returned_replacement_requested_boxes':str(replacement_requested),'total_outstanding_boxes':str(outstanding),'ordered_pieces':str(ordered*boxes_per_case),'accepted_pieces':str(accepted*boxes_per_case),'returned_pieces':str(returned*boxes_per_case),'retained_pieces':str(retained*boxes_per_case),'outstanding_pieces':str(outstanding*boxes_per_case)},
 'invoice_calculation':{'current_invoice':'I77v2','boxes':str(invoice_boxes),'pieces':str(units),'goods_EUR':str(items),'freight_EUR':str(freight),'taxable_base_EUR':str(taxable),'VAT_rate':'20%','VAT_EUR':str(vat),'gross_EUR':str(gross),'credit_CN9_gross_EUR':str(credit_gross),'credit_net_EUR':str(credit_net),'credit_VAT_EUR':str(credit_vat),'net_payable_EUR':str(net_payable),'derived_due_date_from_acceptance_plus_30_calendar_days':term_due.isoformat(),'printed_due_date':'unresolved/blank'},
 'obligations':[{'supplier_association':'Orchid (packet context; invoice issuer and printed buyer unresolved)','currency':'EUR','gross_current_invoice_EUR':str(gross),'credit_EUR':str(credit_gross),'net_EUR':str(net_payable),'release':'HOLD whole payment pending independent validation of conflicting bank instructions and payment authorization'}, {'supplier':'Lotus','currency':'GBP','gross_invoice_GBP':str(lotus),'release':'HOLD; no PO/receipt evidence, bank information or payment authorization'}]
}
(ROOT/'workpaper_output.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
print(json.dumps(result,ensure_ascii=False,indent=2))
