lambda-erp 0.2.1__py3-none-any.whl → 0.2.3__py3-none-any.whl

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.
api/chat.py CHANGED
@@ -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) and 'CH' "
1092
+ "the jurisdiction chart. Available: 'generic' (international, USD); 'CH' "
1088
1093
  "(Switzerland — Kontenrahmen KMU, German account names, CHF, with MWST tax "
1089
- "templates). Any other country falls back to the generic chart tell the "
1090
- "user when `jurisdiction.is_fallback` is true."
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, e.g. 'skr03' (rarely needed)."},
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), 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."
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 = _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,
@@ -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
- # de Germany (SKR03/04) later
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
- # Future packs register on import add them here, e.g.:
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
@@ -0,0 +1,222 @@
1
+ """Germany — SKR03 localization pack (DATEV Prozessgliederungsprinzip).
2
+
3
+ Hand-authored from the published **DATEV SKR03** standard chart — the
4
+ process-ordered Kontenrahmen (class 0 fixed assets & capital, class 1 finance,
5
+ class 3 goods inward, class 4 operating expense, class 8 revenue). SKR03 remains
6
+ the most widely used chart among German SMEs, so it is the default a bare
7
+ ``resolve_pack("de")`` lands on; its sibling ``de_skr04`` carries the *same*
8
+ accounts under the newer balance-sheet-ordered numbering.
9
+
10
+ Account numbers and their German titles are a German functional standard, not
11
+ anyone's copyrightable arrangement, so this is authored from the standard —
12
+ cross-checked against the LGPL Odoo ``l10n_de`` template data — rather than
13
+ copied from a packaged file, the same stance as the generic and Swiss packs.
14
+
15
+ A curated ~60-account core proportionate to the Swiss pack (not the full
16
+ ~1,270-account SKR03). Two German specifics:
17
+ * **EUR** base currency, German account titles carrying their SKR03 number.
18
+ * **Umsatzsteuer / Vorsteuer** — the shared ``make_de_setup_tax`` hook builds
19
+ Sales/Purchase tax templates at the two federal rates (19 % Regelsteuersatz,
20
+ 7 % ermäßigt) against the SKR03 USt / Vorsteuer accounts.
21
+
22
+ Note the process ordering: several accounts sit in a DATEV class whose leading
23
+ digit does not match their balance-sheet section (e.g. ``3980 Waren (Bestand)``
24
+ is an *asset* though it is numbered in the class-3 goods band, and ``1600
25
+ Verbindlichkeiten aLuL`` is a *liability* though numbered in the class-1 finance
26
+ band). The tree is organised by ``root_type`` — the number is only a label.
27
+ """
28
+
29
+ from lambda_erp.accounting.setup import spine
30
+ from lambda_erp.accounting.setup.pack import LocalizationPack, register_pack
31
+ from lambda_erp.accounting.setup.packs.de_common import (
32
+ make_de_setup_tax, STANDARD_RATE, REDUCED_RATE,
33
+ )
34
+
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Base chart — SKR03 (curated core). Numbers live in the account name (the
38
+ # Account doctype is name-keyed, no separate code column). root_type /
39
+ # report_type inherit from the parent; set on the five roots. Organised by
40
+ # balance-sheet section (root_type), NOT by the leading SKR03 digit.
41
+ # ---------------------------------------------------------------------------
42
+ DE_SKR03_CHART = {
43
+ "0-1 Aktiva": {
44
+ "root_type": "Asset",
45
+ "report_type": "Balance Sheet",
46
+ "children": {
47
+ "0 Anlagevermögen": {
48
+ "children": {
49
+ "0200 Technische Anlagen und Maschinen": {"account_type": "Fixed Asset"},
50
+ "0300 Andere Anlagen, Betriebs- und Geschäftsausstattung": {"account_type": "Fixed Asset"},
51
+ "0320 Pkw": {"account_type": "Fixed Asset"},
52
+ "0350 Lkw": {"account_type": "Fixed Asset"},
53
+ "0400 Betriebsausstattung": {"account_type": "Fixed Asset"},
54
+ "0420 Büroeinrichtung": {"account_type": "Fixed Asset"},
55
+ "0250 Wertberichtigungen zu Sachanlagen": {"account_type": "Accumulated Depreciation"},
56
+ },
57
+ },
58
+ "1 Finanz- und Umlaufvermögen": {
59
+ "children": {
60
+ "1000 Kasse": {"account_type": "Cash"},
61
+ "1200 Bank": {"account_type": "Bank"},
62
+ "1400 Forderungen aus Lieferungen und Leistungen": {"account_type": "Receivable"},
63
+ "1460 Geldtransit": {"account_type": ""},
64
+ "1571 Abziehbare Vorsteuer 7 %": {"account_type": ""},
65
+ "1576 Abziehbare Vorsteuer 19 %": {"account_type": ""},
66
+ "0980 Aktive Rechnungsabgrenzung": {"account_type": ""},
67
+ "3980 Waren (Bestand)": {"account_type": "Stock"},
68
+ "7000 Unfertige Erzeugnisse (Bestand)": {"account_type": "Stock"},
69
+ },
70
+ },
71
+ },
72
+ },
73
+ "08 Eigenkapital": {
74
+ "root_type": "Equity",
75
+ "report_type": "Balance Sheet",
76
+ "children": {
77
+ "0800 Gezeichnetes Kapital / Eigenkapital": {"account_type": ""},
78
+ "0840 Kapitalrücklage": {"account_type": ""},
79
+ "0860 Gewinnvortrag vor Verwendung": {"account_type": ""},
80
+ "0868 Verlustvortrag vor Verwendung": {"account_type": ""},
81
+ "9000 Saldenvorträge, Sachkonten (Eröffnungsbilanz)": {"account_type": ""},
82
+ },
83
+ },
84
+ "16-17 Verbindlichkeiten": {
85
+ "root_type": "Liability",
86
+ "report_type": "Balance Sheet",
87
+ "children": {
88
+ "16 Kurzfristige Verbindlichkeiten": {
89
+ "children": {
90
+ "1600 Verbindlichkeiten aus Lieferungen und Leistungen": {"account_type": "Payable"},
91
+ "1608 Wareneingänge ohne Rechnung": {"account_type": "Stock Received But Not Billed"},
92
+ "1740 Verbindlichkeiten aus Lohn und Gehalt": {"account_type": ""},
93
+ "1742 Verbindlichkeiten im Rahmen der sozialen Sicherheit": {"account_type": ""},
94
+ "1771 Umsatzsteuer 7 %": {"account_type": "Tax"},
95
+ "1776 Umsatzsteuer 19 %": {"account_type": "Tax"},
96
+ "0990 Passive Rechnungsabgrenzung": {"account_type": ""},
97
+ },
98
+ },
99
+ "17 Langfristige Verbindlichkeiten": {
100
+ "children": {
101
+ "0630 Verbindlichkeiten gegenüber Kreditinstituten": {"account_type": ""},
102
+ "0700 Verbindlichkeiten aus Darlehen": {"account_type": ""},
103
+ },
104
+ },
105
+ },
106
+ },
107
+ "8 Umsatzerlöse und betriebliche Erträge": {
108
+ "root_type": "Income",
109
+ "report_type": "Profit and Loss",
110
+ "children": {
111
+ "8200 Erlöse": {"account_type": "Income Account"},
112
+ "8300 Erlöse 7 % USt": {"account_type": "Income Account"},
113
+ "8400 Erlöse 19 % USt": {"account_type": "Income Account"},
114
+ "8700 Erlösschmälerungen (Skonti, Rabatte)": {"account_type": ""},
115
+ "2700 Sonstige betriebliche Erträge": {"account_type": ""},
116
+ "2660 Erträge aus Währungsumrechnung": {"account_type": ""},
117
+ },
118
+ },
119
+ "2-4 Aufwendungen": {
120
+ "root_type": "Expense",
121
+ "report_type": "Profit and Loss",
122
+ "children": {
123
+ "3 Wareneingang und Materialaufwand": {
124
+ "children": {
125
+ "3200 Wareneingang": {"account_type": "Cost of Goods Sold"},
126
+ "3300 Wareneingang 7 % Vorsteuer": {"account_type": "Cost of Goods Sold"},
127
+ "3400 Wareneingang 19 % Vorsteuer": {"account_type": "Cost of Goods Sold"},
128
+ "3030 Einkauf Roh-, Hilfs- und Betriebsstoffe": {"account_type": "Cost of Goods Sold"},
129
+ "3800 Bezugsnebenkosten": {"account_type": "Chargeable"},
130
+ "3830 Zölle und Einfuhrabgaben": {"account_type": "Chargeable"},
131
+ "3960 Bestandsveränderungen": {"account_type": "Stock Adjustment"},
132
+ },
133
+ },
134
+ "4 Betriebliche Aufwendungen": {
135
+ "children": {
136
+ "4100 Löhne und Gehälter": {"account_type": ""},
137
+ "4130 Gesetzliche soziale Aufwendungen": {"account_type": ""},
138
+ "4200 Raumkosten": {"account_type": ""},
139
+ "4210 Miete (unbewegliche Wirtschaftsgüter)": {"account_type": ""},
140
+ "4360 Versicherungen": {"account_type": ""},
141
+ "4500 Fahrzeugkosten": {"account_type": ""},
142
+ "4600 Werbekosten": {"account_type": ""},
143
+ "4660 Reisekosten": {"account_type": ""},
144
+ "4910 Porto, Telefon und Bürobedarf": {"account_type": ""},
145
+ "4830 Abschreibungen auf Sachanlagen": {"account_type": "Depreciation"},
146
+ "4840 Kursdifferenzen (Währungsumrechnung)": {"account_type": ""},
147
+ "4845 Kursdifferenzen (nicht realisiert)": {"account_type": ""},
148
+ "4970 Rundungsdifferenzen": {"account_type": "Round Off"},
149
+ },
150
+ },
151
+ "2 Neutrale Aufwendungen": {
152
+ "children": {
153
+ "2100 Zinsen und ähnliche Aufwendungen": {"account_type": ""},
154
+ "2280 Steuern vom Einkommen und Ertrag": {"account_type": ""},
155
+ },
156
+ },
157
+ },
158
+ },
159
+ }
160
+
161
+
162
+ DE_SKR03_ANCHORS = {
163
+ spine.CURRENT_ASSETS: "1 Finanz- und Umlaufvermögen",
164
+ spine.FIXED_ASSETS: "0 Anlagevermögen",
165
+ spine.CURRENT_LIABILITIES: "16 Kurzfristige Verbindlichkeiten",
166
+ spine.EQUITY: "08 Eigenkapital",
167
+ spine.INCOME: "8 Umsatzerlöse und betriebliche Erträge",
168
+ spine.DIRECT_COSTS: "3 Wareneingang und Materialaufwand",
169
+ spine.OPERATING_EXPENSES: "4 Betriebliche Aufwendungen",
170
+ }
171
+
172
+
173
+ # Every default the posting engine references must resolve to an SKR03 leaf, or
174
+ # postings that fall back to it will fail. Keys mirror STANDARD_DEFAULTS.
175
+ DE_SKR03_DEFAULTS = {
176
+ "default_receivable_account": "1400 Forderungen aus Lieferungen und Leistungen",
177
+ "default_payable_account": "1600 Verbindlichkeiten aus Lieferungen und Leistungen",
178
+ "default_income_account": "8400 Erlöse 19 % USt",
179
+ "default_expense_account": "3400 Wareneingang 19 % Vorsteuer",
180
+ "round_off_account": "4970 Rundungsdifferenzen",
181
+ "stock_in_hand_account": "3980 Waren (Bestand)",
182
+ "stock_received_but_not_billed": "1608 Wareneingänge ohne Rechnung",
183
+ "stock_adjustment_account": "3960 Bestandsveränderungen",
184
+ "default_opening_balance_equity": "9000 Saldenvorträge, Sachkonten (Eröffnungsbilanz)",
185
+ "default_freight_in_account": "3800 Bezugsnebenkosten",
186
+ "default_customs_account": "3830 Zölle und Einfuhrabgaben",
187
+ "default_exchange_gain_loss_account": "4840 Kursdifferenzen (Währungsumrechnung)",
188
+ "default_unrealized_exchange_account": "4845 Kursdifferenzen (nicht realisiert)",
189
+ }
190
+
191
+
192
+ # German VAT wiring — (title, rate, account_leaf). Output tax = Umsatzsteuer
193
+ # (liability), input tax = Vorsteuer (recoverable asset).
194
+ _SALES_TAXES = [
195
+ ("Umsatzsteuer 19 % (Regelsteuersatz)", STANDARD_RATE, "1776 Umsatzsteuer 19 %"),
196
+ ("Umsatzsteuer 7 % (ermäßigt)", REDUCED_RATE, "1771 Umsatzsteuer 7 %"),
197
+ ]
198
+ _PURCHASE_TAXES = [
199
+ ("Vorsteuer 19 %", STANDARD_RATE, "1576 Abziehbare Vorsteuer 19 %"),
200
+ ("Vorsteuer 7 %", REDUCED_RATE, "1571 Abziehbare Vorsteuer 7 %"),
201
+ ]
202
+
203
+ de_skr03_setup_tax = make_de_setup_tax(_SALES_TAXES, _PURCHASE_TAXES)
204
+
205
+
206
+ DE_SKR03_PACK = register_pack(LocalizationPack(
207
+ country="de",
208
+ variant="skr03",
209
+ label="Germany (DATEV SKR03)",
210
+ currency="EUR",
211
+ language="de",
212
+ base_chart=DE_SKR03_CHART,
213
+ anchors=DE_SKR03_ANCHORS,
214
+ defaults=DE_SKR03_DEFAULTS,
215
+ setup_tax=de_skr03_setup_tax,
216
+ notes=(
217
+ "German SME chart, DATEV SKR03 (process-ordered), German titles with "
218
+ "SKR03 numbers, EUR. Umsatzsteuer/Vorsteuer templates at 19 / 7 %. "
219
+ "Hand-authored from the published standard, cross-checked against Odoo "
220
+ "l10n_de. The most widely used German chart — the default for a bare `de`."
221
+ ),
222
+ ))
@@ -0,0 +1,220 @@
1
+ """Germany — SKR04 localization pack (DATEV Abschlussgliederungsprinzip).
2
+
3
+ Hand-authored from the published **DATEV SKR04** standard chart — the
4
+ balance-sheet-ordered Kontenrahmen (class 0 Anlagevermögen … class 7 further
5
+ income/expense), which is DATEV's recommendation for newly founded companies.
6
+ Account numbers and their German titles are a German functional standard, not
7
+ anyone's copyrightable arrangement, so this is authored from the standard —
8
+ cross-checked against the LGPL Odoo ``l10n_de`` template data — rather than
9
+ copied from a packaged file, the same stance as the generic and Swiss packs.
10
+
11
+ A curated ~60-account core proportionate to the Swiss pack (not the full
12
+ ~1,200-account SKR04); extend as needed. Two German specifics:
13
+ * **EUR** base currency, German account titles carrying their SKR04 number.
14
+ * **Umsatzsteuer / Vorsteuer** — the shared ``make_de_setup_tax`` hook builds
15
+ Sales/Purchase tax templates at the two federal rates (19 % Regelsteuersatz,
16
+ 7 % ermäßigt) against the SKR04 USt / Vorsteuer accounts.
17
+
18
+ Its sibling ``de_skr03`` carries the *same* accounts under the process-ordered
19
+ numbering; both register ``country="de"`` variants, so ``resolve_pack("de")``
20
+ falls back to the alphabetically-first variant (SKR03) when the caller does not
21
+ name one — SKR03 is still the most widely used chart among German SMEs.
22
+ """
23
+
24
+ from lambda_erp.accounting.setup import spine
25
+ from lambda_erp.accounting.setup.pack import LocalizationPack, register_pack
26
+ from lambda_erp.accounting.setup.packs.de_common import (
27
+ make_de_setup_tax, STANDARD_RATE, REDUCED_RATE,
28
+ )
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Base chart — SKR04 (curated core). Numbers live in the account name (the
33
+ # Account doctype is name-keyed, no separate code column). root_type /
34
+ # report_type inherit from the parent; set on the five roots. Group accounts
35
+ # (with children) carry no account_type; leaves carry the posting role.
36
+ # ---------------------------------------------------------------------------
37
+ DE_SKR04_CHART = {
38
+ "0-1 Aktiva": {
39
+ "root_type": "Asset",
40
+ "report_type": "Balance Sheet",
41
+ "children": {
42
+ "0 Anlagevermögen": {
43
+ "children": {
44
+ "0400 Technische Anlagen und Maschinen": {"account_type": "Fixed Asset"},
45
+ "0500 Andere Anlagen, Betriebs- und Geschäftsausstattung": {"account_type": "Fixed Asset"},
46
+ "0520 Pkw": {"account_type": "Fixed Asset"},
47
+ "0540 Lkw": {"account_type": "Fixed Asset"},
48
+ "0670 Betriebsausstattung": {"account_type": "Fixed Asset"},
49
+ "0690 Geschäftsausstattung": {"account_type": "Fixed Asset"},
50
+ "0810 Abschreibung auf Sachanlagen (Wertberichtigung)": {"account_type": "Accumulated Depreciation"},
51
+ },
52
+ },
53
+ "1 Umlaufvermögen": {
54
+ "children": {
55
+ "1000 Roh-, Hilfs- und Betriebsstoffe": {"account_type": "Stock"},
56
+ "1100 Fertige Erzeugnisse und Waren (Bestand)": {"account_type": "Stock"},
57
+ "1140 Waren (Bestand)": {"account_type": "Stock"},
58
+ "1200 Forderungen aus Lieferungen und Leistungen": {"account_type": "Receivable"},
59
+ "1210 Zweifelhafte Forderungen": {"account_type": ""},
60
+ "1401 Abziehbare Vorsteuer 7 %": {"account_type": ""},
61
+ "1406 Abziehbare Vorsteuer 19 %": {"account_type": ""},
62
+ "1460 Geldtransit": {"account_type": ""},
63
+ "1600 Kasse": {"account_type": "Cash"},
64
+ "1800 Bank": {"account_type": "Bank"},
65
+ "1900 Aktive Rechnungsabgrenzung": {"account_type": ""},
66
+ },
67
+ },
68
+ },
69
+ },
70
+ "2 Eigenkapital": {
71
+ "root_type": "Equity",
72
+ "report_type": "Balance Sheet",
73
+ "children": {
74
+ "2000 Festkapital / Gezeichnetes Kapital": {"account_type": ""},
75
+ "2100 Kapitalrücklage": {"account_type": ""},
76
+ "2970 Gewinnvortrag vor Verwendung": {"account_type": ""},
77
+ "2978 Verlustvortrag vor Verwendung": {"account_type": ""},
78
+ "9000 Saldenvorträge, Sachkonten (Eröffnungsbilanz)": {"account_type": ""},
79
+ },
80
+ },
81
+ "3 Fremdkapital": {
82
+ "root_type": "Liability",
83
+ "report_type": "Balance Sheet",
84
+ "children": {
85
+ "30 Kurzfristige Verbindlichkeiten": {
86
+ "children": {
87
+ "3300 Verbindlichkeiten aus Lieferungen und Leistungen": {"account_type": "Payable"},
88
+ "3395 Noch nicht abgerechnete Wareneingänge": {"account_type": "Stock Received But Not Billed"},
89
+ "3740 Verbindlichkeiten aus Lohn und Gehalt": {"account_type": ""},
90
+ "3790 Verbindlichkeiten im Rahmen der sozialen Sicherheit": {"account_type": ""},
91
+ "3801 Umsatzsteuer 7 %": {"account_type": "Tax"},
92
+ "3806 Umsatzsteuer 19 %": {"account_type": "Tax"},
93
+ "3900 Passive Rechnungsabgrenzung": {"account_type": ""},
94
+ },
95
+ },
96
+ "34 Langfristige Verbindlichkeiten": {
97
+ "children": {
98
+ "3150 Verbindlichkeiten gegenüber Kreditinstituten": {"account_type": ""},
99
+ "3170 Verbindlichkeiten aus Darlehen": {"account_type": ""},
100
+ },
101
+ },
102
+ },
103
+ },
104
+ "4 Umsatzerlöse und betriebliche Erträge": {
105
+ "root_type": "Income",
106
+ "report_type": "Profit and Loss",
107
+ "children": {
108
+ "4000 Umsatzerlöse": {"account_type": "Income Account"},
109
+ "4300 Erlöse 7 % USt": {"account_type": "Income Account"},
110
+ "4400 Erlöse 19 % USt": {"account_type": "Income Account"},
111
+ "4700 Erlösschmälerungen (Skonti, Rabatte)": {"account_type": ""},
112
+ "4830 Sonstige betriebliche Erträge": {"account_type": ""},
113
+ "4840 Erträge aus Währungsumrechnung": {"account_type": ""},
114
+ },
115
+ },
116
+ "5-7 Aufwendungen": {
117
+ "root_type": "Expense",
118
+ "report_type": "Profit and Loss",
119
+ "children": {
120
+ "5 Material- und Wareneinkauf": {
121
+ "children": {
122
+ "5000 Aufwendungen für Roh-, Hilfs- und Betriebsstoffe": {"account_type": "Cost of Goods Sold"},
123
+ "5200 Wareneingang": {"account_type": "Cost of Goods Sold"},
124
+ "5300 Wareneingang 7 % Vorsteuer": {"account_type": "Cost of Goods Sold"},
125
+ "5400 Wareneingang 19 % Vorsteuer": {"account_type": "Cost of Goods Sold"},
126
+ "5800 Bezugsnebenkosten": {"account_type": "Chargeable"},
127
+ "5820 Zölle und Einfuhrabgaben": {"account_type": "Chargeable"},
128
+ "5883 Bestandsveränderungen": {"account_type": "Stock Adjustment"},
129
+ },
130
+ },
131
+ "6 Sonstige betriebliche Aufwendungen": {
132
+ "children": {
133
+ "6000 Löhne und Gehälter": {"account_type": ""},
134
+ "6110 Gesetzliche soziale Aufwendungen": {"account_type": ""},
135
+ "6305 Raumkosten": {"account_type": ""},
136
+ "6310 Miete (unbewegliche Wirtschaftsgüter)": {"account_type": ""},
137
+ "6400 Versicherungen, Beiträge und Abgaben": {"account_type": ""},
138
+ "6500 Fahrzeugkosten": {"account_type": ""},
139
+ "6600 Werbekosten": {"account_type": ""},
140
+ "6650 Reisekosten": {"account_type": ""},
141
+ "6800 Porto, Telefon und Bürobedarf": {"account_type": ""},
142
+ "6220 Abschreibungen auf Sachanlagen": {"account_type": "Depreciation"},
143
+ "6880 Kursdifferenzen (Währungsumrechnung)": {"account_type": ""},
144
+ "6885 Kursdifferenzen (nicht realisiert)": {"account_type": ""},
145
+ "6969 Rundungsdifferenzen": {"account_type": "Round Off"},
146
+ },
147
+ },
148
+ "7 Weitere Aufwendungen": {
149
+ "children": {
150
+ "7300 Zinsen und ähnliche Aufwendungen": {"account_type": ""},
151
+ "7600 Steuern vom Einkommen und Ertrag": {"account_type": ""},
152
+ },
153
+ },
154
+ },
155
+ },
156
+ }
157
+
158
+
159
+ DE_SKR04_ANCHORS = {
160
+ spine.CURRENT_ASSETS: "1 Umlaufvermögen",
161
+ spine.FIXED_ASSETS: "0 Anlagevermögen",
162
+ spine.CURRENT_LIABILITIES: "30 Kurzfristige Verbindlichkeiten",
163
+ spine.EQUITY: "2 Eigenkapital",
164
+ spine.INCOME: "4 Umsatzerlöse und betriebliche Erträge",
165
+ spine.DIRECT_COSTS: "5 Material- und Wareneinkauf",
166
+ spine.OPERATING_EXPENSES: "6 Sonstige betriebliche Aufwendungen",
167
+ }
168
+
169
+
170
+ # Every default the posting engine references must resolve to an SKR04 leaf, or
171
+ # postings that fall back to it will fail. Keys mirror STANDARD_DEFAULTS.
172
+ DE_SKR04_DEFAULTS = {
173
+ "default_receivable_account": "1200 Forderungen aus Lieferungen und Leistungen",
174
+ "default_payable_account": "3300 Verbindlichkeiten aus Lieferungen und Leistungen",
175
+ "default_income_account": "4400 Erlöse 19 % USt",
176
+ "default_expense_account": "5400 Wareneingang 19 % Vorsteuer",
177
+ "round_off_account": "6969 Rundungsdifferenzen",
178
+ "stock_in_hand_account": "1140 Waren (Bestand)",
179
+ "stock_received_but_not_billed": "3395 Noch nicht abgerechnete Wareneingänge",
180
+ "stock_adjustment_account": "5883 Bestandsveränderungen",
181
+ "default_opening_balance_equity": "9000 Saldenvorträge, Sachkonten (Eröffnungsbilanz)",
182
+ "default_freight_in_account": "5800 Bezugsnebenkosten",
183
+ "default_customs_account": "5820 Zölle und Einfuhrabgaben",
184
+ "default_exchange_gain_loss_account": "6880 Kursdifferenzen (Währungsumrechnung)",
185
+ "default_unrealized_exchange_account": "6885 Kursdifferenzen (nicht realisiert)",
186
+ }
187
+
188
+
189
+ # German VAT wiring — (title, rate, account_leaf). Output tax = Umsatzsteuer
190
+ # (liability), input tax = Vorsteuer (recoverable asset).
191
+ _SALES_TAXES = [
192
+ ("Umsatzsteuer 19 % (Regelsteuersatz)", STANDARD_RATE, "3806 Umsatzsteuer 19 %"),
193
+ ("Umsatzsteuer 7 % (ermäßigt)", REDUCED_RATE, "3801 Umsatzsteuer 7 %"),
194
+ ]
195
+ _PURCHASE_TAXES = [
196
+ ("Vorsteuer 19 %", STANDARD_RATE, "1406 Abziehbare Vorsteuer 19 %"),
197
+ ("Vorsteuer 7 %", REDUCED_RATE, "1401 Abziehbare Vorsteuer 7 %"),
198
+ ]
199
+
200
+ de_skr04_setup_tax = make_de_setup_tax(_SALES_TAXES, _PURCHASE_TAXES)
201
+
202
+
203
+ DE_SKR04_PACK = register_pack(LocalizationPack(
204
+ country="de",
205
+ variant="skr04",
206
+ label="Germany (DATEV SKR04)",
207
+ currency="EUR",
208
+ language="de",
209
+ base_chart=DE_SKR04_CHART,
210
+ anchors=DE_SKR04_ANCHORS,
211
+ defaults=DE_SKR04_DEFAULTS,
212
+ setup_tax=de_skr04_setup_tax,
213
+ notes=(
214
+ "German SME chart, DATEV SKR04 (balance-sheet-ordered), German titles "
215
+ "with SKR04 numbers, EUR. Umsatzsteuer/Vorsteuer templates at 19 / 7 %. "
216
+ "Hand-authored from the published standard, cross-checked against Odoo "
217
+ "l10n_de. DATEV's recommendation for newly founded companies; the "
218
+ "process-ordered SKR03 sibling is the default for a bare `de`."
219
+ ),
220
+ ))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lambda-erp
3
- Version: 0.2.1
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) and **Switzerland** — the *Kontenrahmen KMU*, in German, in CHF, with MWST tax templates at the current rates. 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.
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 Switzerland, more coming) and business type, preview the exact chart of accounts, confirm, and it's booked
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). A German VAT pack, a U.S. sales-tax-by-state module, a template for professional services — each is an additive contribution that every deployment picks up. The goal is a true community where running an ERP gets cheaper, faster, and more flexible for everyone, not just the implementer.
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
 
@@ -2,7 +2,7 @@ api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  api/attachments.py,sha256=uRmcwiPnnWOoP91d9-uKXzxSHT9x50zv_azZq0n-DxE,8185
3
3
  api/auth.py,sha256=SIV1Teiv_NGduAOWxmFG14kiO5WykknSECjSeitTzPI,31502
4
4
  api/bootstrap.py,sha256=BesZjTwV7esu7U9NwvT93UwJVIz2w1bMieIr3ReooHc,19949
5
- api/chat.py,sha256=QQCv4m_Toqtj6wkDZ1xELKKy2wCWxLFBI2OI9ohieD4,175774
5
+ api/chat.py,sha256=nBtbHm2eZ3otw5F-i2rexHv_F41FijYRDXIRr8jdAmI,178618
6
6
  api/demo_limits.py,sha256=1vWFqs5z-Wjir6NLeZJPG24Ef6HVJoWT1QD_6y-JPEk,14201
7
7
  api/deps.py,sha256=HaFNz0q0NlX49kNMs1xMe0rrO6Slqpt8-TlPOATirT4,140
8
8
  api/errors.py,sha256=Za9gydi7lPiqpv5zbrnXVrYS_LeFThYHh9VID90aCf0,2307
@@ -35,7 +35,7 @@ lambda_erp/utils.py,sha256=MMNro0FTKCtBZjZDSKG5d1NKUhUbHWvaqT8UEjRFu8s,5684
35
35
  lambda_erp/accounting/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
36
  lambda_erp/accounting/bank_transaction.py,sha256=cliST3i8rld821RO-eaS33BkHRFF_uUXqXa1qd6-FLs,2700
37
37
  lambda_erp/accounting/budget.py,sha256=Iox9RqtvizuHonz57zcJox_g1zeoAaWclnZIuBMo8xQ,3791
38
- lambda_erp/accounting/chart_of_accounts.py,sha256=TO5debOlysMQZGWjo3cKXQK47T1LugRR4t-WUSasEs8,8563
38
+ lambda_erp/accounting/chart_of_accounts.py,sha256=pOS0FbwFQ-GPGl996UXb305ZLum15GYaLuqS2OiPHp0,10207
39
39
  lambda_erp/accounting/general_ledger.py,sha256=FuZWt5hoj3O7uuJtiffLR1I2cfg0LD0gqH19dwV3LSk,13424
40
40
  lambda_erp/accounting/journal_entry.py,sha256=t9aeJcJU6FOPbuqHhkaY8spGtCUNp_xNxhpcqi0K_2w,8968
41
41
  lambda_erp/accounting/payment_entry.py,sha256=gEbaju8TaumjzUzTRyzz-gU5ELg2eO2PSnQ2xvtxv7A,22464
@@ -45,12 +45,15 @@ lambda_erp/accounting/revaluation.py,sha256=ydNA-BztGBlqecnU2fS5x9h0p1mE47vgxL7j
45
45
  lambda_erp/accounting/sales_invoice.py,sha256=8AEC7ch6RbP6TF2bCRnAWSpfsksdM-eM6CXrMkvffeI,21834
46
46
  lambda_erp/accounting/subscription.py,sha256=9XIA1kghPlQgrOkZCsfTTHjYukmCIY8omRZ4MHSy-84,4578
47
47
  lambda_erp/accounting/setup/__init__.py,sha256=-ROSd6ZHTPScYkwbnGIXVBXl-8UH361WwcpWVvXmSHw,2395
48
- lambda_erp/accounting/setup/engine.py,sha256=bHYa7bC-XpeYxOeRH2uPPczRfMma-wUH-3h-asaEtP8,10086
48
+ lambda_erp/accounting/setup/engine.py,sha256=EsV37lt8RXv9_rwZOtNBJIIC1EFVlBMENvftCapQitE,13458
49
49
  lambda_erp/accounting/setup/pack.py,sha256=OakJmOisZjDNpaKlK3nUmgt969DxqDzyqLJonZlQUqM,5569
50
50
  lambda_erp/accounting/setup/profiles.py,sha256=0n0LjLO2nLnonH-WH7RqQAKyQyo0uaHUOu955hCDK0A,17276
51
51
  lambda_erp/accounting/setup/spine.py,sha256=0D_ssCS_9isije9tRgWzEdowaWj98TZOD958YOB7z9g,3593
52
- lambda_erp/accounting/setup/packs/__init__.py,sha256=L_8ZIX0spLYTUzYUI5siii7sXRM_QURKdy_7Ivthij8,693
53
- lambda_erp/accounting/setup/packs/ch.py,sha256=YOrtaG49PWKJipZITL6sfJjTCaHzMaTu7xhXmsK_q_4,12487
52
+ lambda_erp/accounting/setup/packs/__init__.py,sha256=q0O1UP7EFzPuwdsIFaIurW1gW3phIPY08a2mOIrIAtA,795
53
+ lambda_erp/accounting/setup/packs/ch.py,sha256=aSIr9u71GYGDpxMUj6rPQ2VAjTRzd9eKQp4xwSHpCRA,12612
54
+ lambda_erp/accounting/setup/packs/de_common.py,sha256=pyFptA32oStTq_i3w2Vv7YvAkGRWs9HGnb0oiozBQoM,2669
55
+ lambda_erp/accounting/setup/packs/de_skr03.py,sha256=PQt34gpNnFSlWYSTLLz1cSN-lD51Kxvut69T4LutYSw,11233
56
+ lambda_erp/accounting/setup/packs/de_skr04.py,sha256=76SqgsUEzNnt8qOlNrGGwH2nRZgmyv-MS04hsatyNUE,11250
54
57
  lambda_erp/accounting/setup/packs/generic.py,sha256=L8dROCJbWzxt07s5n-_Onw2nAlQAIn-wccBtagPUyMk,2444
55
58
  lambda_erp/buying/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
59
  lambda_erp/buying/purchase_order.py,sha256=WqXxbosKY9wBCu_rL5LByripHseurSzxY3DOsDNk5PU,5776
@@ -68,7 +71,7 @@ lambda_erp/stock/delivery_note.py,sha256=MFMKAoslqYPtsEOZl6f7Yue-p_F_XynOzCEAfQR
68
71
  lambda_erp/stock/purchase_receipt.py,sha256=awbF99frF8YL99UjMR8oIL3dW6i42h8wzf_qVKwZmQI,13444
69
72
  lambda_erp/stock/stock_entry.py,sha256=KTauIe0mZlmo1jMJqp1ct31SbK5o3DzyiK3JchoQkFM,14609
70
73
  lambda_erp/stock/stock_ledger.py,sha256=MZU5naiXxanE_EgpurWEqUjKcmZVP4yISJI2_vXuAfc,12456
71
- lambda_erp-0.2.1.dist-info/METADATA,sha256=tL7KaQjfYI4135e6exHAU9ZGinioXZjnSje4M7J3d4c,27856
72
- lambda_erp-0.2.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
73
- lambda_erp-0.2.1.dist-info/licenses/LICENSE,sha256=_FAQctk8hfkRc1l0UPXPWWWXy_NxMPwA074vY644eC0,1077
74
- lambda_erp-0.2.1.dist-info/RECORD,,
74
+ lambda_erp-0.2.3.dist-info/METADATA,sha256=i3YYuWdlpUUxiSB01w6oEmj_FUNAR_TJqpnEGOKxuv8,28289
75
+ lambda_erp-0.2.3.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
76
+ lambda_erp-0.2.3.dist-info/licenses/LICENSE,sha256=_FAQctk8hfkRc1l0UPXPWWWXy_NxMPwA074vY644eC0,1077
77
+ lambda_erp-0.2.3.dist-info/RECORD,,