lambda-erp 0.2.0__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.
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/PKG-INFO +1 -1
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/chat.py +28 -6
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/accounting/chart_of_accounts.py +59 -27
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/accounting/setup/engine.py +110 -32
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/accounting/setup/pack.py +4 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/accounting/setup/packs/ch.py +3 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/accounting/setup/profiles.py +86 -41
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/pyproject.toml +1 -1
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/.gitignore +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/LICENSE +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/README.md +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/__init__.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/attachments.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/auth.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/bootstrap.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/demo_limits.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/deps.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/errors.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/main.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/oauth.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/pdf.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/providers.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/remarks_md.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/routers/__init__.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/routers/accounting.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/routers/admin.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/routers/analytics.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/routers/chat_api.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/routers/documents.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/routers/masters.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/routers/proposals.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/routers/reports.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/routers/setup.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/services.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/templates/document.html +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/api/templates/proposal.html +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/docs/agents/README.md +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/frontend/README.md +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/frontend/src/api/client.ts +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/accounting/setup/__init__.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/accounting/setup/packs/generic.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/accounting/setup/spine.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/database.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/model.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.2.0 → lambda_erp-0.2.2}/terraform/README.md +0 -0
|
@@ -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),
|
|
1116
|
-
"company default accounts, any jurisdiction tax
|
|
1117
|
-
"cost center — in one step. Only call
|
|
1118
|
-
"
|
|
1119
|
-
"
|
|
1120
|
-
"
|
|
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
|
|
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
|
|
@@ -66,12 +66,27 @@ def _attach(tree: dict, parent_name: str, overlay_acct: dict) -> bool:
|
|
|
66
66
|
return True
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
def _localize_name(acct: dict, language: str) -> str:
|
|
70
|
+
"""The overlay account's name in the pack's language, English otherwise.
|
|
71
|
+
|
|
72
|
+
Profiles carry a jurisdiction-neutral English ``name`` plus an ``i18n`` map
|
|
73
|
+
({lang: name}); on a German (Swiss) chart the German name is used so the
|
|
74
|
+
overlay reads in the same language as the base chart.
|
|
75
|
+
"""
|
|
76
|
+
if language and language != "en":
|
|
77
|
+
localized = (acct.get("i18n") or {}).get(language)
|
|
78
|
+
if localized:
|
|
79
|
+
return localized
|
|
80
|
+
return acct["name"]
|
|
81
|
+
|
|
82
|
+
|
|
69
83
|
def _merge(pack: LocalizationPack, profile: Optional[SectorProfile]):
|
|
70
84
|
"""Return ``(merged_tree, merged_defaults, added_names, skipped)``.
|
|
71
85
|
|
|
72
|
-
``added_names`` are the overlay accounts actually attached
|
|
73
|
-
overlay accounts whose anchor was unmapped or
|
|
74
|
-
so a mis-wired profile/pack pairing is visible
|
|
86
|
+
``added_names`` are the overlay accounts actually attached (in the pack's
|
|
87
|
+
language); ``skipped`` are overlay accounts whose anchor was unmapped or
|
|
88
|
+
whose name collided — surfaced so a mis-wired profile/pack pairing is visible
|
|
89
|
+
rather than silent.
|
|
75
90
|
"""
|
|
76
91
|
tree = copy.deepcopy(pack.base_chart)
|
|
77
92
|
defaults = dict(pack.defaults)
|
|
@@ -79,17 +94,26 @@ def _merge(pack: LocalizationPack, profile: Optional[SectorProfile]):
|
|
|
79
94
|
skipped: list[dict] = []
|
|
80
95
|
|
|
81
96
|
if profile is not None:
|
|
97
|
+
# Map each profile account's neutral English name -> the localized name
|
|
98
|
+
# actually created, so profile.defaults (which reference the English
|
|
99
|
+
# name) resolve to the localized account on this pack.
|
|
100
|
+
name_map: dict[str, str] = {}
|
|
82
101
|
for acct in profile.accounts:
|
|
83
102
|
parent_name = pack.anchor_account(acct["anchor"])
|
|
84
103
|
if parent_name is None:
|
|
85
104
|
skipped.append({**acct, "reason": "anchor not mapped in pack"})
|
|
86
105
|
continue
|
|
87
|
-
|
|
88
|
-
|
|
106
|
+
display = _localize_name(acct, pack.language)
|
|
107
|
+
localized = {**acct, "name": display}
|
|
108
|
+
if _attach(tree, parent_name, localized):
|
|
109
|
+
added.append(display)
|
|
110
|
+
name_map[acct["name"]] = display
|
|
89
111
|
else:
|
|
90
112
|
skipped.append({**acct, "reason": "parent missing or name collision"})
|
|
91
|
-
# Sector default overrides layer on top of the pack's base defaults
|
|
92
|
-
|
|
113
|
+
# Sector default overrides layer on top of the pack's base defaults,
|
|
114
|
+
# remapped to the localized account names created above.
|
|
115
|
+
for field, val in profile.defaults.items():
|
|
116
|
+
defaults[field] = name_map.get(val, val)
|
|
93
117
|
|
|
94
118
|
return tree, defaults, added, skipped
|
|
95
119
|
|
|
@@ -118,6 +142,56 @@ def _resolved_defaults(company_name: str, defaults: dict) -> dict:
|
|
|
118
142
|
return {field: f"{leaf} - {abbr}" for field, leaf in defaults.items()}
|
|
119
143
|
|
|
120
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
|
+
|
|
121
195
|
# ---------------------------------------------------------------------------
|
|
122
196
|
# Public API
|
|
123
197
|
# ---------------------------------------------------------------------------
|
|
@@ -157,6 +231,7 @@ def plan_company_setup(company_name: str,
|
|
|
157
231
|
"accounts": _tree_outline(tree),
|
|
158
232
|
"sector_added_accounts": added,
|
|
159
233
|
"defaults": _resolved_defaults(company_name, defaults),
|
|
234
|
+
"existing": _existing_state(company_name, ccy),
|
|
160
235
|
"warnings": skipped,
|
|
161
236
|
}
|
|
162
237
|
if profile is not None:
|
|
@@ -175,25 +250,33 @@ def apply_company_setup(company_name: str,
|
|
|
175
250
|
variant: Optional[str] = None,
|
|
176
251
|
sector: Optional[str] = None,
|
|
177
252
|
currency: Optional[str] = None,
|
|
178
|
-
|
|
179
|
-
"""
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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.
|
|
185
266
|
"""
|
|
186
267
|
db = get_db()
|
|
187
268
|
pack = resolve_pack(country, variant)
|
|
188
269
|
profile = get_profile(sector)
|
|
189
270
|
ccy = currency or pack.currency
|
|
190
271
|
|
|
191
|
-
|
|
192
|
-
if
|
|
272
|
+
state = _existing_state(company_name, ccy)
|
|
273
|
+
if state["needs_confirmation"] and not confirm_existing:
|
|
193
274
|
return {
|
|
194
275
|
"ok": False,
|
|
195
|
-
"
|
|
196
|
-
|
|
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.",
|
|
197
280
|
}
|
|
198
281
|
|
|
199
282
|
if not db.exists("Company", company_name):
|
|
@@ -206,34 +289,29 @@ def apply_company_setup(company_name: str,
|
|
|
206
289
|
|
|
207
290
|
tree, defaults, added, skipped = _merge(pack, profile)
|
|
208
291
|
|
|
209
|
-
create_accounts_from_tree(company_name, tree, ccy)
|
|
210
|
-
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}
|
|
211
294
|
|
|
212
295
|
tax_summary = []
|
|
213
296
|
if pack.setup_tax is not None:
|
|
214
297
|
tax_summary = pack.setup_tax(company_name, ccy) or []
|
|
215
298
|
|
|
216
|
-
cost_center = setup_cost_center(company_name) # commits internally
|
|
299
|
+
cost_center = setup_cost_center(company_name) # idempotent, commits internally
|
|
217
300
|
db.commit()
|
|
218
301
|
|
|
219
|
-
def _count(nodes):
|
|
220
|
-
n = 0
|
|
221
|
-
for _name, d in nodes.items():
|
|
222
|
-
children = d.get("children")
|
|
223
|
-
if children:
|
|
224
|
-
n += _count(children)
|
|
225
|
-
else:
|
|
226
|
-
n += 1
|
|
227
|
-
return n
|
|
228
|
-
|
|
229
302
|
return {
|
|
230
303
|
"ok": True,
|
|
231
304
|
"company": company_name,
|
|
232
305
|
"currency": ccy,
|
|
233
306
|
"jurisdiction": pack.key,
|
|
234
307
|
"sector": profile.key if profile else None,
|
|
235
|
-
"
|
|
308
|
+
"added_to_existing": state["company_exists"],
|
|
309
|
+
"accounts_created": len(acct["created"]),
|
|
310
|
+
"accounts_skipped": len(acct["skipped"]), # already present, left alone
|
|
236
311
|
"sector_added_accounts": added,
|
|
312
|
+
"defaults_set": dflt["set"],
|
|
313
|
+
"defaults_left_untouched": dflt["left"], # already configured, kept
|
|
314
|
+
"defaults_unresolved": dflt["missing"],
|
|
237
315
|
"cost_center": cost_center,
|
|
238
316
|
"tax_summary": tax_summary,
|
|
239
317
|
"warnings": skipped,
|
|
@@ -58,6 +58,10 @@ class LocalizationPack:
|
|
|
58
58
|
defaults: dict
|
|
59
59
|
variant: Optional[str] = None
|
|
60
60
|
currency: str = "USD"
|
|
61
|
+
# Primary language of the chart's account names (ISO 639-1). Sector-profile
|
|
62
|
+
# overlay accounts are rendered in this language so they match the base
|
|
63
|
+
# chart — "en" for the generic pack, "de" for the Swiss KMU pack, etc.
|
|
64
|
+
language: str = "en"
|
|
61
65
|
setup_tax: Optional[Callable[[str, str], list]] = None
|
|
62
66
|
notes: str = ""
|
|
63
67
|
|
|
@@ -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
|
))
|
|
@@ -244,6 +246,7 @@ CH_PACK = register_pack(LocalizationPack(
|
|
|
244
246
|
variant=None,
|
|
245
247
|
label="Switzerland (Kontenrahmen KMU)",
|
|
246
248
|
currency="CHF",
|
|
249
|
+
language="de",
|
|
247
250
|
base_chart=CH_CHART,
|
|
248
251
|
anchors=CH_ANCHORS,
|
|
249
252
|
defaults=CH_DEFAULTS,
|
|
@@ -64,8 +64,12 @@ class SectorProfile:
|
|
|
64
64
|
)
|
|
65
65
|
|
|
66
66
|
|
|
67
|
-
def _acct(anchor, name, account_type=""):
|
|
68
|
-
|
|
67
|
+
def _acct(anchor, name, account_type="", **i18n):
|
|
68
|
+
"""One overlay account. ``name`` is the jurisdiction-neutral English name;
|
|
69
|
+
``i18n`` holds localized names keyed by ISO 639-1 (e.g. ``de=..., fr=...``)
|
|
70
|
+
used when the active pack's language matches, so overlay accounts read in the
|
|
71
|
+
same language as the base chart."""
|
|
72
|
+
return {"anchor": anchor, "name": name, "account_type": account_type, "i18n": i18n}
|
|
69
73
|
|
|
70
74
|
|
|
71
75
|
# ---------------------------------------------------------------------------
|
|
@@ -86,13 +90,21 @@ SERVICES = SectorProfile(
|
|
|
86
90
|
"tracked separately so they can be re-billed at cost."
|
|
87
91
|
),
|
|
88
92
|
accounts=[
|
|
89
|
-
_acct(spine.CURRENT_ASSETS, "Unbilled Revenue / WIP", ""
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
_acct(spine.
|
|
93
|
-
|
|
93
|
+
_acct(spine.CURRENT_ASSETS, "Unbilled Revenue / WIP", "",
|
|
94
|
+
de="Nicht fakturierte Leistungen / Angefangene Arbeiten",
|
|
95
|
+
fr="Travaux en cours non facturés"),
|
|
96
|
+
_acct(spine.CURRENT_LIABILITIES, "Deferred Revenue", "",
|
|
97
|
+
de="Erhaltene Anzahlungen (passive Abgrenzung)",
|
|
98
|
+
fr="Produits différés"),
|
|
99
|
+
_acct(spine.INCOME, "Consulting Revenue", "Income Account",
|
|
100
|
+
de="Beratungserlöse", fr="Produits de conseil"),
|
|
101
|
+
_acct(spine.OPERATING_EXPENSES, "Subcontractor Costs", "",
|
|
102
|
+
de="Aufwand für Fremdleistungen", fr="Charges de sous-traitance"),
|
|
103
|
+
_acct(spine.OPERATING_EXPENSES, "Reimbursable Client Expenses", "",
|
|
104
|
+
de="Weiterverrechenbare Kundenauslagen",
|
|
105
|
+
fr="Débours refacturables aux clients"),
|
|
94
106
|
],
|
|
95
|
-
defaults={"default_income_account": "
|
|
107
|
+
defaults={"default_income_account": "Consulting Revenue"},
|
|
96
108
|
big_decisions=[
|
|
97
109
|
"Recognise revenue over time (unbilled WIP + deferred revenue) rather "
|
|
98
110
|
"than only at invoice? Recommended for retainers and long engagements.",
|
|
@@ -113,11 +125,16 @@ RETAIL_POS = SectorProfile(
|
|
|
113
125
|
"miscounts) is a normal recurring expense reconciled at stock-take."
|
|
114
126
|
),
|
|
115
127
|
accounts=[
|
|
116
|
-
_acct(spine.CURRENT_ASSETS, "Cash Register / Till", "Cash"
|
|
117
|
-
|
|
118
|
-
_acct(spine.
|
|
119
|
-
|
|
120
|
-
_acct(spine.
|
|
128
|
+
_acct(spine.CURRENT_ASSETS, "Cash Register / Till", "Cash",
|
|
129
|
+
de="Ladenkasse", fr="Caisse (point de vente)"),
|
|
130
|
+
_acct(spine.CURRENT_ASSETS, "Merchant Card Clearing", "",
|
|
131
|
+
de="Kartenzahlungen (Durchlaufkonto)", fr="Compte d'attente cartes"),
|
|
132
|
+
_acct(spine.CURRENT_LIABILITIES, "Sales Tax Payable", "Tax",
|
|
133
|
+
de="Geschuldete Umsatzsteuer (MWST)", fr="TVA due sur les ventes"),
|
|
134
|
+
_acct(spine.INCOME, "Retail Sales", "Income Account",
|
|
135
|
+
de="Detailhandelserlöse", fr="Produits de la vente au détail"),
|
|
136
|
+
_acct(spine.OPERATING_EXPENSES, "Inventory Shrinkage", "Stock Adjustment",
|
|
137
|
+
de="Inventurdifferenzen (Schwund)", fr="Démarque inconnue"),
|
|
121
138
|
],
|
|
122
139
|
defaults={"default_income_account": "Retail Sales"},
|
|
123
140
|
big_decisions=[
|
|
@@ -139,12 +156,18 @@ HOSPITALITY = SectorProfile(
|
|
|
139
156
|
"and spoilage are expected and tracked."
|
|
140
157
|
),
|
|
141
158
|
accounts=[
|
|
142
|
-
_acct(spine.INCOME, "Food Sales", "Income Account"
|
|
143
|
-
|
|
144
|
-
_acct(spine.
|
|
145
|
-
|
|
146
|
-
_acct(spine.
|
|
147
|
-
|
|
159
|
+
_acct(spine.INCOME, "Food Sales", "Income Account",
|
|
160
|
+
de="Küchenumsatz (Speisen)", fr="Ventes de nourriture"),
|
|
161
|
+
_acct(spine.INCOME, "Beverage Sales", "Income Account",
|
|
162
|
+
de="Getränkeumsatz", fr="Ventes de boissons"),
|
|
163
|
+
_acct(spine.DIRECT_COSTS, "Food Cost", "Cost of Goods Sold",
|
|
164
|
+
de="Warenaufwand Küche (Speisen)", fr="Coût des denrées"),
|
|
165
|
+
_acct(spine.DIRECT_COSTS, "Beverage Cost", "Cost of Goods Sold",
|
|
166
|
+
de="Warenaufwand Getränke", fr="Coût des boissons"),
|
|
167
|
+
_acct(spine.CURRENT_LIABILITIES, "Tips Payable", "",
|
|
168
|
+
de="Trinkgelder (durchlaufend)", fr="Pourboires à reverser"),
|
|
169
|
+
_acct(spine.OPERATING_EXPENSES, "Spoilage & Waste", "Stock Adjustment",
|
|
170
|
+
de="Verderb und Abfall", fr="Pertes et gaspillage"),
|
|
148
171
|
],
|
|
149
172
|
defaults={"default_income_account": "Food Sales"},
|
|
150
173
|
big_decisions=[
|
|
@@ -166,11 +189,16 @@ DISTRIBUTION = SectorProfile(
|
|
|
166
189
|
"liability and reduce net revenue rather than being an expense."
|
|
167
190
|
),
|
|
168
191
|
accounts=[
|
|
169
|
-
_acct(spine.CURRENT_ASSETS, "Inventory - Finished Goods", "Stock"
|
|
170
|
-
|
|
171
|
-
_acct(spine.
|
|
172
|
-
|
|
173
|
-
_acct(spine.
|
|
192
|
+
_acct(spine.CURRENT_ASSETS, "Inventory - Finished Goods", "Stock",
|
|
193
|
+
de="Vorräte Fertigwaren", fr="Stock de produits finis"),
|
|
194
|
+
_acct(spine.CURRENT_ASSETS, "Goods in Transit", "Stock",
|
|
195
|
+
de="Waren unterwegs", fr="Marchandises en transit"),
|
|
196
|
+
_acct(spine.INCOME, "Wholesale Revenue", "Income Account",
|
|
197
|
+
de="Grosshandelserlöse", fr="Produits de gros"),
|
|
198
|
+
_acct(spine.OPERATING_EXPENSES, "Freight Out / Delivery", "",
|
|
199
|
+
de="Ausgangsfrachten / Versandkosten", fr="Frais de livraison"),
|
|
200
|
+
_acct(spine.CURRENT_LIABILITIES, "Customer Rebates Payable", "",
|
|
201
|
+
de="Kundenrückvergütungen (Rückstellung)", fr="Ristournes clients à payer"),
|
|
174
202
|
],
|
|
175
203
|
defaults={"default_income_account": "Wholesale Revenue"},
|
|
176
204
|
big_decisions=[
|
|
@@ -195,11 +223,16 @@ IMPORT_EXPORT = SectorProfile(
|
|
|
195
223
|
"Gain/Loss accounts already cover these)."
|
|
196
224
|
),
|
|
197
225
|
accounts=[
|
|
198
|
-
_acct(spine.CURRENT_ASSETS, "Goods in Transit (Imports)", "Stock"
|
|
199
|
-
|
|
200
|
-
_acct(spine.DIRECT_COSTS, "
|
|
201
|
-
|
|
202
|
-
_acct(spine.
|
|
226
|
+
_acct(spine.CURRENT_ASSETS, "Goods in Transit (Imports)", "Stock",
|
|
227
|
+
de="Importwaren unterwegs", fr="Marchandises importées en transit"),
|
|
228
|
+
_acct(spine.DIRECT_COSTS, "Customs Duties & Import Taxes", "Chargeable",
|
|
229
|
+
de="Zölle und Einfuhrabgaben", fr="Droits de douane et taxes à l'importation"),
|
|
230
|
+
_acct(spine.DIRECT_COSTS, "Inbound Freight & Insurance", "Chargeable",
|
|
231
|
+
de="Eingangsfrachten und Versicherung", fr="Fret et assurance à l'import"),
|
|
232
|
+
_acct(spine.CURRENT_LIABILITIES, "Import VAT / GST Payable", "Tax",
|
|
233
|
+
de="Einfuhrsteuer (MWST) geschuldet", fr="TVA à l'importation due"),
|
|
234
|
+
_acct(spine.INCOME, "Export Sales", "Income Account",
|
|
235
|
+
de="Exporterlöse", fr="Produits d'exportation"),
|
|
203
236
|
],
|
|
204
237
|
defaults={
|
|
205
238
|
"default_income_account": "Export Sales",
|
|
@@ -225,11 +258,16 @@ MANUFACTURING = SectorProfile(
|
|
|
225
258
|
"when the product is sold, not when it is made."
|
|
226
259
|
),
|
|
227
260
|
accounts=[
|
|
228
|
-
_acct(spine.CURRENT_ASSETS, "Raw Materials", "Stock"
|
|
229
|
-
|
|
230
|
-
_acct(spine.CURRENT_ASSETS, "
|
|
231
|
-
|
|
232
|
-
_acct(spine.
|
|
261
|
+
_acct(spine.CURRENT_ASSETS, "Raw Materials", "Stock",
|
|
262
|
+
de="Rohstoffe", fr="Matières premières"),
|
|
263
|
+
_acct(spine.CURRENT_ASSETS, "Work in Progress", "Stock",
|
|
264
|
+
de="Angefangene Arbeiten", fr="Travaux en cours"),
|
|
265
|
+
_acct(spine.CURRENT_ASSETS, "Finished Goods", "Stock",
|
|
266
|
+
de="Fertigerzeugnisse", fr="Produits finis"),
|
|
267
|
+
_acct(spine.DIRECT_COSTS, "Direct Labour", "",
|
|
268
|
+
de="Fertigungslöhne (Direktlohn)", fr="Main-d'œuvre directe"),
|
|
269
|
+
_acct(spine.DIRECT_COSTS, "Manufacturing Overhead Applied", "",
|
|
270
|
+
de="Verrechnete Fertigungsgemeinkosten", fr="Frais généraux de production imputés"),
|
|
233
271
|
],
|
|
234
272
|
defaults={},
|
|
235
273
|
big_decisions=[
|
|
@@ -254,13 +292,20 @@ CONSTRUCTION = SectorProfile(
|
|
|
254
292
|
"project."
|
|
255
293
|
),
|
|
256
294
|
accounts=[
|
|
257
|
-
_acct(spine.CURRENT_ASSETS, "Costs in Excess of Billings", ""
|
|
258
|
-
|
|
259
|
-
_acct(spine.
|
|
260
|
-
|
|
261
|
-
_acct(spine.
|
|
262
|
-
|
|
263
|
-
_acct(spine.
|
|
295
|
+
_acct(spine.CURRENT_ASSETS, "Costs in Excess of Billings", "",
|
|
296
|
+
de="Nicht fakturierte Projektleistungen", fr="Travaux en cours excédant la facturation"),
|
|
297
|
+
_acct(spine.CURRENT_ASSETS, "Retention Receivable", "Receivable",
|
|
298
|
+
de="Rückbehalte (Forderungen)", fr="Retenues de garantie à recevoir"),
|
|
299
|
+
_acct(spine.CURRENT_LIABILITIES, "Billings in Excess of Costs", "",
|
|
300
|
+
de="Fakturierte Leistungen über Baufortschritt", fr="Facturation excédant les travaux"),
|
|
301
|
+
_acct(spine.CURRENT_LIABILITIES, "Retention Payable", "",
|
|
302
|
+
de="Rückbehalte (Verbindlichkeiten)", fr="Retenues de garantie à payer"),
|
|
303
|
+
_acct(spine.INCOME, "Contract Revenue", "Income Account",
|
|
304
|
+
de="Projekterlöse (Werkverträge)", fr="Produits sur contrats"),
|
|
305
|
+
_acct(spine.DIRECT_COSTS, "Job Costs - Materials", "",
|
|
306
|
+
de="Projektkosten Material", fr="Coûts de chantier - matériaux"),
|
|
307
|
+
_acct(spine.DIRECT_COSTS, "Job Costs - Subcontractors", "",
|
|
308
|
+
de="Projektkosten Fremdleistungen", fr="Coûts de chantier - sous-traitance"),
|
|
264
309
|
],
|
|
265
310
|
defaults={"default_income_account": "Contract Revenue"},
|
|
266
311
|
big_decisions=[
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|