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,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