lambda-erp 1.0.2__tar.gz → 1.1.1__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-1.0.2 → lambda_erp-1.1.1}/PKG-INFO +1 -1
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/chat.py +18 -12
- lambda_erp-1.1.1/api/chat_links.py +59 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/pdf_profiles.py +6 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/routers/masters.py +5 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/services.py +3 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/pos_invoice.py +42 -17
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/purchase_invoice.py +31 -15
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/sales_invoice.py +37 -22
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/buying/purchase_order.py +11 -15
- lambda_erp-1.1.1/lambda_erp/controllers/defaults.py +114 -0
- lambda_erp-1.1.1/lambda_erp/controllers/item_price.py +247 -0
- lambda_erp-1.1.1/lambda_erp/controllers/pricing_rule.py +273 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/database.py +226 -12
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/selling/quotation.py +15 -15
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/selling/sales_order.py +12 -16
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/pyproject.toml +1 -1
- lambda_erp-1.0.2/lambda_erp/controllers/defaults.py +0 -51
- lambda_erp-1.0.2/lambda_erp/controllers/pricing_rule.py +0 -112
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/.gitignore +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/LICENSE +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/README.md +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/__init__.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/attachments.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/auth.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/bootstrap.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/demo_limits.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/deps.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/errors.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/list_values.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/main.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/oauth.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/pdf.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/pdf_contract.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/pdf_exports.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/providers.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/remarks_md.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/routers/__init__.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/routers/accounting.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/routers/actions.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/routers/admin.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/routers/analytics.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/routers/availability.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/routers/bank_statements.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/routers/chat_api.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/routers/documents.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/routers/mcp.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/routers/proposals.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/routers/reports.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/routers/setup.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/templates/_proposal_totals.html +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/templates/_record_base.html +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/templates/document.html +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/templates/proposal.html +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/templates/record.html +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/time_filters.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/api/tool_permissions.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/docs/agents/README.md +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/frontend/LICENSE +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/frontend/README.md +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/frontend/src/api/client.ts +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/__init__.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/bank_account.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/bank_reconciliation.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/bank_statement_import.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/camt.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/settlement.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/setup/__init__.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/setup/engine.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/setup/pack.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/setup/packs/ch.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/setup/packs/generic.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/setup/profiles.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/setup/spine.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/assets/__init__.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/assets/asset.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/assets/lifecycle.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/assets/reservation.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/controllers/item_prices.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/hooks.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/master_integrity.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/model.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/simulation.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/timestamps.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/utils.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/validation.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/lambda_erp/workflow.py +0 -0
- {lambda_erp-1.0.2 → lambda_erp-1.1.1}/terraform/README.md +0 -0
|
@@ -28,6 +28,7 @@ from openai import OpenAI
|
|
|
28
28
|
|
|
29
29
|
from api.time_filters import TIME_FILTER_SCHEMA, time_filter_fields
|
|
30
30
|
from api import services
|
|
31
|
+
from api.chat_links import with_record_view_urls
|
|
31
32
|
from api.tool_permissions import tool_allowed, tool_permission_error
|
|
32
33
|
from api.demo_limits import (
|
|
33
34
|
demo_call_reserve_usd,
|
|
@@ -1459,6 +1460,7 @@ def _validate_filter_columns(doctype_slug, filters) -> str | None:
|
|
|
1459
1460
|
return None
|
|
1460
1461
|
|
|
1461
1462
|
|
|
1463
|
+
@with_record_view_urls("doctype")
|
|
1462
1464
|
def _handle_list_documents(args):
|
|
1463
1465
|
doctype = args["doctype"]
|
|
1464
1466
|
try:
|
|
@@ -1497,10 +1499,12 @@ def _handle_get_document_fields(args):
|
|
|
1497
1499
|
return services.document_field_metadata(args["doctype"])
|
|
1498
1500
|
|
|
1499
1501
|
|
|
1502
|
+
@with_record_view_urls("doctype")
|
|
1500
1503
|
def _handle_get_document(args):
|
|
1501
1504
|
return services.load_document(args["doctype"], args["name"])
|
|
1502
1505
|
|
|
1503
1506
|
|
|
1507
|
+
@with_record_view_urls("doctype")
|
|
1504
1508
|
def _handle_create_document(args):
|
|
1505
1509
|
data = args.get("data")
|
|
1506
1510
|
if not data:
|
|
@@ -1508,10 +1512,12 @@ def _handle_create_document(args):
|
|
|
1508
1512
|
return services.create_document(args["doctype"], data)
|
|
1509
1513
|
|
|
1510
1514
|
|
|
1515
|
+
@with_record_view_urls("doctype")
|
|
1511
1516
|
def _handle_update_document(args):
|
|
1512
1517
|
return services.update_document(args["doctype"], args["name"], args.get("data", {}))
|
|
1513
1518
|
|
|
1514
1519
|
|
|
1520
|
+
@with_record_view_urls("doctype")
|
|
1515
1521
|
def _handle_batch_update_documents(args):
|
|
1516
1522
|
try:
|
|
1517
1523
|
return services.batch_update_documents(args["doctype"], args.get("updates"))
|
|
@@ -1519,18 +1525,22 @@ def _handle_batch_update_documents(args):
|
|
|
1519
1525
|
return {"error": str(e)}
|
|
1520
1526
|
|
|
1521
1527
|
|
|
1528
|
+
@with_record_view_urls("doctype")
|
|
1522
1529
|
def _handle_submit_document(args):
|
|
1523
1530
|
return services.submit_document(args["doctype"], args["name"])
|
|
1524
1531
|
|
|
1525
1532
|
|
|
1533
|
+
@with_record_view_urls("doctype")
|
|
1526
1534
|
def _handle_cancel_document(args):
|
|
1527
1535
|
return services.cancel_document(args["doctype"], args["name"])
|
|
1528
1536
|
|
|
1529
1537
|
|
|
1538
|
+
@with_record_view_urls("doctype")
|
|
1530
1539
|
def _handle_discard_document(args):
|
|
1531
1540
|
return services.discard_document(args["doctype"], args["name"])
|
|
1532
1541
|
|
|
1533
1542
|
|
|
1543
|
+
@with_record_view_urls("target_doctype")
|
|
1534
1544
|
def _handle_convert_document(args):
|
|
1535
1545
|
return services.convert_document(args["doctype"], args["name"], args["target_doctype"])
|
|
1536
1546
|
|
|
@@ -1607,6 +1617,7 @@ def _handle_get_master_fields(args):
|
|
|
1607
1617
|
}
|
|
1608
1618
|
|
|
1609
1619
|
|
|
1620
|
+
@with_record_view_urls("master_type", master=True)
|
|
1610
1621
|
def _handle_search_masters(args):
|
|
1611
1622
|
db = get_db()
|
|
1612
1623
|
meta = bool(args.get("include_meta")) or args.get("time_filter") is not None
|
|
@@ -1724,6 +1735,7 @@ def _ignored_master_fields(master_type: str, data: dict) -> list[str]:
|
|
|
1724
1735
|
return [k for k in data.keys() if k not in valid]
|
|
1725
1736
|
|
|
1726
1737
|
|
|
1738
|
+
@with_record_view_urls("master_type", master=True)
|
|
1727
1739
|
def _handle_create_master(args):
|
|
1728
1740
|
master_type = args["master_type"]
|
|
1729
1741
|
data = args.get("data")
|
|
@@ -1750,6 +1762,7 @@ def _handle_create_master(args):
|
|
|
1750
1762
|
return result
|
|
1751
1763
|
|
|
1752
1764
|
|
|
1765
|
+
@with_record_view_urls("master_type", master=True)
|
|
1753
1766
|
def _handle_update_master(args):
|
|
1754
1767
|
master_type = args["master_type"]
|
|
1755
1768
|
name = args.get("name")
|
|
@@ -2464,18 +2477,11 @@ Examples:
|
|
|
2464
2477
|
- Correct: `[SINV-0012](/app/sales-invoice/SINV-0012)`
|
|
2465
2478
|
- Avoid: `/reports/analytics?report_id=RPT-AB12CD34` (bare URL)"""
|
|
2466
2479
|
document_links_section = """## Document & Master Links
|
|
2467
|
-
When
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
The doctype slug is the lowercase, hyphenated form: sales-invoice, purchase-order, delivery-note, etc.
|
|
2473
|
-
|
|
2474
|
-
**Master records** (customer, supplier, item, warehouse, company):
|
|
2475
|
-
- **View/edit link:** `/masters/{master-type}/{name}` — e.g. [SUPP-001](/masters/supplier/SUPP-001), [CUST-003](/masters/customer/CUST-003), [ITEM-001](/masters/item/ITEM-001)
|
|
2476
|
-
- NEVER use `/app/...` for masters — that path is only for transactional documents.
|
|
2477
|
-
|
|
2478
|
-
Always include the view link after creating, submitting, converting, or updating a record. Call generate_document_pdf when a printable file is requested. Only confirm and link a successfully generated file."""
|
|
2480
|
+
Document and master tools return a canonical `view_url` on each record, including list/search rows and successful batch results. When linking a record, copy that exact value into a markdown link `[label](view_url)`. Never construct a record URL yourself, add a `#`, change the path, or re-encode the returned URL.
|
|
2481
|
+
`view_url` is tool output metadata, not a document/master field; never include it in create/update data.
|
|
2482
|
+
If `view_url` is null, refer to the record in plain text. For a projected document list or batch result missing parent-page information, call get_document to obtain its view_url if a link is needed. Some records have no page at all.
|
|
2483
|
+
Always include the returned view link, when available, after creating, submitting, converting, or updating a record.
|
|
2484
|
+
For PDF files, call generate_document_pdf and copy its returned download_url only after successful generation. Never construct PDF links yourself."""
|
|
2479
2485
|
|
|
2480
2486
|
if user_role == "admin":
|
|
2481
2487
|
role_desc = "You have **admin** access — full permissions to create, edit, submit, cancel documents, manage master data, run reports, and manage users."
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Canonical record links on chat/MCP tool results, never persisted fields."""
|
|
2
|
+
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from urllib.parse import quote, urlencode
|
|
5
|
+
|
|
6
|
+
from api import services
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def record_view_url(record_type: str, record: dict, *, master: bool = False) -> str | None:
|
|
10
|
+
name = record.get("name")
|
|
11
|
+
if not name:
|
|
12
|
+
return None
|
|
13
|
+
if master:
|
|
14
|
+
if record_type not in services.MASTER_TABLES or record_type == "cost-center":
|
|
15
|
+
return None
|
|
16
|
+
if record_type == "account":
|
|
17
|
+
return "/reports/general-ledger?" + urlencode({"account": name})
|
|
18
|
+
return f"/masters/{quote(record_type, safe='')}/{quote(str(name), safe='')}"
|
|
19
|
+
|
|
20
|
+
slug = services.DOCTYPE_TO_SLUG.get(record_type, record_type)
|
|
21
|
+
if slug not in services.SLUG_TO_DOCTYPE:
|
|
22
|
+
return None
|
|
23
|
+
page = services.chat_doctype_page_info(slug)
|
|
24
|
+
if page:
|
|
25
|
+
if page["kind"] == "none":
|
|
26
|
+
return None
|
|
27
|
+
if page["kind"] == "via":
|
|
28
|
+
name = record.get(page["link_field"])
|
|
29
|
+
if not name:
|
|
30
|
+
# Projected list/batch results may omit the parent. The model
|
|
31
|
+
# can get_document to obtain it; do not guess a destination.
|
|
32
|
+
return None
|
|
33
|
+
slug = page["parent_slug"]
|
|
34
|
+
return f"/app/{quote(slug, safe='')}/{quote(str(name), safe='')}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def with_record_view_urls(type_arg: str, *, master: bool = False):
|
|
38
|
+
"""Enrich records and list/batch envelopes without changing service data."""
|
|
39
|
+
def decorate(handler):
|
|
40
|
+
@wraps(handler)
|
|
41
|
+
def wrapped(args):
|
|
42
|
+
result = handler(args)
|
|
43
|
+
|
|
44
|
+
def enrich(row):
|
|
45
|
+
if not isinstance(row, dict) or "error" in row or row.get("ok") is False:
|
|
46
|
+
return row
|
|
47
|
+
return {**row, "view_url": record_view_url(args.get(type_arg), row, master=master)}
|
|
48
|
+
|
|
49
|
+
if isinstance(result, list):
|
|
50
|
+
return [enrich(row) for row in result]
|
|
51
|
+
if isinstance(result, dict) and "error" not in result:
|
|
52
|
+
if "name" not in result:
|
|
53
|
+
for key in ("rows", "results"):
|
|
54
|
+
if isinstance(result.get(key), list):
|
|
55
|
+
return {**result, key: [enrich(row) for row in result[key]]}
|
|
56
|
+
return enrich(result)
|
|
57
|
+
return result
|
|
58
|
+
return wrapped
|
|
59
|
+
return decorate
|
|
@@ -88,6 +88,12 @@ PROFILES.update({
|
|
|
88
88
|
tables=(PDFTable('monthly_distribution', ('month', 'percentage'), ('month', 'percentage')),)),
|
|
89
89
|
'Pricing Rule': PDFProfile('record', 'Pricing rule overview',
|
|
90
90
|
fields=('title', 'company', 'item_code', 'selling', 'buying', 'rate_or_discount', 'rate', 'discount_percentage', 'discount_amount', 'min_qty', 'valid_from', 'valid_upto', 'priority', 'enabled'), required=('title', 'rate_or_discount')),
|
|
91
|
+
'Price List': PDFProfile('record', 'Price list overview',
|
|
92
|
+
fields=('price_list_name', 'currency', 'selling', 'buying', 'enabled'),
|
|
93
|
+
required=('price_list_name', 'currency')),
|
|
94
|
+
'Item Price': PDFProfile('record', 'Item price overview',
|
|
95
|
+
fields=('item_code', 'price_list', 'customer', 'supplier', 'uom', 'min_qty', 'rate', 'valid_from', 'valid_upto', 'enabled'),
|
|
96
|
+
required=('item_code', 'price_list', 'rate')),
|
|
91
97
|
'Bank Account': PDFProfile('record', 'Bank account overview',
|
|
92
98
|
fields=('account_name', 'company', 'account', 'iban', 'currency', 'bank_name', 'bic', 'disabled'), required=('account_name', 'company', 'account', 'iban', 'currency')),
|
|
93
99
|
'Bank Transaction': PDFProfile('record', 'Bank transaction overview',
|
|
@@ -67,6 +67,8 @@ DELETE_REFERENCE_CHECKS = {
|
|
|
67
67
|
('SELECT 1 FROM "POS Invoice" WHERE customer = ? LIMIT 1', "POS invoice"),
|
|
68
68
|
('SELECT 1 FROM "Payment Entry" WHERE party_type = \'Customer\' AND party = ? LIMIT 1', "payment entry"),
|
|
69
69
|
('SELECT 1 FROM "Subscription" WHERE party_type = \'Customer\' AND party = ? LIMIT 1', "subscription"),
|
|
70
|
+
('SELECT 1 FROM "Item Price" WHERE customer = ? LIMIT 1', "item price"),
|
|
71
|
+
('SELECT 1 FROM "Pricing Rule" WHERE customer = ? LIMIT 1', "pricing rule"),
|
|
70
72
|
],
|
|
71
73
|
"supplier": [
|
|
72
74
|
('SELECT 1 FROM "Purchase Order" WHERE supplier = ? LIMIT 1', "purchase order"),
|
|
@@ -88,6 +90,7 @@ DELETE_REFERENCE_CHECKS = {
|
|
|
88
90
|
('SELECT 1 FROM "Stock Ledger Entry" WHERE item_code = ? LIMIT 1', "stock ledger entry"),
|
|
89
91
|
('SELECT 1 FROM "Bin" WHERE item_code = ? LIMIT 1', "bin"),
|
|
90
92
|
('SELECT 1 FROM "Pricing Rule" WHERE item_code = ? LIMIT 1', "pricing rule"),
|
|
93
|
+
('SELECT 1 FROM "Item Price" WHERE item_code = ? LIMIT 1', "item price"),
|
|
91
94
|
('SELECT 1 FROM "Subscription Plan" WHERE item_code = ? LIMIT 1', "subscription plan"),
|
|
92
95
|
('SELECT 1 FROM "Asset" WHERE item_code = ? LIMIT 1', "asset"),
|
|
93
96
|
('SELECT 1 FROM "Reservation" WHERE item_code = ? LIMIT 1', "reservation"),
|
|
@@ -213,6 +216,8 @@ def _normalize_master_data(data: dict) -> dict:
|
|
|
213
216
|
|
|
214
217
|
|
|
215
218
|
MASTER_LINK_FIELDS = {
|
|
219
|
+
'customer': {'default_price_list': 'Price List'},
|
|
220
|
+
'company': {'default_price_list': 'Price List'},
|
|
216
221
|
'item': {'default_warehouse': 'Warehouse'},
|
|
217
222
|
'warehouse': {'company': 'Company', 'account': 'Account', 'parent_warehouse': 'Warehouse'},
|
|
218
223
|
'account': {'company': 'Company', 'parent_account': 'Account'},
|
|
@@ -23,6 +23,7 @@ from lambda_erp.stock.delivery_note import DeliveryNote, make_delivery_note, mak
|
|
|
23
23
|
from lambda_erp.stock.purchase_receipt import PurchaseReceipt, make_purchase_receipt, make_purchase_receipt_return
|
|
24
24
|
from lambda_erp.accounting.pos_invoice import POSInvoice
|
|
25
25
|
from lambda_erp.controllers.pricing_rule import PricingRule
|
|
26
|
+
from lambda_erp.controllers.item_price import PriceList, ItemPrice
|
|
26
27
|
from lambda_erp.accounting.budget import Budget
|
|
27
28
|
from lambda_erp.accounting.subscription import Subscription
|
|
28
29
|
from lambda_erp.accounting.bank_transaction import BankTransaction
|
|
@@ -47,6 +48,8 @@ DOCUMENT_CLASSES = {
|
|
|
47
48
|
"Purchase Receipt": PurchaseReceipt,
|
|
48
49
|
"POS Invoice": POSInvoice,
|
|
49
50
|
"Pricing Rule": PricingRule,
|
|
51
|
+
"Price List": PriceList,
|
|
52
|
+
"Item Price": ItemPrice,
|
|
50
53
|
"Budget": Budget,
|
|
51
54
|
"Subscription": Subscription,
|
|
52
55
|
"Bank Transaction": BankTransaction,
|
|
@@ -13,7 +13,11 @@ from lambda_erp.model import Document
|
|
|
13
13
|
from lambda_erp.utils import _dict, flt, nowdate
|
|
14
14
|
from lambda_erp.database import get_db
|
|
15
15
|
from lambda_erp.controllers.taxes_and_totals import calculate_taxes_and_totals
|
|
16
|
-
from lambda_erp.controllers.defaults import
|
|
16
|
+
from lambda_erp.controllers.defaults import (
|
|
17
|
+
set_default_currency, apply_external_source_defaults,
|
|
18
|
+
derived_pricing_fields, derived_line_pricing_fields,
|
|
19
|
+
)
|
|
20
|
+
from lambda_erp.controllers.item_price import set_item_defaults
|
|
17
21
|
from lambda_erp.exceptions import ValidationError
|
|
18
22
|
from lambda_erp.stock.stock_ledger import (
|
|
19
23
|
make_sl_entries,
|
|
@@ -65,16 +69,44 @@ class POSInvoice(Document):
|
|
|
65
69
|
if not self.posting_date:
|
|
66
70
|
self.posting_date = nowdate()
|
|
67
71
|
|
|
72
|
+
apply_external_source_defaults(self)
|
|
68
73
|
self._set_customer_name()
|
|
69
74
|
self._set_missing_accounts()
|
|
75
|
+
set_default_currency(self, "Customer", "customer")
|
|
70
76
|
self._set_item_defaults()
|
|
77
|
+
self._validate_external_source_stock()
|
|
71
78
|
if self.is_return:
|
|
72
79
|
self._validate_return()
|
|
73
|
-
|
|
80
|
+
# POS was the only selling document that never applied pricing rules.
|
|
81
|
+
# Nothing about a till sale makes a discount inapplicable, so the same
|
|
82
|
+
# item rang up at a different price depending on which document the
|
|
83
|
+
# sale happened to be written to.
|
|
84
|
+
from lambda_erp.controllers.pricing_rule import apply_pricing_rules
|
|
85
|
+
apply_pricing_rules(self)
|
|
86
|
+
|
|
74
87
|
calculate_taxes_and_totals(self)
|
|
75
88
|
self._calculate_payments()
|
|
76
89
|
self._set_status()
|
|
77
90
|
|
|
91
|
+
def _validate_external_source_stock(self):
|
|
92
|
+
"""An upstream system's stock movement already accounts for the goods.
|
|
93
|
+
|
|
94
|
+
`_validate_no_double_shipment` catches the same mistake on the Sales
|
|
95
|
+
Order -> Delivery Note path by looking up `sales_order_item`. A document
|
|
96
|
+
created from an external system has no such link, so that lookup finds
|
|
97
|
+
nothing and passes. Without this guard the upstream issue and this
|
|
98
|
+
document's own update_stock each decrement the same quantity.
|
|
99
|
+
"""
|
|
100
|
+
if not self._data.get("external_source"):
|
|
101
|
+
return
|
|
102
|
+
if flt(self.get("update_stock")):
|
|
103
|
+
raise ValidationError(
|
|
104
|
+
f"Cannot submit with update_stock=1: this document is sourced "
|
|
105
|
+
f"from {self._data['external_source']}, which already recorded "
|
|
106
|
+
f"the stock movement. Use this invoice as a bill only and "
|
|
107
|
+
f"record any required stock movement in a separate stock document."
|
|
108
|
+
)
|
|
109
|
+
|
|
78
110
|
def _validate_return(self):
|
|
79
111
|
from lambda_erp.workflow import validate_return
|
|
80
112
|
validate_return(self)
|
|
@@ -100,18 +132,8 @@ class POSInvoice(Document):
|
|
|
100
132
|
item["cost_center"] = default_cc
|
|
101
133
|
|
|
102
134
|
def _set_item_defaults(self):
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if item.get("item_code") and not item.get("item_name"):
|
|
106
|
-
item_data = db.get_value(
|
|
107
|
-
"Item", item["item_code"],
|
|
108
|
-
["item_name", "description", "stock_uom", "standard_rate"]
|
|
109
|
-
)
|
|
110
|
-
if item_data:
|
|
111
|
-
item["item_name"] = item_data.item_name
|
|
112
|
-
item["uom"] = item.get("uom") or item_data.stock_uom
|
|
113
|
-
if item.get("rate") is None and item.get("price_list_rate") is None:
|
|
114
|
-
item["rate"] = flt(item_data.standard_rate)
|
|
135
|
+
"""Names, units and unsupplied rates. See controllers/item_price.py."""
|
|
136
|
+
set_item_defaults(self, "Customer", "customer", set_description=False)
|
|
115
137
|
|
|
116
138
|
def _calculate_payments(self):
|
|
117
139
|
paid = sum(flt(p.get("amount", 0)) for p in self.get("payments") or [])
|
|
@@ -197,16 +219,17 @@ class POSInvoice(Document):
|
|
|
197
219
|
account = item.get("income_account")
|
|
198
220
|
if not account:
|
|
199
221
|
continue
|
|
200
|
-
|
|
222
|
+
key = (account, item.get("cost_center"))
|
|
223
|
+
income_accounts[key] = income_accounts.get(key, 0) + flt(item.get("net_amount", 0))
|
|
201
224
|
|
|
202
|
-
for account, amount in income_accounts.items():
|
|
225
|
+
for (account, cost_center), amount in income_accounts.items():
|
|
203
226
|
gl_entries.append(_dict(
|
|
204
227
|
account=account,
|
|
205
228
|
credit=flt(amount, 2),
|
|
206
229
|
credit_in_account_currency=flt(amount, 2),
|
|
207
230
|
debit=0,
|
|
208
231
|
debit_in_account_currency=0,
|
|
209
|
-
cost_center=
|
|
232
|
+
cost_center=cost_center,
|
|
210
233
|
voucher_type=self.DOCTYPE,
|
|
211
234
|
voucher_no=self.name,
|
|
212
235
|
posting_date=self.posting_date,
|
|
@@ -288,6 +311,7 @@ def make_pos_return(posi_name):
|
|
|
288
311
|
raise ValidationError("Cannot create a return against a return")
|
|
289
312
|
|
|
290
313
|
return_pos = POSInvoice(
|
|
314
|
+
**derived_pricing_fields(original, is_return=True),
|
|
291
315
|
customer=original.customer,
|
|
292
316
|
company=original.company,
|
|
293
317
|
currency=original.get("currency") or "USD",
|
|
@@ -302,6 +326,7 @@ def make_pos_return(posi_name):
|
|
|
302
326
|
from lambda_erp.workflow import returnable_rows
|
|
303
327
|
for item in returnable_rows(original):
|
|
304
328
|
return_pos.append("items", _dict(
|
|
329
|
+
**derived_line_pricing_fields(item),
|
|
305
330
|
item_code=item.get("item_code"),
|
|
306
331
|
item_name=item.get("item_name"),
|
|
307
332
|
description=item.get("description"),
|
|
@@ -14,7 +14,11 @@ from lambda_erp.model import Document
|
|
|
14
14
|
from lambda_erp.utils import _dict, flt, nowdate, add_days
|
|
15
15
|
from lambda_erp.database import get_db
|
|
16
16
|
from lambda_erp.controllers.taxes_and_totals import calculate_taxes_and_totals
|
|
17
|
-
from lambda_erp.controllers.defaults import
|
|
17
|
+
from lambda_erp.controllers.defaults import (
|
|
18
|
+
set_default_currency, apply_external_source_defaults,
|
|
19
|
+
derived_pricing_fields, derived_line_pricing_fields,
|
|
20
|
+
)
|
|
21
|
+
from lambda_erp.controllers.item_price import set_item_defaults
|
|
18
22
|
from lambda_erp.accounting.general_ledger import make_gl_entries, make_reverse_gl_entries, to_base_currency
|
|
19
23
|
from lambda_erp.stock.stock_ledger import (
|
|
20
24
|
make_sl_entries,
|
|
@@ -65,11 +69,14 @@ class PurchaseInvoice(Document):
|
|
|
65
69
|
if not self.posting_date:
|
|
66
70
|
self.posting_date = nowdate()
|
|
67
71
|
|
|
72
|
+
apply_external_source_defaults(self)
|
|
68
73
|
self._set_supplier_name()
|
|
69
74
|
self._set_missing_accounts()
|
|
75
|
+
set_default_currency(self, "Supplier", "supplier")
|
|
70
76
|
self._set_item_defaults()
|
|
71
77
|
self._validate_stock_warehouses()
|
|
72
78
|
self._validate_no_double_receipt()
|
|
79
|
+
self._validate_external_source_stock()
|
|
73
80
|
|
|
74
81
|
if self.is_return:
|
|
75
82
|
self._validate_return()
|
|
@@ -77,7 +84,6 @@ class PurchaseInvoice(Document):
|
|
|
77
84
|
from lambda_erp.controllers.pricing_rule import apply_pricing_rules
|
|
78
85
|
apply_pricing_rules(self)
|
|
79
86
|
|
|
80
|
-
set_default_currency(self, "Supplier", "supplier")
|
|
81
87
|
|
|
82
88
|
calculate_taxes_and_totals(self)
|
|
83
89
|
|
|
@@ -172,6 +178,25 @@ class PurchaseInvoice(Document):
|
|
|
172
178
|
f"is already allocated against it. Cancel the Payment Entry first."
|
|
173
179
|
)
|
|
174
180
|
|
|
181
|
+
def _validate_external_source_stock(self):
|
|
182
|
+
"""An upstream system's stock movement already accounts for the goods.
|
|
183
|
+
|
|
184
|
+
`_validate_no_double_shipment` catches the same mistake on the Sales
|
|
185
|
+
Order -> Delivery Note path by looking up `sales_order_item`. A document
|
|
186
|
+
created from an external system has no such link, so that lookup finds
|
|
187
|
+
nothing and passes. Without this guard the upstream issue and this
|
|
188
|
+
document's own update_stock each decrement the same quantity.
|
|
189
|
+
"""
|
|
190
|
+
if not self._data.get("external_source"):
|
|
191
|
+
return
|
|
192
|
+
if flt(self.get("update_stock")):
|
|
193
|
+
raise ValidationError(
|
|
194
|
+
f"Cannot submit with update_stock=1: this document is sourced "
|
|
195
|
+
f"from {self._data['external_source']}, which already recorded "
|
|
196
|
+
f"the stock movement. Use this invoice as a bill only and "
|
|
197
|
+
f"record any required stock movement in a separate stock document."
|
|
198
|
+
)
|
|
199
|
+
|
|
175
200
|
def _validate_no_double_receipt(self):
|
|
176
201
|
"""Block update_stock=1 when the referenced Purchase Order already has
|
|
177
202
|
a Purchase Receipt for the line. Otherwise stock arrives twice: once
|
|
@@ -202,19 +227,8 @@ class PurchaseInvoice(Document):
|
|
|
202
227
|
)
|
|
203
228
|
|
|
204
229
|
def _set_item_defaults(self):
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if item.get("item_code") and not item.get("item_name"):
|
|
208
|
-
item_data = db.get_value(
|
|
209
|
-
"Item", item["item_code"],
|
|
210
|
-
["item_name", "description", "stock_uom", "standard_rate"]
|
|
211
|
-
)
|
|
212
|
-
if item_data:
|
|
213
|
-
item["item_name"] = item_data.item_name
|
|
214
|
-
item["description"] = item.get("description") or item_data.description
|
|
215
|
-
item["uom"] = item.get("uom") or item_data.stock_uom
|
|
216
|
-
if item.get("rate") is None and item.get("price_list_rate") is None:
|
|
217
|
-
item["rate"] = flt(item_data.standard_rate)
|
|
230
|
+
"""Names, units and unsupplied rates. See controllers/item_price.py."""
|
|
231
|
+
set_item_defaults(self, "Supplier", "supplier")
|
|
218
232
|
|
|
219
233
|
def _validate_return(self):
|
|
220
234
|
from lambda_erp.workflow import validate_return
|
|
@@ -408,6 +422,7 @@ def make_purchase_return(pinv_name):
|
|
|
408
422
|
raise ValidationError("Cannot create a return against a return")
|
|
409
423
|
|
|
410
424
|
return_inv = PurchaseInvoice(
|
|
425
|
+
**derived_pricing_fields(original, is_return=True),
|
|
411
426
|
supplier=original.supplier,
|
|
412
427
|
company=original.company,
|
|
413
428
|
currency=original.get("currency") or "USD",
|
|
@@ -425,6 +440,7 @@ def make_purchase_return(pinv_name):
|
|
|
425
440
|
from lambda_erp.workflow import returnable_rows
|
|
426
441
|
for item in returnable_rows(original):
|
|
427
442
|
return_inv.append("items", _dict(
|
|
443
|
+
**derived_line_pricing_fields(item),
|
|
428
444
|
item_code=item.get("item_code"),
|
|
429
445
|
item_name=item.get("item_name"),
|
|
430
446
|
description=item.get("description"),
|
|
@@ -18,7 +18,11 @@ from lambda_erp.model import Document
|
|
|
18
18
|
from lambda_erp.utils import _dict, flt, getdate, nowdate, add_days
|
|
19
19
|
from lambda_erp.database import get_db
|
|
20
20
|
from lambda_erp.controllers.taxes_and_totals import calculate_taxes_and_totals
|
|
21
|
-
from lambda_erp.controllers.defaults import
|
|
21
|
+
from lambda_erp.controllers.defaults import (
|
|
22
|
+
set_default_currency, apply_external_source_defaults,
|
|
23
|
+
derived_pricing_fields, derived_line_pricing_fields,
|
|
24
|
+
)
|
|
25
|
+
from lambda_erp.controllers.item_price import set_item_defaults
|
|
22
26
|
from lambda_erp.accounting.general_ledger import make_gl_entries, make_reverse_gl_entries, to_base_currency
|
|
23
27
|
from lambda_erp.stock.stock_ledger import (
|
|
24
28
|
make_sl_entries,
|
|
@@ -81,10 +85,13 @@ class SalesInvoice(Document):
|
|
|
81
85
|
if not self.posting_date:
|
|
82
86
|
self.posting_date = nowdate()
|
|
83
87
|
|
|
88
|
+
apply_external_source_defaults(self)
|
|
84
89
|
self._set_customer_name()
|
|
85
90
|
self._set_missing_accounts()
|
|
91
|
+
set_default_currency(self, "Customer", "customer")
|
|
86
92
|
self._set_item_defaults()
|
|
87
93
|
self._validate_no_double_shipment()
|
|
94
|
+
self._validate_external_source_stock()
|
|
88
95
|
|
|
89
96
|
if self.is_return:
|
|
90
97
|
self._validate_return()
|
|
@@ -92,7 +99,6 @@ class SalesInvoice(Document):
|
|
|
92
99
|
from lambda_erp.controllers.pricing_rule import apply_pricing_rules
|
|
93
100
|
apply_pricing_rules(self)
|
|
94
101
|
|
|
95
|
-
set_default_currency(self, "Customer", "customer")
|
|
96
102
|
|
|
97
103
|
calculate_taxes_and_totals(self)
|
|
98
104
|
|
|
@@ -133,19 +139,27 @@ class SalesInvoice(Document):
|
|
|
133
139
|
item["cost_center"] = default_cc
|
|
134
140
|
|
|
135
141
|
def _set_item_defaults(self):
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
142
|
+
"""Names, units and unsupplied rates. See controllers/item_price.py."""
|
|
143
|
+
set_item_defaults(self, "Customer", "customer")
|
|
144
|
+
|
|
145
|
+
def _validate_external_source_stock(self):
|
|
146
|
+
"""An upstream system's stock movement already accounts for the goods.
|
|
147
|
+
|
|
148
|
+
`_validate_no_double_shipment` catches the same mistake on the Sales
|
|
149
|
+
Order -> Delivery Note path by looking up `sales_order_item`. A document
|
|
150
|
+
created from an external system has no such link, so that lookup finds
|
|
151
|
+
nothing and passes. Without this guard the upstream issue and this
|
|
152
|
+
document's own update_stock each decrement the same quantity.
|
|
153
|
+
"""
|
|
154
|
+
if not self._data.get("external_source"):
|
|
155
|
+
return
|
|
156
|
+
if flt(self.get("update_stock")):
|
|
157
|
+
raise ValidationError(
|
|
158
|
+
f"Cannot submit with update_stock=1: this document is sourced "
|
|
159
|
+
f"from {self._data['external_source']}, which already recorded "
|
|
160
|
+
f"the stock movement. Use this invoice as a bill only and "
|
|
161
|
+
f"record any required stock movement in a separate stock document."
|
|
162
|
+
)
|
|
149
163
|
|
|
150
164
|
def _validate_no_double_shipment(self):
|
|
151
165
|
"""Block update_stock=1 when the referenced Sales Order already has a
|
|
@@ -338,15 +352,14 @@ class SalesInvoice(Document):
|
|
|
338
352
|
)
|
|
339
353
|
|
|
340
354
|
# 2. Credit: Income accounts (per item)
|
|
341
|
-
#
|
|
355
|
+
# Group only lines with the same account AND cost centre.
|
|
342
356
|
income_accounts = {}
|
|
343
357
|
for item in self.get("items"):
|
|
344
358
|
account = item.get("income_account")
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
income_accounts[account] += flt(item.get("net_amount", 0))
|
|
359
|
+
key = (account, item.get("cost_center"))
|
|
360
|
+
income_accounts[key] = income_accounts.get(key, 0) + flt(item.get("net_amount", 0))
|
|
348
361
|
|
|
349
|
-
for account, amount in income_accounts.items():
|
|
362
|
+
for (account, cost_center), amount in income_accounts.items():
|
|
350
363
|
gl_entries.append(
|
|
351
364
|
_dict(
|
|
352
365
|
account=account,
|
|
@@ -354,7 +367,7 @@ class SalesInvoice(Document):
|
|
|
354
367
|
credit_in_account_currency=flt(amount, 2),
|
|
355
368
|
debit=0,
|
|
356
369
|
debit_in_account_currency=0,
|
|
357
|
-
cost_center=
|
|
370
|
+
cost_center=cost_center,
|
|
358
371
|
voucher_type=self.DOCTYPE,
|
|
359
372
|
voucher_no=self.name,
|
|
360
373
|
posting_date=self.posting_date,
|
|
@@ -373,7 +386,7 @@ class SalesInvoice(Document):
|
|
|
373
386
|
credit_in_account_currency=flt(tax["tax_amount"], 2),
|
|
374
387
|
debit=0,
|
|
375
388
|
debit_in_account_currency=0,
|
|
376
|
-
cost_center=
|
|
389
|
+
cost_center=get_db().get_value("Company", self.company, "default_cost_center"),
|
|
377
390
|
voucher_type=self.DOCTYPE,
|
|
378
391
|
voucher_no=self.name,
|
|
379
392
|
posting_date=self.posting_date,
|
|
@@ -404,6 +417,7 @@ def make_sales_return(sinv_name):
|
|
|
404
417
|
raise ValidationError("Cannot create a return against a return")
|
|
405
418
|
|
|
406
419
|
return_inv = SalesInvoice(
|
|
420
|
+
**derived_pricing_fields(original, is_return=True),
|
|
407
421
|
customer=original.customer,
|
|
408
422
|
company=original.company,
|
|
409
423
|
currency=original.get("currency") or "USD",
|
|
@@ -421,6 +435,7 @@ def make_sales_return(sinv_name):
|
|
|
421
435
|
from lambda_erp.workflow import returnable_rows
|
|
422
436
|
for item in returnable_rows(original):
|
|
423
437
|
return_inv.append("items", _dict(
|
|
438
|
+
**derived_line_pricing_fields(item),
|
|
424
439
|
item_code=item.get("item_code"),
|
|
425
440
|
item_name=item.get("item_name"),
|
|
426
441
|
description=item.get("description"),
|
|
@@ -14,7 +14,11 @@ from lambda_erp.model import Document
|
|
|
14
14
|
from lambda_erp.utils import _dict, flt, getdate, nowdate
|
|
15
15
|
from lambda_erp.database import get_db
|
|
16
16
|
from lambda_erp.controllers.taxes_and_totals import calculate_taxes_and_totals
|
|
17
|
-
from lambda_erp.controllers.defaults import
|
|
17
|
+
from lambda_erp.controllers.defaults import (
|
|
18
|
+
set_default_currency, apply_external_source_defaults,
|
|
19
|
+
derived_pricing_fields, derived_line_pricing_fields,
|
|
20
|
+
)
|
|
21
|
+
from lambda_erp.controllers.item_price import set_item_defaults
|
|
18
22
|
from lambda_erp.exceptions import ValidationError
|
|
19
23
|
|
|
20
24
|
class PurchaseOrder(Document):
|
|
@@ -49,13 +53,14 @@ class PurchaseOrder(Document):
|
|
|
49
53
|
if not self.transaction_date:
|
|
50
54
|
self.transaction_date = nowdate()
|
|
51
55
|
|
|
56
|
+
apply_external_source_defaults(self)
|
|
52
57
|
self._set_supplier_name()
|
|
58
|
+
set_default_currency(self, "Supplier", "supplier")
|
|
53
59
|
self._set_item_defaults()
|
|
54
60
|
|
|
55
61
|
from lambda_erp.controllers.pricing_rule import apply_pricing_rules
|
|
56
62
|
apply_pricing_rules(self)
|
|
57
63
|
|
|
58
|
-
set_default_currency(self, "Supplier", "supplier")
|
|
59
64
|
|
|
60
65
|
calculate_taxes_and_totals(self)
|
|
61
66
|
|
|
@@ -65,19 +70,8 @@ class PurchaseOrder(Document):
|
|
|
65
70
|
self.supplier_name = db.get_value("Supplier", self.supplier, "supplier_name")
|
|
66
71
|
|
|
67
72
|
def _set_item_defaults(self):
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if item.get("item_code") and not item.get("item_name"):
|
|
71
|
-
item_data = db.get_value(
|
|
72
|
-
"Item", item["item_code"],
|
|
73
|
-
["item_name", "description", "stock_uom", "standard_rate"]
|
|
74
|
-
)
|
|
75
|
-
if item_data:
|
|
76
|
-
item["item_name"] = item_data.item_name
|
|
77
|
-
item["description"] = item.get("description") or item_data.description
|
|
78
|
-
item["uom"] = item.get("uom") or item_data.stock_uom
|
|
79
|
-
if item.get("rate") is None and item.get("price_list_rate") is None:
|
|
80
|
-
item["rate"] = flt(item_data.standard_rate)
|
|
73
|
+
"""Names, units and unsupplied rates. See controllers/item_price.py."""
|
|
74
|
+
set_item_defaults(self, "Supplier", "supplier")
|
|
81
75
|
|
|
82
76
|
def _update_ordered_qty(self, direction=1):
|
|
83
77
|
from lambda_erp.workflow import refresh_order_progress
|
|
@@ -101,6 +95,7 @@ def make_purchase_invoice(purchase_order_name):
|
|
|
101
95
|
raise ValidationError("Purchase Order must be submitted before creating Purchase Invoice")
|
|
102
96
|
|
|
103
97
|
pi = PurchaseInvoice(
|
|
98
|
+
**derived_pricing_fields(po, is_return=False),
|
|
104
99
|
supplier=po.supplier,
|
|
105
100
|
supplier_name=po.supplier_name,
|
|
106
101
|
company=po.company,
|
|
@@ -116,6 +111,7 @@ def make_purchase_invoice(purchase_order_name):
|
|
|
116
111
|
continue
|
|
117
112
|
|
|
118
113
|
pi.append("items", _dict(
|
|
114
|
+
**derived_line_pricing_fields(item),
|
|
119
115
|
item_code=item.get("item_code"),
|
|
120
116
|
item_name=item.get("item_name"),
|
|
121
117
|
description=item.get("description"),
|