lambda-erp 0.1.25__tar.gz → 0.1.26__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 (69) hide show
  1. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/PKG-INFO +1 -1
  2. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/chat.py +15 -3
  3. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/pdf.py +6 -0
  4. lambda_erp-0.1.26/api/remarks_md.py +91 -0
  5. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/templates/document.html +13 -2
  6. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/pyproject.toml +1 -1
  7. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/.gitignore +0 -0
  8. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/LICENSE +0 -0
  9. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/README.md +0 -0
  10. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/__init__.py +0 -0
  11. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/attachments.py +0 -0
  12. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/auth.py +0 -0
  13. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/bootstrap.py +0 -0
  14. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/demo_limits.py +0 -0
  15. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/deps.py +0 -0
  16. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/errors.py +0 -0
  17. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/main.py +0 -0
  18. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/providers.py +0 -0
  19. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/routers/__init__.py +0 -0
  20. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/routers/accounting.py +0 -0
  21. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/routers/admin.py +0 -0
  22. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/routers/analytics.py +0 -0
  23. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/routers/bank_reconciliation.py +0 -0
  24. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/routers/documents.py +0 -0
  25. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/routers/masters.py +0 -0
  26. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/routers/proposals.py +0 -0
  27. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/routers/reports.py +0 -0
  28. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/routers/setup.py +0 -0
  29. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/services.py +0 -0
  30. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/api/templates/proposal.html +0 -0
  31. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/docs/agents/README.md +0 -0
  32. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/frontend/README.md +0 -0
  33. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/frontend/src/api/client.ts +0 -0
  34. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/__init__.py +0 -0
  35. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/accounting/__init__.py +0 -0
  36. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/accounting/bank_transaction.py +0 -0
  37. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/accounting/budget.py +0 -0
  38. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/accounting/chart_of_accounts.py +0 -0
  39. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/accounting/general_ledger.py +0 -0
  40. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/accounting/journal_entry.py +0 -0
  41. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/accounting/payment_entry.py +0 -0
  42. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/accounting/pos_invoice.py +0 -0
  43. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/accounting/purchase_invoice.py +0 -0
  44. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/accounting/revaluation.py +0 -0
  45. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/accounting/sales_invoice.py +0 -0
  46. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/accounting/subscription.py +0 -0
  47. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/buying/__init__.py +0 -0
  48. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/buying/purchase_order.py +0 -0
  49. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/controllers/__init__.py +0 -0
  50. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/controllers/currency.py +0 -0
  51. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/controllers/defaults.py +0 -0
  52. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/controllers/pricing_rule.py +0 -0
  53. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/controllers/taxes_and_totals.py +0 -0
  54. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/database.py +0 -0
  55. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/exceptions.py +0 -0
  56. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/hooks.py +0 -0
  57. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/model.py +0 -0
  58. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/selling/__init__.py +0 -0
  59. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/selling/proposal.py +0 -0
  60. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/selling/quotation.py +0 -0
  61. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/selling/sales_order.py +0 -0
  62. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/simulation.py +0 -0
  63. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/stock/__init__.py +0 -0
  64. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/stock/delivery_note.py +0 -0
  65. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/stock/purchase_receipt.py +0 -0
  66. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/stock/stock_entry.py +0 -0
  67. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/stock/stock_ledger.py +0 -0
  68. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/lambda_erp/utils.py +0 -0
  69. {lambda_erp-0.1.25 → lambda_erp-0.1.26}/terraform/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lambda-erp
3
- Version: 0.1.25
3
+ Version: 0.1.26
4
4
  Summary: Core ERP logic - accounting, sales, purchasing, inventory
5
5
  Author: TORUS INVESTMENTS AG
6
6
  License-Expression: MIT
@@ -603,7 +603,7 @@ TOOLS = [
603
603
  "type": "function",
604
604
  "function": {
605
605
  "name": "get_master_fields",
606
- "description": "List the available columns of a master type (customer, supplier, item, ...). Call this BEFORE passing `fields` to search_masters (or before guessing column names) so you target real fields instead of guessing. Returns: `fields` (all columns), `default_search_fields` (what search_masters searches when `fields` is omitted), and `bulk_text_fields` (large text columns searched only when named in `fields`).",
606
+ "description": "List the available columns of a master type (customer, supplier, item, ...). Call this WHENEVER you're unsure which columns exist: before passing `fields` to search_masters, and before building a `data` payload for create_master/update_master when you're not certain a field exists or where a value belongs (e.g. a contact person, a tax id, a payment term). It lets you target real fields instead of guessing — or wrongly concluding a value can't be stored. Returns: `fields` (all columns), `default_search_fields` (what search_masters searches when `fields` is omitted), and `bulk_text_fields` (large text columns searched only when named in `fields`).",
607
607
  "parameters": {
608
608
  "type": "object",
609
609
  "properties": {
@@ -628,8 +628,10 @@ TOOLS = [
628
628
  "**Account fields:** name (REQUIRED — full account id, conventionally \"<account_name> - <company abbr>\", e.g. \"Marketing Expenses - LAMB\"), account_name (required), company (required), root_type (Asset/Liability/Equity/Income/Expense), report_type (\"Balance Sheet\" or \"Profit and Loss\"), account_type (e.g. Receivable, Payable, Bank, Cash, Stock, Tax), parent_account, account_currency, is_group (0/1).\n"
629
629
  "**Cost Center fields:** name (REQUIRED — e.g. \"Marketing - LAMB\"), cost_center_name (required), company (required), parent_cost_center, is_group (0/1).\n\n"
630
630
  "zip_code is free text (e.g. \"8400\", \"ZH 8400\", \"59123\"), never numeric.\n"
631
+ "When the user names a contact person at a customer (e.g. \"Kontakt/Ansprechpartner ist Marlene Voss, 079 123 45 67\"), put the person's name in contact_person and their phone/email in contact_phone/contact_email — NOT in the company-level phone/email. There is no separate contact tool; contact-person data lives on these Customer columns, so never claim you cannot store a contact person.\n"
631
632
  "Supplier example: {\"master_type\":\"supplier\",\"data\":{\"supplier_name\":\"Schlafteq\",\"email\":\"jacob@schlafteq.ch\",\"phone\":\"+1 555-0104\",\"address\":\"145 Harbor Rd\",\"city\":\"Seattle\",\"zip_code\":\"98101\",\"country\":\"US\",\"tax_id\":\"98-7654321\"}}\n"
632
- "Item example (custom code): {\"master_type\":\"item\",\"data\":{\"item_code\":\"SVC-SPARK\",\"item_name\":\"Spark\",\"item_group\":\"Services\",\"is_stock_item\":0,\"standard_rate\":310}}"
633
+ "Item example (custom code): {\"master_type\":\"item\",\"data\":{\"item_code\":\"SVC-SPARK\",\"item_name\":\"Spark\",\"item_group\":\"Services\",\"is_stock_item\":0,\"standard_rate\":310}}\n"
634
+ "Customer example (with contact person): {\"master_type\":\"customer\",\"data\":{\"customer_name\":\"Foglio AG\",\"address\":\"Seeweg 12\",\"city\":\"Bramblewick\",\"zip_code\":\"9999\",\"contact_person\":\"Marlene Voss\",\"contact_phone\":\"079 123 45 67\"}}"
633
635
  ),
634
636
  "parameters": {
635
637
  "type": "object",
@@ -1268,7 +1270,8 @@ def _handle_create_master(args):
1268
1270
  if ignored:
1269
1271
  result["_warning"] = (
1270
1272
  f"These fields were IGNORED because they are not valid columns on the {master_type}: "
1271
- f"{ignored}. Retry the call using the correct field names from the create_master tool description."
1273
+ f"{ignored}. Call get_master_fields(master_type=\"{master_type}\") to see the real columns, "
1274
+ f"then retry mapping those values onto valid field names."
1272
1275
  )
1273
1276
  return result
1274
1277
 
@@ -1705,6 +1708,14 @@ Shape — note it does NOT use `items`:
1705
1708
 
1706
1709
  To build one: ensure each offer already exists as its own Quotation (create them first if needed), then call create_document with doctype "proposal" and a data object whose `quotations` array references those quotations by name. Do NOT submit it; link the user to the PDF at `/api/documents/proposal/<name>/pdf` (and the editor at `/app/proposal/<name>`).
1707
1710
 
1711
+ ### Notes / Terms markup (the `remarks` field)
1712
+ The `remarks` (Notes / Terms) field on quotations, sales orders, and invoices renders on the PDF with a small markup vocabulary, so when a user dictates offer notes, conditions, recurring services, or a sign-off you can compose a polished closing block instead of a flat paragraph. Put this ONLY in `remarks` (never in item descriptions):
1713
+ - `# Heading` at the start of a line -> bold heading
1714
+ - `*italic*` or `_italic_` -> italic; `**bold**` -> bold
1715
+ - `>> Period | Amount` at the start of a line -> a right-aligned price line that sits beside the heading/description above it; use it for separately- or recurring-billed items, e.g. `>> Monatlich | CHF 380.—`
1716
+ - Separate blocks with a blank line; a single newline is just a line break.
1717
+ Plain text (no special characters) still renders fine, so only reach for the markup when it improves a customer-facing note.
1718
+
1708
1719
  ### Purchase Cycle
1709
1720
  Purchase Order → Purchase Receipt (receiving) / Purchase Invoice (billing) → Payment Entry
1710
1721
 
@@ -1863,6 +1874,7 @@ If the user refers to something by its human name ("bill them 8 hours of project
1863
1874
  - Same for customers, suppliers, warehouses, etc. — `search_masters` is **case-insensitive** and falls back to **fuzzy matching for misspellings**, so a typo'd name ("Meynex") still resolves. Trust its results instead of concluding "not found" after one narrow try.
1864
1875
  - **Prefer narrowing with `fields`** whenever you know the attribute: search a customer by city with `fields=["city"]`, by email with `fields=["contact_email"]`, etc. It's faster and avoids false matches from unrelated columns. Omitting `fields` searches all standard text fields (a good fallback when you're unsure where the value lives), but large free-text columns (e.g. item `description`) are only searched when you name them explicitly in `fields`.
1865
1876
  - **Don't guess column names** — call `get_master_fields(master_type=...)` first to see the real columns (and which are searched by default), then pass the exact names to `search_masters` `fields`. This also tells you which large text fields (like `description`) you must name explicitly to search.
1877
+ - **When unsure where a value belongs, discover the schema — don't guess or give up.** Before a `create_master`/`update_master` where you're not certain a field exists or which column fits (the user gives a contact person, a VAT/tax id, a payment term, an IBAN, …), call `get_master_fields(master_type=...)` and map the value onto the real column. Never tell the user you can't store something without checking the fields first — the master usually has a column for it (e.g. a customer's contact person goes in `contact_person`/`contact_phone`/`contact_email`, not the company-level `phone`/`email`).
1866
1878
 
1867
1879
  When you list masters back to the user (items on an invoice, customers on a report), include the key in parentheses so follow-ups are unambiguous. Example: "Project Management (SVC-005) — 16 Hour".
1868
1880
 
@@ -6,6 +6,7 @@ from jinja2 import Environment, FileSystemLoader, ChoiceLoader
6
6
  from weasyprint import HTML
7
7
  from lambda_erp.database import get_db
8
8
  from api.services import load_document
9
+ from api.remarks_md import render_remarks
9
10
 
10
11
  TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates")
11
12
 
@@ -201,6 +202,11 @@ def generate_pdf(doctype_slug: str, name: str) -> bytes:
201
202
  taxes=taxes,
202
203
  show_warehouse=doctype in SHOW_WAREHOUSE,
203
204
  page_size=page_size,
205
+ # Notes / Terms rendered from a small markup subset (headings, bold/
206
+ # italic, right-aligned price lines) into safe HTML; templates style the
207
+ # emitted classes. Falls back to plain `doc.remarks` if a template
208
+ # doesn't use it. See api/remarks_md.py.
209
+ remarks_html=render_remarks(doc.get("remarks")),
204
210
  )
205
211
 
206
212
  # Let deployment plugins augment the context (e.g. a Swiss QR-bill image for
@@ -0,0 +1,91 @@
1
+ """Lightweight markup for the Notes / Terms (`remarks`) block on documents.
2
+
3
+ `remarks` is free text the user types on a document (quotation, sales order,
4
+ invoice, …), but on the PDF it often wants to read like a real offer: italic
5
+ asides, bold service headings, and a right-aligned recurring-price line beside a
6
+ description. Rather than pulling in a full Markdown dependency we support a tiny,
7
+ predictable subset and render it to **safe** HTML. `generate_pdf()` exposes the
8
+ result as `remarks_html`; templates render it with `| safe` and style the emitted
9
+ class names (`.rm-block`, `.rm-h`, `.rm-p`, `.rm-amt`) however they like — so a
10
+ branded template can restyle the same markup without changing this converter.
11
+
12
+ Syntax (authored in the same textarea, also what the chat assistant emits):
13
+ blank line -> separates blocks (a float stays beside its block)
14
+ # Heading -> bold heading line (leading #'s stripped)
15
+ *italic* _italic_ -> italic
16
+ **bold** -> bold
17
+ >> Monatlich | CHF 380.— -> right-aligned price box (period over amount),
18
+ floated beside the block's heading/description
19
+ Everything is HTML-escaped first, so user text can't inject markup.
20
+ """
21
+ import html
22
+ import re
23
+
24
+
25
+ def _inline(text):
26
+ """Escape, then apply inline **bold** / *italic* / _italic_."""
27
+ out = html.escape(text)
28
+ out = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", out)
29
+ out = re.sub(r"\*(.+?)\*", r"<em>\1</em>", out)
30
+ out = re.sub(r"_(.+?)_", r"<em>\1</em>", out)
31
+ return out
32
+
33
+
34
+ def _price(line):
35
+ """`>> period | amount` -> a right-floated box with period over amount."""
36
+ body = line.lstrip()[2:].strip() # drop the leading '>>'
37
+ if "|" in body:
38
+ period, amount = (p.strip() for p in body.split("|", 1))
39
+ else:
40
+ period, amount = "", body
41
+ parts = []
42
+ if period:
43
+ parts.append(f'<div class="rm-amt-period">{_inline(period)}</div>')
44
+ if amount:
45
+ parts.append(f'<div class="rm-amt-val">{_inline(amount)}</div>')
46
+ return f'<div class="rm-amt">{"".join(parts)}</div>'
47
+
48
+
49
+ def _block(lines):
50
+ """Render one block (lines between blank lines) to HTML.
51
+
52
+ Price line(s) are emitted first and floated right so they sit beside the
53
+ heading/description that follow, matching a typical offer layout.
54
+ """
55
+ price_html = ""
56
+ body = ""
57
+ para = []
58
+
59
+ def flush_para():
60
+ nonlocal body, para
61
+ if para:
62
+ body += '<div class="rm-p">' + "<br>".join(_inline(p) for p in para) + "</div>"
63
+ para = []
64
+
65
+ for raw in lines:
66
+ stripped = raw.lstrip()
67
+ if stripped.startswith(">>"):
68
+ flush_para()
69
+ price_html += _price(raw)
70
+ elif stripped.startswith("#"):
71
+ flush_para()
72
+ heading = stripped.lstrip("#").strip()
73
+ body += f'<div class="rm-h">{_inline(heading)}</div>'
74
+ else:
75
+ para.append(raw)
76
+ flush_para()
77
+ return f'<div class="rm-block">{price_html}{body}</div>'
78
+
79
+
80
+ def render_remarks(text):
81
+ """Convert the remarks free text to safe styled HTML (or '' if empty)."""
82
+ if not text or not text.strip():
83
+ return ""
84
+ text = text.replace("\r\n", "\n").replace("\r", "\n")
85
+ blocks = []
86
+ for raw_block in re.split(r"\n[ \t]*\n", text):
87
+ lines = raw_block.split("\n")
88
+ if not any(l.strip() for l in lines):
89
+ continue
90
+ blocks.append(_block(lines))
91
+ return "".join(blocks)
@@ -48,6 +48,15 @@
48
48
 
49
49
  .footer { margin-top: 30px; padding-top: 10px; border-top: 1px solid #e5e7eb; font-size: 7.5pt; color: #999; text-align: center; }
50
50
  .remarks { margin-top: 15px; padding: 8px; background: #fefce8; border-radius: 4px; font-size: 8pt; color: #713f12; }
51
+ /* Structured notes (remarks_html). A template can restyle these freely. */
52
+ .remarks .rm-block { overflow: hidden; margin-bottom: 8px; }
53
+ .remarks .rm-block:last-child { margin-bottom: 0; }
54
+ .remarks .rm-h { font-weight: 700; }
55
+ .remarks .rm-h + .rm-p, .remarks .rm-p + .rm-p { margin-top: 2px; }
56
+ .remarks .rm-amt { float: right; text-align: right; margin: 0 0 4px 16px; }
57
+ .remarks .rm-amt-val { white-space: nowrap; }
58
+ .remarks em { font-style: italic; }
59
+ .remarks strong { font-weight: 700; }
51
60
  </style>
52
61
  </head>
53
62
  <body>
@@ -182,8 +191,10 @@
182
191
  </div>
183
192
 
184
193
  {# ---- Remarks ---- #}
185
- {% if doc.remarks %}
186
- <div class="remarks">
194
+ {% if remarks_html is defined and remarks_html %}
195
+ <div class="remarks">{{ remarks_html | safe }}</div>
196
+ {% elif doc.remarks %}
197
+ <div class="remarks" style="white-space: pre-line;">
187
198
  <strong>Remarks:</strong> {{ doc.remarks }}
188
199
  </div>
189
200
  {% endif %}
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lambda-erp"
3
- version = "0.1.25"
3
+ version = "0.1.26"
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