Auto generate engagement tasks after AQMM activation

This commit is contained in:
A R R R Associates
2026-09-22 16:22:08 +05:30
parent cb3da81f6f
commit de47ceb72d
5 changed files with 579 additions and 375 deletions
@@ -88,6 +88,17 @@ def _amount(value: Any) -> float:
return 0.0
ZERO_TOLERANCE = 0.005
def _nonzero(value: Any) -> bool:
return abs(_amount(value)) >= ZERO_TOLERANCE
def _either_nonzero(current: Any, previous: Any) -> bool:
return _nonzero(current) or _nonzero(previous)
def _root_group(group_name: str, parents: dict[str, str]) -> str:
current = str(group_name or "").strip()
if not current:
@@ -463,6 +474,43 @@ def build_financial_statements(
previous_bs["PARTNER_FUNDS"] = round(previous_bs.get("PARTNER_FUNDS", 0.0) + previous_profit, 2)
previous_liabilities = round(sum(previous_bs.get(k,0.0) for k in liability_codes),2)
# Presentation metadata: suppress rows/schedules only when BOTH comparative
# years are nil. This keeps the review engine complete while the final
# statements remain concise and print-ready. Note numbering is shared by
# the web view and Excel export so references remain stable.
presentation_notes: dict[str, int] = {}
next_note = 1
visible_bs_liability_codes: list[str] = []
visible_bs_asset_codes: list[str] = []
for code, meta in sorted(SCHEDULES.items(), key=lambda kv: int(kv[1].get("order", 999))):
if meta.get("statement") != "BS":
continue
cur = current_bs.get(code, 0.0)
prev = previous_bs.get(code, 0.0)
if not _either_nonzero(cur, prev):
continue
presentation_notes[code] = next_note
next_note += 1
if meta.get("bs_side") == "LIABILITY":
visible_bs_liability_codes.append(code)
elif meta.get("bs_side") == "ASSET":
visible_bs_asset_codes.append(code)
visible_pl_note_codes: list[str] = []
for code in ["REVENUE", "OTHER_INCOME"]:
if _either_nonzero(current_pl.get(code, 0.0), previous_pl.get(code, 0.0)):
presentation_notes[code] = next_note
visible_pl_note_codes.append(code)
next_note += 1
if _either_nonzero(current_pl.get("COST_OF_GOODS_SOLD", 0.0), previous_pl.get("COST_OF_GOODS_SOLD", 0.0)):
presentation_notes["COST_OF_GOODS_SOLD"] = next_note
next_note += 1
for code in ["DIRECT_EXPENSES", "EMPLOYEE_BENEFITS", "FINANCE_COST", "DEPRECIATION", "OTHER_EXPENSES"]:
if _either_nonzero(current_pl.get(code, 0.0), previous_pl.get(code, 0.0)):
presentation_notes[code] = next_note
visible_pl_note_codes.append(code)
next_note += 1
return {
"current_totals": current_totals,
"previous_totals": previous_totals,
@@ -480,6 +528,10 @@ def build_financial_statements(
"profit_transfer_status": profit_transfer_status,
"balance_sheet_difference": round(current_assets - current_liabilities, 2),
"unmapped": [x for x in current_lines if x.schedule_code == "UNMAPPED"],
"presentation_notes": presentation_notes,
"visible_bs_liability_codes": visible_bs_liability_codes,
"visible_bs_asset_codes": visible_bs_asset_codes,
"visible_pl_note_codes": visible_pl_note_codes,
}
@@ -545,325 +597,458 @@ def _schedule_sheet_name(note_no: int, label: str) -> str:
return f"N{note_no:02d} {clean}"[:31]
def _excel_formula(cell, formula: str) -> None:
cell.value = formula
cell.number_format = '#,##0;[Red](#,##0);-'
cell.alignment = Alignment(horizontal="right")
def _excel_quote_sheet(name: str) -> str:
return "'" + str(name).replace("'", "''") + "'"
def _union_schedule_rows(code: str, current_lines: list[LedgerLine], previous_lines: list[LedgerLine]) -> list[tuple[str, float, float]]:
cur = {_norm(x.ledger_name): x for x in current_lines if x.schedule_code == code}
prev = {_norm(x.ledger_name): x for x in previous_lines if x.schedule_code == code}
display_names: dict[str, str] = {}
for x in current_lines:
if x.schedule_code == code:
display_names.setdefault(_norm(x.ledger_name), x.ledger_name)
for x in previous_lines:
if x.schedule_code == code:
display_names.setdefault(_norm(x.ledger_name), x.ledger_name)
rows: list[tuple[str, float, float]] = []
for key in sorted(set(cur) | set(prev), key=lambda k: display_names.get(k, k).casefold()):
current = cur.get(key).reporting_amount if key in cur else 0.0
previous = prev.get(key).reporting_amount if key in prev else 0.0
# Presentation rule requested by the firm: omit a ledger only when BOTH
# comparative-year balances are nil. Abnormal balances are retained.
if _either_nonzero(current, previous):
rows.append((display_names.get(key, key), current, previous))
return rows
def build_excel_workbook(
*, client_name: str, address: str, financial_year: str, previous_fy: str,
current_lines: list[LedgerLine], previous_lines: list[LedgerLine],
statements: dict[str, Any], partnership_shares: list[dict[str, Any]] | None = None,
) -> bytes:
wb=Workbook()
wb = Workbook()
wb.remove(wb.active)
start_year=int(financial_year.split('-')[0])
current_end=f"31st March {start_year+1}"
previous_end=f"31st March {start_year}"
try:
wb.calculation.calcMode = "auto"
wb.calculation.fullCalcOnLoad = True
wb.calculation.forceFullCalc = True
except Exception:
pass
schedule_codes=[]
for code,meta in sorted(SCHEDULES.items(), key=lambda kv:int(kv[1].get('order',999))):
if code=="UNMAPPED":
continue
if any(x.schedule_code==code for x in current_lines) or any(x.schedule_code==code for x in previous_lines):
schedule_codes.append(code)
note_map={code:i+1 for i,code in enumerate(schedule_codes)}
start_year = int(financial_year.split('-')[0])
current_end = f"31st March {start_year + 1}"
previous_end = f"31st March {start_year}"
note_map = dict(statements.get("presentation_notes") or {})
# Trial Balance
ws=wb.create_sheet("Trial Balance")
_set_page(ws, landscape=True, repeat_rows="1:5")
_title(ws,client_name,address,"Trial Balance",current_end,8)
_header_row(ws,5,["Ledger","Tally Group","Opening Dr","Opening Cr","Current Dr","Current Cr","Closing Dr","Closing Cr"])
r=6
for line in sorted(current_lines,key=lambda x:(x.root_group.lower(),x.parent_group.lower(),x.ledger_name.lower())):
ws.cell(r,1,line.ledger_name); ws.cell(r,2,line.parent_group)
for col,val in [(3,line.opening_debit),(4,line.opening_credit),(5,line.debit),(6,line.credit),(7,line.closing_debit),(8,line.closing_credit)]: _money(ws.cell(r,col),val)
r+=1
ws.cell(r,1,"Total").font=Font(bold=True)
for col in range(3,9):
_money(ws.cell(r,col),sum(float(ws.cell(rr,col).value or 0) for rr in range(6,r)))
ws.cell(r,col).font=Font(bold=True); ws.cell(r,col).border=Border(top=DOUBLE,bottom=DOUBLE)
_apply_table_border(ws,5,r,1,8)
widths=[34,24,14,14,14,14,14,14]
for i,w in enumerate(widths,1): ws.column_dimensions[get_column_letter(i)].width=w
ws.print_area = f"A1:H{r}"
# ----------------------- Trial Balance (review sheet) -----------------------
ws_tb = wb.create_sheet("Trial Balance")
_set_page(ws_tb, landscape=True, repeat_rows="1:5")
_title(ws_tb, client_name, address, "Trial Balance", current_end, 8)
_header_row(ws_tb, 5, ["Ledger", "Tally Group", "Opening Dr", "Opening Cr", "Current Dr", "Current Cr", "Closing Dr", "Closing Cr"])
r = 6
for line in sorted(current_lines, key=lambda x: (x.root_group.casefold(), x.parent_group.casefold(), x.ledger_name.casefold())):
ws_tb.cell(r, 1, line.ledger_name)
ws_tb.cell(r, 2, line.parent_group)
for col, val in [(3, line.opening_debit), (4, line.opening_credit), (5, line.debit), (6, line.credit), (7, line.closing_debit), (8, line.closing_credit)]:
_money(ws_tb.cell(r, col), val)
r += 1
total_row = r
ws_tb.cell(total_row, 1, "Total").font = Font(bold=True)
for col in range(3, 9):
letter = get_column_letter(col)
_excel_formula(ws_tb.cell(total_row, col), f"=SUM({letter}6:{letter}{total_row - 1})")
ws_tb.cell(total_row, col).font = Font(bold=True)
ws_tb.cell(total_row, col).border = Border(top=DOUBLE, bottom=DOUBLE)
_apply_table_border(ws_tb, 5, total_row, 1, 8)
for i, w in enumerate([34, 24, 14, 14, 14, 14, 14, 14], 1):
ws_tb.column_dimensions[get_column_letter(i)].width = w
ws_tb.print_area = f"A1:H{total_row}"
# Balance Sheet in sample format
ws=wb.create_sheet("Balance Sheet")
_set_page(ws, repeat_rows="1:5")
_title(ws,client_name,address,"Statement of Balance Sheet",current_end,4)
_header_row(ws,5,["Particulars","Note",current_end,previous_end])
r=6
ws.cell(r,1,"I EQUITY AND LIABILITIES").font=Font(bold=True); r+=1
liability_codes=[c for c in schedule_codes if SCHEDULES[c].get("bs_side")=="LIABILITY"]
for code in liability_codes:
meta=SCHEDULES[code]; ws.cell(r,1,meta["label"]); ws.cell(r,2,note_map[code])
ws.cell(r,2).hyperlink=f"#'{_schedule_sheet_name(note_map[code],meta['label'])}'!A1"
_money(ws.cell(r,3),statements["current_bs"].get(code,0)); _money(ws.cell(r,4),statements.get("previous_bs", statements["previous_totals"]).get(code,0)); r+=1
ws.cell(r,1,"Total Liabilities").font=Font(bold=True)
_money(ws.cell(r,3),statements["current_liabilities"]); _money(ws.cell(r,4),statements["previous_liabilities"])
for c in range(1,5): ws.cell(r,c).border=Border(top=DOUBLE,bottom=DOUBLE)
r+=2; ws.cell(r,1,"II ASSETS").font=Font(bold=True); r+=1
asset_codes=[c for c in schedule_codes if SCHEDULES[c].get("bs_side")=="ASSET"]
for code in asset_codes:
meta=SCHEDULES[code]; ws.cell(r,1,meta["label"]); ws.cell(r,2,note_map[code])
ws.cell(r,2).hyperlink=f"#'{_schedule_sheet_name(note_map[code],meta['label'])}'!A1"
_money(ws.cell(r,3),statements["current_bs"].get(code,0)); _money(ws.cell(r,4),statements.get("previous_bs", statements["previous_totals"]).get(code,0)); r+=1
ws.cell(r,1,"Total Assets").font=Font(bold=True)
_money(ws.cell(r,3),statements["current_assets"]); _money(ws.cell(r,4),statements["previous_assets"])
for c in range(1,5): ws.cell(r,c).border=Border(top=DOUBLE,bottom=DOUBLE)
r+=2; ws.merge_cells(start_row=r,start_column=1,end_row=r,end_column=4)
ws.cell(r,1,"The accompanying notes are an integral part of the financial statements").font=Font(italic=True,size=8)
_apply_table_border(ws,5,r-2,1,4)
for col,w in {1:46,2:9,3:18,4:18}.items(): ws.column_dimensions[get_column_letter(col)].width=w
ws.print_area = f"A1:D{r}"
# Profit and Loss
ws=wb.create_sheet("Profit and Loss")
_set_page(ws, repeat_rows="1:5")
_title(ws,client_name,address,"Statement of Profit and Loss",current_end,4)
_header_row(ws,5,["Particulars","Note",current_end,previous_end])
cp=statements["current_pl"]; pp=statements["previous_pl"]
rows=[
("I", "Revenue from operations", "REVENUE", "REVENUE"),
("II", "Other Income", "OTHER_INCOME", "OTHER_INCOME"),
("III", "Total Income (I+II)", None, "TOTAL_INCOME"),
("IV", "Expenses:", None, None),
("(a)", "Cost of goods sold", None, "COST_OF_GOODS_SOLD"),
("(b)", "Direct Expenses", "DIRECT_EXPENSES", "DIRECT_EXPENSES"),
("(c)", "Employee benefits expense", "EMPLOYEE_BENEFITS", "EMPLOYEE_BENEFITS"),
("(d)", "Finance Cost", "FINANCE_COST", "FINANCE_COST"),
("(e)", "Depreciation and amortization expense", "DEPRECIATION", "DEPRECIATION"),
("(f)", "Other expenses", "OTHER_EXPENSES", "OTHER_EXPENSES"),
("", "Total expenses", None, "TOTAL_EXPENSES"),
("V", "Profit/(loss) before exceptional and extraordinary items (III-IV)", None, "PROFIT"),
("VI", "Exceptional items", None, None),
("VII", "Profit/(loss) before extraordinary items (V-VI)", None, "PROFIT"),
("VIII", "Extraordinary items", None, None),
("IX", "Profit before tax (VII-VIII)", None, "PROFIT"),
("X", "Profit/(Loss) for the year", None, "PROFIT"),
]
r=6
for roman,label,note_code,key in rows:
ws.cell(r,1,f"{roman} {label}".strip())
if roman in {"III","IV","V","VII","IX","X"} or label=="Total expenses": ws.cell(r,1).font=Font(bold=True)
if note_code and note_code in note_map:
ws.cell(r,2,note_map[note_code]); ws.cell(r,2).hyperlink=f"#'{_schedule_sheet_name(note_map[note_code],SCHEDULES[note_code]['label'])}'!A1"
if key:
_money(ws.cell(r,3),cp.get(key,0)); _money(ws.cell(r,4),pp.get(key,0))
if roman in {"III","V","VII","IX","X"} or label=="Total expenses":
for c in range(1,5): ws.cell(r,c).border=Border(top=THIN,bottom=DOUBLE)
r+=1
r+=1; ws.merge_cells(start_row=r,start_column=1,end_row=r,end_column=4)
ws.cell(r,1,"The accompanying notes are an integral part of the financial statements").font=Font(italic=True,size=8)
_apply_table_border(ws,5,r-2,1,4)
for col,w in {1:56,2:9,3:18,4:18}.items(): ws.column_dimensions[get_column_letter(col)].width=w
ws.print_area = f"A1:D{r}"
# Cost of goods sold schedule (opening inventory + purchases - closing inventory).
ws=wb.create_sheet("Cost of Goods Sold")
_set_page(ws)
_title(ws,client_name,address,"Note - Cost of Goods Sold",current_end,3)
_header_row(ws,5,["Particulars",current_end,previous_end])
cpl=statements["current_pl"]; ppl=statements["previous_pl"]
cogs_rows=[
("Inventory at the beginning of the year","OPENING_STOCK"),
("Add: Purchases during the year","PURCHASES"),
("Less: Inventory at the end of the year","CLOSING_STOCK"),
("Cost of goods sold","COST_OF_GOODS_SOLD"),
]
r=6
for label,key in cogs_rows:
ws.cell(r,1,label); _money(ws.cell(r,2),cpl.get(key,0)); _money(ws.cell(r,3),ppl.get(key,0))
if key=="COST_OF_GOODS_SOLD":
ws.cell(r,1).font=Font(bold=True)
for c in range(1,4): ws.cell(r,c).border=Border(top=DOUBLE,bottom=DOUBLE)
r+=1
_apply_table_border(ws,5,r-1,1,3)
ws.column_dimensions['A'].width=52; ws.column_dimensions['B'].width=18; ws.column_dimensions['C'].width=18
# Notes/schedules
prev_by_name={_norm(x.ledger_name):x for x in previous_lines}
for code in schedule_codes:
meta=SCHEDULES[code]; note=note_map[code]
ws=wb.create_sheet(_schedule_sheet_name(note,meta["label"]))
_set_page(ws, repeat_rows="1:5")
_title(ws,client_name,address,f"Note - {note}: {meta['label']}",current_end,3)
_header_row(ws,5,["Particulars",current_end,previous_end])
cur=[x for x in current_lines if x.schedule_code==code]
r=6
for line in sorted(cur,key=lambda x:x.ledger_name.lower()):
ws.cell(r,1,line.ledger_name); _money(ws.cell(r,2),line.reporting_amount)
old=prev_by_name.get(_norm(line.ledger_name)); _money(ws.cell(r,3),old.reporting_amount if old else 0); r+=1
if code=="PARTNER_FUNDS" and statements.get("profit_transfer_applied"):
if statements.get("partnership_allocations"):
for alloc in statements["partnership_allocations"]:
ws.cell(r,1,f"Add: Share of Profit / (Loss) - {alloc.get('partner_name')}")
_money(ws.cell(r,2),alloc.get("amount") or 0); _money(ws.cell(r,3),0); r+=1
else:
ws.cell(r,1,"Add: Current Year Profit / (Loss)")
_money(ws.cell(r,2),statements.get("current_pl",{}).get("PROFIT",0)); _money(ws.cell(r,3),0); r+=1
ws.cell(r,1,"Total").font=Font(bold=True)
_money(ws.cell(r,2),statements["current_bs"].get(code,statements["current_totals"].get(code,0)))
_money(ws.cell(r,3),statements.get("previous_bs", statements["previous_totals"]).get(code,0))
for c in range(1,4): ws.cell(r,c).border=Border(top=DOUBLE,bottom=DOUBLE)
_apply_table_border(ws,5,r,1,3)
ws.column_dimensions['A'].width=52; ws.column_dimensions['B'].width=18; ws.column_dimensions['C'].width=18
if partnership_shares:
ws=wb.create_sheet("Partner Profit Allocation")
_set_page(ws)
_title(ws,client_name,address,"Partnership Profit Allocation",current_end,4)
_header_row(ws,5,["Partner","Profit Share %","Target Capital / Current Ledger","Share of Profit / (Loss)"])
alloc_by_name={_norm(x.get('partner_name')):x for x in statements.get('partnership_allocations') or []}
r=6
for share in partnership_shares:
ws.cell(r,1,share.get('partner_name')); ws.cell(r,2,float(share.get('profit_share_percent') or 0)); ws.cell(r,2).number_format='0.00%'
# Value stored as percent e.g. 20; Excel percentage needs /100
ws.cell(r,2).value=float(share.get('profit_share_percent') or 0)/100.0
ws.cell(r,3,share.get('target_ledger_name') or '')
alloc=alloc_by_name.get(_norm(share.get('partner_name'))); _money(ws.cell(r,4),(alloc or {}).get('amount') or 0); r+=1
ws.cell(r,1,"Total").font=Font(bold=True); ws.cell(r,2,sum(float(x.get('profit_share_percent') or 0) for x in partnership_shares)/100.0); ws.cell(r,2).number_format='0.00%'
_money(ws.cell(r,4),sum(float((alloc_by_name.get(_norm(x.get('partner_name'))) or {}).get('amount') or 0) for x in partnership_shares))
_apply_table_border(ws,5,r,1,4)
for col,w in {1:32,2:16,3:38,4:20}.items(): ws.column_dimensions[get_column_letter(col)].width=w
# Consolidated Notes & Schedules: all notes and annexures in one printable
# sheet, aligned one below another. Individual note sheets above are kept
# (hidden) for backward compatibility with existing workbook consumers.
# ---------------- Consolidated Notes & Schedules (primary schedule pack) ----------------
ws_notes = wb.create_sheet("Notes & Schedules")
_set_page(ws_notes, repeat_rows=None)
_set_page(ws_notes)
ws_notes.freeze_panes = "A5"
ws_notes.page_setup.orientation = "portrait"
ws_notes.page_setup.fitToWidth = 1
ws_notes.page_setup.fitToHeight = 0
ws_notes.sheet_properties.pageSetUpPr.fitToPage = True
ws_notes.sheet_view.showGridLines = False
ws_notes.page_margins.left = 0.28
ws_notes.page_margins.right = 0.28
ws_notes.page_margins.top = 0.35
ws_notes.page_margins.bottom = 0.35
ws_notes.merge_cells("A1:C1"); ws_notes["A1"] = client_name; ws_notes["A1"].font = Font(bold=True,size=11)
ws_notes.merge_cells("A2:C2"); ws_notes["A2"] = address; ws_notes["A2"].font = Font(size=9)
ws_notes.merge_cells("A3:C3"); ws_notes["A3"] = f"Notes Forming Part of the Financial Statements for the Year Ended {current_end}"
ws_notes["A3"].font = Font(bold=True,underline="single",size=10)
ws_notes.merge_cells("A1:C1")
ws_notes["A1"] = client_name
ws_notes["A1"].font = Font(bold=True, size=11)
ws_notes.merge_cells("A2:C2")
ws_notes["A2"] = address
ws_notes["A2"].font = Font(size=9)
ws_notes.merge_cells("A3:C3")
ws_notes["A3"] = f"Notes Forming Part of the Financial Statements for the Year Ended {current_end}"
ws_notes["A3"].font = Font(bold=True, underline="single", size=10)
ws_notes.column_dimensions['A'].width = 58
ws_notes.column_dimensions['B'].width = 18
ws_notes.column_dimensions['C'].width = 18
consolidated_anchor: dict[str, int] = {}
note_anchor: dict[str, int] = {}
note_total_refs: dict[str, tuple[str, str]] = {}
partner_formula_cells: list[tuple[str, float]] = []
r_notes = 5
prev_by_name={_norm(x.ledger_name):x for x in previous_lines}
for code in schedule_codes:
meta = SCHEDULES[code]; note = note_map[code]
consolidated_anchor[code] = r_notes
ws_notes.merge_cells(start_row=r_notes,start_column=1,end_row=r_notes,end_column=3)
title_cell = ws_notes.cell(r_notes,1,f"Note - {note}: {meta['label']}")
title_cell.font = Font(bold=True,size=9)
title_cell.fill = PatternFill("solid", fgColor="EDEDED")
title_cell.border = Border(top=MEDIUM,bottom=MEDIUM,left=MEDIUM,right=MEDIUM)
# Only numbered notes that are actually visible in the final statements.
visible_note_codes = sorted(note_map, key=lambda c: note_map[c])
for code in visible_note_codes:
if code == "COST_OF_GOODS_SOLD":
continue
if code not in SCHEDULES:
continue
meta = SCHEDULES[code]
note_anchor[code] = r_notes
ws_notes.merge_cells(start_row=r_notes, start_column=1, end_row=r_notes, end_column=3)
tc = ws_notes.cell(r_notes, 1, f"Note - {note_map[code]}: {meta['label']}")
tc.font = Font(bold=True, size=9)
tc.fill = PatternFill("solid", fgColor="EDEDED")
tc.border = Border(top=MEDIUM, bottom=MEDIUM, left=MEDIUM, right=MEDIUM)
r_notes += 1
_header_row(ws_notes,r_notes,["Particulars",current_end,previous_end])
_header_row(ws_notes, r_notes, ["Particulars", current_end, previous_end])
block_start = r_notes
detail_start = r_notes + 1
r_notes += 1
cur=[x for x in current_lines if x.schedule_code==code]
for line in sorted(cur,key=lambda x:x.ledger_name.lower()):
ws_notes.cell(r_notes,1,line.ledger_name)
_money(ws_notes.cell(r_notes,2),line.reporting_amount)
old=prev_by_name.get(_norm(line.ledger_name))
_money(ws_notes.cell(r_notes,3),old.reporting_amount if old and old.schedule_code==code else 0)
rows = _union_schedule_rows(code, current_lines, previous_lines)
for label, current_value, previous_value in rows:
ws_notes.cell(r_notes, 1, label)
_money(ws_notes.cell(r_notes, 2), current_value)
_money(ws_notes.cell(r_notes, 3), previous_value)
r_notes += 1
if code=="PARTNER_FUNDS" and statements.get("profit_transfer_applied"):
if statements.get("partnership_allocations"):
for alloc in statements["partnership_allocations"]:
ws_notes.cell(r_notes,1,f"Add: Share of Profit / (Loss) - {alloc.get('partner_name')}")
_money(ws_notes.cell(r_notes,2),alloc.get("amount") or 0); _money(ws_notes.cell(r_notes,3),0); r_notes += 1
# Read-only finalisation entries are formulas, not hard-coded values.
adjustment_start = r_notes
if code == "PARTNER_FUNDS" and statements.get("profit_transfer_applied"):
allocations = statements.get("partnership_allocations") or []
if allocations:
for alloc in allocations:
ws_notes.cell(r_notes, 1, f"Add: Share of Profit / (Loss) - {alloc.get('partner_name')}")
ratio = float(alloc.get("profit_share_percent") or 0) / 100.0
partner_formula_cells.append((f"B{r_notes}", ratio))
_excel_formula(ws_notes.cell(r_notes, 2), "=0") # patched after P&L row is known
_money(ws_notes.cell(r_notes, 3), 0)
r_notes += 1
else:
ws_notes.cell(r_notes,1,"Add: Current Year Profit / (Loss)")
_money(ws_notes.cell(r_notes,2),statements.get("current_pl",{}).get("PROFIT",0)); _money(ws_notes.cell(r_notes,3),0); r_notes += 1
ws_notes.cell(r_notes,1,"Total").font = Font(bold=True)
_money(ws_notes.cell(r_notes,2),statements["current_bs"].get(code,statements["current_totals"].get(code,0)))
_money(ws_notes.cell(r_notes,3),statements.get("previous_bs",statements["previous_totals"]).get(code,0))
for c in range(1,4): ws_notes.cell(r_notes,c).border=Border(top=DOUBLE,bottom=DOUBLE)
_apply_table_border(ws_notes,block_start,r_notes,1,3)
ws_notes.cell(r_notes, 1, "Add: Current Year Profit / (Loss)")
partner_formula_cells.append((f"B{r_notes}", 1.0))
_excel_formula(ws_notes.cell(r_notes, 2), "=0")
_money(ws_notes.cell(r_notes, 3), 0)
r_notes += 1
total = r_notes
ws_notes.cell(total, 1, "Total").font = Font(bold=True)
if total - 1 >= detail_start:
_excel_formula(ws_notes.cell(total, 2), f"=SUM(B{detail_start}:B{total - 1})")
_excel_formula(ws_notes.cell(total, 3), f"=SUM(C{detail_start}:C{total - 1})")
else:
_excel_formula(ws_notes.cell(total, 2), "=0")
_excel_formula(ws_notes.cell(total, 3), "=0")
for cc in range(1, 4):
ws_notes.cell(total, cc).border = Border(top=DOUBLE, bottom=DOUBLE)
_apply_table_border(ws_notes, block_start, total, 1, 3)
note_total_refs[code] = (f"B{total}", f"C{total}")
r_notes = total + 3
# Purchase detail annexure is formula-linked into Cost of Goods Sold.
purchase_anchor = r_notes
ws_notes.merge_cells(start_row=r_notes, start_column=1, end_row=r_notes, end_column=3)
pc = ws_notes.cell(r_notes, 1, "Annexure - Purchases")
pc.font = Font(bold=True, size=9)
pc.fill = PatternFill("solid", fgColor="F6F6F6")
pc.border = Border(top=MEDIUM, bottom=MEDIUM, left=MEDIUM, right=MEDIUM)
r_notes += 1
_header_row(ws_notes, r_notes, ["Particulars", current_end, previous_end])
purchase_block_start = r_notes
purchase_detail_start = r_notes + 1
r_notes += 1
for label, current_value, previous_value in _union_schedule_rows("PURCHASES", current_lines, previous_lines):
ws_notes.cell(r_notes, 1, label)
_money(ws_notes.cell(r_notes, 2), current_value)
_money(ws_notes.cell(r_notes, 3), previous_value)
r_notes += 1
purchase_total_row = r_notes
ws_notes.cell(r_notes, 1, "Total Purchases").font = Font(bold=True)
if purchase_total_row - 1 >= purchase_detail_start:
_excel_formula(ws_notes.cell(r_notes, 2), f"=SUM(B{purchase_detail_start}:B{purchase_total_row - 1})")
_excel_formula(ws_notes.cell(r_notes, 3), f"=SUM(C{purchase_detail_start}:C{purchase_total_row - 1})")
else:
_excel_formula(ws_notes.cell(r_notes, 2), "=0")
_excel_formula(ws_notes.cell(r_notes, 3), "=0")
for cc in range(1, 4): ws_notes.cell(r_notes, cc).border = Border(top=DOUBLE, bottom=DOUBLE)
_apply_table_border(ws_notes, purchase_block_start, purchase_total_row, 1, 3)
r_notes += 3
# Numbered Cost of Goods Sold note, matching the final P&L presentation.
if "COST_OF_GOODS_SOLD" in note_map:
code = "COST_OF_GOODS_SOLD"
note_anchor[code] = r_notes
ws_notes.merge_cells(start_row=r_notes, start_column=1, end_row=r_notes, end_column=3)
cc = ws_notes.cell(r_notes, 1, f"Note - {note_map[code]}: Cost of Goods Sold")
cc.font = Font(bold=True, size=9)
cc.fill = PatternFill("solid", fgColor="EDEDED")
cc.border = Border(top=MEDIUM, bottom=MEDIUM, left=MEDIUM, right=MEDIUM)
r_notes += 1
_header_row(ws_notes, r_notes, ["Particulars", current_end, previous_end])
cogs_block_start = r_notes
r_notes += 1
inventory_ref = note_total_refs.get("INVENTORIES")
ws_notes.cell(r_notes, 1, "Inventory at the beginning of the year")
# Current opening inventory equals previous-year closing inventory when comparative note exists.
if inventory_ref:
_excel_formula(ws_notes.cell(r_notes, 2), f"={inventory_ref[1]}")
else:
_money(ws_notes.cell(r_notes, 2), statements["current_pl"].get("OPENING_STOCK", 0))
_money(ws_notes.cell(r_notes, 3), statements["previous_pl"].get("OPENING_STOCK", 0))
r_notes += 1
ws_notes.cell(r_notes, 1, "Add: Purchases during the year")
_excel_formula(ws_notes.cell(r_notes, 2), f"=B{purchase_total_row}")
_excel_formula(ws_notes.cell(r_notes, 3), f"=C{purchase_total_row}")
r_notes += 1
ws_notes.cell(r_notes, 1, "Less: Inventory at the end of the year")
if inventory_ref:
_excel_formula(ws_notes.cell(r_notes, 2), f"={inventory_ref[0]}")
_excel_formula(ws_notes.cell(r_notes, 3), f"={inventory_ref[1]}")
else:
_money(ws_notes.cell(r_notes, 2), statements["current_pl"].get("CLOSING_STOCK", 0))
_money(ws_notes.cell(r_notes, 3), statements["previous_pl"].get("CLOSING_STOCK", 0))
r_notes += 1
cogs_total_row = r_notes
ws_notes.cell(r_notes, 1, "Cost of goods sold").font = Font(bold=True)
_excel_formula(ws_notes.cell(r_notes, 2), f"=B{r_notes-3}+B{r_notes-2}-B{r_notes-1}")
_excel_formula(ws_notes.cell(r_notes, 3), f"=C{r_notes-3}+C{r_notes-2}-C{r_notes-1}")
for cidx in range(1, 4): ws_notes.cell(r_notes, cidx).border = Border(top=DOUBLE, bottom=DOUBLE)
_apply_table_border(ws_notes, cogs_block_start, cogs_total_row, 1, 3)
note_total_refs[code] = (f"B{cogs_total_row}", f"C{cogs_total_row}")
r_notes += 3
# Cost of Goods Sold annexure follows the numbered notes.
cogs_anchor = r_notes
ws_notes.merge_cells(start_row=r_notes,start_column=1,end_row=r_notes,end_column=3)
c=ws_notes.cell(r_notes,1,"Annexure - Cost of Goods Sold")
c.font=Font(bold=True,size=9); c.fill=PatternFill("solid",fgColor="EDEDED")
c.border=Border(top=MEDIUM,bottom=MEDIUM,left=MEDIUM,right=MEDIUM)
r_notes += 1
_header_row(ws_notes,r_notes,["Particulars",current_end,previous_end]); block_start=r_notes; r_notes += 1
for label,key in [("Inventory at the beginning of the year","OPENING_STOCK"),("Add: Purchases during the year","PURCHASES"),("Less: Inventory at the end of the year","CLOSING_STOCK"),("Cost of goods sold","COST_OF_GOODS_SOLD")]:
ws_notes.cell(r_notes,1,label); _money(ws_notes.cell(r_notes,2),statements["current_pl"].get(key,0)); _money(ws_notes.cell(r_notes,3),statements["previous_pl"].get(key,0))
if key=="COST_OF_GOODS_SOLD":
ws_notes.cell(r_notes,1).font=Font(bold=True)
for cc in range(1,4): ws_notes.cell(r_notes,cc).border=Border(top=DOUBLE,bottom=DOUBLE)
r_notes += 1
_apply_table_border(ws_notes,block_start,r_notes-1,1,3)
r_notes += 2
# Optional partnership annexure remains visible and formula-driven.
allocation_cells: list[tuple[str, float]] = []
if partnership_shares:
ws_notes.merge_cells(start_row=r_notes,start_column=1,end_row=r_notes,end_column=3)
c=ws_notes.cell(r_notes,1,"Annexure - Partnership Profit Allocation")
c.font=Font(bold=True,size=9); c.fill=PatternFill("solid",fgColor="EDEDED")
c.border=Border(top=MEDIUM,bottom=MEDIUM,left=MEDIUM,right=MEDIUM)
ws_notes.merge_cells(start_row=r_notes, start_column=1, end_row=r_notes, end_column=3)
ac = ws_notes.cell(r_notes, 1, "Annexure - Partnership Profit Allocation")
ac.font = Font(bold=True, size=9)
ac.fill = PatternFill("solid", fgColor="F6F6F6")
ac.border = Border(top=MEDIUM, bottom=MEDIUM, left=MEDIUM, right=MEDIUM)
r_notes += 1
_header_row(ws_notes, r_notes, ["Partner / Target Ledger", "Profit Share %", "Share of Profit / (Loss)"])
alloc_block_start = r_notes
r_notes += 1
_header_row(ws_notes,r_notes,["Partner / Target Ledger","Profit Share %","Share of Profit / (Loss)"]); block_start=r_notes; r_notes += 1
alloc_by_name={_norm(x.get('partner_name')):x for x in statements.get('partnership_allocations') or []}
for share in partnership_shares:
label=f"{share.get('partner_name') or ''}"
if share.get('target_ledger_name'): label += f" - {share.get('target_ledger_name')}"
ws_notes.cell(r_notes,1,label)
ws_notes.cell(r_notes,2,float(share.get('profit_share_percent') or 0)/100.0); ws_notes.cell(r_notes,2).number_format='0.00%'
alloc=alloc_by_name.get(_norm(share.get('partner_name'))); _money(ws_notes.cell(r_notes,3),(alloc or {}).get('amount') or 0); r_notes += 1
ws_notes.cell(r_notes,1,"Total").font=Font(bold=True)
ws_notes.cell(r_notes,2,sum(float(x.get('profit_share_percent') or 0) for x in partnership_shares)/100.0); ws_notes.cell(r_notes,2).number_format='0.00%'
_money(ws_notes.cell(r_notes,3),sum(float((alloc_by_name.get(_norm(x.get('partner_name'))) or {}).get('amount') or 0) for x in partnership_shares))
for cc in range(1,4): ws_notes.cell(r_notes,cc).border=Border(top=DOUBLE,bottom=DOUBLE)
_apply_table_border(ws_notes,block_start,r_notes,1,3)
label = str(share.get('partner_name') or '')
if share.get('target_ledger_name'):
label += f" - {share.get('target_ledger_name')}"
ws_notes.cell(r_notes, 1, label)
ratio = float(share.get('profit_share_percent') or 0) / 100.0
ws_notes.cell(r_notes, 2, ratio)
ws_notes.cell(r_notes, 2).number_format = '0.00%'
allocation_cells.append((f"C{r_notes}", ratio))
_excel_formula(ws_notes.cell(r_notes, 3), "=0") # patched after P&L
r_notes += 1
alloc_total_row = r_notes
ws_notes.cell(r_notes, 1, "Total").font = Font(bold=True)
_excel_formula(ws_notes.cell(r_notes, 2), f"=SUM(B{alloc_block_start+1}:B{r_notes-1})")
ws_notes.cell(r_notes, 2).number_format = '0.00%'
_excel_formula(ws_notes.cell(r_notes, 3), f"=SUM(C{alloc_block_start+1}:C{r_notes-1})")
for cidx in range(1, 4): ws_notes.cell(r_notes, cidx).border = Border(top=DOUBLE, bottom=DOUBLE)
_apply_table_border(ws_notes, alloc_block_start, alloc_total_row, 1, 3)
r_notes += 2
ws_notes.print_area = f"A1:C{max(1,r_notes)}"
ws_notes.print_area = f"A1:C{max(1, r_notes)}"
# Point Balance Sheet and P&L note links to the consolidated schedule sheet.
for sheet_name in ("Balance Sheet","Profit and Loss"):
ws_link = wb[sheet_name]
for rr in range(6, ws_link.max_row + 1):
note_val = ws_link.cell(rr,2).value
try:
note_no = int(note_val)
except (TypeError,ValueError):
continue
code = next((k for k,v in note_map.items() if int(v)==note_no),None)
if code and code in consolidated_anchor:
ws_link.cell(rr,2).hyperlink = f"#'Notes & Schedules'!A{consolidated_anchor[code]}"
# Cost-of-goods-sold line links to its annexure without disturbing note numbering.
ws_pl = wb["Profit and Loss"]
for rr in range(6, ws_pl.max_row + 1):
if "cost of goods sold" in _norm(ws_pl.cell(rr,1).value):
ws_pl.cell(rr,2).value = "Sch."
ws_pl.cell(rr,2).hyperlink = f"#'Notes & Schedules'!A{cogs_anchor}"
break
# ----------------------------- Balance Sheet -----------------------------
ws_bs = wb.create_sheet("Balance Sheet")
_set_page(ws_bs, repeat_rows="1:5")
_title(ws_bs, client_name, address, "Statement of Balance Sheet", current_end, 4)
_header_row(ws_bs, 5, ["Particulars", "Note", current_end, previous_end])
r = 6
ws_bs.cell(r, 1, "I EQUITY AND LIABILITIES").font = Font(bold=True)
for cidx in range(1,5): ws_bs.cell(r,cidx).border = Border(top=MEDIUM,bottom=THIN)
r += 1
liability_start = r
for code in statements.get("visible_bs_liability_codes") or []:
meta = SCHEDULES[code]
ws_bs.cell(r, 1, meta["label"])
ws_bs.cell(r, 2, note_map.get(code, ""))
if code in note_anchor:
ws_bs.cell(r, 2).hyperlink = f"#'Notes & Schedules'!A{note_anchor[code]}"
ref = note_total_refs.get(code)
if ref:
_excel_formula(ws_bs.cell(r, 3), f"='Notes & Schedules'!{ref[0]}")
_excel_formula(ws_bs.cell(r, 4), f"='Notes & Schedules'!{ref[1]}")
else:
_money(ws_bs.cell(r, 3), statements["current_bs"].get(code,0))
_money(ws_bs.cell(r, 4), statements.get("previous_bs",{}).get(code,0))
r += 1
total_liability_row = r
ws_bs.cell(r,1,"Total Liabilities").font = Font(bold=True)
if r-1 >= liability_start:
_excel_formula(ws_bs.cell(r,3), f"=SUM(C{liability_start}:C{r-1})")
_excel_formula(ws_bs.cell(r,4), f"=SUM(D{liability_start}:D{r-1})")
else:
_excel_formula(ws_bs.cell(r,3), "=0"); _excel_formula(ws_bs.cell(r,4), "=0")
for cidx in range(1,5): ws_bs.cell(r,cidx).border=Border(top=DOUBLE,bottom=DOUBLE)
r += 2
ws_bs.cell(r,1,"II ASSETS").font=Font(bold=True)
for cidx in range(1,5): ws_bs.cell(r,cidx).border=Border(top=MEDIUM,bottom=THIN)
r += 1
asset_start = r
for code in statements.get("visible_bs_asset_codes") or []:
meta=SCHEDULES[code]
ws_bs.cell(r,1,meta["label"])
ws_bs.cell(r,2,note_map.get(code,""))
if code in note_anchor: ws_bs.cell(r,2).hyperlink=f"#'Notes & Schedules'!A{note_anchor[code]}"
ref=note_total_refs.get(code)
if ref:
_excel_formula(ws_bs.cell(r,3),f"='Notes & Schedules'!{ref[0]}")
_excel_formula(ws_bs.cell(r,4),f"='Notes & Schedules'!{ref[1]}")
else:
_money(ws_bs.cell(r,3),statements["current_bs"].get(code,0)); _money(ws_bs.cell(r,4),statements.get("previous_bs",{}).get(code,0))
r+=1
total_asset_row=r
ws_bs.cell(r,1,"Total Assets").font=Font(bold=True)
if r-1>=asset_start:
_excel_formula(ws_bs.cell(r,3),f"=SUM(C{asset_start}:C{r-1})"); _excel_formula(ws_bs.cell(r,4),f"=SUM(D{asset_start}:D{r-1})")
else:
_excel_formula(ws_bs.cell(r,3),"=0"); _excel_formula(ws_bs.cell(r,4),"=0")
for cidx in range(1,5): ws_bs.cell(r,cidx).border=Border(top=DOUBLE,bottom=DOUBLE)
r += 2
ws_bs.merge_cells(start_row=r,start_column=1,end_row=r,end_column=4)
ws_bs.cell(r,1,"The accompanying notes are an integral part of the financial statements").font=Font(italic=True,size=8)
r += 3
ws_bs.cell(r,1,f"For {client_name}").font=Font(bold=True,size=9)
ws_bs.cell(r,4,"For the Auditors").font=Font(bold=True,size=9)
r += 3
ws_bs.cell(r,1,"Partner / Proprietor / Authorised Signatory").font=Font(size=8)
ws_bs.cell(r,4,"Chartered Accountant").font=Font(size=8)
_apply_table_border(ws_bs,5,total_asset_row,1,4)
for col,w in {1:48,2:9,3:18,4:18}.items(): ws_bs.column_dimensions[get_column_letter(col)].width=w
ws_bs.print_area=f"A1:D{r}"
# Keep the existing individual note/annexure sheets for compatibility, but
# hide them from the normal workbook view now that the consolidated sheet is
# the printable schedule pack.
for sh in wb.worksheets:
if sh.title.startswith("N") and len(sh.title) > 3 and sh.title[1:3].isdigit():
sh.sheet_state = "hidden"
if sh.title in {"Cost of Goods Sold", "Partner Profit Allocation"}:
sh.sheet_state = "hidden"
# Place consolidated schedules immediately after the two statements.
wb._sheets.remove(ws_notes)
wb._sheets.insert(3, ws_notes)
# ---------------------------- Profit & Loss ----------------------------
ws_pl=wb.create_sheet("Profit and Loss")
_set_page(ws_pl, repeat_rows="1:5")
_title(ws_pl,client_name,address,"Statement of Profit and Loss",current_end,4)
_header_row(ws_pl,5,["Particulars","Note",current_end,previous_end])
r=6
pl_rows: list[tuple[str,str,str|None,str|None]] = []
if _either_nonzero(statements["current_pl"].get("REVENUE",0),statements["previous_pl"].get("REVENUE",0)):
pl_rows.append(("I","Revenue from operations","REVENUE","REVENUE"))
if _either_nonzero(statements["current_pl"].get("OTHER_INCOME",0),statements["previous_pl"].get("OTHER_INCOME",0)):
pl_rows.append(("II","Other Income","OTHER_INCOME","OTHER_INCOME"))
pl_rows.append(("III","Total Income (I+II)",None,"TOTAL_INCOME"))
expense_source=[
("(a)","Cost of goods sold","COST_OF_GOODS_SOLD","COST_OF_GOODS_SOLD"),
("(b)","Direct Expenses","DIRECT_EXPENSES","DIRECT_EXPENSES"),
("(c)","Employee benefits expense","EMPLOYEE_BENEFITS","EMPLOYEE_BENEFITS"),
("(d)","Finance Cost","FINANCE_COST","FINANCE_COST"),
("(e)","Depreciation and amortization expense","DEPRECIATION","DEPRECIATION"),
("(f)","Other expenses","OTHER_EXPENSES","OTHER_EXPENSES"),
]
visible_expenses=[x for x in expense_source if _either_nonzero(statements["current_pl"].get(x[3],0),statements["previous_pl"].get(x[3],0))]
if visible_expenses:
pl_rows.append(("IV","Expenses:",None,None))
pl_rows.extend(visible_expenses)
pl_rows.append(("","Total expenses",None,"TOTAL_EXPENSES"))
pl_rows.append(("V","Profit/(Loss) for the year",None,"PROFIT"))
income_value_rows=[]; expense_value_rows=[]; profit_row=None
for roman,label,note_code,key in pl_rows:
ws_pl.cell(r,1,f"{roman} {label}".strip())
if label in {"Total Income (I+II)","Expenses:","Total expenses","Profit/(Loss) for the year"}: ws_pl.cell(r,1).font=Font(bold=True)
if note_code and note_code in note_map:
ws_pl.cell(r,2,note_map[note_code])
if note_code in note_anchor: ws_pl.cell(r,2).hyperlink=f"#'Notes & Schedules'!A{note_anchor[note_code]}"
ref=note_total_refs.get(note_code)
if ref:
_excel_formula(ws_pl.cell(r,3),f"='Notes & Schedules'!{ref[0]}")
_excel_formula(ws_pl.cell(r,4),f"='Notes & Schedules'!{ref[1]}")
elif key == "TOTAL_INCOME":
current_cells = [f"C{rr}" for rr in income_value_rows]
previous_cells = [f"D{rr}" for rr in income_value_rows]
_excel_formula(ws_pl.cell(r,3),"=" + "+".join(current_cells) if current_cells else "=0")
_excel_formula(ws_pl.cell(r,4),"=" + "+".join(previous_cells) if previous_cells else "=0")
elif key == "TOTAL_EXPENSES":
_excel_formula(ws_pl.cell(r,3),"=" + "+".join(f"C{rr}" for rr in expense_value_rows) if expense_value_rows else "=0")
_excel_formula(ws_pl.cell(r,4),"=" + "+".join(f"D{rr}" for rr in expense_value_rows) if expense_value_rows else "=0")
elif key == "PROFIT":
total_income_row = next(rr for rr in range(6,r) if "Total Income" in str(ws_pl.cell(rr,1).value))
total_exp_row = next((rr for rr in range(6,r) if "Total expenses" in str(ws_pl.cell(rr,1).value)), None)
_excel_formula(ws_pl.cell(r,3),f"=C{total_income_row}" + (f"-C{total_exp_row}" if total_exp_row else ""))
_excel_formula(ws_pl.cell(r,4),f"=D{total_income_row}" + (f"-D{total_exp_row}" if total_exp_row else ""))
profit_row=r
if key in {"REVENUE","OTHER_INCOME"}: income_value_rows.append(r)
if key in {"COST_OF_GOODS_SOLD","DIRECT_EXPENSES","EMPLOYEE_BENEFITS","FINANCE_COST","DEPRECIATION","OTHER_EXPENSES"}: expense_value_rows.append(r)
if label in {"Total Income (I+II)","Total expenses","Profit/(Loss) for the year"}:
for cidx in range(1,5): ws_pl.cell(r,cidx).border=Border(top=THIN,bottom=DOUBLE)
r += 1
r += 1
ws_pl.merge_cells(start_row=r,start_column=1,end_row=r,end_column=4)
ws_pl.cell(r,1,"The accompanying notes are an integral part of the financial statements").font=Font(italic=True,size=8)
r += 3
ws_pl.cell(r,1,f"For {client_name}").font=Font(bold=True,size=9)
ws_pl.cell(r,4,"For the Auditors").font=Font(bold=True,size=9)
_apply_table_border(ws_pl,5,r-4,1,4)
for col,w in {1:56,2:9,3:18,4:18}.items(): ws_pl.column_dimensions[get_column_letter(col)].width=w
ws_pl.print_area=f"A1:D{r}"
# Patch partnership formulas now that the formula-driven P&L profit row exists.
if profit_row:
quoted_pl=_excel_quote_sheet("Profit and Loss")
for cell_ref,ratio in partner_formula_cells:
_excel_formula(ws_notes[cell_ref],f"={quoted_pl}!C{profit_row}*{ratio:.10f}")
for cell_ref,ratio in allocation_cells:
_excel_formula(ws_notes[cell_ref],f"={quoted_pl}!C{profit_row}*{ratio:.10f}")
# -------------------- Hidden compatibility note sheets --------------------
# Existing consumers that expect Nxx sheets continue to work. Each hidden
# compatibility sheet links to the consolidated schedule rather than copying
# values, so formulas are preserved and there is only one source of totals.
for code in visible_note_codes:
if code not in note_anchor:
continue
label = "Cost of Goods Sold" if code == "COST_OF_GOODS_SOLD" else SCHEDULES[code]["label"]
sh = wb.create_sheet(_schedule_sheet_name(note_map[code], label))
_set_page(sh)
_title(sh,client_name,address,f"Note - {note_map[code]}: {label}",current_end,3)
_header_row(sh,5,["Particulars",current_end,previous_end])
sh.cell(6,1,"See consolidated Notes & Schedules")
sh.cell(6,1).hyperlink=f"#'Notes & Schedules'!A{note_anchor[code]}"
ref=note_total_refs.get(code)
if ref:
_excel_formula(sh.cell(6,2),f"='Notes & Schedules'!{ref[0]}")
_excel_formula(sh.cell(6,3),f"='Notes & Schedules'!{ref[1]}")
_apply_table_border(sh,5,6,1,3)
sh.sheet_state="hidden"
# Unmapped review remains available and unsuppressed.
if statements.get("unmapped"):
ws=wb.create_sheet("Unmapped Ledgers")
_set_page(ws, landscape=True)
_title(ws,client_name,address,"Unmapped Ledgers - Review Required",current_end,5)
_header_row(ws,5,["Ledger","Immediate Tally Group","Resolved Root Group","Closing Debit","Closing Credit"])
ws_um=wb.create_sheet("Unmapped Ledgers")
_set_page(ws_um,landscape=True)
_title(ws_um,client_name,address,"Unmapped Ledgers - Review Required",current_end,5)
_header_row(ws_um,5,["Ledger","Immediate Tally Group","Resolved Root Group","Closing Debit","Closing Credit"])
r=6
for line in statements["unmapped"]:
ws.cell(r,1,line.ledger_name); ws.cell(r,2,line.parent_group); ws.cell(r,3,line.root_group); _money(ws.cell(r,4),line.closing_debit); _money(ws.cell(r,5),line.closing_credit); r+=1
_apply_table_border(ws,5,r-1,1,5)
for col,w in {1:35,2:28,3:28,4:18,5:18}.items(): ws.column_dimensions[get_column_letter(col)].width=w
ws_um.cell(r,1,line.ledger_name); ws_um.cell(r,2,line.parent_group); ws_um.cell(r,3,line.root_group)
_money(ws_um.cell(r,4),line.closing_debit); _money(ws_um.cell(r,5),line.closing_credit); r+=1
_apply_table_border(ws_um,5,r-1,1,5)
for col,w in {1:35,2:28,3:28,4:18,5:18}.items(): ws_um.column_dimensions[get_column_letter(col)].width=w
# Professional print order: statements first, then one continuous schedule
# pack, followed by review/audit workings. No formulas are converted to values.
preferred=["Balance Sheet","Profit and Loss","Notes & Schedules","Trial Balance"]
ordered=[]
for name in preferred:
if name in wb.sheetnames: ordered.append(wb[name])
ordered += [sh for sh in wb.worksheets if sh not in ordered]
wb._sheets = ordered
wb.active = 0
bio=BytesIO()
wb.save(bio)
return bio.getvalue()
bio=BytesIO(); wb.save(bio); return bio.getvalue()
@@ -5,11 +5,11 @@
<div>
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-brand-600">Tools · Accounting</p>
<h1 class="mt-1 text-2xl font-semibold text-slate-900">Trial Balance & Financial Statements</h1>
<p class="mt-1 max-w-4xl text-sm text-slate-500">Generate Trial Balance, Profit & Loss Account, Balance Sheet and ledger schedules directly from the registered Client/FY Accounting Mirror. The mirror remains read-only.</p>
<p class="mt-1 max-w-4xl text-sm text-slate-500">Presentation view from the registered Client/FY Accounting Mirror. Zero-value rows are suppressed only when both current and comparative years are nil; the underlying Trial Balance remains complete and read-only.</p>
</div>
<div class="flex gap-2">
<a href="/tools/tally{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700">Back to Tally Accounting</a>
{% if selected_client and report %}<a href="/tools/accounting/financial-statements/excel?client_id={{ selected_client.id }}&financial_year={{ financial_year }}" class="rounded-xl bg-emerald-600 px-4 py-2 text-sm font-semibold text-white shadow-soft hover:bg-emerald-700">Download Excel</a>{% endif %}
{% if selected_client and report %}<a href="/tools/accounting/financial-statements/excel?client_id={{ selected_client.id }}&financial_year={{ financial_year }}" class="rounded-xl bg-emerald-600 px-4 py-2 text-sm font-semibold text-white shadow-soft hover:bg-emerald-700">Download Formula Excel</a>{% endif %}
</div>
</div>
@@ -18,100 +18,72 @@
<section class="rounded-2xl bg-white p-5 shadow-soft">
<form method="get" class="grid gap-4 md:grid-cols-[minmax(0,1fr)_220px_auto] md:items-end">
<label class="block">
<span class="text-xs font-semibold uppercase tracking-wide text-slate-500">ERP Client</span>
<select name="client_id" class="mt-2 w-full rounded-xl border-slate-300 text-sm">
<option value="">Select client</option>
{% for client in clients %}<option value="{{ client.id }}" {% if selected_client and client.id == selected_client.id %}selected{% endif %}>{{ client.client_name }}</option>{% endfor %}
</select>
</label>
<label class="block">
<span class="text-xs font-semibold uppercase tracking-wide text-slate-500">Financial Year</span>
<select name="financial_year" class="mt-2 w-full rounded-xl border-slate-300 text-sm">
{% for fy in financial_year_options %}<option value="{{ fy }}" {% if fy == financial_year %}selected{% endif %}>FY {{ fy }}</option>{% endfor %}
</select>
</label>
<label class="block"><span class="text-xs font-semibold uppercase tracking-wide text-slate-500">ERP Client</span><select name="client_id" class="mt-2 w-full rounded-xl border-slate-300 text-sm"><option value="">Select client</option>{% for client in clients %}<option value="{{ client.id }}" {% if selected_client and client.id == selected_client.id %}selected{% endif %}>{{ client.client_name }}</option>{% endfor %}</select></label>
<label class="block"><span class="text-xs font-semibold uppercase tracking-wide text-slate-500">Financial Year</span><select name="financial_year" class="mt-2 w-full rounded-xl border-slate-300 text-sm">{% for fy in financial_year_options %}<option value="{{ fy }}" {% if fy == financial_year %}selected{% endif %}>FY {{ fy }}</option>{% endfor %}</select></label>
<button class="rounded-xl bg-brand-600 px-5 py-2.5 text-sm font-semibold text-white hover:bg-brand-700">Load from Mirror</button>
</form>
</section>
{% if selected_client and report %}
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Mirror FY</div><div class="mt-2 text-lg font-semibold text-slate-900">{{ financial_year }}</div><div class="mt-1 text-xs text-slate-500">Version {{ report.current_payload.mirror_version_no or report.current_payload.version_no or '-' }}</div></div>
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Ledgers</div><div class="mt-2 text-2xl font-semibold text-slate-900">{{ report.current.count }}</div><div class="mt-1 text-xs text-slate-500">{{ report.current.group_count }} Tally groups loaded</div></div>
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Previous Year</div><div class="mt-2 text-lg font-semibold {% if previous_available %}text-emerald-700{% else %}text-amber-700{% endif %}">{{ previous_fy }}</div><div class="mt-1 text-xs text-slate-500">{{ 'Comparative mirror loaded' if previous_available else 'No registered comparative mirror' }}</div></div>
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Mapping Review</div><div class="mt-2 text-2xl font-semibold {% if statements.unmapped|length == 0 %}text-emerald-700{% else %}text-amber-700{% endif %}">{{ statements.unmapped|length }}</div><div class="mt-1 text-xs text-slate-500">Unmapped ledgers</div></div>
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Mirror FY</div><div class="mt-2 text-lg font-semibold text-slate-900">{{ financial_year }}</div><div class="mt-1 text-xs text-slate-500">Version {{ report.current_payload.mirror_version_no or report.current_payload.version_no or '-' }}</div></div>
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Ledgers</div><div class="mt-2 text-2xl font-semibold text-slate-900">{{ report.current.count }}</div><div class="mt-1 text-xs text-slate-500">{{ report.current.group_count }} Tally groups loaded</div></div>
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Comparative</div><div class="mt-2 text-lg font-semibold {% if previous_available %}text-emerald-700{% else %}text-amber-700{% endif %}">{{ previous_fy }}</div><div class="mt-1 text-xs text-slate-500">{{ 'Previous-year mirror loaded' if previous_available else 'No registered comparative mirror' }}</div></div>
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">Mapping Review</div><div class="mt-2 text-2xl font-semibold {% if statements.unmapped|length == 0 %}text-emerald-700{% else %}text-amber-700{% endif %}">{{ statements.unmapped|length }}</div><div class="mt-1 text-xs text-slate-500">Unmapped ledgers</div></div>
</section>
{% if report.current.hierarchy_warning %}
<div class="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">The mirror Trial Balance loaded, but the Local Agent could not return the full Tally group hierarchy. Update the Local Agent to the latest version for automatic custom-group resolution. Immediate parent-group classification is still being used.</div>
{% endif %}
{% if report.current.hierarchy_warning %}<div class="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">The full Tally group hierarchy was not returned by the Local Agent. The standard Tally fallback hierarchy and ledger rules are active; custom-group hierarchy will improve automatically when the latest Local Agent is available.</div>{% endif %}
{% if statements.balance_sheet_difference|abs > 1 %}<div class="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">Balance Sheet review required: Assets and Equity/Liabilities differ by ₹ {{ '{:,.2f}'.format(statements.balance_sheet_difference) }} after mapped finalisation. Review classification/abnormal balances before final use.</div>{% elif statements.profit_transfer_applied %}<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">Current-year profit/loss was not yet reflected in source capital balances, so the ERP applied a read-only finalisation transfer. The mirror itself was not changed.</div>{% endif %}
{% if statements.balance_sheet_difference|abs > 1 %}
<div class="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">Balance Sheet review required: Assets and Equity/Liabilities differ by ₹ {{ '{:,.2f}'.format(statements.balance_sheet_difference) }} after mapped finalisation. Review unmapped or incorrectly classified ledgers before using the statements as final.</div>
{% elif statements.profit_transfer_applied %}
<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">Current-year profit/loss was not yet reflected in the source capital balances, so the ERP applied a read-only finalisation transfer for this financial statement view. The Accounting Mirror was not changed.</div>
{% endif %}
<section class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-soft">
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 px-5 py-4"><div><h2 class="text-lg font-semibold text-slate-900">Statement of Balance Sheet</h2><p class="text-xs text-slate-500">Only comparative headings with a value in at least one year are shown.</p></div><div class="text-xs font-medium text-slate-500">₹ · Comparative presentation</div></div>
<div class="overflow-x-auto p-5"><div class="mx-auto max-w-5xl">
<div class="mb-3 text-center"><div class="font-semibold text-slate-900">{{ selected_client.client_name }}</div><div class="text-sm font-semibold underline">Statement of Balance Sheet for the Year Ended 31st March {{ financial_year[:4]|int + 1 }}</div></div>
<table class="w-full border-collapse text-sm">
<thead><tr class="bg-slate-50"><th class="border border-slate-500 px-3 py-2 text-left">Particulars</th><th class="w-16 border border-slate-500 px-2 py-2 text-center">Note</th><th class="w-36 border border-slate-500 px-3 py-2 text-right">31 March {{ financial_year[:4]|int + 1 }}</th><th class="w-36 border border-slate-500 px-3 py-2 text-right">31 March {{ financial_year[:4] }}</th></tr></thead>
<tbody>
<tr><td colspan="4" class="border-x border-slate-500 px-3 py-2 font-bold">I&nbsp;&nbsp;EQUITY AND LIABILITIES</td></tr>
{% for code in statements.visible_bs_liability_codes %}{% set meta=schedules.get(code) %}<tr><td class="border-x border-slate-300 px-3 py-1.5">{{ meta.label }}</td><td class="border-x border-slate-300 px-2 py-1.5 text-center">{{ statements.presentation_notes.get(code,'') }}</td><td class="border-x border-slate-300 px-3 py-1.5 text-right">{{ '{:,.0f}'.format(statements.current_bs.get(code,0)) }}</td><td class="border-x border-slate-300 px-3 py-1.5 text-right">{{ '{:,.0f}'.format(statements.previous_bs.get(code,0)) }}</td></tr>{% endfor %}
<tr class="font-bold"><td class="border border-slate-500 px-3 py-2 text-right">Total Liabilities</td><td class="border border-slate-500"></td><td class="border border-slate-500 px-3 py-2 text-right">{{ '{:,.0f}'.format(statements.current_liabilities) }}</td><td class="border border-slate-500 px-3 py-2 text-right">{{ '{:,.0f}'.format(statements.previous_liabilities) }}</td></tr>
<tr><td colspan="4" class="border-x border-slate-500 px-3 py-2 font-bold">II&nbsp;&nbsp;ASSETS</td></tr>
{% for code in statements.visible_bs_asset_codes %}{% set meta=schedules.get(code) %}<tr><td class="border-x border-slate-300 px-3 py-1.5">{{ meta.label }}</td><td class="border-x border-slate-300 px-2 py-1.5 text-center">{{ statements.presentation_notes.get(code,'') }}</td><td class="border-x border-slate-300 px-3 py-1.5 text-right">{{ '{:,.0f}'.format(statements.current_bs.get(code,0)) }}</td><td class="border-x border-slate-300 px-3 py-1.5 text-right">{{ '{:,.0f}'.format(statements.previous_bs.get(code,0)) }}</td></tr>{% endfor %}
<tr class="font-bold"><td class="border border-slate-500 px-3 py-2 text-right">Total Assets</td><td class="border border-slate-500"></td><td class="border border-slate-500 px-3 py-2 text-right">{{ '{:,.0f}'.format(statements.current_assets) }}</td><td class="border border-slate-500 px-3 py-2 text-right">{{ '{:,.0f}'.format(statements.previous_assets) }}</td></tr>
</tbody>
</table>
<p class="mt-2 text-xs italic text-slate-500">The accompanying notes are an integral part of the financial statements.</p>
</div></div>
</section>
<div class="grid gap-6 xl:grid-cols-2">
<section class="overflow-hidden rounded-2xl bg-white shadow-soft">
<div class="border-b border-slate-200 px-5 py-4"><h2 class="font-semibold text-slate-900">Balance Sheet</h2><p class="text-xs text-slate-500">Presentation follows the ruled comparative format used in your existing statements.</p></div>
<div class="overflow-x-auto p-5">
<table class="w-full border-collapse text-sm">
<thead><tr class="bg-slate-50"><th class="border border-slate-400 px-3 py-2 text-left">Particulars</th><th class="border border-slate-400 px-3 py-2">Note</th><th class="border border-slate-400 px-3 py-2 text-right">31 Mar {{ financial_year[:4]|int + 1 }}</th><th class="border border-slate-400 px-3 py-2 text-right">31 Mar {{ financial_year[:4] }}</th></tr></thead>
<tbody>
<tr><td colspan="4" class="border-x border-slate-400 px-3 py-2 font-bold">EQUITY AND LIABILITIES</td></tr>
{% for code, meta in schedules.items() if meta.statement == 'BS' and meta.bs_side == 'LIABILITY' and (statements.current_bs.get(code,0) or statements.previous_bs.get(code,0)) %}
<tr><td class="border-x border-slate-300 px-3 py-1.5">{{ meta.label }}</td><td class="border-x border-slate-300 px-3 py-1.5 text-center">{{ loop.index }}</td><td class="border-x border-slate-300 px-3 py-1.5 text-right">{{ '{:,.0f}'.format(statements.current_bs.get(code,0)) }}</td><td class="border-x border-slate-300 px-3 py-1.5 text-right">{{ '{:,.0f}'.format(statements.previous_bs.get(code,0)) }}</td></tr>
{% endfor %}
<tr class="font-bold"><td class="border border-slate-500 px-3 py-2">Total Liabilities</td><td class="border border-slate-500"></td><td class="border border-slate-500 px-3 py-2 text-right">{{ '{:,.0f}'.format(statements.current_liabilities) }}</td><td class="border border-slate-500 px-3 py-2 text-right">{{ '{:,.0f}'.format(statements.previous_liabilities) }}</td></tr>
<tr><td colspan="4" class="border-x border-slate-400 px-3 py-2 font-bold">ASSETS</td></tr>
{% for code, meta in schedules.items() if meta.statement == 'BS' and meta.bs_side == 'ASSET' and (statements.current_bs.get(code,0) or statements.previous_bs.get(code,0)) %}
<tr><td class="border-x border-slate-300 px-3 py-1.5">{{ meta.label }}</td><td class="border-x border-slate-300 px-3 py-1.5 text-center">-</td><td class="border-x border-slate-300 px-3 py-1.5 text-right">{{ '{:,.0f}'.format(statements.current_bs.get(code,0)) }}</td><td class="border-x border-slate-300 px-3 py-1.5 text-right">{{ '{:,.0f}'.format(statements.previous_bs.get(code,0)) }}</td></tr>
{% endfor %}
<tr class="font-bold"><td class="border border-slate-500 px-3 py-2">Total Assets</td><td class="border border-slate-500"></td><td class="border border-slate-500 px-3 py-2 text-right">{{ '{:,.0f}'.format(statements.current_assets) }}</td><td class="border border-slate-500 px-3 py-2 text-right">{{ '{:,.0f}'.format(statements.previous_assets) }}</td></tr>
</tbody>
</table>
</div>
</section>
<section class="overflow-hidden rounded-2xl bg-white shadow-soft">
<div class="border-b border-slate-200 px-5 py-4"><h2 class="font-semibold text-slate-900">Profit & Loss Account</h2><p class="text-xs text-slate-500">Generated from mapped mirror ledgers; inventory movement is used for cost of goods sold.</p></div>
<div class="overflow-x-auto p-5">
{% set cp=statements.current_pl %}{% set pp=statements.previous_pl %}
<table class="w-full border-collapse text-sm">
<thead><tr class="bg-slate-50"><th class="border border-slate-400 px-3 py-2 text-left">Particulars</th><th class="border border-slate-400 px-3 py-2 text-right">Current FY</th><th class="border border-slate-400 px-3 py-2 text-right">Previous FY</th></tr></thead>
<tbody>
{% for label,key in [('Revenue from operations','REVENUE'),('Other Income','OTHER_INCOME'),('Total Income','TOTAL_INCOME'),('Cost of goods sold','COST_OF_GOODS_SOLD'),('Direct Expenses','DIRECT_EXPENSES'),('Employee benefits expense','EMPLOYEE_BENEFITS'),('Finance Cost','FINANCE_COST'),('Depreciation and amortization expense','DEPRECIATION'),('Other expenses','OTHER_EXPENSES'),('Total expenses','TOTAL_EXPENSES'),('Profit/(Loss) for the year','PROFIT')] %}
<tr class="{% if key in ['TOTAL_INCOME','TOTAL_EXPENSES','PROFIT'] %}font-bold{% endif %}"><td class="border border-slate-300 px-3 py-1.5">{{ label }}</td><td class="border border-slate-300 px-3 py-1.5 text-right">{{ '{:,.0f}'.format(cp.get(key,0)) }}</td><td class="border border-slate-300 px-3 py-1.5 text-right">{{ '{:,.0f}'.format(pp.get(key,0)) }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
</div>
<section class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-soft">
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 px-5 py-4"><div><h2 class="text-lg font-semibold text-slate-900">Statement of Profit & Loss</h2><p class="text-xs text-slate-500">Nil rows are hidden only when both years are nil.</p></div><div class="text-xs font-medium text-slate-500">₹ · Formula-driven in Excel</div></div>
<div class="overflow-x-auto p-5"><div class="mx-auto max-w-5xl">
{% set cp=statements.current_pl %}{% set pp=statements.previous_pl %}
<div class="mb-3 text-center"><div class="font-semibold text-slate-900">{{ selected_client.client_name }}</div><div class="text-sm font-semibold underline">Statement of Profit and Loss for the Year Ended 31st March {{ financial_year[:4]|int + 1 }}</div></div>
<table class="w-full border-collapse text-sm"><thead><tr class="bg-slate-50"><th class="border border-slate-500 px-3 py-2 text-left">Particulars</th><th class="w-16 border border-slate-500 px-2 py-2 text-center">Note</th><th class="w-36 border border-slate-500 px-3 py-2 text-right">31 March {{ financial_year[:4]|int + 1 }}</th><th class="w-36 border border-slate-500 px-3 py-2 text-right">31 March {{ financial_year[:4] }}</th></tr></thead><tbody>
{% if cp.REVENUE|abs >= 0.005 or pp.REVENUE|abs >= 0.005 %}<tr><td class="border-x border-slate-300 px-3 py-1.5">I&nbsp;&nbsp;Revenue from operations</td><td class="border-x border-slate-300 px-2 py-1.5 text-center">{{ statements.presentation_notes.get('REVENUE','') }}</td><td class="border-x border-slate-300 px-3 py-1.5 text-right">{{ '{:,.0f}'.format(cp.REVENUE) }}</td><td class="border-x border-slate-300 px-3 py-1.5 text-right">{{ '{:,.0f}'.format(pp.REVENUE) }}</td></tr>{% endif %}
{% if cp.OTHER_INCOME|abs >= 0.005 or pp.OTHER_INCOME|abs >= 0.005 %}<tr><td class="border-x border-slate-300 px-3 py-1.5">II&nbsp;&nbsp;Other Income</td><td class="border-x border-slate-300 px-2 py-1.5 text-center">{{ statements.presentation_notes.get('OTHER_INCOME','') }}</td><td class="border-x border-slate-300 px-3 py-1.5 text-right">{{ '{:,.0f}'.format(cp.OTHER_INCOME) }}</td><td class="border-x border-slate-300 px-3 py-1.5 text-right">{{ '{:,.0f}'.format(pp.OTHER_INCOME) }}</td></tr>{% endif %}
<tr class="font-bold"><td class="border border-slate-500 px-3 py-2">III&nbsp;&nbsp;Total Income (I+II)</td><td class="border border-slate-500"></td><td class="border border-slate-500 px-3 py-2 text-right">{{ '{:,.0f}'.format(cp.TOTAL_INCOME) }}</td><td class="border border-slate-500 px-3 py-2 text-right">{{ '{:,.0f}'.format(pp.TOTAL_INCOME) }}</td></tr>
<tr><td colspan="4" class="border-x border-slate-500 px-3 py-2 font-bold">IV&nbsp;&nbsp;Expenses</td></tr>
{% for label,key,ncode in [('Cost of goods sold','COST_OF_GOODS_SOLD','COST_OF_GOODS_SOLD'),('Direct Expenses','DIRECT_EXPENSES','DIRECT_EXPENSES'),('Employee benefits expense','EMPLOYEE_BENEFITS','EMPLOYEE_BENEFITS'),('Finance Cost','FINANCE_COST','FINANCE_COST'),('Depreciation and amortization expense','DEPRECIATION','DEPRECIATION'),('Other expenses','OTHER_EXPENSES','OTHER_EXPENSES')] %}{% if cp.get(key,0)|abs >= 0.005 or pp.get(key,0)|abs >= 0.005 %}<tr><td class="border-x border-slate-300 px-6 py-1.5">{{ label }}</td><td class="border-x border-slate-300 px-2 py-1.5 text-center">{{ statements.presentation_notes.get(ncode,'') }}</td><td class="border-x border-slate-300 px-3 py-1.5 text-right">{{ '{:,.0f}'.format(cp.get(key,0)) }}</td><td class="border-x border-slate-300 px-3 py-1.5 text-right">{{ '{:,.0f}'.format(pp.get(key,0)) }}</td></tr>{% endif %}{% endfor %}
<tr class="font-bold"><td class="border border-slate-500 px-3 py-2 text-right">Total expenses</td><td class="border border-slate-500"></td><td class="border border-slate-500 px-3 py-2 text-right">{{ '{:,.0f}'.format(cp.TOTAL_EXPENSES) }}</td><td class="border border-slate-500 px-3 py-2 text-right">{{ '{:,.0f}'.format(pp.TOTAL_EXPENSES) }}</td></tr>
<tr class="font-bold"><td class="border border-slate-500 px-3 py-2">V&nbsp;&nbsp;Profit/(Loss) for the year</td><td class="border border-slate-500"></td><td class="border border-slate-500 px-3 py-2 text-right">{{ '{:,.0f}'.format(cp.PROFIT) }}</td><td class="border border-slate-500 px-3 py-2 text-right">{{ '{:,.0f}'.format(pp.PROFIT) }}</td></tr>
</tbody></table>
<p class="mt-2 text-xs italic text-slate-500">The accompanying notes are an integral part of the financial statements.</p>
</div></div>
</section>
{% if is_partnership %}
<section class="rounded-2xl bg-white p-5 shadow-soft">
<section class="rounded-2xl border border-slate-200 bg-white p-5 shadow-soft">
<div class="flex flex-wrap items-center justify-between gap-3"><div><h2 class="font-semibold text-slate-900">Partnership Profit Allocation</h2><p class="mt-1 text-sm text-slate-500">Ratios are Client/FY-specific. When they total 100%, the year's profit/loss is allocated to partner funds for the final Balance Sheet without changing the mirror.</p></div><div class="text-sm font-semibold {% if statements.profit_share_total == 100 %}text-emerald-700{% else %}text-amber-700{% endif %}">Configured ratio: {{ '%.2f'|format(statements.profit_share_total) }}%</div></div>
<form method="post" action="/tools/accounting/financial-statements/partnership" class="mt-4 space-y-3">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="client_id" value="{{ selected_client.id }}"><input type="hidden" name="financial_year" value="{{ financial_year }}">
<div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr class="bg-slate-50"><th class="px-3 py-2 text-left">Partner</th><th class="px-3 py-2 text-right">Profit Share %</th><th class="px-3 py-2 text-left">Capital / Current Ledger (optional)</th></tr></thead><tbody>
{% for row in partner_rows %}<tr><td class="px-3 py-2"><input name="partner_name" value="{{ row.partner_name }}" class="w-full rounded-lg border-slate-300"></td><td class="px-3 py-2"><input name="profit_share_percent" type="number" step="0.0001" value="{{ row.profit_share_percent }}" class="w-32 rounded-lg border-slate-300 text-right"></td><td class="px-3 py-2"><input name="target_ledger_name" value="{{ row.target_ledger_name }}" class="w-full rounded-lg border-slate-300" placeholder="e.g. Partner Current Account"></td></tr>{% endfor %}
{% if not partner_rows %}{% for i in range(2) %}<tr><td class="px-3 py-2"><input name="partner_name" class="w-full rounded-lg border-slate-300"></td><td class="px-3 py-2"><input name="profit_share_percent" type="number" step="0.0001" class="w-32 rounded-lg border-slate-300 text-right"></td><td class="px-3 py-2"><input name="target_ledger_name" class="w-full rounded-lg border-slate-300"></td></tr>{% endfor %}{% endif %}
</tbody></table></div>
<button class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white">Save Ratio & Recalculate</button>
</form>
<form method="post" action="/tools/accounting/financial-statements/partnership" class="mt-4 space-y-3"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="client_id" value="{{ selected_client.id }}"><input type="hidden" name="financial_year" value="{{ financial_year }}"><div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr class="bg-slate-50"><th class="px-3 py-2 text-left">Partner</th><th class="px-3 py-2 text-right">Profit Share %</th><th class="px-3 py-2 text-left">Capital / Current Ledger (optional)</th></tr></thead><tbody>{% for row in partner_rows %}<tr><td class="px-3 py-2"><input name="partner_name" value="{{ row.partner_name }}" class="w-full rounded-lg border-slate-300"></td><td class="px-3 py-2"><input name="profit_share_percent" type="number" step="0.0001" value="{{ row.profit_share_percent }}" class="w-32 rounded-lg border-slate-300 text-right"></td><td class="px-3 py-2"><input name="target_ledger_name" value="{{ row.target_ledger_name }}" class="w-full rounded-lg border-slate-300" placeholder="e.g. Partner Current Account"></td></tr>{% endfor %}{% if not partner_rows %}{% for i in range(2) %}<tr><td class="px-3 py-2"><input name="partner_name" class="w-full rounded-lg border-slate-300"></td><td class="px-3 py-2"><input name="profit_share_percent" type="number" step="0.0001" class="w-32 rounded-lg border-slate-300 text-right"></td><td class="px-3 py-2"><input name="target_ledger_name" class="w-full rounded-lg border-slate-300"></td></tr>{% endfor %}{% endif %}</tbody></table></div><button class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white">Save Ratio & Recalculate</button></form>
{% if statements.partnership_allocations %}<div class="mt-4 grid gap-2 md:grid-cols-2 xl:grid-cols-5">{% for row in statements.partnership_allocations %}<div class="rounded-xl border border-slate-200 p-3"><div class="text-xs text-slate-500">{{ row.partner_name }} · {{ row.profit_share_percent }}%</div><div class="mt-1 font-semibold text-slate-900">₹ {{ '{:,.0f}'.format(row.amount) }}</div></div>{% endfor %}</div>{% endif %}
</section>
{% endif %}
<section class="rounded-2xl bg-white p-5 shadow-soft">
<div class="flex items-center justify-between gap-3"><div><h2 class="font-semibold text-slate-900">Trial Balance & Ledger Mapping</h2><p class="mt-1 text-sm text-slate-500">Default classification follows the complete Tally group hierarchy. Use an override only when a ledger needs a different financial-statement schedule.</p></div></div>
<div class="mt-4 overflow-x-auto"><table class="min-w-full text-xs"><thead><tr class="bg-slate-50"><th class="px-2 py-2 text-left">Ledger</th><th class="px-2 py-2 text-left">Tally Group</th><th class="px-2 py-2 text-left">Root Group</th><th class="px-2 py-2 text-left">Schedule</th><th class="px-2 py-2 text-right">Closing Dr</th><th class="px-2 py-2 text-right">Closing Cr</th><th class="px-2 py-2">Mapping</th></tr></thead><tbody class="divide-y divide-slate-100">
{% for line in current_lines %}<tr class="{% if line.schedule_code == 'UNMAPPED' %}bg-amber-50{% endif %}"><td class="px-2 py-2 font-medium">{{ line.ledger_name }}</td><td class="px-2 py-2">{{ line.parent_group }}</td><td class="px-2 py-2">{{ line.root_group }}</td><td class="px-2 py-2">{{ line.schedule_label }}</td><td class="px-2 py-2 text-right">{{ '{:,.0f}'.format(line.closing_debit) }}</td><td class="px-2 py-2 text-right">{{ '{:,.0f}'.format(line.closing_credit) }}</td><td class="px-2 py-2"><form method="post" action="/tools/accounting/financial-statements/mapping" class="flex min-w-[300px] items-center gap-2"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="client_id" value="{{ selected_client.id }}"><input type="hidden" name="financial_year" value="{{ financial_year }}"><input type="hidden" name="ledger_name" value="{{ line.ledger_name }}"><select name="schedule_code" class="w-52 rounded-lg border-slate-300 text-xs">{% for code,label,statement in mapping_choices %}<option value="{{ code }}" {% if code == line.schedule_code %}selected{% endif %}>{{ label }}</option>{% endfor %}</select><button class="rounded-lg border border-slate-300 bg-white px-2 py-1 font-semibold">Save</button></form></td></tr>{% endfor %}
</tbody></table></div>
</section>
<details class="rounded-2xl border border-slate-200 bg-white shadow-soft">
<summary class="cursor-pointer list-none px-5 py-4"><div class="flex items-center justify-between"><div><h2 class="font-semibold text-slate-900">Trial Balance & Ledger Mapping</h2><p class="mt-1 text-sm text-slate-500">Review mode retains the complete ledger master, including zero-balance ledgers, so mappings are not lost between years.</p></div><span class="rounded-lg bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-600">Open Review</span></div></summary>
<div class="border-t border-slate-200 p-5"><div class="overflow-x-auto"><table class="min-w-full text-xs"><thead><tr class="bg-slate-50"><th class="px-2 py-2 text-left">Ledger</th><th class="px-2 py-2 text-left">Tally Group</th><th class="px-2 py-2 text-left">Root Group</th><th class="px-2 py-2 text-left">Schedule</th><th class="px-2 py-2 text-right">Closing Dr</th><th class="px-2 py-2 text-right">Closing Cr</th><th class="px-2 py-2">Mapping</th></tr></thead><tbody class="divide-y divide-slate-100">{% for line in current_lines %}<tr class="{% if line.schedule_code == 'UNMAPPED' %}bg-amber-50{% endif %}"><td class="px-2 py-2 font-medium">{{ line.ledger_name }}</td><td class="px-2 py-2">{{ line.parent_group }}</td><td class="px-2 py-2">{{ line.root_group }}</td><td class="px-2 py-2">{{ line.schedule_label }}</td><td class="px-2 py-2 text-right">{{ '{:,.0f}'.format(line.closing_debit) }}</td><td class="px-2 py-2 text-right">{{ '{:,.0f}'.format(line.closing_credit) }}</td><td class="px-2 py-2"><form method="post" action="/tools/accounting/financial-statements/mapping" class="flex min-w-[300px] items-center gap-2"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="client_id" value="{{ selected_client.id }}"><input type="hidden" name="financial_year" value="{{ financial_year }}"><input type="hidden" name="ledger_name" value="{{ line.ledger_name }}"><select name="schedule_code" class="w-52 rounded-lg border-slate-300 text-xs">{% for code,label,statement in mapping_choices %}<option value="{{ code }}" {% if code == line.schedule_code %}selected{% endif %}>{{ label }}</option>{% endfor %}</select><button class="rounded-lg border border-slate-300 bg-white px-2 py-1 font-semibold">Save</button></form></td></tr>{% endfor %}</tbody></table></div></div>
</details>
{% endif %}
</div>
{% endblock %}