lambda-erp 0.2.1__tar.gz → 0.2.2__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.
Files changed (79) hide show
  1. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/PKG-INFO +1 -1
  2. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/chat.py +28 -6
  3. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/accounting/chart_of_accounts.py +59 -27
  4. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/accounting/setup/engine.py +79 -25
  5. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/accounting/setup/packs/ch.py +2 -0
  6. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/pyproject.toml +1 -1
  7. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/.gitignore +0 -0
  8. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/LICENSE +0 -0
  9. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/README.md +0 -0
  10. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/__init__.py +0 -0
  11. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/attachments.py +0 -0
  12. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/auth.py +0 -0
  13. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/bootstrap.py +0 -0
  14. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/demo_limits.py +0 -0
  15. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/deps.py +0 -0
  16. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/errors.py +0 -0
  17. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/main.py +0 -0
  18. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/oauth.py +0 -0
  19. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/pdf.py +0 -0
  20. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/providers.py +0 -0
  21. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/remarks_md.py +0 -0
  22. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/routers/__init__.py +0 -0
  23. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/routers/accounting.py +0 -0
  24. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/routers/admin.py +0 -0
  25. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/routers/analytics.py +0 -0
  26. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/routers/bank_reconciliation.py +0 -0
  27. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/routers/chat_api.py +0 -0
  28. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/routers/documents.py +0 -0
  29. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/routers/masters.py +0 -0
  30. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/routers/proposals.py +0 -0
  31. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/routers/reports.py +0 -0
  32. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/routers/setup.py +0 -0
  33. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/services.py +0 -0
  34. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/templates/document.html +0 -0
  35. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/api/templates/proposal.html +0 -0
  36. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/docs/agents/README.md +0 -0
  37. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/frontend/README.md +0 -0
  38. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/frontend/src/api/client.ts +0 -0
  39. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/__init__.py +0 -0
  40. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/accounting/__init__.py +0 -0
  41. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/accounting/bank_transaction.py +0 -0
  42. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/accounting/budget.py +0 -0
  43. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/accounting/general_ledger.py +0 -0
  44. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/accounting/journal_entry.py +0 -0
  45. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/accounting/payment_entry.py +0 -0
  46. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/accounting/pos_invoice.py +0 -0
  47. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/accounting/purchase_invoice.py +0 -0
  48. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/accounting/revaluation.py +0 -0
  49. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/accounting/sales_invoice.py +0 -0
  50. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/accounting/setup/__init__.py +0 -0
  51. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/accounting/setup/pack.py +0 -0
  52. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
  53. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/accounting/setup/packs/generic.py +0 -0
  54. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/accounting/setup/profiles.py +0 -0
  55. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/accounting/setup/spine.py +0 -0
  56. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/accounting/subscription.py +0 -0
  57. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/buying/__init__.py +0 -0
  58. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/buying/purchase_order.py +0 -0
  59. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/controllers/__init__.py +0 -0
  60. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/controllers/currency.py +0 -0
  61. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/controllers/defaults.py +0 -0
  62. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/controllers/pricing_rule.py +0 -0
  63. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  64. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/database.py +0 -0
  65. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/exceptions.py +0 -0
  66. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/hooks.py +0 -0
  67. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/model.py +0 -0
  68. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/selling/__init__.py +0 -0
  69. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/selling/proposal.py +0 -0
  70. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/selling/quotation.py +0 -0
  71. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/selling/sales_order.py +0 -0
  72. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/simulation.py +0 -0
  73. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/stock/__init__.py +0 -0
  74. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/stock/delivery_note.py +0 -0
  75. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/stock/purchase_receipt.py +0 -0
  76. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/stock/stock_entry.py +0 -0
  77. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/stock/stock_ledger.py +0 -0
  78. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/lambda_erp/utils.py +0 -0
  79. {lambda_erp-0.2.1 → lambda_erp-0.2.2}/terraform/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lambda-erp
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: MIT
@@ -1081,6 +1081,11 @@ 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 "
@@ -1112,12 +1117,21 @@ TOOLS = [
1112
1117
  "function": {
1113
1118
  "name": "apply_company_setup",
1114
1119
  "description": (
1115
- "CREATE the company, its chart of accounts (base + sector overlay), the "
1116
- "company default accounts, any jurisdiction tax accounts, and the default "
1117
- "cost center — in one step. Only call this AFTER `plan_company_setup` and "
1118
- "AFTER the user has explicitly confirmed the plan and any big decisions. "
1119
- "Refuses if the company already has accounts (admin-only). Pass the SAME "
1120
- "arguments you used for the plan the user approved."
1120
+ "CREATE/converge the company, its chart of accounts (base + sector overlay), "
1121
+ "the company default accounts, any jurisdiction tax templates, and the default "
1122
+ "cost center — in one step. Only call AFTER `plan_company_setup` and AFTER the "
1123
+ "user has confirmed the plan and any big decisions. Pass the SAME arguments you "
1124
+ "used for the approved plan.\n\n"
1125
+ "This is idempotent and safe to run alongside an existing deployment: it only "
1126
+ "adds accounts that are missing, fills only empty defaults, and never modifies "
1127
+ "or deletes existing data. The result is a reconciliation report "
1128
+ "(accounts_created vs accounts_skipped, defaults_set vs defaults_left_untouched) "
1129
+ "— relay it plainly.\n\n"
1130
+ "GUARDRAIL: if the company is already configured, or its currency differs from "
1131
+ "this setup, the call returns `{ok:false, needs_confirmation:true, advisory}` "
1132
+ "instead of proceeding. In that case, WARN the user clearly using the advisory "
1133
+ "(especially a currency/jurisdiction mismatch, which mixes two charts), and only "
1134
+ "re-call with `confirm_existing=true` if the user explicitly insists."
1121
1135
  ),
1122
1136
  "parameters": {
1123
1137
  "type": "object",
@@ -1131,6 +1145,11 @@ TOOLS = [
1131
1145
  "country": {"type": "string"},
1132
1146
  "variant": {"type": "string"},
1133
1147
  "currency": {"type": "string"},
1148
+ "confirm_existing": {
1149
+ "type": "boolean",
1150
+ "description": "Set true ONLY to proceed after a needs_confirmation "
1151
+ "response and the user has insisted despite the warning.",
1152
+ },
1134
1153
  },
1135
1154
  "required": ["name"],
1136
1155
  },
@@ -1677,6 +1696,7 @@ def _handle_apply_company_setup(args):
1677
1696
  variant=(args.get("variant") or None),
1678
1697
  sector=(args.get("sector") or None),
1679
1698
  currency=(args.get("currency") or None),
1699
+ confirm_existing=bool(args.get("confirm_existing")),
1680
1700
  )
1681
1701
 
1682
1702
 
@@ -1857,6 +1877,8 @@ When the user wants to **set up a new company**, get started, or create their bo
1857
1877
  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
1878
  4. **Only after they confirm, call `apply_company_setup`** with the SAME arguments to create the company, accounts, defaults, and cost center.
1859
1879
  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.
1880
+
1881
+ **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
1882
  """
1861
1883
  else:
1862
1884
  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 = _dict(
160
- name=name,
161
- account_name=account_name,
162
- parent_account=parent,
163
- company=company_name,
164
- root_type=rt,
165
- report_type=rpt,
166
- account_type=details.get("account_type", ""),
167
- account_currency=currency,
168
- is_group=1 if has_children else 0,
169
- )
170
- db.insert("Account", account)
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 the created leaf accounts.
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. Does NOT commit.
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
- resolved = {field: f"{leaf} - {abbr}" for field, leaf in defaults.items()}
186
- if resolved:
187
- db = get_db()
188
- db.set_value("Company", company_name, resolved)
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.insert("Cost Center", _dict(
211
- name=name,
212
- cost_center_name="Main",
213
- company=company_name,
214
- is_group=0,
215
- ))
216
- db.set_value("Company", company_name, "default_cost_center", name)
217
- db.set_value("Company", company_name, "round_off_cost_center", name)
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
- force: bool = False) -> dict:
203
- """Create the company's chart of accounts, defaults, tax, and cost center.
204
-
205
- Idempotency guard: refuses if the company already has accounts unless
206
- ``force=True``. Creates the Company row if it does not exist yet (so the chat
207
- can drive setup end-to-end); an already-created company is left in place.
208
- Runs in a single transaction.
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
- existing = db.get_all("Account", filters={"company": company_name}, fields=["name"])
216
- if existing and not force:
272
+ state = _existing_state(company_name, ccy)
273
+ if state["needs_confirmation"] and not confirm_existing:
217
274
  return {
218
275
  "ok": False,
219
- "error": f"{company_name} already has {len(existing)} accounts. "
220
- "Pass force=True to rebuild (this does not delete the old ones).",
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
- "accounts_created": _count(tree),
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,
@@ -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
  ))
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lambda-erp"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "Core ERP logic - accounting, sales, purchasing, inventory"
5
5
  readme = "README.md"
6
6
  license = "MIT"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes