lambda-erp 0.2.1__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/PKG-INFO +4 -4
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/README.md +3 -3
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/chat.py +37 -11
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/accounting/chart_of_accounts.py +59 -27
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/accounting/setup/engine.py +79 -25
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/accounting/setup/packs/__init__.py +4 -4
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/accounting/setup/packs/ch.py +2 -0
- lambda_erp-0.2.3/lambda_erp/accounting/setup/packs/de_common.py +67 -0
- lambda_erp-0.2.3/lambda_erp/accounting/setup/packs/de_skr03.py +222 -0
- lambda_erp-0.2.3/lambda_erp/accounting/setup/packs/de_skr04.py +220 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/pyproject.toml +1 -1
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/.gitignore +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/LICENSE +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/__init__.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/attachments.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/auth.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/bootstrap.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/demo_limits.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/deps.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/errors.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/main.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/oauth.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/pdf.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/providers.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/remarks_md.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/routers/__init__.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/routers/accounting.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/routers/admin.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/routers/analytics.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/routers/chat_api.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/routers/documents.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/routers/masters.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/routers/proposals.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/routers/reports.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/routers/setup.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/services.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/templates/document.html +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/api/templates/proposal.html +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/docs/agents/README.md +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/frontend/README.md +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/frontend/src/api/client.ts +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/accounting/setup/__init__.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/accounting/setup/pack.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/accounting/setup/packs/generic.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/accounting/setup/profiles.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/accounting/setup/spine.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/database.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/model.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.2.1 → lambda_erp-0.2.3}/terraform/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lambda-erp
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Core ERP logic - accounting, sales, purchasing, inventory
|
|
5
5
|
Author: TORUS INVESTMENTS AG
|
|
6
6
|
License-Expression: MIT
|
|
@@ -141,7 +141,7 @@ Standing up a new company usually means an accountant hand-building a chart of a
|
|
|
141
141
|
|
|
142
142
|
Two dimensions shape the chart, and they compose:
|
|
143
143
|
|
|
144
|
-
- **Localization packs** bring a country's real chart and tax setup. Today: a **generic / international** chart (also the permanent fallback for anywhere not yet localized)
|
|
144
|
+
- **Localization packs** bring a country's real chart and tax setup. Today: a **generic / international** chart (also the permanent fallback for anywhere not yet localized); **Switzerland** — the *Kontenrahmen KMU*, in German, in CHF, with MWST tax templates at the current rates; and **Germany** — the DATEV standard in both common variants, **SKR03** and **SKR04**, in German, in EUR, with Umsatzsteuer/Vorsteuer templates at 19 % and 7 %. Where a country ships more than one standard chart (Germany's SKR03 vs SKR04), you pick the variant; a bare "Germany" defaults to the more widely used SKR03. Each country is a self-contained pack, so more can land without disturbing the ones already there — that's the roadmap, one jurisdiction at a time.
|
|
145
145
|
- **Sector profiles** tailor the chart to how you operate — **services, retail / POS, hospitality, wholesale / distribution, import / export, manufacturing, construction** — adding the accounts that model actually needs (work-in-progress and retention for construction, food-vs-beverage cost lines for hospitality, landed-cost accounts for import/export, and so on). Profiles are country-independent: the same seven apply on top of every localization.
|
|
146
146
|
|
|
147
147
|
The result is booked in one step — chart of accounts, sensible default accounts, tax templates, and a cost center — ready for opening balances. New here? The in-app **Tutorial** has a one-click **Get started** that opens the wizard in chat.
|
|
@@ -150,7 +150,7 @@ The result is booked in one step — chart of accounts, sensible default account
|
|
|
150
150
|
|
|
151
151
|
## What works today
|
|
152
152
|
|
|
153
|
-
- **Guided company setup through chat** — pick a country (generic or
|
|
153
|
+
- **Guided company setup through chat** — pick a country (generic, Switzerland, or Germany SKR03/SKR04, more coming) and business type, preview the exact chart of accounts, confirm, and it's booked
|
|
154
154
|
- Full sales and purchase cycles (Quotation → Sales Order → Delivery Note → Sales Invoice → Payment Entry, and the buying equivalents)
|
|
155
155
|
- Returns / credit notes / debit notes with proper GL and stock reversal
|
|
156
156
|
- Moving-average stock ledger with negative-stock protection
|
|
@@ -190,7 +190,7 @@ Four things had to be true for this to work, and they all became true in the las
|
|
|
190
190
|
|
|
191
191
|
Every company configuring an ERP runs into the same problems: local tax rules, common workflow patterns, industry-specific accounting quirks. Most of that knowledge isn't a competitive advantage — it's the same ground being re-covered separately at every implementation, by every team, in slightly different ways.
|
|
192
192
|
|
|
193
|
-
We want Lambda ERP to be where that knowledge lives in public. The base system is MIT-licensed, and the repo has real seams for that knowledge to slot into: a new country's chart of accounts and tax rules is a self-contained **localization pack**, and an industry's way of operating is a **sector profile** — both live in `lambda_erp/accounting/setup/` and are documented for contributors in [`docs/agents/company_setup.md`](docs/agents/company_setup.md).
|
|
193
|
+
We want Lambda ERP to be where that knowledge lives in public. The base system is MIT-licensed, and the repo has real seams for that knowledge to slot into: a new country's chart of accounts and tax rules is a self-contained **localization pack**, and an industry's way of operating is a **sector profile** — both live in `lambda_erp/accounting/setup/` and are documented for contributors in [`docs/agents/company_setup.md`](docs/agents/company_setup.md). An Austrian or French chart, a U.S. sales-tax-by-state module, a template for professional services — each is an additive contribution that every deployment picks up (the Swiss KMU and German DATEV packs already in the tree are the worked examples). The goal is a true community where running an ERP gets cheaper, faster, and more flexible for everyone, not just the implementer.
|
|
194
194
|
|
|
195
195
|
---
|
|
196
196
|
|
|
@@ -114,7 +114,7 @@ Standing up a new company usually means an accountant hand-building a chart of a
|
|
|
114
114
|
|
|
115
115
|
Two dimensions shape the chart, and they compose:
|
|
116
116
|
|
|
117
|
-
- **Localization packs** bring a country's real chart and tax setup. Today: a **generic / international** chart (also the permanent fallback for anywhere not yet localized)
|
|
117
|
+
- **Localization packs** bring a country's real chart and tax setup. Today: a **generic / international** chart (also the permanent fallback for anywhere not yet localized); **Switzerland** — the *Kontenrahmen KMU*, in German, in CHF, with MWST tax templates at the current rates; and **Germany** — the DATEV standard in both common variants, **SKR03** and **SKR04**, in German, in EUR, with Umsatzsteuer/Vorsteuer templates at 19 % and 7 %. Where a country ships more than one standard chart (Germany's SKR03 vs SKR04), you pick the variant; a bare "Germany" defaults to the more widely used SKR03. Each country is a self-contained pack, so more can land without disturbing the ones already there — that's the roadmap, one jurisdiction at a time.
|
|
118
118
|
- **Sector profiles** tailor the chart to how you operate — **services, retail / POS, hospitality, wholesale / distribution, import / export, manufacturing, construction** — adding the accounts that model actually needs (work-in-progress and retention for construction, food-vs-beverage cost lines for hospitality, landed-cost accounts for import/export, and so on). Profiles are country-independent: the same seven apply on top of every localization.
|
|
119
119
|
|
|
120
120
|
The result is booked in one step — chart of accounts, sensible default accounts, tax templates, and a cost center — ready for opening balances. New here? The in-app **Tutorial** has a one-click **Get started** that opens the wizard in chat.
|
|
@@ -123,7 +123,7 @@ The result is booked in one step — chart of accounts, sensible default account
|
|
|
123
123
|
|
|
124
124
|
## What works today
|
|
125
125
|
|
|
126
|
-
- **Guided company setup through chat** — pick a country (generic or
|
|
126
|
+
- **Guided company setup through chat** — pick a country (generic, Switzerland, or Germany SKR03/SKR04, more coming) and business type, preview the exact chart of accounts, confirm, and it's booked
|
|
127
127
|
- Full sales and purchase cycles (Quotation → Sales Order → Delivery Note → Sales Invoice → Payment Entry, and the buying equivalents)
|
|
128
128
|
- Returns / credit notes / debit notes with proper GL and stock reversal
|
|
129
129
|
- Moving-average stock ledger with negative-stock protection
|
|
@@ -163,7 +163,7 @@ Four things had to be true for this to work, and they all became true in the las
|
|
|
163
163
|
|
|
164
164
|
Every company configuring an ERP runs into the same problems: local tax rules, common workflow patterns, industry-specific accounting quirks. Most of that knowledge isn't a competitive advantage — it's the same ground being re-covered separately at every implementation, by every team, in slightly different ways.
|
|
165
165
|
|
|
166
|
-
We want Lambda ERP to be where that knowledge lives in public. The base system is MIT-licensed, and the repo has real seams for that knowledge to slot into: a new country's chart of accounts and tax rules is a self-contained **localization pack**, and an industry's way of operating is a **sector profile** — both live in `lambda_erp/accounting/setup/` and are documented for contributors in [`docs/agents/company_setup.md`](docs/agents/company_setup.md).
|
|
166
|
+
We want Lambda ERP to be where that knowledge lives in public. The base system is MIT-licensed, and the repo has real seams for that knowledge to slot into: a new country's chart of accounts and tax rules is a self-contained **localization pack**, and an industry's way of operating is a **sector profile** — both live in `lambda_erp/accounting/setup/` and are documented for contributors in [`docs/agents/company_setup.md`](docs/agents/company_setup.md). An Austrian or French chart, a U.S. sales-tax-by-state module, a template for professional services — each is an additive contribution that every deployment picks up (the Swiss KMU and German DATEV packs already in the tree are the worked examples). The goal is a true community where running an ERP gets cheaper, faster, and more flexible for everyone, not just the implementer.
|
|
167
167
|
|
|
168
168
|
---
|
|
169
169
|
|
|
@@ -1081,13 +1081,22 @@ TOOLS = [
|
|
|
1081
1081
|
"applying). Call this first, walk the user through the plan and the big "
|
|
1082
1082
|
"decisions in plain language, and only call `apply_company_setup` once they "
|
|
1083
1083
|
"confirm.\n\n"
|
|
1084
|
+
"It also returns an `existing` block: if `existing.company_exists` is true, tell "
|
|
1085
|
+
"the user up front that the company already exists — setup will only add what's "
|
|
1086
|
+
"missing and leave the rest. If `existing.needs_confirmation` is true (already "
|
|
1087
|
+
"configured, or a currency/jurisdiction mismatch), surface `existing.advisory` as "
|
|
1088
|
+
"a clear warning before proceeding.\n\n"
|
|
1084
1089
|
"`sector` is one of: services, retail_pos, hospitality, distribution, "
|
|
1085
1090
|
"import_export, manufacturing, construction. If you're unsure which fits, "
|
|
1086
1091
|
"ASK the user about their business rather than guessing. `country` selects "
|
|
1087
|
-
"the jurisdiction chart. Available: 'generic' (international, USD)
|
|
1092
|
+
"the jurisdiction chart. Available: 'generic' (international, USD); 'CH' "
|
|
1088
1093
|
"(Switzerland — Kontenrahmen KMU, German account names, CHF, with MWST tax "
|
|
1089
|
-
"templates)
|
|
1090
|
-
"
|
|
1094
|
+
"templates); and 'DE' (Germany — DATEV chart, German account names, EUR, with "
|
|
1095
|
+
"Umsatzsteuer/Vorsteuer tax templates at 19 %/7 %). Germany has two chart "
|
|
1096
|
+
"`variant`s: 'skr03' (process-ordered, the most common — the default for a bare "
|
|
1097
|
+
"'DE') and 'skr04' (balance-sheet-ordered, DATEV's recommendation for newly "
|
|
1098
|
+
"founded companies); ask which they use if it matters. Any other country falls "
|
|
1099
|
+
"back to the generic chart — tell the user when `jurisdiction.is_fallback` is true."
|
|
1091
1100
|
),
|
|
1092
1101
|
"parameters": {
|
|
1093
1102
|
"type": "object",
|
|
@@ -1099,8 +1108,8 @@ TOOLS = [
|
|
|
1099
1108
|
"import_export", "manufacturing", "construction"],
|
|
1100
1109
|
"description": "Operating-mode profile. Omit if the business is generic/unknown.",
|
|
1101
1110
|
},
|
|
1102
|
-
"country": {"type": "string", "description": "Jurisdiction code, e.g. 'CH'. Defaults to generic."},
|
|
1103
|
-
"variant": {"type": "string", "description": "Sub-chart variant
|
|
1111
|
+
"country": {"type": "string", "description": "Jurisdiction code, e.g. 'CH' or 'DE'. Defaults to generic."},
|
|
1112
|
+
"variant": {"type": "string", "description": "Sub-chart variant. For 'DE': 'skr03' (default) or 'skr04'. Omit otherwise."},
|
|
1104
1113
|
"currency": {"type": "string", "description": "Base currency, e.g. 'USD', 'CHF'. Defaults to the pack currency."},
|
|
1105
1114
|
},
|
|
1106
1115
|
"required": ["name"],
|
|
@@ -1112,12 +1121,21 @@ TOOLS = [
|
|
|
1112
1121
|
"function": {
|
|
1113
1122
|
"name": "apply_company_setup",
|
|
1114
1123
|
"description": (
|
|
1115
|
-
"CREATE the company, its chart of accounts (base + sector overlay),
|
|
1116
|
-
"company default accounts, any jurisdiction tax
|
|
1117
|
-
"cost center — in one step. Only call
|
|
1118
|
-
"
|
|
1119
|
-
"
|
|
1120
|
-
"
|
|
1124
|
+
"CREATE/converge the company, its chart of accounts (base + sector overlay), "
|
|
1125
|
+
"the company default accounts, any jurisdiction tax templates, and the default "
|
|
1126
|
+
"cost center — in one step. Only call AFTER `plan_company_setup` and AFTER the "
|
|
1127
|
+
"user has confirmed the plan and any big decisions. Pass the SAME arguments you "
|
|
1128
|
+
"used for the approved plan.\n\n"
|
|
1129
|
+
"This is idempotent and safe to run alongside an existing deployment: it only "
|
|
1130
|
+
"adds accounts that are missing, fills only empty defaults, and never modifies "
|
|
1131
|
+
"or deletes existing data. The result is a reconciliation report "
|
|
1132
|
+
"(accounts_created vs accounts_skipped, defaults_set vs defaults_left_untouched) "
|
|
1133
|
+
"— relay it plainly.\n\n"
|
|
1134
|
+
"GUARDRAIL: if the company is already configured, or its currency differs from "
|
|
1135
|
+
"this setup, the call returns `{ok:false, needs_confirmation:true, advisory}` "
|
|
1136
|
+
"instead of proceeding. In that case, WARN the user clearly using the advisory "
|
|
1137
|
+
"(especially a currency/jurisdiction mismatch, which mixes two charts), and only "
|
|
1138
|
+
"re-call with `confirm_existing=true` if the user explicitly insists."
|
|
1121
1139
|
),
|
|
1122
1140
|
"parameters": {
|
|
1123
1141
|
"type": "object",
|
|
@@ -1131,6 +1149,11 @@ TOOLS = [
|
|
|
1131
1149
|
"country": {"type": "string"},
|
|
1132
1150
|
"variant": {"type": "string"},
|
|
1133
1151
|
"currency": {"type": "string"},
|
|
1152
|
+
"confirm_existing": {
|
|
1153
|
+
"type": "boolean",
|
|
1154
|
+
"description": "Set true ONLY to proceed after a needs_confirmation "
|
|
1155
|
+
"response and the user has insisted despite the warning.",
|
|
1156
|
+
},
|
|
1134
1157
|
},
|
|
1135
1158
|
"required": ["name"],
|
|
1136
1159
|
},
|
|
@@ -1677,6 +1700,7 @@ def _handle_apply_company_setup(args):
|
|
|
1677
1700
|
variant=(args.get("variant") or None),
|
|
1678
1701
|
sector=(args.get("sector") or None),
|
|
1679
1702
|
currency=(args.get("currency") or None),
|
|
1703
|
+
confirm_existing=bool(args.get("confirm_existing")),
|
|
1680
1704
|
)
|
|
1681
1705
|
|
|
1682
1706
|
|
|
@@ -1857,6 +1881,8 @@ When the user wants to **set up a new company**, get started, or create their bo
|
|
|
1857
1881
|
3. **Walk them through the plan** in plain language: the sector-specific accounts (`sector_added_accounts`) and the `sector.guidance` (why the chart is shaped this way). Present every item in `sector.big_decisions` and get an explicit yes/no on each — these are the decisions you must not make for them.
|
|
1858
1882
|
4. **Only after they confirm, call `apply_company_setup`** with the SAME arguments to create the company, accounts, defaults, and cost center.
|
|
1859
1883
|
If `jurisdiction.is_fallback` is true, tell the user their country isn't localized yet and you're using the generic international chart for now.
|
|
1884
|
+
|
|
1885
|
+
**Existing companies.** Setup is idempotent — it adds only missing accounts, fills only empty defaults, and never modifies or deletes existing data, so it runs safely alongside an existing deployment. Use the `existing` block the plan returns: if the company already exists, say so up front ("it already exists — I'll add what's missing and leave the rest"). If `apply_company_setup` returns `needs_confirmation` (the company is already configured, or the currency/jurisdiction differs and would mix two charts), do NOT force it — relay the `advisory` as a clear warning and re-call with `confirm_existing: true` only if the user explicitly insists. After applying, relay the reconciliation report plainly (how many accounts were created vs already present).
|
|
1860
1886
|
"""
|
|
1861
1887
|
else:
|
|
1862
1888
|
company_setup_section = ""
|
|
@@ -147,6 +147,8 @@ def create_accounts_from_tree(company_name, tree, currency="USD"):
|
|
|
147
147
|
Does NOT commit — the caller owns the transaction boundary.
|
|
148
148
|
"""
|
|
149
149
|
db = get_db()
|
|
150
|
+
created: list[str] = []
|
|
151
|
+
skipped: list[str] = []
|
|
150
152
|
|
|
151
153
|
def _create(subtree, parent=None, root_type=None, report_type=None):
|
|
152
154
|
for account_name, details in subtree.items():
|
|
@@ -156,36 +158,59 @@ def create_accounts_from_tree(company_name, tree, currency="USD"):
|
|
|
156
158
|
|
|
157
159
|
name = f"{account_name} - {account_abbr(company_name)}"
|
|
158
160
|
|
|
159
|
-
account
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
161
|
+
# Idempotent: skip an account that already exists rather than failing
|
|
162
|
+
# on the primary key. Lets setup run alongside an existing deployment
|
|
163
|
+
# and fill in only what's missing. Recursion into children still runs
|
|
164
|
+
# so a partially-built chart converges to complete.
|
|
165
|
+
if db.exists("Account", name):
|
|
166
|
+
skipped.append(name)
|
|
167
|
+
else:
|
|
168
|
+
db.insert("Account", _dict(
|
|
169
|
+
name=name,
|
|
170
|
+
account_name=account_name,
|
|
171
|
+
parent_account=parent,
|
|
172
|
+
company=company_name,
|
|
173
|
+
root_type=rt,
|
|
174
|
+
report_type=rpt,
|
|
175
|
+
account_type=details.get("account_type", ""),
|
|
176
|
+
account_currency=currency,
|
|
177
|
+
is_group=1 if has_children else 0,
|
|
178
|
+
))
|
|
179
|
+
created.append(name)
|
|
171
180
|
|
|
172
181
|
if has_children:
|
|
173
182
|
_create(details["children"], parent=name, root_type=rt, report_type=rpt)
|
|
174
183
|
|
|
175
184
|
_create(tree)
|
|
185
|
+
return {"created": created, "skipped": skipped}
|
|
176
186
|
|
|
177
187
|
|
|
178
188
|
def apply_company_defaults(company_name, defaults):
|
|
179
|
-
"""Point Company default-account fields at
|
|
189
|
+
"""Point Company default-account fields at their leaf accounts.
|
|
180
190
|
|
|
181
191
|
``defaults`` is ``{company_field: leaf_account_name}`` (un-suffixed); the
|
|
182
|
-
company suffix is appended here.
|
|
192
|
+
company suffix is appended here. **Fill-only-empty:** a field already set is
|
|
193
|
+
left untouched (never clobber a configured company). A target account that
|
|
194
|
+
doesn't exist is skipped (never point a default at a missing account). Does
|
|
195
|
+
NOT commit. Returns ``{"set", "left", "missing"}``.
|
|
183
196
|
"""
|
|
184
197
|
abbr = account_abbr(company_name)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
198
|
+
db = get_db()
|
|
199
|
+
to_set: dict[str, str] = {}
|
|
200
|
+
result = {"set": [], "left": [], "missing": []}
|
|
201
|
+
for field, leaf in defaults.items():
|
|
202
|
+
if db.get_value("Company", company_name, field):
|
|
203
|
+
result["left"].append(field) # already configured — keep it
|
|
204
|
+
continue
|
|
205
|
+
target = f"{leaf} - {abbr}"
|
|
206
|
+
if not db.exists("Account", target):
|
|
207
|
+
result["missing"].append((field, target))
|
|
208
|
+
continue
|
|
209
|
+
to_set[field] = target
|
|
210
|
+
result["set"].append(field)
|
|
211
|
+
if to_set:
|
|
212
|
+
db.set_value("Company", company_name, to_set)
|
|
213
|
+
return result
|
|
189
214
|
|
|
190
215
|
|
|
191
216
|
def setup_chart_of_accounts(company_name, currency="USD"):
|
|
@@ -204,16 +229,23 @@ def setup_chart_of_accounts(company_name, currency="USD"):
|
|
|
204
229
|
|
|
205
230
|
|
|
206
231
|
def setup_cost_center(company_name):
|
|
207
|
-
"""Create default cost center for a company.
|
|
232
|
+
"""Create the default cost center for a company (idempotent).
|
|
233
|
+
|
|
234
|
+
Skips creation if it already exists, and fills the cost-center company
|
|
235
|
+
defaults only when empty, so it is safe to run alongside an existing setup.
|
|
236
|
+
"""
|
|
208
237
|
db = get_db()
|
|
209
238
|
name = f"Main - {company_name[:4].upper()}"
|
|
210
|
-
db.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
db.
|
|
239
|
+
if not db.exists("Cost Center", name):
|
|
240
|
+
db.insert("Cost Center", _dict(
|
|
241
|
+
name=name,
|
|
242
|
+
cost_center_name="Main",
|
|
243
|
+
company=company_name,
|
|
244
|
+
is_group=0,
|
|
245
|
+
))
|
|
246
|
+
if not db.get_value("Company", company_name, "default_cost_center"):
|
|
247
|
+
db.set_value("Company", company_name, "default_cost_center", name)
|
|
248
|
+
if not db.get_value("Company", company_name, "round_off_cost_center"):
|
|
249
|
+
db.set_value("Company", company_name, "round_off_cost_center", name)
|
|
218
250
|
db.commit()
|
|
219
251
|
return name
|
|
@@ -142,6 +142,56 @@ def _resolved_defaults(company_name: str, defaults: dict) -> dict:
|
|
|
142
142
|
return {field: f"{leaf} - {abbr}" for field, leaf in defaults.items()}
|
|
143
143
|
|
|
144
144
|
|
|
145
|
+
# A company counts as "already configured" if any of its core posting defaults
|
|
146
|
+
# is set — the signal that it has a real, working chart rather than a couple of
|
|
147
|
+
# stray accounts from an abandoned attempt.
|
|
148
|
+
_CORE_DEFAULTS = ("default_income_account", "default_receivable_account",
|
|
149
|
+
"default_payable_account")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _existing_state(company_name: str, requested_currency: str) -> dict:
|
|
153
|
+
"""Describe any pre-existing company so setup can warn instead of dead-end.
|
|
154
|
+
|
|
155
|
+
Setup is idempotent (it only ever adds missing accounts and fills empty
|
|
156
|
+
defaults), so an existing company is never a hard error. But two situations
|
|
157
|
+
warrant an explicit "are you sure": a company already **configured** with its
|
|
158
|
+
own chart, and a **currency mismatch** (adding a second chart in a different
|
|
159
|
+
currency mixes two charts). Those set ``needs_confirmation`` — the caller must
|
|
160
|
+
only proceed when the user insists.
|
|
161
|
+
"""
|
|
162
|
+
db = get_db()
|
|
163
|
+
if not db.exists("Company", company_name):
|
|
164
|
+
return {"company_exists": False, "account_count": 0, "configured": False,
|
|
165
|
+
"existing_currency": None, "currency_mismatch": False,
|
|
166
|
+
"needs_confirmation": False, "advisory": ""}
|
|
167
|
+
|
|
168
|
+
count = len(db.get_all("Account", filters={"company": company_name}, fields=["name"]))
|
|
169
|
+
configured = any(db.get_value("Company", company_name, f) for f in _CORE_DEFAULTS)
|
|
170
|
+
existing_ccy = db.get_value("Company", company_name, "default_currency") or None
|
|
171
|
+
mismatch = bool(existing_ccy and requested_currency and existing_ccy != requested_currency)
|
|
172
|
+
|
|
173
|
+
if mismatch:
|
|
174
|
+
advisory = (
|
|
175
|
+
f"'{company_name}' already exists in {existing_ccy}, but this setup is in "
|
|
176
|
+
f"{requested_currency}. Adding a chart in a different currency mixes two "
|
|
177
|
+
"charts of accounts — warn the user clearly and proceed only if they insist.")
|
|
178
|
+
elif configured:
|
|
179
|
+
advisory = (
|
|
180
|
+
f"'{company_name}' is already configured ({count} accounts, defaults set). "
|
|
181
|
+
"Setup will only add accounts that are missing and change nothing else — "
|
|
182
|
+
"confirm the user wants to add to an already-configured company.")
|
|
183
|
+
elif count:
|
|
184
|
+
advisory = (
|
|
185
|
+
f"'{company_name}' already exists with {count} account(s) but isn't fully "
|
|
186
|
+
"configured. Setup will fill in the rest and leave the existing accounts alone.")
|
|
187
|
+
else:
|
|
188
|
+
advisory = ""
|
|
189
|
+
|
|
190
|
+
return {"company_exists": True, "account_count": count, "configured": configured,
|
|
191
|
+
"existing_currency": existing_ccy, "currency_mismatch": mismatch,
|
|
192
|
+
"needs_confirmation": bool(mismatch or configured), "advisory": advisory}
|
|
193
|
+
|
|
194
|
+
|
|
145
195
|
# ---------------------------------------------------------------------------
|
|
146
196
|
# Public API
|
|
147
197
|
# ---------------------------------------------------------------------------
|
|
@@ -181,6 +231,7 @@ def plan_company_setup(company_name: str,
|
|
|
181
231
|
"accounts": _tree_outline(tree),
|
|
182
232
|
"sector_added_accounts": added,
|
|
183
233
|
"defaults": _resolved_defaults(company_name, defaults),
|
|
234
|
+
"existing": _existing_state(company_name, ccy),
|
|
184
235
|
"warnings": skipped,
|
|
185
236
|
}
|
|
186
237
|
if profile is not None:
|
|
@@ -199,25 +250,33 @@ def apply_company_setup(company_name: str,
|
|
|
199
250
|
variant: Optional[str] = None,
|
|
200
251
|
sector: Optional[str] = None,
|
|
201
252
|
currency: Optional[str] = None,
|
|
202
|
-
|
|
203
|
-
"""
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
253
|
+
confirm_existing: bool = False) -> dict:
|
|
254
|
+
"""Converge a company toward the desired chart of accounts (idempotent).
|
|
255
|
+
|
|
256
|
+
Runs *alongside* an existing deployment: it creates the company if missing,
|
|
257
|
+
creates only the accounts that don't already exist, fills only the company
|
|
258
|
+
defaults that are still empty (never clobbering a configured company), and
|
|
259
|
+
creates the tax templates / cost center only if absent. Existing data is
|
|
260
|
+
never modified or deleted, so this is safe to re-run.
|
|
261
|
+
|
|
262
|
+
Guardrail (see :func:`_existing_state`): if the company is already configured
|
|
263
|
+
or the currency mismatches, it returns ``needs_confirmation`` instead of
|
|
264
|
+
proceeding — the caller must pass ``confirm_existing=True`` (only after the
|
|
265
|
+
user insists). Runs in a single transaction; returns a reconciliation report.
|
|
209
266
|
"""
|
|
210
267
|
db = get_db()
|
|
211
268
|
pack = resolve_pack(country, variant)
|
|
212
269
|
profile = get_profile(sector)
|
|
213
270
|
ccy = currency or pack.currency
|
|
214
271
|
|
|
215
|
-
|
|
216
|
-
if
|
|
272
|
+
state = _existing_state(company_name, ccy)
|
|
273
|
+
if state["needs_confirmation"] and not confirm_existing:
|
|
217
274
|
return {
|
|
218
275
|
"ok": False,
|
|
219
|
-
"
|
|
220
|
-
|
|
276
|
+
"needs_confirmation": True,
|
|
277
|
+
"existing": state,
|
|
278
|
+
"advisory": state["advisory"],
|
|
279
|
+
"hint": "Warn the user, then re-call with confirm_existing=True only if they insist.",
|
|
221
280
|
}
|
|
222
281
|
|
|
223
282
|
if not db.exists("Company", company_name):
|
|
@@ -230,34 +289,29 @@ def apply_company_setup(company_name: str,
|
|
|
230
289
|
|
|
231
290
|
tree, defaults, added, skipped = _merge(pack, profile)
|
|
232
291
|
|
|
233
|
-
create_accounts_from_tree(company_name, tree, ccy)
|
|
234
|
-
apply_company_defaults(company_name, defaults)
|
|
292
|
+
acct = create_accounts_from_tree(company_name, tree, ccy) # {created, skipped}
|
|
293
|
+
dflt = apply_company_defaults(company_name, defaults) # {set, left, missing}
|
|
235
294
|
|
|
236
295
|
tax_summary = []
|
|
237
296
|
if pack.setup_tax is not None:
|
|
238
297
|
tax_summary = pack.setup_tax(company_name, ccy) or []
|
|
239
298
|
|
|
240
|
-
cost_center = setup_cost_center(company_name) # commits internally
|
|
299
|
+
cost_center = setup_cost_center(company_name) # idempotent, commits internally
|
|
241
300
|
db.commit()
|
|
242
301
|
|
|
243
|
-
def _count(nodes):
|
|
244
|
-
n = 0
|
|
245
|
-
for _name, d in nodes.items():
|
|
246
|
-
children = d.get("children")
|
|
247
|
-
if children:
|
|
248
|
-
n += _count(children)
|
|
249
|
-
else:
|
|
250
|
-
n += 1
|
|
251
|
-
return n
|
|
252
|
-
|
|
253
302
|
return {
|
|
254
303
|
"ok": True,
|
|
255
304
|
"company": company_name,
|
|
256
305
|
"currency": ccy,
|
|
257
306
|
"jurisdiction": pack.key,
|
|
258
307
|
"sector": profile.key if profile else None,
|
|
259
|
-
"
|
|
308
|
+
"added_to_existing": state["company_exists"],
|
|
309
|
+
"accounts_created": len(acct["created"]),
|
|
310
|
+
"accounts_skipped": len(acct["skipped"]), # already present, left alone
|
|
260
311
|
"sector_added_accounts": added,
|
|
312
|
+
"defaults_set": dflt["set"],
|
|
313
|
+
"defaults_left_untouched": dflt["left"], # already configured, kept
|
|
314
|
+
"defaults_unresolved": dflt["missing"],
|
|
261
315
|
"cost_center": cost_center,
|
|
262
316
|
"tax_summary": tax_summary,
|
|
263
317
|
"warnings": skipped,
|
|
@@ -6,11 +6,11 @@ imported. Adding a jurisdiction = drop a module here and import it below.
|
|
|
6
6
|
|
|
7
7
|
generic the jurisdiction-neutral / international pack (and fallback)
|
|
8
8
|
ch Switzerland (Kontenrahmen KMU)
|
|
9
|
-
|
|
9
|
+
de.skr03 Germany, DATEV SKR03 (process-ordered) — default for a bare `de`
|
|
10
|
+
de.skr04 Germany, DATEV SKR04 (balance-sheet-ordered)
|
|
10
11
|
"""
|
|
11
12
|
|
|
12
13
|
from lambda_erp.accounting.setup.packs import generic # noqa: F401
|
|
13
14
|
from lambda_erp.accounting.setup.packs import ch # noqa: F401
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
# from lambda_erp.accounting.setup.packs import de_skr03 # noqa: F401
|
|
15
|
+
from lambda_erp.accounting.setup.packs import de_skr03 # noqa: F401
|
|
16
|
+
from lambda_erp.accounting.setup.packs import de_skr04 # noqa: F401
|
|
@@ -217,6 +217,8 @@ def ch_setup_tax(company_name, currency):
|
|
|
217
217
|
|
|
218
218
|
def _template(title, tax_type, rate, account_leaf):
|
|
219
219
|
name = f"{title} - {abbr}"
|
|
220
|
+
if db.exists("Tax Template", name):
|
|
221
|
+
return # idempotent: leave an existing template
|
|
220
222
|
db.insert("Tax Template", _dict(
|
|
221
223
|
name=name, title=title, company=company_name, tax_type=tax_type,
|
|
222
224
|
))
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Shared German (DATEV) helpers for the SKR03 / SKR04 packs.
|
|
2
|
+
|
|
3
|
+
The two German charts differ only in their *numbering scheme* — SKR03 is
|
|
4
|
+
process-ordered (Prozessgliederung), SKR04 balance-sheet-ordered
|
|
5
|
+
(Abschlussgliederung) — but the tax mechanics are identical: the same German
|
|
6
|
+
VAT (Umsatzsteuer / Vorsteuer) at the same two federal rates. Rather than copy
|
|
7
|
+
the ~30-line tax-template builder into both variant modules, the shared factory
|
|
8
|
+
lives here and each variant passes in its own tax-account leaf names.
|
|
9
|
+
|
|
10
|
+
German VAT rates (federal, unchanged since 2007; the 2020 COVID cut reverted
|
|
11
|
+
2021-01-01):
|
|
12
|
+
* **19 %** Regelsteuersatz (standard rate)
|
|
13
|
+
* **7 %** ermäßigter Steuersatz (reduced rate — food, books, transit, …)
|
|
14
|
+
|
|
15
|
+
Output tax = *Umsatzsteuer* (a liability), input tax = *Vorsteuer* (recoverable).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from lambda_erp.utils import _dict
|
|
19
|
+
from lambda_erp.database import get_db
|
|
20
|
+
from lambda_erp.accounting.chart_of_accounts import account_abbr
|
|
21
|
+
|
|
22
|
+
# The two federal rates, shared by both charts.
|
|
23
|
+
STANDARD_RATE = 19.0
|
|
24
|
+
REDUCED_RATE = 7.0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def make_de_setup_tax(sales_taxes, purchase_taxes):
|
|
28
|
+
"""Build a pack ``setup_tax(company, currency)`` hook for a German chart.
|
|
29
|
+
|
|
30
|
+
``sales_taxes`` / ``purchase_taxes`` are lists of ``(title, rate,
|
|
31
|
+
account_leaf)`` where ``account_leaf`` is the tax account's name *within this
|
|
32
|
+
variant's chart* (the ``- <abbr>`` company suffix is appended here). Mirrors
|
|
33
|
+
``ch.ch_setup_tax``: idempotent, does not commit (the engine owns the
|
|
34
|
+
transaction), returns a short human summary.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def de_setup_tax(company_name, currency):
|
|
38
|
+
db = get_db()
|
|
39
|
+
abbr = account_abbr(company_name)
|
|
40
|
+
summary = []
|
|
41
|
+
|
|
42
|
+
def _template(title, tax_type, rate, account_leaf):
|
|
43
|
+
name = f"{title} - {abbr}"
|
|
44
|
+
if db.exists("Tax Template", name):
|
|
45
|
+
return # idempotent: leave an existing template
|
|
46
|
+
db.insert("Tax Template", _dict(
|
|
47
|
+
name=name, title=title, company=company_name, tax_type=tax_type,
|
|
48
|
+
))
|
|
49
|
+
db.insert("Tax Template Detail", _dict(
|
|
50
|
+
name=f"{name}-1",
|
|
51
|
+
parent=name,
|
|
52
|
+
charge_type="On Net Total",
|
|
53
|
+
account_head=f"{account_leaf} - {abbr}",
|
|
54
|
+
rate=rate,
|
|
55
|
+
description=title,
|
|
56
|
+
idx=1,
|
|
57
|
+
))
|
|
58
|
+
summary.append(f"{tax_type}: {title} @ {rate}%")
|
|
59
|
+
|
|
60
|
+
for title, rate, leaf in sales_taxes:
|
|
61
|
+
_template(title, "Sales", rate, leaf)
|
|
62
|
+
for title, rate, leaf in purchase_taxes:
|
|
63
|
+
_template(title, "Purchase", rate, leaf)
|
|
64
|
+
|
|
65
|
+
return summary
|
|
66
|
+
|
|
67
|
+
return de_setup_tax
|