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.
- api/__init__.py +0 -0
- api/attachments.py +229 -0
- api/auth.py +511 -0
- api/bootstrap.py +498 -0
- api/chat.py +2764 -0
- api/demo_limits.py +400 -0
- api/deps.py +7 -0
- api/errors.py +56 -0
- api/main.py +182 -0
- api/pdf.py +151 -0
- api/providers.py +116 -0
- api/routers/__init__.py +0 -0
- api/routers/accounting.py +63 -0
- api/routers/admin.py +122 -0
- api/routers/analytics.py +1009 -0
- api/routers/bank_reconciliation.py +31 -0
- api/routers/documents.py +100 -0
- api/routers/masters.py +396 -0
- api/routers/reports.py +735 -0
- api/routers/setup.py +387 -0
- api/services.py +372 -0
- api/templates/document.html +197 -0
- lambda_erp/__init__.py +3 -0
- lambda_erp/accounting/__init__.py +0 -0
- lambda_erp/accounting/bank_transaction.py +76 -0
- lambda_erp/accounting/budget.py +117 -0
- lambda_erp/accounting/chart_of_accounts.py +183 -0
- lambda_erp/accounting/general_ledger.py +362 -0
- lambda_erp/accounting/journal_entry.py +235 -0
- lambda_erp/accounting/payment_entry.py +515 -0
- lambda_erp/accounting/pos_invoice.py +342 -0
- lambda_erp/accounting/purchase_invoice.py +504 -0
- lambda_erp/accounting/revaluation.py +172 -0
- lambda_erp/accounting/sales_invoice.py +523 -0
- lambda_erp/accounting/subscription.py +132 -0
- lambda_erp/buying/__init__.py +0 -0
- lambda_erp/buying/purchase_order.py +165 -0
- lambda_erp/controllers/__init__.py +0 -0
- lambda_erp/controllers/currency.py +52 -0
- lambda_erp/controllers/defaults.py +51 -0
- lambda_erp/controllers/pricing_rule.py +103 -0
- lambda_erp/controllers/taxes_and_totals.py +369 -0
- lambda_erp/database.py +1543 -0
- lambda_erp/exceptions.py +37 -0
- lambda_erp/hooks.py +37 -0
- lambda_erp/model.py +462 -0
- lambda_erp/selling/__init__.py +0 -0
- lambda_erp/selling/quotation.py +263 -0
- lambda_erp/selling/sales_order.py +214 -0
- lambda_erp/simulation.py +704 -0
- lambda_erp/stock/__init__.py +0 -0
- lambda_erp/stock/delivery_note.py +254 -0
- lambda_erp/stock/purchase_receipt.py +356 -0
- lambda_erp/stock/stock_entry.py +330 -0
- lambda_erp/stock/stock_ledger.py +337 -0
- lambda_erp/utils.py +167 -0
- lambda_erp-0.1.0.dist-info/METADATA +454 -0
- lambda_erp-0.1.0.dist-info/RECORD +60 -0
- lambda_erp-0.1.0.dist-info/WHEEL +4 -0
- 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
|