lambda-erp 0.1.0__py3-none-any.whl

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 (60) hide show
  1. api/__init__.py +0 -0
  2. api/attachments.py +229 -0
  3. api/auth.py +511 -0
  4. api/bootstrap.py +498 -0
  5. api/chat.py +2764 -0
  6. api/demo_limits.py +400 -0
  7. api/deps.py +7 -0
  8. api/errors.py +56 -0
  9. api/main.py +182 -0
  10. api/pdf.py +151 -0
  11. api/providers.py +116 -0
  12. api/routers/__init__.py +0 -0
  13. api/routers/accounting.py +63 -0
  14. api/routers/admin.py +122 -0
  15. api/routers/analytics.py +1009 -0
  16. api/routers/bank_reconciliation.py +31 -0
  17. api/routers/documents.py +100 -0
  18. api/routers/masters.py +396 -0
  19. api/routers/reports.py +735 -0
  20. api/routers/setup.py +387 -0
  21. api/services.py +372 -0
  22. api/templates/document.html +197 -0
  23. lambda_erp/__init__.py +3 -0
  24. lambda_erp/accounting/__init__.py +0 -0
  25. lambda_erp/accounting/bank_transaction.py +76 -0
  26. lambda_erp/accounting/budget.py +117 -0
  27. lambda_erp/accounting/chart_of_accounts.py +183 -0
  28. lambda_erp/accounting/general_ledger.py +362 -0
  29. lambda_erp/accounting/journal_entry.py +235 -0
  30. lambda_erp/accounting/payment_entry.py +515 -0
  31. lambda_erp/accounting/pos_invoice.py +342 -0
  32. lambda_erp/accounting/purchase_invoice.py +504 -0
  33. lambda_erp/accounting/revaluation.py +172 -0
  34. lambda_erp/accounting/sales_invoice.py +523 -0
  35. lambda_erp/accounting/subscription.py +132 -0
  36. lambda_erp/buying/__init__.py +0 -0
  37. lambda_erp/buying/purchase_order.py +165 -0
  38. lambda_erp/controllers/__init__.py +0 -0
  39. lambda_erp/controllers/currency.py +52 -0
  40. lambda_erp/controllers/defaults.py +51 -0
  41. lambda_erp/controllers/pricing_rule.py +103 -0
  42. lambda_erp/controllers/taxes_and_totals.py +369 -0
  43. lambda_erp/database.py +1543 -0
  44. lambda_erp/exceptions.py +37 -0
  45. lambda_erp/hooks.py +37 -0
  46. lambda_erp/model.py +462 -0
  47. lambda_erp/selling/__init__.py +0 -0
  48. lambda_erp/selling/quotation.py +263 -0
  49. lambda_erp/selling/sales_order.py +214 -0
  50. lambda_erp/simulation.py +704 -0
  51. lambda_erp/stock/__init__.py +0 -0
  52. lambda_erp/stock/delivery_note.py +254 -0
  53. lambda_erp/stock/purchase_receipt.py +356 -0
  54. lambda_erp/stock/stock_entry.py +330 -0
  55. lambda_erp/stock/stock_ledger.py +337 -0
  56. lambda_erp/utils.py +167 -0
  57. lambda_erp-0.1.0.dist-info/METADATA +454 -0
  58. lambda_erp-0.1.0.dist-info/RECORD +60 -0
  59. lambda_erp-0.1.0.dist-info/WHEEL +4 -0
  60. lambda_erp-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,235 @@
1
+ """
2
+ Journal Entry.
3
+
4
+ Journal Entry is the manual bookkeeping tool - allows direct debit/credit
5
+ entries to any accounts. Used for:
6
+ - Opening balances
7
+ - Adjustments
8
+ - Expense accruals
9
+ - Inter-company transactions
10
+ - Write-offs
11
+
12
+ GL entries are created directly from the Journal Entry Account rows.
13
+ The key constraint: total debits MUST equal total credits.
14
+ """
15
+
16
+ from lambda_erp.model import Document
17
+ from lambda_erp.utils import _dict, flt, nowdate
18
+ from lambda_erp.database import get_db
19
+ from lambda_erp.accounting.general_ledger import make_gl_entries, make_reverse_gl_entries
20
+ from lambda_erp.exceptions import ValidationError, DebitCreditNotEqual
21
+
22
+ class JournalEntry(Document):
23
+ DOCTYPE = "Journal Entry"
24
+ CHILD_TABLES = {
25
+ "accounts": ("Journal Entry Account", None),
26
+ }
27
+ PREFIX = "JV"
28
+
29
+ LINK_FIELDS = {
30
+ "company": "Company",
31
+ }
32
+ CHILD_LINK_FIELDS = {
33
+ "accounts": {
34
+ "account": "Account",
35
+ "cost_center": "Cost Center",
36
+ },
37
+ }
38
+ CHILD_DYNAMIC_LINK_FIELDS = {
39
+ "accounts": {
40
+ "party": ("party_type", {"Customer": "Customer", "Supplier": "Supplier"}),
41
+ "reference_name": ("reference_doctype", {
42
+ "Sales Invoice": "Sales Invoice",
43
+ "Purchase Invoice": "Purchase Invoice",
44
+ "POS Invoice": "POS Invoice",
45
+ }),
46
+ },
47
+ }
48
+
49
+ _REFERENCE_META = {
50
+ "Sales Invoice": {
51
+ "party_field": "customer",
52
+ "allowed_party_type": "Customer",
53
+ "reduction": lambda row: flt(row.get("credit")) - flt(row.get("debit")),
54
+ },
55
+ "POS Invoice": {
56
+ "party_field": "customer",
57
+ "allowed_party_type": "Customer",
58
+ "reduction": lambda row: flt(row.get("credit")) - flt(row.get("debit")),
59
+ },
60
+ "Purchase Invoice": {
61
+ "party_field": "supplier",
62
+ "allowed_party_type": "Supplier",
63
+ "reduction": lambda row: flt(row.get("debit")) - flt(row.get("credit")),
64
+ },
65
+ }
66
+
67
+ def validate(self):
68
+ if not self.get("accounts"):
69
+ raise ValidationError("At least one account row is required")
70
+ if not self.posting_date:
71
+ self.posting_date = nowdate()
72
+
73
+ self._validate_debit_credit()
74
+ self._validate_references()
75
+ self._set_totals()
76
+
77
+ def _validate_debit_credit(self):
78
+ """Ensure total debits == total credits.
79
+
80
+ This is the fundamental double-entry bookkeeping check.
81
+ """
82
+ total_debit = sum(flt(row.get("debit")) for row in self.get("accounts"))
83
+ total_credit = sum(flt(row.get("credit")) for row in self.get("accounts"))
84
+
85
+ diff = abs(total_debit - total_credit)
86
+ if diff > 0.01:
87
+ raise DebitCreditNotEqual(
88
+ f"Total Debit ({total_debit}) must equal Total Credit ({total_credit}). "
89
+ f"Difference: {diff}"
90
+ )
91
+
92
+ def _set_totals(self):
93
+ self._data["total_debit"] = flt(
94
+ sum(flt(row.get("debit")) for row in self.get("accounts")), 2
95
+ )
96
+ self._data["total_credit"] = flt(
97
+ sum(flt(row.get("credit")) for row in self.get("accounts")), 2
98
+ )
99
+
100
+ def on_submit(self):
101
+ """Post GL entries directly from account rows."""
102
+ gl_entries = self._get_gl_entries()
103
+ make_gl_entries(gl_entries)
104
+ self._update_referenced_outstanding()
105
+
106
+ def on_cancel(self):
107
+ make_reverse_gl_entries(
108
+ voucher_type=self.DOCTYPE,
109
+ voucher_no=self.name,
110
+ )
111
+ self._update_referenced_outstanding(cancel=True)
112
+
113
+ def _validate_references(self):
114
+ """Referenced party-ledger rows must be coherent with the invoice they
115
+ claim to settle. Otherwise the JE can post a valid GL but corrupt the
116
+ subledger by driving the invoice past zero or tagging the wrong party.
117
+ """
118
+ db = get_db()
119
+ for idx, row in enumerate(self.get("accounts") or [], start=1):
120
+ ref_dt = row.get("reference_doctype")
121
+ ref_name = row.get("reference_name")
122
+ if not ref_dt and not ref_name:
123
+ continue
124
+ if not ref_dt or not ref_name:
125
+ raise ValidationError(
126
+ f"Journal Entry: row {idx} must set both reference_doctype and reference_name"
127
+ )
128
+
129
+ meta = self._REFERENCE_META.get(ref_dt)
130
+ if not meta:
131
+ raise ValidationError(
132
+ f"Journal Entry: row {idx} cannot reference unsupported doctype {ref_dt}"
133
+ )
134
+
135
+ invoice = db.get_value(
136
+ ref_dt,
137
+ ref_name,
138
+ [meta["party_field"], "docstatus", "outstanding_amount"],
139
+ )
140
+ if not invoice:
141
+ raise ValidationError(
142
+ f"Journal Entry: row {idx} reference {ref_dt} {ref_name} does not exist"
143
+ )
144
+ if flt(invoice.get("docstatus")) != 1:
145
+ raise ValidationError(
146
+ f"Journal Entry: row {idx} reference {ref_dt} {ref_name} is not submitted"
147
+ )
148
+
149
+ if row.get("party_type") != meta["allowed_party_type"]:
150
+ raise ValidationError(
151
+ f"Journal Entry: row {idx} reference {ref_dt} requires party_type "
152
+ f"{meta['allowed_party_type']}"
153
+ )
154
+
155
+ expected_party = invoice.get(meta["party_field"])
156
+ if row.get("party") != expected_party:
157
+ raise ValidationError(
158
+ f"Journal Entry: row {idx} reference {ref_dt} {ref_name} belongs to "
159
+ f"{meta['allowed_party_type']} '{expected_party}', not '{row.get('party')}'"
160
+ )
161
+
162
+ reduction = meta["reduction"](row)
163
+ if not reduction:
164
+ continue
165
+
166
+ current = flt(invoice.get("outstanding_amount"))
167
+ if reduction > abs(current) + 0.01:
168
+ raise ValidationError(
169
+ f"Journal Entry: row {idx} reduces {ref_dt} {ref_name} by {reduction}, "
170
+ f"which exceeds its remaining outstanding ({abs(current)})"
171
+ )
172
+
173
+ def _update_referenced_outstanding(self, cancel=False):
174
+ """When a Journal Entry row carries a reference_doctype + reference_name
175
+ (e.g. a write-off against a Sales Invoice), mirror the accounting
176
+ effect onto the invoice's outstanding_amount. Without this, the GL
177
+ is correct but AR/AP aging stays stale — a write-off JE that credits
178
+ AR would zero the account while the invoice still shows outstanding.
179
+ """
180
+ db = get_db()
181
+ for row in self.get("accounts") or []:
182
+ ref_dt = row.get("reference_doctype")
183
+ ref_name = row.get("reference_name")
184
+ if not ref_dt or not ref_name:
185
+ continue
186
+ meta = self._REFERENCE_META.get(ref_dt)
187
+ if not meta:
188
+ continue
189
+
190
+ reduction = meta["reduction"](row)
191
+ if cancel:
192
+ reduction = -reduction
193
+ if not reduction:
194
+ continue
195
+
196
+ current = flt(db.get_value(ref_dt, ref_name, "outstanding_amount"))
197
+ new_outstanding = current - reduction
198
+ if abs(new_outstanding) < 0.01:
199
+ new_outstanding = 0
200
+ db.set_value(ref_dt, ref_name, "outstanding_amount", flt(new_outstanding, 2))
201
+ db.commit()
202
+
203
+ def _get_gl_entries(self):
204
+ """Build GL entries from Journal Entry Account rows.
205
+
206
+ Unlike invoices where GL entries are computed from item/tax totals,
207
+ journal entries map 1:1 from account rows to GL entries.
208
+ """
209
+ gl_entries = []
210
+
211
+ for row in self.get("accounts"):
212
+ if not (flt(row.get("debit")) or flt(row.get("credit"))):
213
+ continue
214
+
215
+ gl_entries.append(
216
+ _dict(
217
+ account=row.get("account"),
218
+ party_type=row.get("party_type"),
219
+ party=row.get("party"),
220
+ cost_center=row.get("cost_center"),
221
+ debit=flt(row.get("debit"), 2),
222
+ credit=flt(row.get("credit"), 2),
223
+ debit_in_account_currency=flt(row.get("debit_in_account_currency") or row.get("debit"), 2),
224
+ credit_in_account_currency=flt(row.get("credit_in_account_currency") or row.get("credit"), 2),
225
+ against_voucher_type=row.get("reference_doctype") or row.get("reference_type"),
226
+ against_voucher=row.get("reference_name"),
227
+ voucher_type=self.DOCTYPE,
228
+ voucher_no=self.name,
229
+ posting_date=self.posting_date,
230
+ company=self.company,
231
+ remarks=self.remark or "Journal Entry",
232
+ )
233
+ )
234
+
235
+ return gl_entries