diff --git a/Dockerfile b/Dockerfile index 62de574..5165d70 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,4 +24,6 @@ RUN mkdir -p /app/data/storage /app/data/year_backups EXPOSE 8000 -CMD ["sh", "-c", "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000"] \ No newline at end of file +HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=10 CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/', timeout=3)" + +CMD ["sh", "-c", "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000"] diff --git a/app/modules/accounting/gst_reconciliation_ui.py b/app/modules/accounting/gst_reconciliation_ui.py index 99647a4..9837194 100644 --- a/app/modules/accounting/gst_reconciliation_ui.py +++ b/app/modules/accounting/gst_reconciliation_ui.py @@ -27,9 +27,122 @@ from app.modules.credential_vault.crypto import decrypt_value from app.modules.credential_vault.models import CredentialVaultEntry from app.modules.credential_vault.service import can_view_entry, log_access from app.modules.documents.services import client_folder_parts, get_active_storage_node_for_branch, sanitize_segment -from app.modules.documents.local_agent_runtime.erp_local_agent.gst_portal_runtime import extract_invoice_rows, extract_gstr3b_itc from app.modules.registrations.models import ClientRegistration, RegistrationType + + +def _gst_parse_date(value: str) -> str: + text = str(value or "").strip() + for fmt in ("%d-%m-%Y", "%d/%m/%Y", "%Y-%m-%d", "%d-%b-%Y", "%d/%m/%y"): + try: + return datetime.strptime(text, fmt).date().isoformat() + except Exception: + pass + return text + + +def _gst_float(value) -> float: + try: + return float(value or 0) + except Exception: + return 0.0 + + +def _gst_doc_key(value: str) -> str: + return "".join(ch for ch in str(value or "").upper() if ch.isalnum()) + + +def _gst_walk(obj): + if isinstance(obj, dict): + yield obj + for value in obj.values(): + yield from _gst_walk(value) + elif isinstance(obj, list): + for value in obj: + yield from _gst_walk(value) + + +def _gst_tax_from_invoice(inv: dict) -> dict[str, float]: + out = {"taxable": 0.0, "igst": 0.0, "cgst": 0.0, "sgst": 0.0, "cess": 0.0} + for item in inv.get("itms") or inv.get("items") or []: + detail = item.get("itm_det") or item.get("item_det") or item + out["taxable"] += _gst_float(detail.get("txval")) + out["igst"] += _gst_float(detail.get("iamt")) + out["cgst"] += _gst_float(detail.get("camt")) + out["sgst"] += _gst_float(detail.get("samt")) + out["cess"] += _gst_float(detail.get("csamt")) + if not any(out.values()): + out["taxable"] = _gst_float(inv.get("txval") or inv.get("taxable_value")) + out["igst"] = _gst_float(inv.get("iamt") or inv.get("igst")) + out["cgst"] = _gst_float(inv.get("camt") or inv.get("cgst")) + out["sgst"] = _gst_float(inv.get("samt") or inv.get("sgst")) + out["cess"] = _gst_float(inv.get("csamt") or inv.get("cess")) + return out + + +def extract_invoice_rows(data) -> list[dict]: + """Normalize invoice-like rows from GST JSON without importing workstation runtime code.""" + rows: list[dict] = [] + seen: set[tuple[str, str, str]] = set() + for obj in _gst_walk(data): + invno = str( + obj.get("inum") + or obj.get("inv_num") + or obj.get("doc_no") + or obj.get("invoiceNumber") + or "" + ).strip() + if not invno: + continue + gstin = str( + obj.get("ctin") + or obj.get("stin") + or obj.get("supplier_gstin") + or obj.get("recipientGstin") + or "" + ).strip().upper() + invoice_date = _gst_parse_date( + str( + obj.get("idt") + or obj.get("inv_date") + or obj.get("doc_date") + or obj.get("invoiceDate") + or "" + ) + ) + key = (gstin, _gst_doc_key(invno), invoice_date) + if key in seen: + continue + seen.add(key) + tax = _gst_tax_from_invoice(obj) + rows.append( + { + "gstin": gstin, + "invoice_no": invno, + "invoice_key": _gst_doc_key(invno), + "invoice_date": invoice_date, + "invoice_value": _gst_float( + obj.get("val") or obj.get("invoice_value") or obj.get("invoiceValue") + ), + **tax, + } + ) + return rows + + +def extract_gstr3b_itc(data) -> dict[str, float]: + """Normalize GSTR-3B eligible-ITC totals from uploaded portal JSON.""" + totals = {"igst": 0.0, "cgst": 0.0, "sgst": 0.0, "cess": 0.0} + if not isinstance(data, dict): + return totals + itc = data.get("itc_elg") or {} + for item in itc.get("itc_avl") or []: + totals["igst"] += _gst_float(item.get("iamt")) + totals["cgst"] += _gst_float(item.get("camt")) + totals["sgst"] += _gst_float(item.get("samt")) + totals["cess"] += _gst_float(item.get("csamt")) + return totals + router = APIRouter(prefix="/tools/accounting/gst-reconciliation", tags=["accounting-gst-reconciliation-ui"]) _TOKEN_PURPOSE = "gst_operator_browser_v1" _TOKEN_MINUTES = 30