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.
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/PKG-INFO +1 -1
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/bootstrap.py +3 -1
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/pdf.py +30 -2
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/routers/masters.py +9 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/routers/setup.py +11 -10
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/frontend/src/api/client.ts +11 -2
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/pyproject.toml +1 -1
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/.gitignore +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/LICENSE +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/README.md +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/__init__.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/attachments.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/auth.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/chat.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/demo_limits.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/deps.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/errors.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/main.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/providers.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/routers/__init__.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/routers/accounting.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/routers/admin.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/routers/analytics.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/routers/documents.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/routers/reports.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/services.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/api/templates/document.html +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/docs/agents/README.md +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/frontend/README.md +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/database.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/model.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.1.9 → lambda_erp-0.1.11}/terraform/README.md +0 -0
|
@@ -83,7 +83,9 @@ def bootstrap_demo() -> None:
|
|
|
83
83
|
from api.routers.setup import create_company
|
|
84
84
|
|
|
85
85
|
create_company(
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
81
|
-
#
|
|
82
|
-
|
|
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
|
-
|
|
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: {
|
|
329
|
-
|
|
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
|
|
|
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
|