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,342 @@
1
+ """
2
+ POS Invoice.
3
+
4
+ POS Invoice is a Sales Invoice with immediate payment. It combines:
5
+ - Sales Invoice GL entries (Dr: Receivable, Cr: Income)
6
+ - Payment GL entries (Dr: Cash/Bank, Cr: Receivable)
7
+ - Optional stock update (Dr: COGS, Cr: Stock In Hand)
8
+
9
+ The receivable is created and immediately settled in one transaction.
10
+ """
11
+
12
+ from lambda_erp.model import Document
13
+ from lambda_erp.utils import _dict, flt, nowdate
14
+ from lambda_erp.database import get_db
15
+ from lambda_erp.controllers.taxes_and_totals import calculate_taxes_and_totals
16
+ from lambda_erp.controllers.defaults import set_default_currency
17
+ from lambda_erp.exceptions import ValidationError
18
+ from lambda_erp.stock.stock_ledger import (
19
+ make_sl_entries,
20
+ build_sell_side_sles,
21
+ build_cost_basis_gl,
22
+ reverse_stock_sles,
23
+ )
24
+ from lambda_erp.accounting.general_ledger import make_gl_entries, make_reverse_gl_entries, to_base_currency
25
+
26
+ class POSInvoice(Document):
27
+ DOCTYPE = "POS Invoice"
28
+ CHILD_TABLES = {
29
+ "items": ("POS Invoice Item", None),
30
+ "taxes": ("Sales Taxes and Charges", None),
31
+ "payments": ("POS Invoice Payment", None),
32
+ }
33
+ PREFIX = "POS"
34
+
35
+ LINK_FIELDS = {
36
+ "customer": "Customer",
37
+ "company": "Company",
38
+ "debit_to": "Account",
39
+ }
40
+ CHILD_LINK_FIELDS = {
41
+ "items": {
42
+ "item_code": "Item",
43
+ "warehouse": "Warehouse",
44
+ "income_account": "Account",
45
+ "cost_center": "Cost Center",
46
+ },
47
+ "taxes": {
48
+ "account_head": "Account",
49
+ "cost_center": "Cost Center",
50
+ },
51
+ }
52
+ ACCOUNT_TYPE_CONSTRAINTS = {
53
+ "debit_to": {"account_type": "Receivable"},
54
+ }
55
+ CHILD_ACCOUNT_TYPE_CONSTRAINTS = {
56
+ "items": {"income_account": {"root_type": "Income"}},
57
+ }
58
+
59
+ def validate(self):
60
+ if not self.customer:
61
+ raise ValidationError("Customer is required")
62
+ if not self.get("items"):
63
+ raise ValidationError("At least one item is required")
64
+ if not self.posting_date:
65
+ self.posting_date = nowdate()
66
+
67
+ self._set_customer_name()
68
+ self._set_missing_accounts()
69
+ self._set_item_defaults()
70
+ if self.is_return:
71
+ self._validate_return()
72
+ set_default_currency(self, "Customer", "customer")
73
+ calculate_taxes_and_totals(self)
74
+ self._calculate_payments()
75
+ self._set_status()
76
+
77
+ def _validate_return(self):
78
+ if not self.return_against:
79
+ raise ValidationError("return_against is required for a POS return")
80
+ db = get_db()
81
+ original = db.get_value(self.DOCTYPE, self.return_against, ["name", "docstatus"])
82
+ if not original:
83
+ raise ValidationError(f"Original POS Invoice {self.return_against} not found")
84
+ if original.docstatus != 1:
85
+ raise ValidationError(f"Original POS Invoice {self.return_against} must be submitted")
86
+
87
+ original_doc = POSInvoice.load(self.return_against)
88
+ original_items = {
89
+ item["item_code"]: flt(item["qty"]) for item in original_doc.get("items")
90
+ }
91
+ for item in self.get("items"):
92
+ orig_qty = original_items.get(item.get("item_code"), 0)
93
+ return_qty = abs(flt(item.get("qty")))
94
+ if return_qty > orig_qty:
95
+ raise ValidationError(
96
+ f"Return qty ({return_qty}) for {item.get('item_code')} exceeds "
97
+ f"original qty ({orig_qty})"
98
+ )
99
+
100
+ def _set_customer_name(self):
101
+ if not self.customer_name and self.customer:
102
+ db = get_db()
103
+ self.customer_name = db.get_value("Customer", self.customer, "customer_name")
104
+
105
+ def _set_missing_accounts(self):
106
+ db = get_db()
107
+ if self.company:
108
+ if not self.debit_to:
109
+ self._data["debit_to"] = db.get_value(
110
+ "Company", self.company, "default_receivable_account"
111
+ )
112
+ default_income = db.get_value("Company", self.company, "default_income_account")
113
+ default_cc = db.get_value("Company", self.company, "default_cost_center")
114
+ for item in self.get("items"):
115
+ if not item.get("income_account"):
116
+ item["income_account"] = default_income
117
+ if not item.get("cost_center"):
118
+ item["cost_center"] = default_cc
119
+
120
+ def _set_item_defaults(self):
121
+ db = get_db()
122
+ for item in self.get("items"):
123
+ if item.get("item_code") and not item.get("item_name"):
124
+ item_data = db.get_value(
125
+ "Item", item["item_code"],
126
+ ["item_name", "description", "stock_uom", "standard_rate"]
127
+ )
128
+ if item_data:
129
+ item["item_name"] = item_data.item_name
130
+ item["uom"] = item.get("uom") or item_data.stock_uom
131
+ if not item.get("rate"):
132
+ item["rate"] = flt(item_data.standard_rate)
133
+
134
+ def _calculate_payments(self):
135
+ paid = sum(flt(p.get("amount", 0)) for p in self.get("payments") or [])
136
+ self._data["paid_amount"] = flt(paid, 2)
137
+ grand = flt(self.grand_total, 2)
138
+ self._data["change_amount"] = flt(max(0, paid - grand), 2)
139
+ self._data["outstanding_amount"] = flt(max(0, grand - paid), 2)
140
+
141
+ def _set_status(self):
142
+ if self.docstatus == 0:
143
+ self._data["status"] = "Draft"
144
+ elif self.docstatus == 2:
145
+ self._data["status"] = "Cancelled"
146
+ elif self.docstatus == 1:
147
+ if flt(self.outstanding_amount) <= 0:
148
+ self._data["status"] = "Paid"
149
+ else:
150
+ self._data["status"] = "Unpaid"
151
+
152
+ def on_submit(self):
153
+ # POS sales must be paid at time of submit. Returns are the exception:
154
+ # refund payments are optional — the user can settle the refund later
155
+ # via a Payment Entry or off-ledger.
156
+ if not self.is_return and not self.get("payments"):
157
+ raise ValidationError("At least one payment is required for POS Invoice")
158
+
159
+ gl_entries = self._get_gl_entries()
160
+ # Financial side (AR/income/tax/payments) is in document currency;
161
+ # convert to base before posting. Cost-of-goods entries below are
162
+ # already in base currency.
163
+ to_base_currency(gl_entries, self.get("conversion_rate"))
164
+
165
+ # When update_stock=1, post SLEs first so stock_value_difference is
166
+ # available on the persisted rows, then append the matching stock-side
167
+ # GL entries (Dr COGS / Cr Stock In Hand at cost). Without this, stock
168
+ # leaves physically but the balance sheet never reflects it.
169
+ if flt(self._data.get("update_stock")):
170
+ sl_entries = self._get_sl_entries()
171
+ if sl_entries:
172
+ make_sl_entries(sl_entries)
173
+ gl_entries.extend(build_cost_basis_gl(self, remarks=f"POS sale via {self.name}"))
174
+
175
+ if gl_entries:
176
+ make_gl_entries(gl_entries)
177
+
178
+ def on_cancel(self):
179
+ if flt(self._data.get("update_stock")):
180
+ reversed_sles = reverse_stock_sles(self._get_sl_entries())
181
+ if reversed_sles:
182
+ make_sl_entries(reversed_sles, allow_negative_stock=True)
183
+
184
+ make_reverse_gl_entries(
185
+ voucher_type=self.DOCTYPE,
186
+ voucher_no=self.name,
187
+ )
188
+
189
+ def _get_gl_entries(self):
190
+ db = get_db()
191
+ gl_entries = []
192
+
193
+ grand_total = flt(self.grand_total, 2)
194
+ if not grand_total:
195
+ return []
196
+
197
+ # 1. Dr: Accounts Receivable
198
+ gl_entries.append(_dict(
199
+ account=self.debit_to,
200
+ party_type="Customer",
201
+ party=self.customer,
202
+ debit=grand_total,
203
+ debit_in_account_currency=grand_total,
204
+ credit=0,
205
+ credit_in_account_currency=0,
206
+ voucher_type=self.DOCTYPE,
207
+ voucher_no=self.name,
208
+ posting_date=self.posting_date,
209
+ company=self.company,
210
+ ))
211
+
212
+ # 2. Cr: Income accounts
213
+ income_accounts = {}
214
+ for item in self.get("items"):
215
+ account = item.get("income_account")
216
+ if not account:
217
+ continue
218
+ income_accounts[account] = income_accounts.get(account, 0) + flt(item.get("net_amount", 0))
219
+
220
+ for account, amount in income_accounts.items():
221
+ gl_entries.append(_dict(
222
+ account=account,
223
+ credit=flt(amount, 2),
224
+ credit_in_account_currency=flt(amount, 2),
225
+ debit=0,
226
+ debit_in_account_currency=0,
227
+ cost_center=db.get_value("Company", self.company, "default_cost_center"),
228
+ voucher_type=self.DOCTYPE,
229
+ voucher_no=self.name,
230
+ posting_date=self.posting_date,
231
+ company=self.company,
232
+ ))
233
+
234
+ # 3. Cr: Tax accounts
235
+ for tax in self.get("taxes") or []:
236
+ if flt(tax.get("tax_amount")):
237
+ gl_entries.append(_dict(
238
+ account=tax.get("account_head"),
239
+ credit=flt(tax["tax_amount"], 2),
240
+ credit_in_account_currency=flt(tax["tax_amount"], 2),
241
+ debit=0,
242
+ debit_in_account_currency=0,
243
+ voucher_type=self.DOCTYPE,
244
+ voucher_no=self.name,
245
+ posting_date=self.posting_date,
246
+ company=self.company,
247
+ ))
248
+
249
+ # 4. Payment entries — Dr: Cash/Bank, Cr: Receivable
250
+ for payment in self.get("payments") or []:
251
+ pay_amount = flt(payment.get("amount", 0), 2)
252
+ if not pay_amount:
253
+ continue
254
+ pay_account = payment.get("account")
255
+ if not pay_account:
256
+ pay_account = db.get_value("Company", self.company, "default_bank_account")
257
+ if not pay_account:
258
+ continue
259
+
260
+ # Dr: Cash/Bank
261
+ gl_entries.append(_dict(
262
+ account=pay_account,
263
+ debit=pay_amount,
264
+ debit_in_account_currency=pay_amount,
265
+ credit=0,
266
+ credit_in_account_currency=0,
267
+ voucher_type=self.DOCTYPE,
268
+ voucher_no=self.name,
269
+ posting_date=self.posting_date,
270
+ company=self.company,
271
+ ))
272
+
273
+ # Cr: Receivable (settles the debit from step 1)
274
+ gl_entries.append(_dict(
275
+ account=self.debit_to,
276
+ party_type="Customer",
277
+ party=self.customer,
278
+ debit=0,
279
+ debit_in_account_currency=0,
280
+ credit=pay_amount,
281
+ credit_in_account_currency=pay_amount,
282
+ voucher_type=self.DOCTYPE,
283
+ voucher_no=self.name,
284
+ posting_date=self.posting_date,
285
+ company=self.company,
286
+ ))
287
+
288
+ return gl_entries
289
+
290
+ def _get_sl_entries(self):
291
+ """SLEs for direct-ship POS sales. See stock_ledger.build_sell_side_sles
292
+ for semantics (rates passed as 0 so moving-average cost is used)."""
293
+ return build_sell_side_sles(self, self.get("items"))
294
+
295
+ def make_pos_return(posi_name):
296
+ """Create a return POS Invoice from an existing one. Carries update_stock
297
+ so the return also reverses inventory when the original was direct-ship.
298
+ Refund payments are optional — a POS return with no payment row leaves
299
+ the refund as an AR balance the original customer can draw from, or the
300
+ user can post a Payment Entry separately."""
301
+ original = POSInvoice.load(posi_name)
302
+
303
+ if original.docstatus != 1:
304
+ raise ValidationError("POS Invoice must be submitted before creating a return")
305
+ if original.is_return:
306
+ raise ValidationError("Cannot create a return against a return")
307
+
308
+ return_pos = POSInvoice(
309
+ customer=original.customer,
310
+ company=original.company,
311
+ currency=original.get("currency") or "USD",
312
+ conversion_rate=original.get("conversion_rate") or 1.0,
313
+ posting_date=nowdate(),
314
+ debit_to=original.debit_to,
315
+ is_return=1,
316
+ return_against=original.name,
317
+ update_stock=flt(original.get("update_stock")) or 0,
318
+ )
319
+
320
+ for item in original.get("items"):
321
+ return_pos.append("items", _dict(
322
+ item_code=item.get("item_code"),
323
+ item_name=item.get("item_name"),
324
+ description=item.get("description"),
325
+ qty=-flt(item.get("qty")),
326
+ uom=item.get("uom"),
327
+ rate=flt(item.get("rate")),
328
+ income_account=item.get("income_account"),
329
+ cost_center=item.get("cost_center"),
330
+ warehouse=item.get("warehouse"),
331
+ ))
332
+
333
+ for tax in original.get("taxes") or []:
334
+ return_pos.append("taxes", _dict(
335
+ charge_type=tax.get("charge_type"),
336
+ account_head=tax.get("account_head"),
337
+ description=tax.get("description"),
338
+ rate=flt(tax.get("rate")),
339
+ tax_amount=-flt(tax.get("tax_amount")),
340
+ ))
341
+
342
+ return return_pos