lambda-erp 0.1.16__tar.gz → 0.1.18__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.16 → lambda_erp-0.1.18}/PKG-INFO +1 -1
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/api/chat.py +21 -1
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/api/routers/documents.py +12 -2
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/api/services.py +26 -2
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/frontend/src/api/client.ts +3 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/database.py +31 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/model.py +29 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/pyproject.toml +1 -1
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/.gitignore +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/LICENSE +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/README.md +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/api/__init__.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/api/attachments.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/api/auth.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/api/bootstrap.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/api/demo_limits.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/api/deps.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/api/errors.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/api/main.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/api/pdf.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/api/providers.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/api/routers/__init__.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/api/routers/accounting.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/api/routers/admin.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/api/routers/analytics.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/api/routers/masters.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/api/routers/reports.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/api/routers/setup.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/api/templates/document.html +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/docs/agents/README.md +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/frontend/README.md +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.1.16 → lambda_erp-0.1.18}/terraform/README.md +0 -0
|
@@ -533,7 +533,22 @@ TOOLS = [
|
|
|
533
533
|
"type": "function",
|
|
534
534
|
"function": {
|
|
535
535
|
"name": "cancel_document",
|
|
536
|
-
"description": "Cancel a
|
|
536
|
+
"description": "Cancel a SUBMITTED document (reverses its GL/stock entries). Only works on submitted documents (docstatus=1). For an unwanted DRAFT (docstatus=0), use discard_document instead — do NOT submit a draft just to cancel it.",
|
|
537
|
+
"parameters": {
|
|
538
|
+
"type": "object",
|
|
539
|
+
"properties": {
|
|
540
|
+
"doctype": {"type": "string", "enum": DOCUMENT_SLUGS},
|
|
541
|
+
"name": {"type": "string"},
|
|
542
|
+
},
|
|
543
|
+
"required": ["doctype", "name"],
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
"type": "function",
|
|
549
|
+
"function": {
|
|
550
|
+
"name": "discard_document",
|
|
551
|
+
"description": "Void/discard an unwanted DRAFT document (docstatus=0). This is a soft delete: the record is kept for the audit trail but flagged 'Discarded' and hidden from default lists. It has no financial or stock impact (a draft never posted anything). Only works on drafts — a submitted document must be cancelled instead. There is no hard delete in the system.",
|
|
537
552
|
"parameters": {
|
|
538
553
|
"type": "object",
|
|
539
554
|
"properties": {
|
|
@@ -1051,6 +1066,10 @@ def _handle_cancel_document(args):
|
|
|
1051
1066
|
return services.cancel_document(args["doctype"], args["name"])
|
|
1052
1067
|
|
|
1053
1068
|
|
|
1069
|
+
def _handle_discard_document(args):
|
|
1070
|
+
return services.discard_document(args["doctype"], args["name"])
|
|
1071
|
+
|
|
1072
|
+
|
|
1054
1073
|
def _handle_convert_document(args):
|
|
1055
1074
|
return services.convert_document(args["doctype"], args["name"], args["target_doctype"])
|
|
1056
1075
|
|
|
@@ -1384,6 +1403,7 @@ TOOL_HANDLERS = {
|
|
|
1384
1403
|
"update_document": _handle_update_document,
|
|
1385
1404
|
"submit_document": _handle_submit_document,
|
|
1386
1405
|
"cancel_document": _handle_cancel_document,
|
|
1406
|
+
"discard_document": _handle_discard_document,
|
|
1387
1407
|
"convert_document": _handle_convert_document,
|
|
1388
1408
|
"search_masters": _handle_search_masters,
|
|
1389
1409
|
"create_master": _handle_create_master,
|
|
@@ -8,6 +8,7 @@ from api.services import (
|
|
|
8
8
|
update_document,
|
|
9
9
|
submit_document,
|
|
10
10
|
cancel_document,
|
|
11
|
+
discard_document,
|
|
11
12
|
convert_document,
|
|
12
13
|
list_documents,
|
|
13
14
|
count_documents,
|
|
@@ -29,6 +30,7 @@ def list_docs(
|
|
|
29
30
|
from_date: str | None = None,
|
|
30
31
|
to_date: str | None = None,
|
|
31
32
|
docstatus: int | None = None,
|
|
33
|
+
include_discarded: bool = False,
|
|
32
34
|
limit: int = Query(default=50, le=500),
|
|
33
35
|
offset: int = Query(default=0, ge=0),
|
|
34
36
|
_user: dict = _viewer,
|
|
@@ -44,8 +46,9 @@ def list_docs(
|
|
|
44
46
|
filters["from_date"] = from_date
|
|
45
47
|
if to_date:
|
|
46
48
|
filters["to_date"] = to_date
|
|
47
|
-
rows = list_documents(doctype_slug, filters=filters, limit=limit, offset=offset
|
|
48
|
-
|
|
49
|
+
rows = list_documents(doctype_slug, filters=filters, limit=limit, offset=offset,
|
|
50
|
+
include_discarded=include_discarded)
|
|
51
|
+
total = count_documents(doctype_slug, filters=filters, include_discarded=include_discarded)
|
|
49
52
|
return {"rows": rows, "total": total, "limit": limit, "offset": offset}
|
|
50
53
|
|
|
51
54
|
|
|
@@ -92,6 +95,13 @@ def cancel_doc(doctype_slug: str, name: str, _user: dict = _manager):
|
|
|
92
95
|
return cancel_document(doctype_slug, name)
|
|
93
96
|
|
|
94
97
|
|
|
98
|
+
@router.post("/{doctype_slug}/{name}/discard")
|
|
99
|
+
def discard_doc(doctype_slug: str, name: str, _user: dict = _manager):
|
|
100
|
+
"""Void an unwanted draft (soft delete — kept for the audit trail, hidden
|
|
101
|
+
from default lists). Only valid on drafts; submitted docs must be cancelled."""
|
|
102
|
+
return discard_document(doctype_slug, name)
|
|
103
|
+
|
|
104
|
+
|
|
95
105
|
@router.post("/{doctype_slug}/{name}/convert")
|
|
96
106
|
def convert_doc(doctype_slug: str, name: str, data: dict, _user: dict = _manager):
|
|
97
107
|
target = data.get("target_doctype")
|
|
@@ -155,6 +155,16 @@ def cancel_document(doctype_slug: str, name: str) -> dict:
|
|
|
155
155
|
return doc.as_dict()
|
|
156
156
|
|
|
157
157
|
|
|
158
|
+
def discard_document(doctype_slug: str, name: str) -> dict:
|
|
159
|
+
"""Void an unwanted draft (soft delete — keeps the row, hides it from lists)."""
|
|
160
|
+
doctype, cls = get_document_class(doctype_slug)
|
|
161
|
+
if not cls:
|
|
162
|
+
raise ValueError(f"Unknown document type: {doctype_slug}")
|
|
163
|
+
doc = cls.load(name)
|
|
164
|
+
doc.discard()
|
|
165
|
+
return doc.as_dict()
|
|
166
|
+
|
|
167
|
+
|
|
158
168
|
def register_converter(source_doctype: str, target_doctype: str, fn) -> None:
|
|
159
169
|
"""Register (or override) the converter for a (source, target) pair.
|
|
160
170
|
|
|
@@ -206,7 +216,17 @@ DATE_FIELDS = {
|
|
|
206
216
|
}
|
|
207
217
|
|
|
208
218
|
|
|
209
|
-
def
|
|
219
|
+
def _exclude_discarded(db, doctype: str, db_filters: dict, include_discarded: bool) -> None:
|
|
220
|
+
"""Hide voided drafts (discarded=1) unless explicitly requested. Guarded on
|
|
221
|
+
the column existing so non-submittable doctypes are unaffected."""
|
|
222
|
+
if include_discarded:
|
|
223
|
+
return
|
|
224
|
+
if "discarded" in db._get_table_columns(doctype) and "discarded" not in db_filters:
|
|
225
|
+
db_filters["discarded"] = 0
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def list_documents(doctype_slug: str, filters: dict = None, limit: int = 50, offset: int = 0,
|
|
229
|
+
include_discarded: bool = False) -> list:
|
|
210
230
|
doctype = SLUG_TO_DOCTYPE.get(doctype_slug)
|
|
211
231
|
if not doctype:
|
|
212
232
|
raise ValueError(f"Unknown document type: {doctype_slug}")
|
|
@@ -226,6 +246,8 @@ def list_documents(doctype_slug: str, filters: dict = None, limit: int = 50, off
|
|
|
226
246
|
else:
|
|
227
247
|
db_filters[key] = value
|
|
228
248
|
|
|
249
|
+
_exclude_discarded(db, doctype, db_filters, include_discarded)
|
|
250
|
+
|
|
229
251
|
# Date range filtering via the doctype's primary date field
|
|
230
252
|
date_field = DATE_FIELDS.get(doctype)
|
|
231
253
|
if date_field:
|
|
@@ -261,7 +283,7 @@ def list_documents(doctype_slug: str, filters: dict = None, limit: int = 50, off
|
|
|
261
283
|
return _attach_children(db, doctype_slug, rows)
|
|
262
284
|
|
|
263
285
|
|
|
264
|
-
def count_documents(doctype_slug: str, filters: dict = None) -> int:
|
|
286
|
+
def count_documents(doctype_slug: str, filters: dict = None, include_discarded: bool = False) -> int:
|
|
265
287
|
"""Count documents matching the filters (ignores limit/offset)."""
|
|
266
288
|
doctype = SLUG_TO_DOCTYPE.get(doctype_slug)
|
|
267
289
|
if not doctype:
|
|
@@ -282,6 +304,8 @@ def count_documents(doctype_slug: str, filters: dict = None) -> int:
|
|
|
282
304
|
else:
|
|
283
305
|
db_filters[key] = value
|
|
284
306
|
|
|
307
|
+
_exclude_discarded(db, doctype, db_filters, include_discarded)
|
|
308
|
+
|
|
285
309
|
date_field = DATE_FIELDS.get(doctype)
|
|
286
310
|
where_parts = []
|
|
287
311
|
params = []
|
|
@@ -130,6 +130,9 @@ export const api = {
|
|
|
130
130
|
cancelDocument: (doctype: string, name: string) =>
|
|
131
131
|
request<any>(`/documents/${doctype}/${encodeURIComponent(name)}/cancel`, { method: "POST" }),
|
|
132
132
|
|
|
133
|
+
discardDocument: (doctype: string, name: string) =>
|
|
134
|
+
request<any>(`/documents/${doctype}/${encodeURIComponent(name)}/discard`, { method: "POST" }),
|
|
135
|
+
|
|
133
136
|
convertDocument: (doctype: string, name: string, targetDoctype: string) =>
|
|
134
137
|
request<any>(`/documents/${doctype}/${encodeURIComponent(name)}/convert`, {
|
|
135
138
|
method: "POST",
|
|
@@ -492,6 +492,7 @@ class Database:
|
|
|
492
492
|
remarks TEXT,
|
|
493
493
|
status TEXT DEFAULT 'Draft',
|
|
494
494
|
docstatus INTEGER DEFAULT 0,
|
|
495
|
+
discarded INTEGER DEFAULT 0,
|
|
495
496
|
creation TEXT,
|
|
496
497
|
modified TEXT
|
|
497
498
|
)""",
|
|
@@ -545,6 +546,7 @@ class Database:
|
|
|
545
546
|
remarks TEXT,
|
|
546
547
|
status TEXT DEFAULT 'Draft',
|
|
547
548
|
docstatus INTEGER DEFAULT 0,
|
|
549
|
+
discarded INTEGER DEFAULT 0,
|
|
548
550
|
creation TEXT,
|
|
549
551
|
modified TEXT
|
|
550
552
|
)""",
|
|
@@ -601,6 +603,7 @@ class Database:
|
|
|
601
603
|
remarks TEXT,
|
|
602
604
|
status TEXT DEFAULT 'Draft',
|
|
603
605
|
docstatus INTEGER DEFAULT 0,
|
|
606
|
+
discarded INTEGER DEFAULT 0,
|
|
604
607
|
creation TEXT,
|
|
605
608
|
modified TEXT
|
|
606
609
|
)""",
|
|
@@ -664,6 +667,7 @@ class Database:
|
|
|
664
667
|
per_billed REAL DEFAULT 0,
|
|
665
668
|
status TEXT DEFAULT 'Draft',
|
|
666
669
|
docstatus INTEGER DEFAULT 0,
|
|
670
|
+
discarded INTEGER DEFAULT 0,
|
|
667
671
|
remarks TEXT,
|
|
668
672
|
creation TEXT,
|
|
669
673
|
modified TEXT
|
|
@@ -727,6 +731,7 @@ class Database:
|
|
|
727
731
|
purchase_order TEXT,
|
|
728
732
|
status TEXT DEFAULT 'Draft',
|
|
729
733
|
docstatus INTEGER DEFAULT 0,
|
|
734
|
+
discarded INTEGER DEFAULT 0,
|
|
730
735
|
remarks TEXT,
|
|
731
736
|
creation TEXT,
|
|
732
737
|
modified TEXT
|
|
@@ -780,6 +785,7 @@ class Database:
|
|
|
780
785
|
cost_center TEXT,
|
|
781
786
|
status TEXT DEFAULT 'Draft',
|
|
782
787
|
docstatus INTEGER DEFAULT 0,
|
|
788
|
+
discarded INTEGER DEFAULT 0,
|
|
783
789
|
remarks TEXT,
|
|
784
790
|
creation TEXT,
|
|
785
791
|
modified TEXT
|
|
@@ -808,6 +814,7 @@ class Database:
|
|
|
808
814
|
remark TEXT,
|
|
809
815
|
status TEXT DEFAULT 'Draft',
|
|
810
816
|
docstatus INTEGER DEFAULT 0,
|
|
817
|
+
discarded INTEGER DEFAULT 0,
|
|
811
818
|
creation TEXT,
|
|
812
819
|
modified TEXT
|
|
813
820
|
)""",
|
|
@@ -844,6 +851,7 @@ class Database:
|
|
|
844
851
|
total_amount REAL DEFAULT 0,
|
|
845
852
|
status TEXT DEFAULT 'Draft',
|
|
846
853
|
docstatus INTEGER DEFAULT 0,
|
|
854
|
+
discarded INTEGER DEFAULT 0,
|
|
847
855
|
remarks TEXT,
|
|
848
856
|
creation TEXT,
|
|
849
857
|
modified TEXT
|
|
@@ -906,6 +914,7 @@ class Database:
|
|
|
906
914
|
return_against TEXT,
|
|
907
915
|
status TEXT DEFAULT 'Draft',
|
|
908
916
|
docstatus INTEGER DEFAULT 0,
|
|
917
|
+
discarded INTEGER DEFAULT 0,
|
|
909
918
|
remarks TEXT,
|
|
910
919
|
creation TEXT,
|
|
911
920
|
modified TEXT
|
|
@@ -958,6 +967,7 @@ class Database:
|
|
|
958
967
|
return_against TEXT,
|
|
959
968
|
status TEXT DEFAULT 'Draft',
|
|
960
969
|
docstatus INTEGER DEFAULT 0,
|
|
970
|
+
discarded INTEGER DEFAULT 0,
|
|
961
971
|
remarks TEXT,
|
|
962
972
|
creation TEXT,
|
|
963
973
|
modified TEXT
|
|
@@ -1015,6 +1025,7 @@ class Database:
|
|
|
1015
1025
|
return_against TEXT,
|
|
1016
1026
|
status TEXT DEFAULT 'Draft',
|
|
1017
1027
|
docstatus INTEGER DEFAULT 0,
|
|
1028
|
+
discarded INTEGER DEFAULT 0,
|
|
1018
1029
|
remarks TEXT,
|
|
1019
1030
|
creation TEXT,
|
|
1020
1031
|
modified TEXT
|
|
@@ -1713,6 +1724,25 @@ def _m014_company_iban(db: "Database") -> None:
|
|
|
1713
1724
|
db._add_column_if_missing("Company", "iban", "TEXT")
|
|
1714
1725
|
|
|
1715
1726
|
|
|
1727
|
+
# Submittable transactional doctypes — those with a Draft/Submitted/Cancelled
|
|
1728
|
+
# lifecycle. `discarded` is the soft-delete flag for voided drafts.
|
|
1729
|
+
_SUBMITTABLE_DOCTYPES = (
|
|
1730
|
+
"Quotation", "Sales Order", "Sales Invoice", "Delivery Note",
|
|
1731
|
+
"Purchase Order", "Purchase Invoice", "Purchase Receipt",
|
|
1732
|
+
"Payment Entry", "Journal Entry", "Stock Entry", "POS Invoice",
|
|
1733
|
+
)
|
|
1734
|
+
|
|
1735
|
+
|
|
1736
|
+
def _m015_document_discarded(db: "Database") -> None:
|
|
1737
|
+
"""Add the `discarded` soft-delete flag to every submittable doctype.
|
|
1738
|
+
|
|
1739
|
+
Voiding an unwanted DRAFT sets discarded=1 (status 'Discarded') instead of
|
|
1740
|
+
hard-deleting it — the row is kept for the audit trail and hidden from
|
|
1741
|
+
default lists. Cancel (for submitted docs) is unaffected."""
|
|
1742
|
+
for table in _SUBMITTABLE_DOCTYPES:
|
|
1743
|
+
db._add_column_if_missing(table, "discarded", "INTEGER DEFAULT 0")
|
|
1744
|
+
|
|
1745
|
+
|
|
1716
1746
|
Database.MIGRATIONS = [
|
|
1717
1747
|
(1, "chat_message_session_id", _m001_chat_message_session_id),
|
|
1718
1748
|
(2, "chat_session_user_id", _m002_chat_session_user_id),
|
|
@@ -1728,6 +1758,7 @@ Database.MIGRATIONS = [
|
|
|
1728
1758
|
(12, "exchange_gain_loss_account", _m012_exchange_gain_loss_account),
|
|
1729
1759
|
(13, "unrealized_exchange_account", _m013_unrealized_exchange_account),
|
|
1730
1760
|
(14, "company_iban", _m014_company_iban),
|
|
1761
|
+
(15, "document_discarded", _m015_document_discarded),
|
|
1731
1762
|
]
|
|
1732
1763
|
|
|
1733
1764
|
|
|
@@ -309,6 +309,13 @@ class Document:
|
|
|
309
309
|
raise DocumentStatusError(
|
|
310
310
|
f"Cannot submit {self.DOCTYPE} {self.name}: docstatus is {self.docstatus}"
|
|
311
311
|
)
|
|
312
|
+
# A discarded draft is voided — refuse to submit it, otherwise it would
|
|
313
|
+
# post to the ledger while staying hidden from lists ("posted but
|
|
314
|
+
# invisible"). Discard is terminal.
|
|
315
|
+
if self._data.get("discarded"):
|
|
316
|
+
raise DocumentStatusError(
|
|
317
|
+
f"Cannot submit {self.DOCTYPE} {self.name}: it was discarded."
|
|
318
|
+
)
|
|
312
319
|
|
|
313
320
|
db = get_db()
|
|
314
321
|
db._in_transaction = True
|
|
@@ -368,6 +375,28 @@ class Document:
|
|
|
368
375
|
run_hooks(f"{self.DOCTYPE}:after_cancel", self)
|
|
369
376
|
return self
|
|
370
377
|
|
|
378
|
+
def discard(self):
|
|
379
|
+
"""Void an unwanted DRAFT (docstatus 0) — a soft delete that keeps the row.
|
|
380
|
+
|
|
381
|
+
Unlike cancel (which reverses a submission's GL/stock postings), a draft
|
|
382
|
+
has never posted anything, so there is nothing to reverse. Discarding
|
|
383
|
+
just flags the document `discarded` and sets status 'Discarded' so it
|
|
384
|
+
drops out of default lists, while the record itself is preserved for the
|
|
385
|
+
audit trail (no hard delete — see docs/agents/invariants.md). Only valid
|
|
386
|
+
on drafts; a submitted document must be cancelled instead.
|
|
387
|
+
"""
|
|
388
|
+
if self.docstatus != DRAFT:
|
|
389
|
+
raise DocumentStatusError(
|
|
390
|
+
f"Cannot discard {self.DOCTYPE} {self.name}: only drafts can be "
|
|
391
|
+
f"discarded (docstatus is {self.docstatus}). A submitted document "
|
|
392
|
+
f"must be cancelled."
|
|
393
|
+
)
|
|
394
|
+
self._data["discarded"] = 1
|
|
395
|
+
self._data["status"] = "Discarded"
|
|
396
|
+
self._data["modified"] = now()
|
|
397
|
+
self._persist()
|
|
398
|
+
return self
|
|
399
|
+
|
|
371
400
|
def _persist(self, commit=True):
|
|
372
401
|
"""Save document and child tables to database."""
|
|
373
402
|
db = get_db()
|
|
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
|