lambda-erp 0.1.9__tar.gz → 0.1.11__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/PKG-INFO +1 -1
  2. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/bootstrap.py +3 -1
  3. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/pdf.py +30 -2
  4. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/routers/masters.py +9 -0
  5. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/routers/setup.py +11 -10
  6. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/frontend/src/api/client.ts +11 -2
  7. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/pyproject.toml +1 -1
  8. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/.gitignore +0 -0
  9. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/LICENSE +0 -0
  10. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/README.md +0 -0
  11. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/__init__.py +0 -0
  12. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/attachments.py +0 -0
  13. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/auth.py +0 -0
  14. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/chat.py +0 -0
  15. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/demo_limits.py +0 -0
  16. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/deps.py +0 -0
  17. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/errors.py +0 -0
  18. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/main.py +0 -0
  19. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/providers.py +0 -0
  20. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/routers/__init__.py +0 -0
  21. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/routers/accounting.py +0 -0
  22. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/routers/admin.py +0 -0
  23. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/routers/analytics.py +0 -0
  24. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/routers/bank_reconciliation.py +0 -0
  25. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/routers/documents.py +0 -0
  26. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/routers/reports.py +0 -0
  27. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/services.py +0 -0
  28. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/templates/document.html +0 -0
  29. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/docs/agents/README.md +0 -0
  30. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/frontend/README.md +0 -0
  31. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/__init__.py +0 -0
  32. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/__init__.py +0 -0
  33. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/bank_transaction.py +0 -0
  34. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/budget.py +0 -0
  35. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/chart_of_accounts.py +0 -0
  36. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/general_ledger.py +0 -0
  37. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/journal_entry.py +0 -0
  38. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/payment_entry.py +0 -0
  39. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/pos_invoice.py +0 -0
  40. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/purchase_invoice.py +0 -0
  41. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/revaluation.py +0 -0
  42. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/sales_invoice.py +0 -0
  43. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/subscription.py +0 -0
  44. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/buying/__init__.py +0 -0
  45. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/buying/purchase_order.py +0 -0
  46. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/controllers/__init__.py +0 -0
  47. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/controllers/currency.py +0 -0
  48. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/controllers/defaults.py +0 -0
  49. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/controllers/pricing_rule.py +0 -0
  50. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  51. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/database.py +0 -0
  52. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/exceptions.py +0 -0
  53. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/hooks.py +0 -0
  54. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/model.py +0 -0
  55. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/selling/__init__.py +0 -0
  56. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/selling/quotation.py +0 -0
  57. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/selling/sales_order.py +0 -0
  58. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/simulation.py +0 -0
  59. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/stock/__init__.py +0 -0
  60. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/stock/delivery_note.py +0 -0
  61. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/stock/purchase_receipt.py +0 -0
  62. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/stock/stock_entry.py +0 -0
  63. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/stock/stock_ledger.py +0 -0
  64. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/utils.py +0 -0
  65. {lambda_erp-0.1.9 → lambda_erp-0.1.11}/terraform/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lambda-erp
3
- Version: 0.1.9
3
+ Version: 0.1.11
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: MIT
@@ -83,7 +83,9 @@ def bootstrap_demo() -> None:
83
83
  from api.routers.setup import create_company
84
84
 
85
85
  create_company(
86
- {"name": DEMO_COMPANY, "currency": demo_currency},
86
+ # Demo company: opt into the pseudo-random address so seeded
87
+ # invoices/PDFs look complete. Real setups (the /setup form) don't.
88
+ {"name": DEMO_COMPANY, "currency": demo_currency, "autofill_address": True},
87
89
  _user=None,
88
90
  )
89
91
  else:
@@ -1,13 +1,41 @@
1
1
  """PDF generation for ERP documents."""
2
2
 
3
3
  import os
4
- from jinja2 import Environment, FileSystemLoader
4
+ from jinja2 import Environment, FileSystemLoader, ChoiceLoader
5
5
  from weasyprint import HTML
6
6
  from lambda_erp.database import get_db
7
7
  from api.services import load_document
8
8
 
9
9
  TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates")
10
- _jinja_env = Environment(loader=FileSystemLoader(TEMPLATE_DIR), autoescape=True)
10
+
11
+ # Extension seam: deployment plugins can register their own template
12
+ # directories (searched BEFORE the built-in one) to override document.html with
13
+ # custom branding/styling. The render context in generate_pdf is unchanged, so
14
+ # a custom template just re-styles the same data.
15
+ _plugin_template_dirs: list[str] = []
16
+
17
+
18
+ def _build_loader():
19
+ return ChoiceLoader(
20
+ [FileSystemLoader(d) for d in _plugin_template_dirs] + [FileSystemLoader(TEMPLATE_DIR)]
21
+ )
22
+
23
+
24
+ _jinja_env = Environment(loader=_build_loader(), autoescape=True)
25
+
26
+
27
+ def register_pdf_template_dir(path: str) -> None:
28
+ """Register a directory of PDF templates searched before the built-in ones.
29
+
30
+ Call from a plugin's register(). Drop a `document.html` (and/or other
31
+ templates) in `path` to override the built-in PDF layout with your own
32
+ styling — the template receives the same context generate_pdf() builds
33
+ (doc, title, company_info, party_info, currency, meta_fields, items, taxes,
34
+ page_size, ...). Later registrations win.
35
+ """
36
+ if path and path not in _plugin_template_dirs:
37
+ _plugin_template_dirs.insert(0, path)
38
+ _jinja_env.loader = _build_loader()
11
39
 
12
40
  # Document type → (title, party_field, party_name_field, party_doctype, party_label)
13
41
  DOC_CONFIG = {
@@ -222,6 +222,15 @@ def create_master_record(master_type: str, data: dict) -> dict:
222
222
  if db.exists(doctype, doc["name"]):
223
223
  raise HTTPException(status_code=409, detail=f"{doctype} {doc['name']} already exists")
224
224
 
225
+ # Party masters inherit the company's base currency when none is specified.
226
+ # Otherwise the Customer/Supplier default_currency column default ('USD')
227
+ # wins, so a non-USD company (e.g. CHF) gets USD customers that then force
228
+ # every sales/purchase document to USD (and fail with no USD->base rate).
229
+ if master_type in ("customer", "supplier") and not doc.get("default_currency"):
230
+ companies = db.get_all("Company", fields=["name", "default_currency"], limit=1)
231
+ if companies:
232
+ doc["default_currency"] = companies[0].get("default_currency") or "USD"
233
+
225
234
  db.insert(doctype, doc)
226
235
  row = db.get_all(doctype, filters={"name": doc["name"]}, fields=["*"])[0]
227
236
  return _echo_identity_alias(master_type, row)
@@ -77,20 +77,21 @@ def create_company(data: dict, _user: dict = Depends(require_role("admin"))):
77
77
  if db.exists("Company", name):
78
78
  return {"detail": f"Company {name} already exists"}
79
79
 
80
- # Auto-fill contact info with a deterministic random address if the caller
81
- # didn't supply one. This keeps the PDFs looking complete for demo users.
82
- auto = _random_address_for(name)
80
+ # Contact/address fields come from the caller. Only when the caller opts in
81
+ # (autofill_address the demo seeding does) do we invent a deterministic
82
+ # pseudo-random address so the demo's PDFs look complete. A real deployment
83
+ # leaves anything it doesn't provide blank rather than getting a fake US
84
+ # address printed on its invoices.
85
+ fields = ("email", "phone", "address", "city", "zip_code", "country", "tax_id")
86
+ contact = {k: (data.get(k) or "") for k in fields}
87
+ if data.get("autofill_address"):
88
+ auto = _random_address_for(name)
89
+ contact = {k: (data.get(k) or auto[k]) for k in fields}
83
90
  db.insert("Company", _dict(
84
91
  name=name,
85
92
  company_name=name,
86
93
  default_currency=currency,
87
- email=data.get("email") or auto["email"],
88
- phone=data.get("phone") or auto["phone"],
89
- address=data.get("address") or auto["address"],
90
- city=data.get("city") or auto["city"],
91
- zip_code=data.get("zip_code") or auto["zip_code"],
92
- country=data.get("country") or auto["country"],
93
- tax_id=data.get("tax_id") or auto["tax_id"],
94
+ **contact,
94
95
  ))
95
96
 
96
97
  setup_chart_of_accounts(name, currency)
@@ -325,8 +325,17 @@ export const api = {
325
325
  // Setup
326
326
  setupStatus: () => request<{ setup_complete: boolean; companies: any[] }>("/setup/status"),
327
327
 
328
- createCompany: (data: { name: string; currency?: string }) =>
329
- request<any>("/setup/company", { method: "POST", body: JSON.stringify(data) }),
328
+ createCompany: (data: {
329
+ name: string;
330
+ currency?: string;
331
+ email?: string;
332
+ phone?: string;
333
+ address?: string;
334
+ city?: string;
335
+ zip_code?: string;
336
+ country?: string;
337
+ tax_id?: string;
338
+ }) => request<any>("/setup/company", { method: "POST", body: JSON.stringify(data) }),
330
339
 
331
340
  seedDemo: () => request<any>("/setup/seed-demo", { method: "POST" }),
332
341
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lambda-erp"
3
- version = "0.1.9"
3
+ version = "0.1.11"
4
4
  description = "Core ERP logic - accounting, sales, purchasing, inventory"
5
5
  readme = "README.md"
6
6
  license = "MIT"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes