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,263 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quotation (Sales Offer / Proposal).
|
|
3
|
+
|
|
4
|
+
A Quotation is the first step in the sales cycle:
|
|
5
|
+
Quotation -> Sales Order -> Delivery Note -> Sales Invoice
|
|
6
|
+
|
|
7
|
+
It represents an offer to a customer with pricing, validity dates, and terms.
|
|
8
|
+
Key behaviors:
|
|
9
|
+
- Does NOT create GL entries (it's just a proposal)
|
|
10
|
+
- Can be converted to a Sales Order via make_sales_order()
|
|
11
|
+
- Tracks status: Draft -> Submitted/Open -> Ordered/Lost/Expired
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from lambda_erp.model import Document
|
|
15
|
+
from lambda_erp.utils import _dict, flt, getdate, nowdate, new_name
|
|
16
|
+
from lambda_erp.database import get_db
|
|
17
|
+
from lambda_erp.controllers.taxes_and_totals import calculate_taxes_and_totals
|
|
18
|
+
from lambda_erp.controllers.defaults import set_default_currency
|
|
19
|
+
from lambda_erp.exceptions import ValidationError
|
|
20
|
+
|
|
21
|
+
class Quotation(Document):
|
|
22
|
+
DOCTYPE = "Quotation"
|
|
23
|
+
CHILD_TABLES = {
|
|
24
|
+
"items": ("Quotation Item", None),
|
|
25
|
+
"taxes": ("Sales Taxes and Charges", None),
|
|
26
|
+
}
|
|
27
|
+
PREFIX = "QTN"
|
|
28
|
+
|
|
29
|
+
LINK_FIELDS = {
|
|
30
|
+
"customer": "Customer",
|
|
31
|
+
"company": "Company",
|
|
32
|
+
}
|
|
33
|
+
CHILD_LINK_FIELDS = {
|
|
34
|
+
"items": {
|
|
35
|
+
"item_code": "Item",
|
|
36
|
+
"warehouse": "Warehouse",
|
|
37
|
+
},
|
|
38
|
+
"taxes": {
|
|
39
|
+
"account_head": "Account",
|
|
40
|
+
"cost_center": "Cost Center",
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
def validate(self):
|
|
45
|
+
"""Validate the quotation before saving.
|
|
46
|
+
|
|
47
|
+
Mirrors the reference implementation's Quotation.validate() which calls:
|
|
48
|
+
- super().validate() (SellingController -> AccountsController -> TransactionBase)
|
|
49
|
+
- validate_uom_is_integer
|
|
50
|
+
- validate_valid_till
|
|
51
|
+
- set_customer_name
|
|
52
|
+
"""
|
|
53
|
+
if not self.customer:
|
|
54
|
+
raise ValidationError("Customer is required")
|
|
55
|
+
if not self.get("items"):
|
|
56
|
+
raise ValidationError("At least one item is required")
|
|
57
|
+
if not self.transaction_date:
|
|
58
|
+
self.transaction_date = nowdate()
|
|
59
|
+
|
|
60
|
+
self._validate_valid_till()
|
|
61
|
+
self._set_customer_name()
|
|
62
|
+
self._set_item_defaults()
|
|
63
|
+
|
|
64
|
+
from lambda_erp.controllers.pricing_rule import apply_pricing_rules
|
|
65
|
+
apply_pricing_rules(self)
|
|
66
|
+
|
|
67
|
+
set_default_currency(self, "Customer", "customer")
|
|
68
|
+
|
|
69
|
+
# Calculate taxes and totals (the core shared calculation)
|
|
70
|
+
calculate_taxes_and_totals(self)
|
|
71
|
+
|
|
72
|
+
def _validate_valid_till(self):
|
|
73
|
+
if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date):
|
|
74
|
+
raise ValidationError("Valid till date cannot be before transaction date")
|
|
75
|
+
|
|
76
|
+
def _set_customer_name(self):
|
|
77
|
+
if not self.customer_name and self.customer:
|
|
78
|
+
db = get_db()
|
|
79
|
+
self.customer_name = db.get_value("Customer", self.customer, "customer_name")
|
|
80
|
+
|
|
81
|
+
def _set_item_defaults(self):
|
|
82
|
+
"""Fill in item names and rates from master data."""
|
|
83
|
+
db = get_db()
|
|
84
|
+
for item in self.get("items"):
|
|
85
|
+
if item.get("item_code") and not item.get("item_name"):
|
|
86
|
+
item_data = db.get_value(
|
|
87
|
+
"Item", item["item_code"], ["item_name", "description", "stock_uom", "standard_rate"]
|
|
88
|
+
)
|
|
89
|
+
if item_data:
|
|
90
|
+
item["item_name"] = item_data.item_name
|
|
91
|
+
item["description"] = item.get("description") or item_data.description
|
|
92
|
+
item["uom"] = item.get("uom") or item_data.stock_uom
|
|
93
|
+
if not item.get("rate") and not item.get("price_list_rate"):
|
|
94
|
+
item["rate"] = flt(item_data.standard_rate)
|
|
95
|
+
|
|
96
|
+
def on_submit(self):
|
|
97
|
+
"""On submit, set status to Open."""
|
|
98
|
+
self._data["status"] = "Open"
|
|
99
|
+
self._persist()
|
|
100
|
+
|
|
101
|
+
def on_cancel(self):
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
def is_expired(self):
|
|
105
|
+
"""Check if quotation validity has expired."""
|
|
106
|
+
if self.valid_till:
|
|
107
|
+
return getdate(self.valid_till) < getdate(nowdate())
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
def make_sales_order(quotation_name):
|
|
111
|
+
"""Convert a Quotation into a Sales Order.
|
|
112
|
+
|
|
113
|
+
This is the standard document flow: Quotation -> Sales Order.
|
|
114
|
+
|
|
115
|
+
the reference implementation uses get_mapped_doc() which copies fields from source to target
|
|
116
|
+
based on a mapping configuration. We do the same thing directly.
|
|
117
|
+
"""
|
|
118
|
+
from lambda_erp.selling.sales_order import SalesOrder
|
|
119
|
+
|
|
120
|
+
db = get_db()
|
|
121
|
+
quotation = Quotation.load(quotation_name)
|
|
122
|
+
|
|
123
|
+
if quotation.docstatus != 1:
|
|
124
|
+
raise ValidationError("Quotation must be submitted before creating Sales Order")
|
|
125
|
+
|
|
126
|
+
if quotation.is_expired():
|
|
127
|
+
raise ValidationError("Validity period of this quotation has ended")
|
|
128
|
+
|
|
129
|
+
# Map Quotation fields to Sales Order
|
|
130
|
+
so = SalesOrder(
|
|
131
|
+
customer=quotation.customer,
|
|
132
|
+
customer_name=quotation.customer_name,
|
|
133
|
+
company=quotation.company,
|
|
134
|
+
currency=quotation.currency,
|
|
135
|
+
conversion_rate=quotation.conversion_rate,
|
|
136
|
+
transaction_date=nowdate(),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Map items
|
|
140
|
+
for item in quotation.get("items"):
|
|
141
|
+
so.append("items", _dict(
|
|
142
|
+
item_code=item.get("item_code"),
|
|
143
|
+
item_name=item.get("item_name"),
|
|
144
|
+
description=item.get("description"),
|
|
145
|
+
qty=item.get("qty"),
|
|
146
|
+
uom=item.get("uom"),
|
|
147
|
+
rate=item.get("rate"),
|
|
148
|
+
price_list_rate=item.get("price_list_rate"),
|
|
149
|
+
discount_percentage=item.get("discount_percentage"),
|
|
150
|
+
warehouse=item.get("warehouse"),
|
|
151
|
+
quotation_item=item.get("name"),
|
|
152
|
+
))
|
|
153
|
+
|
|
154
|
+
# Map taxes
|
|
155
|
+
for tax in quotation.get("taxes") or []:
|
|
156
|
+
so.append("taxes", _dict(
|
|
157
|
+
charge_type=tax.get("charge_type"),
|
|
158
|
+
account_head=tax.get("account_head"),
|
|
159
|
+
description=tax.get("description"),
|
|
160
|
+
rate=tax.get("rate"),
|
|
161
|
+
tax_amount=0, # will be recalculated
|
|
162
|
+
included_in_print_rate=tax.get("included_in_print_rate"),
|
|
163
|
+
))
|
|
164
|
+
|
|
165
|
+
# Update quotation status
|
|
166
|
+
db.set_value("Quotation", quotation_name, "status", "Ordered")
|
|
167
|
+
|
|
168
|
+
return so
|
|
169
|
+
|
|
170
|
+
def make_sales_invoice_from_quotation(quotation_name):
|
|
171
|
+
"""Convert a Quotation directly into a Sales Invoice (skip Sales Order)."""
|
|
172
|
+
from lambda_erp.accounting.sales_invoice import SalesInvoice
|
|
173
|
+
|
|
174
|
+
db = get_db()
|
|
175
|
+
quotation = Quotation.load(quotation_name)
|
|
176
|
+
|
|
177
|
+
if quotation.docstatus != 1:
|
|
178
|
+
raise ValidationError("Quotation must be submitted before creating Sales Invoice")
|
|
179
|
+
|
|
180
|
+
if quotation.is_expired():
|
|
181
|
+
raise ValidationError("Validity period of this quotation has ended")
|
|
182
|
+
|
|
183
|
+
sinv = SalesInvoice(
|
|
184
|
+
customer=quotation.customer,
|
|
185
|
+
customer_name=quotation.customer_name,
|
|
186
|
+
company=quotation.company,
|
|
187
|
+
currency=quotation.currency,
|
|
188
|
+
conversion_rate=quotation.conversion_rate,
|
|
189
|
+
posting_date=nowdate(),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
for item in quotation.get("items"):
|
|
193
|
+
sinv.append("items", _dict(
|
|
194
|
+
item_code=item.get("item_code"),
|
|
195
|
+
item_name=item.get("item_name"),
|
|
196
|
+
description=item.get("description"),
|
|
197
|
+
qty=item.get("qty"),
|
|
198
|
+
uom=item.get("uom"),
|
|
199
|
+
rate=item.get("rate"),
|
|
200
|
+
price_list_rate=item.get("price_list_rate"),
|
|
201
|
+
discount_percentage=item.get("discount_percentage"),
|
|
202
|
+
warehouse=item.get("warehouse"),
|
|
203
|
+
))
|
|
204
|
+
|
|
205
|
+
for tax in quotation.get("taxes") or []:
|
|
206
|
+
sinv.append("taxes", _dict(
|
|
207
|
+
charge_type=tax.get("charge_type"),
|
|
208
|
+
account_head=tax.get("account_head"),
|
|
209
|
+
description=tax.get("description"),
|
|
210
|
+
rate=tax.get("rate"),
|
|
211
|
+
tax_amount=0,
|
|
212
|
+
included_in_print_rate=tax.get("included_in_print_rate"),
|
|
213
|
+
))
|
|
214
|
+
|
|
215
|
+
db.set_value("Quotation", quotation_name, "status", "Ordered")
|
|
216
|
+
|
|
217
|
+
return sinv
|
|
218
|
+
|
|
219
|
+
def make_delivery_note_from_quotation(quotation_name):
|
|
220
|
+
"""Convert a Quotation directly into a Delivery Note (skip Sales Order)."""
|
|
221
|
+
from lambda_erp.stock.delivery_note import DeliveryNote
|
|
222
|
+
|
|
223
|
+
db = get_db()
|
|
224
|
+
quotation = Quotation.load(quotation_name)
|
|
225
|
+
|
|
226
|
+
if quotation.docstatus != 1:
|
|
227
|
+
raise ValidationError("Quotation must be submitted before creating Delivery Note")
|
|
228
|
+
|
|
229
|
+
if quotation.is_expired():
|
|
230
|
+
raise ValidationError("Validity period of this quotation has ended")
|
|
231
|
+
|
|
232
|
+
dn = DeliveryNote(
|
|
233
|
+
customer=quotation.customer,
|
|
234
|
+
customer_name=quotation.customer_name,
|
|
235
|
+
company=quotation.company,
|
|
236
|
+
currency=quotation.currency,
|
|
237
|
+
conversion_rate=quotation.conversion_rate,
|
|
238
|
+
posting_date=nowdate(),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
for item in quotation.get("items"):
|
|
242
|
+
dn.append("items", _dict(
|
|
243
|
+
item_code=item.get("item_code"),
|
|
244
|
+
item_name=item.get("item_name"),
|
|
245
|
+
description=item.get("description"),
|
|
246
|
+
qty=item.get("qty"),
|
|
247
|
+
uom=item.get("uom"),
|
|
248
|
+
rate=item.get("rate"),
|
|
249
|
+
warehouse=item.get("warehouse"),
|
|
250
|
+
))
|
|
251
|
+
|
|
252
|
+
for tax in quotation.get("taxes") or []:
|
|
253
|
+
dn.append("taxes", _dict(
|
|
254
|
+
charge_type=tax.get("charge_type"),
|
|
255
|
+
account_head=tax.get("account_head"),
|
|
256
|
+
description=tax.get("description"),
|
|
257
|
+
rate=tax.get("rate"),
|
|
258
|
+
tax_amount=0,
|
|
259
|
+
))
|
|
260
|
+
|
|
261
|
+
db.set_value("Quotation", quotation_name, "status", "Ordered")
|
|
262
|
+
|
|
263
|
+
return dn
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sales Order.
|
|
3
|
+
|
|
4
|
+
Sales Order sits between Quotation and Invoice in the sales cycle:
|
|
5
|
+
Quotation -> Sales Order -> Delivery Note -> Sales Invoice
|
|
6
|
+
|
|
7
|
+
Key behaviors:
|
|
8
|
+
- Does NOT create GL entries (no financial impact yet)
|
|
9
|
+
- Reserves stock (updates ordered_qty in Bin)
|
|
10
|
+
- Tracks delivery and billing status (per_delivered, per_billed)
|
|
11
|
+
- Can be converted to Sales Invoice via make_sales_invoice()
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from lambda_erp.model import Document
|
|
15
|
+
from lambda_erp.utils import _dict, flt, getdate, nowdate
|
|
16
|
+
from lambda_erp.database import get_db
|
|
17
|
+
from lambda_erp.controllers.taxes_and_totals import calculate_taxes_and_totals
|
|
18
|
+
from lambda_erp.controllers.defaults import set_default_currency
|
|
19
|
+
from lambda_erp.exceptions import ValidationError
|
|
20
|
+
|
|
21
|
+
class SalesOrder(Document):
|
|
22
|
+
DOCTYPE = "Sales Order"
|
|
23
|
+
CHILD_TABLES = {
|
|
24
|
+
"items": ("Sales Order Item", None),
|
|
25
|
+
"taxes": ("Sales Taxes and Charges", None),
|
|
26
|
+
}
|
|
27
|
+
PREFIX = "SO"
|
|
28
|
+
|
|
29
|
+
LINK_FIELDS = {
|
|
30
|
+
"customer": "Customer",
|
|
31
|
+
"company": "Company",
|
|
32
|
+
}
|
|
33
|
+
CHILD_LINK_FIELDS = {
|
|
34
|
+
"items": {
|
|
35
|
+
"item_code": "Item",
|
|
36
|
+
"warehouse": "Warehouse",
|
|
37
|
+
},
|
|
38
|
+
"taxes": {
|
|
39
|
+
"account_head": "Account",
|
|
40
|
+
"cost_center": "Cost Center",
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
def validate(self):
|
|
45
|
+
"""Validate sales order.
|
|
46
|
+
|
|
47
|
+
Mirrors the reference implementation's SalesOrder.validate() which calls the full controller
|
|
48
|
+
chain: SellingController -> StockController -> AccountsController.
|
|
49
|
+
"""
|
|
50
|
+
if not self.customer:
|
|
51
|
+
raise ValidationError("Customer is required")
|
|
52
|
+
if not self.get("items"):
|
|
53
|
+
raise ValidationError("At least one item is required")
|
|
54
|
+
if not self.transaction_date:
|
|
55
|
+
self.transaction_date = nowdate()
|
|
56
|
+
|
|
57
|
+
self._set_customer_name()
|
|
58
|
+
self._set_item_defaults()
|
|
59
|
+
self._validate_delivery_date()
|
|
60
|
+
|
|
61
|
+
from lambda_erp.controllers.pricing_rule import apply_pricing_rules
|
|
62
|
+
apply_pricing_rules(self)
|
|
63
|
+
|
|
64
|
+
set_default_currency(self, "Customer", "customer")
|
|
65
|
+
|
|
66
|
+
# Calculate taxes and totals
|
|
67
|
+
calculate_taxes_and_totals(self)
|
|
68
|
+
|
|
69
|
+
self._set_status()
|
|
70
|
+
|
|
71
|
+
def _set_customer_name(self):
|
|
72
|
+
if not self.customer_name and self.customer:
|
|
73
|
+
db = get_db()
|
|
74
|
+
self.customer_name = db.get_value("Customer", self.customer, "customer_name")
|
|
75
|
+
|
|
76
|
+
def _set_item_defaults(self):
|
|
77
|
+
"""Fill in item names and rates from master data."""
|
|
78
|
+
db = get_db()
|
|
79
|
+
for item in self.get("items"):
|
|
80
|
+
if item.get("item_code") and not item.get("item_name"):
|
|
81
|
+
item_data = db.get_value(
|
|
82
|
+
"Item", item["item_code"],
|
|
83
|
+
["item_name", "description", "stock_uom", "standard_rate"]
|
|
84
|
+
)
|
|
85
|
+
if item_data:
|
|
86
|
+
item["item_name"] = item_data.item_name
|
|
87
|
+
item["description"] = item.get("description") or item_data.description
|
|
88
|
+
item["uom"] = item.get("uom") or item_data.stock_uom
|
|
89
|
+
if not item.get("rate") and not item.get("price_list_rate"):
|
|
90
|
+
item["rate"] = flt(item_data.standard_rate)
|
|
91
|
+
|
|
92
|
+
def _validate_delivery_date(self):
|
|
93
|
+
if self.delivery_date and getdate(self.delivery_date) < getdate(self.transaction_date):
|
|
94
|
+
raise ValidationError("Expected delivery date cannot be before order date")
|
|
95
|
+
|
|
96
|
+
def _set_status(self):
|
|
97
|
+
"""Set status based on delivery and billing."""
|
|
98
|
+
if self.docstatus == 0:
|
|
99
|
+
self._data["status"] = "Draft"
|
|
100
|
+
elif self.docstatus == 2:
|
|
101
|
+
self._data["status"] = "Cancelled"
|
|
102
|
+
elif self.docstatus == 1:
|
|
103
|
+
per_delivered = flt(self.per_delivered)
|
|
104
|
+
per_billed = flt(self.per_billed)
|
|
105
|
+
|
|
106
|
+
if per_delivered >= 100 and per_billed >= 100:
|
|
107
|
+
self._data["status"] = "Completed"
|
|
108
|
+
elif per_delivered > 0 and per_delivered < 100:
|
|
109
|
+
self._data["status"] = "To Deliver"
|
|
110
|
+
elif per_billed > 0 and per_billed < 100:
|
|
111
|
+
self._data["status"] = "To Bill"
|
|
112
|
+
else:
|
|
113
|
+
self._data["status"] = "To Deliver and Bill"
|
|
114
|
+
|
|
115
|
+
def on_submit(self):
|
|
116
|
+
"""On submit, update stock reservations (ordered_qty in Bin).
|
|
117
|
+
|
|
118
|
+
In the reference implementation, submitting a Sales Order updates the Bin.ordered_qty
|
|
119
|
+
so that MRP/stock planning can account for upcoming demand.
|
|
120
|
+
"""
|
|
121
|
+
self._update_reserved_qty(1)
|
|
122
|
+
|
|
123
|
+
def on_cancel(self):
|
|
124
|
+
"""Reverse stock reservations."""
|
|
125
|
+
self._update_reserved_qty(-1)
|
|
126
|
+
|
|
127
|
+
def _update_reserved_qty(self, direction=1):
|
|
128
|
+
"""Update Bin.reserved_qty for each item+warehouse."""
|
|
129
|
+
db = get_db()
|
|
130
|
+
for item in self.get("items"):
|
|
131
|
+
if item.get("warehouse") and item.get("item_code"):
|
|
132
|
+
qty = flt(item.get("qty", 0)) * direction
|
|
133
|
+
bin_data = db.get_value(
|
|
134
|
+
"Bin",
|
|
135
|
+
{"item_code": item["item_code"], "warehouse": item["warehouse"]},
|
|
136
|
+
["name", "reserved_qty"],
|
|
137
|
+
)
|
|
138
|
+
if bin_data:
|
|
139
|
+
new_reserved = flt(bin_data.reserved_qty) + qty
|
|
140
|
+
db.set_value("Bin", bin_data.name, "reserved_qty", max(0, new_reserved))
|
|
141
|
+
db.commit()
|
|
142
|
+
|
|
143
|
+
def update_delivery_status(self):
|
|
144
|
+
"""Update per_delivered based on delivered quantities."""
|
|
145
|
+
total_qty = sum(flt(item.get("qty")) for item in self.get("items"))
|
|
146
|
+
delivered_qty = sum(flt(item.get("delivered_qty")) for item in self.get("items"))
|
|
147
|
+
if total_qty:
|
|
148
|
+
self._data["per_delivered"] = flt(delivered_qty / total_qty * 100, 2)
|
|
149
|
+
self._set_status()
|
|
150
|
+
self._persist()
|
|
151
|
+
|
|
152
|
+
def update_billing_status(self):
|
|
153
|
+
"""Update per_billed based on billed quantities."""
|
|
154
|
+
total_qty = sum(flt(item.get("qty")) for item in self.get("items"))
|
|
155
|
+
billed_qty = sum(flt(item.get("billed_qty")) for item in self.get("items"))
|
|
156
|
+
if total_qty:
|
|
157
|
+
self._data["per_billed"] = flt(billed_qty / total_qty * 100, 2)
|
|
158
|
+
self._set_status()
|
|
159
|
+
self._persist()
|
|
160
|
+
|
|
161
|
+
def make_sales_invoice(sales_order_name):
|
|
162
|
+
"""Convert a Sales Order into a Sales Invoice.
|
|
163
|
+
|
|
164
|
+
This is the standard flow: Sales Order -> Sales Invoice.
|
|
165
|
+
"""
|
|
166
|
+
from lambda_erp.accounting.sales_invoice import SalesInvoice
|
|
167
|
+
|
|
168
|
+
db = get_db()
|
|
169
|
+
so = SalesOrder.load(sales_order_name)
|
|
170
|
+
|
|
171
|
+
if so.docstatus != 1:
|
|
172
|
+
raise ValidationError("Sales Order must be submitted before creating Sales Invoice")
|
|
173
|
+
|
|
174
|
+
si = SalesInvoice(
|
|
175
|
+
customer=so.customer,
|
|
176
|
+
customer_name=so.customer_name,
|
|
177
|
+
company=so.company,
|
|
178
|
+
currency=so.currency,
|
|
179
|
+
conversion_rate=so.conversion_rate,
|
|
180
|
+
posting_date=nowdate(),
|
|
181
|
+
sales_order=so.name,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
for item in so.get("items"):
|
|
185
|
+
unbilled_qty = flt(item.get("qty")) - flt(item.get("billed_qty"))
|
|
186
|
+
if unbilled_qty <= 0:
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
si.append("items", _dict(
|
|
190
|
+
item_code=item.get("item_code"),
|
|
191
|
+
item_name=item.get("item_name"),
|
|
192
|
+
description=item.get("description"),
|
|
193
|
+
qty=unbilled_qty,
|
|
194
|
+
uom=item.get("uom"),
|
|
195
|
+
rate=item.get("rate"),
|
|
196
|
+
price_list_rate=item.get("price_list_rate"),
|
|
197
|
+
discount_percentage=item.get("discount_percentage"),
|
|
198
|
+
warehouse=item.get("warehouse"),
|
|
199
|
+
cost_center=item.get("cost_center"),
|
|
200
|
+
sales_order=so.name,
|
|
201
|
+
sales_order_item=item.get("name"),
|
|
202
|
+
))
|
|
203
|
+
|
|
204
|
+
for tax in so.get("taxes") or []:
|
|
205
|
+
si.append("taxes", _dict(
|
|
206
|
+
charge_type=tax.get("charge_type"),
|
|
207
|
+
account_head=tax.get("account_head"),
|
|
208
|
+
description=tax.get("description"),
|
|
209
|
+
rate=tax.get("rate"),
|
|
210
|
+
tax_amount=0,
|
|
211
|
+
included_in_print_rate=tax.get("included_in_print_rate"),
|
|
212
|
+
))
|
|
213
|
+
|
|
214
|
+
return si
|