File size: 6,429 Bytes
63f0c94 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | from __future__ import annotations
import io
from dataclasses import dataclass
from openpyxl import Workbook
from tools.document_parser import classify_transaction_row, parse_csv_file, parse_text_file
@dataclass
class MockUploadedFile:
name: str
payload: bytes
def getvalue(self) -> bytes:
return self.payload
def test_parse_income_statement_csv_extracts_totals() -> None:
csv_payload = (
"Date,Item,Category,Amount,Type\n"
"2026-04-01,Sales revenue,Sales,4500,Revenue\n"
"2026-04-01,Stock purchases,Cost of Goods Sold,2700,Expense\n"
"2026-04-01,Rent,Operating Expense,500,Expense\n"
"2026-04-01,Transport,Operating Expense,250,Expense\n"
"2026-04-01,Other expenses,Operating Expense,300,Expense\n"
).encode("utf-8")
uploaded = MockUploadedFile(name="income_statement.csv", payload=csv_payload)
result = parse_csv_file(uploaded, "Income Statement")
assert result["document_type"] == "Income Statement"
assert result["source_type"] == "csv"
assert result["rows_analyzed"] == 5
assert result["revenue"] == 4500
assert result["expenses"] == 3750
assert result["profit"] == 750
assert result["profit_margin"] == 16.67
def test_parse_cash_flow_record_detects_inflows_and_outflows() -> None:
csv_payload = (
"Date,Description,Cash In,Cash Out,Category\n"
"2026-04-01,Customer sales,1200,0,Sales\n"
"2026-04-01,Stock purchase,0,700,Stock\n"
"2026-04-02,Customer sales,900,0,Sales\n"
"2026-04-02,Transport payment,0,100,Transport\n"
).encode("utf-8")
uploaded = MockUploadedFile(name="cash_flow.csv", payload=csv_payload)
result = parse_csv_file(uploaded, "Cash Flow Record")
assert result["cash_inflow"] == 2100
assert result["cash_outflow"] == 800
assert result["revenue"] == 2100
assert result["expenses"] == 800
assert result["profit"] == 1300
def test_parse_text_file_uses_text_parser() -> None:
text_payload = (
"I made K4,500 in sales and spent K2,700 on stock, K500 on rent, "
"K250 on transport, and K300 on other expenses. I owe K1,000."
).encode("utf-8")
uploaded = MockUploadedFile(name="notes.txt", payload=text_payload)
result = parse_text_file(uploaded, "General Business Notes")
assert result["source_type"] == "txt"
assert result["document_type"] == "General Business Notes"
assert result["revenue"] == 4500
assert result["expenses"] == 3750
assert result["debt"] == 1000
assert result["profit"] == 750
def test_classify_transaction_row_uses_document_type() -> None:
sales_row = {"Product": "Mealie meal", "Total Sales": 900}
expense_row = {"Description": "Rent", "Amount": 500}
assert classify_transaction_row(sales_row, "Sales Record") == "revenue"
assert classify_transaction_row(expense_row, "Expense Record") == "expense"
def test_parse_excel_income_statement_derives_totals_when_total_cells_blank() -> None:
wb = Workbook()
ws = wb.active
ws.title = "Income Statement"
ws.append(["Line Item", "Nov 2025", "Dec 2025", "Jan 2026", "Feb 2026", "Mar 2026", "Apr 2026"])
ws.append(["REVENUE", None, None, None, None, None, None])
ws.append(["Product sales", 8200, 8900, 6800, 6400, 6100, 5900])
ws.append(["Service income", 1200, 1400, 900, 800, 750, 700])
ws.append(["Other income", 300, 400, 200, 150, 100, 80])
ws.append(["Total Revenue", None, None, None, None, None, None])
ws.append(["COST OF GOODS SOLD", None, None, None, None, None, None])
ws.append(["Cost of goods sold", 6800, 7400, 6200, 6000, 5900, 5800])
ws.append(["Freight & delivery", 450, 500, 380, 360, 340, 330])
ws.append(["OPERATING EXPENSES", None, None, None, None, None, None])
ws.append(["Rent", 3200, 3200, 3200, 3200, 3200, 3200])
ws.append(["Staff wages", 2800, 2800, 2800, 2800, 2800, 2800])
ws.append(["Loan repayment", 800, 800, 800, 800, 800, 800])
ws.append(["OTHER EXPENSES", None, None, None, None, None, None])
ws.append(["Other expenses", 420, 380, 310, 290, 270, 260])
ws.append(["Bank interest (overdraft)", 120, 130, 140, 150, 160, 170])
ws.append(["Supplier late payment fees", 80, 0, 200, 150, 100, 80])
ws.append(["Total Opex", 8900, None, None, None, None, None])
ws.append(["NET PROFIT / (LOSS)", None, None, None, None, None, None])
payload = io.BytesIO()
wb.save(payload)
uploaded = MockUploadedFile(name="FinAgent_Loss_Income_Statement.xlsx", payload=payload.getvalue())
# Totals expected from summing monthly detailed rows
expected_revenue = 49280.0
expected_expenses = 84670.0
expected_profit = -35390.0
from tools.document_parser import parse_excel_file
result = parse_excel_file(uploaded, "Income Statement")
assert result["source_type"] == "xlsx"
assert result["document_type"] == "Income Statement"
assert result["warnings"] == []
assert result["rows_analyzed"] == 19
assert result["period_description"] == "Nov 2025 – Apr 2026"
assert result["revenue"] == expected_revenue
assert result["expenses"] == expected_expenses
assert result["profit"] == expected_profit
def test_parse_excel_income_statement_reconciles_operating_vs_net_profit() -> None:
wb = Workbook()
ws = wb.active
ws.title = "Income Statement"
ws.append(["Line Item", "Nov 2025", "Dec 2025", "Jan 2026", "Feb 2026", "Mar 2026", "Apr 2026", "Total"])
ws.append(["Total Revenue", 9850, 10890, 8100, 7575, 7230, 6908, 50553])
ws.append(["Total COGS", 6575, 7264, 5404, 5054, 4826, 4611, 33734])
ws.append(["Total Operating Expenses", 7245, 7380, 6845, 6788, 6715, 6660, 41633])
ws.append(["OPERATING PROFIT (EBIT)", -3970, -3754, -4149, -4267, -4311, -4363, -24814])
ws.append(["Total Finance Costs", 97, 92, 87, 82, 77, 72, 507])
ws.append(["NET PROFIT", -4067, -3846, -4236, -4349, -4388, -4435, -25321])
payload = io.BytesIO()
wb.save(payload)
uploaded = MockUploadedFile(name="Duka_AI_Income_Statement.xlsx", payload=payload.getvalue())
from tools.document_parser import parse_excel_file
result = parse_excel_file(uploaded, "Income Statement")
assert result["revenue"] == 50553.0
# Should reconcile to net profit, not operating-expense subtotal.
assert result["profit"] == -25321.0
assert result["expenses"] == 75874.0
|