From 87bfc13cbdde0541daa04e479549c460dbad5ed7 Mon Sep 17 00:00:00 2001 From: A R R R Associates Date: Fri, 4 Sep 2026 15:53:06 +0530 Subject: [PATCH] Add opening balance action summary to Excel export --- app/modules/accounting/opening_balance_ui.py | 123 ++++++++++++++++--- 1 file changed, 105 insertions(+), 18 deletions(-) diff --git a/app/modules/accounting/opening_balance_ui.py b/app/modules/accounting/opening_balance_ui.py index 67b6877..3c7561e 100644 --- a/app/modules/accounting/opening_balance_ui.py +++ b/app/modules/accounting/opening_balance_ui.py @@ -284,7 +284,7 @@ def page( @router.get("/download/{run_id}") def download_report(request: Request, run_id: int, client_id: int | None = None): - """Download the complete opening-balance comparison as an Excel workbook.""" + """Download the complete opening-balance review, including the web summary action reports.""" db = CommonSessionLocal() try: user, denied = _require_partner(request, db, "accounting.tally.view") @@ -298,11 +298,103 @@ def download_report(request: Request, run_id: int, client_id: int | None = None) if not selected or (client_id and int(client_id) != int(run.client_id)): return _go(error="Opening balance comparison run is not visible.") + # Complete detailed reports. ledger_rows = ledger_items(db, run_id=run.id) stock_rows = stock_items(db, run_id=run.id) + + # These are the same action-required populations shown on the web Summary tab. + ledger_action_rows = ledger_items(db, run_id=run.id, status="difference") + stock_action_rows = [] + for action_status in ( + "quantity_difference", + "value_difference", + "quantity_and_value_difference", + "unit_difference", + ): + stock_action_rows.extend(stock_items(db, run_id=run.id, status=action_status)) + + try: + summary_counts = _summary_counts(json.loads(run.summary_json or "{}")) + except Exception: + summary_counts = _summary_counts({}) + wb = Workbook() - ws = wb.active - ws.title = "Balance Sheet Ledgers" + meta = wb.active + meta.title = "Summary" + meta.append(["Opening Balance Review"]) + meta.append(["Client", getattr(selected, "client_name", "") or getattr(selected, "name", "") or str(selected.id)]) + meta.append(["Run ID", run.id]) + meta.append(["Previous Tally Company", run.previous_company_name]) + meta.append(["Current Tally Company", run.current_company_name]) + meta.append(["Scope", "Balance Sheet ledgers and Stock Items only"]) + meta.append(["Balance Sheet Ledger Rows", len(ledger_rows)]) + meta.append(["Stock Item Rows", len(stock_rows)]) + meta.append(["Ledger Differences — Action Required", len(ledger_action_rows)]) + meta.append(["Stock Differences — Action Required", len(stock_action_rows)]) + meta.append([]) + meta.append(["Ledger Status", "Count"]) + for label, key in ( + ("Matched", "matched"), + ("Difference", "difference"), + ("Missing in Current Year", "missing_in_current_year"), + ("New in Current Year", "new_in_current_year"), + ): + meta.append([label, summary_counts["ledgers"].get(key, 0)]) + meta.append([]) + meta.append(["Stock Item Status", "Count"]) + for label, key in ( + ("Matched", "matched"), + ("Quantity Difference", "quantity_difference"), + ("Value Difference", "value_difference"), + ("Quantity & Value Difference", "quantity_and_value_difference"), + ("Unit Difference", "unit_difference"), + ("Missing in Current Year", "missing_in_current_year"), + ("New in Current Year", "new_in_current_year"), + ): + meta.append([label, summary_counts["stock_items"].get(key, 0)]) + meta.append([]) + meta.append(["Generated", datetime.now().strftime("%Y-%m-%d %H:%M:%S")]) + + # Mirror the web Summary -> Ledger Differences — Action Required table. + law = wb.create_sheet("Ledger Action Summary") + ledger_action_headers = [ + "Apply", "Previous Ledger", "Previous Group", "PY Closing", + "Current Ledger", "Current Group", "CY Opening", "Difference", + "Match", "Match Method", "Confidence %", "Correction Status", "Correction Note", + ] + law.append(ledger_action_headers) + for row in ledger_action_rows: + law.append([ + "", row.previous_name or "", row.previous_group or "", + float(row.previous_closing_balance or 0), row.current_name or "", + row.current_group or "", float(row.current_opening_balance or 0), + float(row.difference or 0), row.match_status or "", row.match_method or "", + int(row.confidence or 0), row.correction_status or "", row.correction_note or "", + ]) + + # Mirror the web Summary -> Stock Differences — Action Required table. + saw = wb.create_sheet("Stock Action Summary") + stock_action_headers = [ + "Apply", "Previous Stock Item", "Previous Group", "HSN", "Unit", + "PY Closing Qty", "PY Closing Value", "Current Stock Item", "Current Group", + "Current HSN", "Current Unit", "CY Opening Qty", "CY Opening Value", + "Qty Difference", "Value Difference", "Status", "Match Method", + "Confidence %", "Correction Status", "Correction Note", + ] + saw.append(stock_action_headers) + for row in stock_action_rows: + saw.append([ + "", row.previous_name or "", row.previous_group or "", row.previous_hsn or "", + row.previous_unit or "", float(row.previous_closing_qty or 0), + float(row.previous_closing_value or 0), row.current_name or "", + row.current_group or "", row.current_hsn or "", row.current_unit or "", + float(row.current_opening_qty or 0), float(row.current_opening_value or 0), + float(row.quantity_difference or 0), float(row.value_difference or 0), + row.match_status or "", row.match_method or "", int(row.confidence or 0), + row.correction_status or "", row.correction_note or "", + ]) + + ws = wb.create_sheet("Balance Sheet Ledgers") headers = [ "Previous Ledger", "Previous Group", "PY Closing", "Current Ledger", "Current Group", "CY Opening", "Difference", "Status", @@ -337,32 +429,27 @@ def download_report(request: Request, run_id: int, client_id: int | None = None) row.correction_status or "", row.correction_note or "", ]) - meta = wb.create_sheet("Summary", 0) - meta.append(["Opening Balance Review"]) - meta.append(["Client", getattr(selected, "client_name", "") or getattr(selected, "name", "") or str(selected.id)]) - meta.append(["Run ID", run.id]) - meta.append(["Previous Tally Company", run.previous_company_name]) - meta.append(["Current Tally Company", run.current_company_name]) - meta.append(["Scope", "Balance Sheet ledgers and Stock Items only"]) - meta.append(["Balance Sheet Ledger Rows", len(ledger_rows)]) - meta.append(["Stock Item Rows", len(stock_rows)]) - meta.append(["Generated", datetime.now().strftime("%Y-%m-%d %H:%M:%S")]) - header_fill = PatternFill("solid", fgColor="DCE6F1") - for sheet in (ws, sws): + action_fill = PatternFill("solid", fgColor="FCE4D6") + for sheet in (law, saw, ws, sws): sheet.freeze_panes = "A2" sheet.auto_filter.ref = sheet.dimensions for cell in sheet[1]: cell.font = Font(bold=True) - cell.fill = header_fill + cell.fill = action_fill if sheet in (law, saw) else header_fill cell.alignment = Alignment(vertical="center", wrap_text=True) for col in sheet.columns: letter = col[0].column_letter - width = min(38, max(10, max(len(str(c.value or "")) for c in col[:200]) + 2)) + width = min(42, max(10, max(len(str(c.value or "")) for c in col[:200]) + 2)) sheet.column_dimensions[letter].width = width + meta["A1"].font = Font(bold=True, size=14) - meta.column_dimensions["A"].width = 28 + meta.column_dimensions["A"].width = 38 meta.column_dimensions["B"].width = 48 + for row_no in (12, 18): + for cell in meta[row_no]: + cell.font = Font(bold=True) + cell.fill = header_fill output = BytesIO() wb.save(output)