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
File without changes
@@ -0,0 +1,254 @@
1
+ """
2
+ Delivery Note.
3
+
4
+ Delivery Note records goods shipped to a customer from a Sales Order:
5
+ Sales Order -> Delivery Note -> Sales Invoice
6
+
7
+ Key behaviors:
8
+ - Creates Stock Ledger Entries (stock OUT from warehouse)
9
+ - Creates GL entries (Dr: COGS, Cr: Stock In Hand)
10
+ - Updates delivered_qty on the linked Sales Order
11
+ """
12
+
13
+ from lambda_erp.model import Document
14
+ from lambda_erp.utils import _dict, flt, nowdate
15
+ from lambda_erp.database import get_db
16
+ from lambda_erp.controllers.taxes_and_totals import calculate_taxes_and_totals
17
+ from lambda_erp.controllers.defaults import set_default_currency
18
+ from lambda_erp.exceptions import ValidationError
19
+ from lambda_erp.stock.stock_ledger import (
20
+ make_sl_entries,
21
+ build_sell_side_sles,
22
+ build_cost_basis_gl,
23
+ reverse_stock_sles,
24
+ )
25
+ from lambda_erp.accounting.general_ledger import make_gl_entries, make_reverse_gl_entries
26
+
27
+ class DeliveryNote(Document):
28
+ DOCTYPE = "Delivery Note"
29
+ CHILD_TABLES = {
30
+ "items": ("Delivery Note Item", None),
31
+ "taxes": ("Sales Taxes and Charges", None),
32
+ }
33
+ PREFIX = "DN"
34
+
35
+ LINK_FIELDS = {
36
+ "customer": "Customer",
37
+ "company": "Company",
38
+ }
39
+ CHILD_LINK_FIELDS = {
40
+ "items": {
41
+ "item_code": "Item",
42
+ "warehouse": "Warehouse",
43
+ },
44
+ "taxes": {
45
+ "account_head": "Account",
46
+ "cost_center": "Cost Center",
47
+ },
48
+ }
49
+
50
+ def validate(self):
51
+ if not self.customer:
52
+ raise ValidationError("Customer is required")
53
+ if not self.get("items"):
54
+ raise ValidationError("At least one item is required")
55
+ if not self.posting_date:
56
+ self.posting_date = nowdate()
57
+
58
+ self._set_customer_name()
59
+ self._set_item_defaults()
60
+
61
+ if self.is_return:
62
+ self._validate_return()
63
+
64
+ set_default_currency(self, "Customer", "customer")
65
+ calculate_taxes_and_totals(self)
66
+ self._set_status()
67
+
68
+ def _set_customer_name(self):
69
+ if not self.customer_name and self.customer:
70
+ db = get_db()
71
+ self.customer_name = db.get_value("Customer", self.customer, "customer_name")
72
+
73
+ def _set_item_defaults(self):
74
+ db = get_db()
75
+ for item in self.get("items"):
76
+ if item.get("item_code") and not item.get("item_name"):
77
+ item_data = db.get_value(
78
+ "Item", item["item_code"],
79
+ ["item_name", "description", "stock_uom", "standard_rate"]
80
+ )
81
+ if item_data:
82
+ item["item_name"] = item_data.item_name
83
+ item["description"] = item.get("description") or item_data.description
84
+ item["uom"] = item.get("uom") or item_data.stock_uom
85
+ if not item.get("rate"):
86
+ item["rate"] = flt(item_data.standard_rate)
87
+
88
+ def _validate_return(self):
89
+ """Validate return-specific rules."""
90
+ if not self.return_against:
91
+ raise ValidationError("Return Against is required for a return Delivery Note")
92
+
93
+ db = get_db()
94
+ original = db.get_value(self.DOCTYPE, self.return_against, ["name", "docstatus"])
95
+ if not original:
96
+ raise ValidationError(f"Original Delivery Note {self.return_against} not found")
97
+ if original.docstatus != 1:
98
+ raise ValidationError(f"Original Delivery Note {self.return_against} must be submitted")
99
+
100
+ original_doc = DeliveryNote.load(self.return_against)
101
+ original_items = {item["item_code"]: flt(item["qty"]) for item in original_doc.get("items")}
102
+ for item in self.get("items"):
103
+ orig_qty = original_items.get(item.get("item_code"), 0)
104
+ return_qty = abs(flt(item.get("qty")))
105
+ if return_qty > orig_qty:
106
+ raise ValidationError(
107
+ f"Return qty ({return_qty}) for {item.get('item_code')} exceeds "
108
+ f"original qty ({orig_qty})"
109
+ )
110
+
111
+ def _set_status(self):
112
+ if self.docstatus == 0:
113
+ self._data["status"] = "Draft"
114
+ elif self.docstatus == 2:
115
+ self._data["status"] = "Cancelled"
116
+ elif self.docstatus == 1:
117
+ if flt(self.per_billed) >= 100:
118
+ self._data["status"] = "Completed"
119
+ else:
120
+ self._data["status"] = "To Bill"
121
+
122
+ def on_submit(self):
123
+ sl_entries = self._get_sl_entries()
124
+ make_sl_entries(sl_entries)
125
+
126
+ gl_entries = self._get_gl_entries()
127
+ if gl_entries:
128
+ make_gl_entries(gl_entries)
129
+
130
+ self._update_sales_order_delivered()
131
+
132
+ def on_cancel(self):
133
+ reversed_sles = reverse_stock_sles(self._get_sl_entries())
134
+ if reversed_sles:
135
+ make_sl_entries(reversed_sles, allow_negative_stock=True)
136
+
137
+ make_reverse_gl_entries(
138
+ voucher_type=self.DOCTYPE,
139
+ voucher_no=self.name,
140
+ )
141
+
142
+ self._update_sales_order_delivered(cancel=True)
143
+
144
+ def _get_sl_entries(self):
145
+ return build_sell_side_sles(self, self.get("items"))
146
+
147
+ def _get_gl_entries(self):
148
+ return build_cost_basis_gl(self, remarks=f"Delivery Note {self.name}")
149
+
150
+ def _update_sales_order_delivered(self, cancel=False):
151
+ db = get_db()
152
+ updated_sos = set()
153
+ so_details = set()
154
+ for item in self.get("items"):
155
+ if item.get("against_sales_order"):
156
+ updated_sos.add(item["against_sales_order"])
157
+ if item.get("so_detail"):
158
+ so_details.add(item["so_detail"])
159
+
160
+ for so_detail in so_details:
161
+ result = db.sql(
162
+ """SELECT COALESCE(SUM(qty), 0) as total_delivered
163
+ FROM "Delivery Note Item"
164
+ WHERE so_detail = ?
165
+ AND parent IN (
166
+ SELECT name FROM "Delivery Note" WHERE docstatus = 1
167
+ )""",
168
+ [so_detail],
169
+ )
170
+ delivered = flt(result[0]["total_delivered"]) if result else 0
171
+ db.set_value("Sales Order Item", so_detail, "delivered_qty", delivered)
172
+
173
+ for so_name in updated_sos:
174
+ from lambda_erp.selling.sales_order import SalesOrder
175
+ so = SalesOrder.load(so_name)
176
+ so.update_delivery_status()
177
+
178
+ def make_delivery_note(sales_order_name):
179
+ """Convert a Sales Order into a Delivery Note."""
180
+ from lambda_erp.selling.sales_order import SalesOrder
181
+
182
+ so = SalesOrder.load(sales_order_name)
183
+ if so.docstatus != 1:
184
+ raise ValidationError("Sales Order must be submitted before creating Delivery Note")
185
+
186
+ dn = DeliveryNote(
187
+ customer=so.customer,
188
+ customer_name=so.customer_name,
189
+ company=so.company,
190
+ currency=so.currency,
191
+ conversion_rate=so.conversion_rate,
192
+ posting_date=nowdate(),
193
+ )
194
+
195
+ for item in so.get("items"):
196
+ undelivered = flt(item.get("qty")) - flt(item.get("delivered_qty"))
197
+ if undelivered <= 0:
198
+ continue
199
+ dn.append("items", _dict(
200
+ item_code=item.get("item_code"),
201
+ item_name=item.get("item_name"),
202
+ description=item.get("description"),
203
+ qty=undelivered,
204
+ uom=item.get("uom"),
205
+ rate=item.get("rate"),
206
+ warehouse=item.get("warehouse"),
207
+ against_sales_order=so.name,
208
+ so_detail=item.get("name"),
209
+ ))
210
+
211
+ for tax in so.get("taxes") or []:
212
+ dn.append("taxes", _dict(
213
+ charge_type=tax.get("charge_type"),
214
+ account_head=tax.get("account_head"),
215
+ description=tax.get("description"),
216
+ rate=tax.get("rate"),
217
+ tax_amount=0,
218
+ ))
219
+
220
+ return dn
221
+
222
+ def make_delivery_return(dn_name):
223
+ """Create a return Delivery Note (stock back in) from an existing Delivery Note."""
224
+ original = DeliveryNote.load(dn_name)
225
+
226
+ if original.docstatus != 1:
227
+ raise ValidationError("Delivery Note must be submitted before creating a return")
228
+ if original.is_return:
229
+ raise ValidationError("Cannot create a return against a return")
230
+
231
+ return_dn = DeliveryNote(
232
+ customer=original.customer,
233
+ company=original.company,
234
+ currency=original.get("currency") or "USD",
235
+ conversion_rate=original.get("conversion_rate") or 1.0,
236
+ posting_date=nowdate(),
237
+ is_return=1,
238
+ return_against=original.name,
239
+ )
240
+
241
+ for item in original.get("items"):
242
+ return_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=-flt(item.get("qty")),
247
+ uom=item.get("uom"),
248
+ rate=flt(item.get("rate")),
249
+ warehouse=item.get("warehouse"),
250
+ against_sales_order=item.get("against_sales_order"),
251
+ so_detail=item.get("so_detail"),
252
+ ))
253
+
254
+ return return_dn
@@ -0,0 +1,356 @@
1
+ """
2
+ Purchase Receipt.
3
+
4
+ Purchase Receipt records goods received from a supplier against a Purchase Order:
5
+ Purchase Order -> Purchase Receipt -> Purchase Invoice
6
+
7
+ Key behaviors:
8
+ - Creates Stock Ledger Entries (stock IN to warehouse)
9
+ - Creates GL entries (Dr: Stock In Hand, Cr: Stock Received But Not Billed)
10
+ - Updates received_qty on the linked Purchase Order
11
+ """
12
+
13
+ from lambda_erp.model import Document
14
+ from lambda_erp.utils import _dict, flt, nowdate
15
+ from lambda_erp.database import get_db
16
+ from lambda_erp.controllers.taxes_and_totals import calculate_taxes_and_totals
17
+ from lambda_erp.controllers.defaults import set_default_currency
18
+ from lambda_erp.exceptions import ValidationError
19
+ from lambda_erp.stock.stock_ledger import make_sl_entries
20
+ from lambda_erp.accounting.general_ledger import make_gl_entries, make_reverse_gl_entries, to_base_currency
21
+
22
+ class PurchaseReceipt(Document):
23
+ DOCTYPE = "Purchase Receipt"
24
+ CHILD_TABLES = {
25
+ "items": ("Purchase Receipt Item", None),
26
+ "taxes": ("Sales Taxes and Charges", None),
27
+ }
28
+ PREFIX = "PREC"
29
+
30
+ LINK_FIELDS = {
31
+ "supplier": "Supplier",
32
+ "company": "Company",
33
+ }
34
+ CHILD_LINK_FIELDS = {
35
+ "items": {
36
+ "item_code": "Item",
37
+ "warehouse": "Warehouse",
38
+ },
39
+ "taxes": {
40
+ "account_head": "Account",
41
+ "cost_center": "Cost Center",
42
+ },
43
+ }
44
+
45
+ def validate(self):
46
+ if not self.supplier:
47
+ raise ValidationError("Supplier is required")
48
+ if not self.get("items"):
49
+ raise ValidationError("At least one item is required")
50
+ if not self.posting_date:
51
+ self.posting_date = nowdate()
52
+
53
+ self._set_supplier_name()
54
+ self._set_item_defaults()
55
+
56
+ if self.is_return:
57
+ self._validate_return()
58
+
59
+ set_default_currency(self, "Supplier", "supplier")
60
+ calculate_taxes_and_totals(self)
61
+ self._set_status()
62
+
63
+ def _set_supplier_name(self):
64
+ if not self.supplier_name and self.supplier:
65
+ db = get_db()
66
+ self.supplier_name = db.get_value("Supplier", self.supplier, "supplier_name")
67
+
68
+ def _set_item_defaults(self):
69
+ db = get_db()
70
+ for item in self.get("items"):
71
+ if item.get("item_code") and not item.get("item_name"):
72
+ item_data = db.get_value(
73
+ "Item", item["item_code"],
74
+ ["item_name", "description", "stock_uom", "standard_rate"]
75
+ )
76
+ if item_data:
77
+ item["item_name"] = item_data.item_name
78
+ item["description"] = item.get("description") or item_data.description
79
+ item["uom"] = item.get("uom") or item_data.stock_uom
80
+ if not item.get("rate"):
81
+ item["rate"] = flt(item_data.standard_rate)
82
+
83
+ def _validate_return(self):
84
+ """Validate return-specific rules."""
85
+ if not self.return_against:
86
+ raise ValidationError("Return Against is required for a return Purchase Receipt")
87
+
88
+ db = get_db()
89
+ original = db.get_value(self.DOCTYPE, self.return_against, ["name", "docstatus"])
90
+ if not original:
91
+ raise ValidationError(f"Original Purchase Receipt {self.return_against} not found")
92
+ if original.docstatus != 1:
93
+ raise ValidationError(f"Original Purchase Receipt {self.return_against} must be submitted")
94
+
95
+ original_doc = PurchaseReceipt.load(self.return_against)
96
+ original_items = {item["item_code"]: flt(item["qty"]) for item in original_doc.get("items")}
97
+ for item in self.get("items"):
98
+ orig_qty = original_items.get(item.get("item_code"), 0)
99
+ return_qty = abs(flt(item.get("qty")))
100
+ if return_qty > orig_qty:
101
+ raise ValidationError(
102
+ f"Return qty ({return_qty}) for {item.get('item_code')} exceeds "
103
+ f"original qty ({orig_qty})"
104
+ )
105
+
106
+ def _set_status(self):
107
+ if self.docstatus == 0:
108
+ self._data["status"] = "Draft"
109
+ elif self.docstatus == 2:
110
+ self._data["status"] = "Cancelled"
111
+ elif self.docstatus == 1:
112
+ if flt(self.per_billed) >= 100:
113
+ self._data["status"] = "Completed"
114
+ else:
115
+ self._data["status"] = "To Bill"
116
+
117
+ def on_submit(self):
118
+ sl_entries = self._get_sl_entries()
119
+ make_sl_entries(sl_entries)
120
+
121
+ gl_entries = self._get_gl_entries()
122
+ if gl_entries:
123
+ # Dr Stock-In-Hand / Cr SRBNB are built in document currency; convert
124
+ # to base. _get_sl_entries already values the stock in base, so the
125
+ # SIH GL matches the stock-ledger value.
126
+ to_base_currency(gl_entries, self.get("conversion_rate"))
127
+ make_gl_entries(gl_entries)
128
+
129
+ self._update_purchase_order_received()
130
+
131
+ def on_cancel(self):
132
+ # Must be the first thing — if we've already touched the ledgers,
133
+ # raising here leaves them half-reversed until the outer transaction
134
+ # rolls back. Cheap check first, expensive work after.
135
+ self._check_no_linked_purchase_invoice()
136
+
137
+ sl_entries = self._get_sl_entries()
138
+ for sle in sl_entries:
139
+ sle["actual_qty"] = -flt(sle["actual_qty"])
140
+ incoming = sle.get("incoming_rate", 0)
141
+ outgoing = sle.get("outgoing_rate", 0)
142
+ sle["incoming_rate"] = outgoing
143
+ sle["outgoing_rate"] = incoming
144
+ make_sl_entries(sl_entries, allow_negative_stock=True)
145
+
146
+ make_reverse_gl_entries(
147
+ voucher_type=self.DOCTYPE,
148
+ voucher_no=self.name,
149
+ )
150
+
151
+ self._update_purchase_order_received(cancel=True)
152
+
153
+ def _check_no_linked_purchase_invoice(self):
154
+ """Block cancel if a submitted Purchase Invoice for the same PO has
155
+ already cleared this receipt's SRBNB entry. Otherwise the reversal
156
+ here (Cr SIH / Dr SRBNB) runs against a SRBNB account the PI already
157
+ zeroed, producing an orphan SRBNB debit with no real payable to match.
158
+ User's recourse: cancel the PI first, then retry the PR cancel.
159
+ """
160
+ db = get_db()
161
+ pos = {
162
+ item.get("against_purchase_order")
163
+ for item in self.get("items")
164
+ if item.get("against_purchase_order")
165
+ }
166
+ if not pos:
167
+ return
168
+
169
+ placeholders = ",".join("?" * len(pos))
170
+ rows = db.sql(
171
+ f'SELECT DISTINCT pi_parent.name AS pi_name '
172
+ f'FROM "Purchase Invoice Item" pii '
173
+ f'JOIN "Purchase Invoice" pi_parent ON pi_parent.name = pii.parent '
174
+ f'WHERE pi_parent.docstatus = 1 '
175
+ f' AND pii.purchase_order IN ({placeholders})',
176
+ list(pos),
177
+ )
178
+ if rows:
179
+ raise ValidationError(
180
+ f"Cannot cancel {self.name}: Purchase Invoice {rows[0]['pi_name']} "
181
+ f"is already submitted against the same Purchase Order. Cancel "
182
+ f"the Purchase Invoice first so SRBNB returns to its pre-bill state."
183
+ )
184
+
185
+ def _get_sl_entries(self):
186
+ # Inventory is valued in the company's base currency; the line rate is
187
+ # in document currency, so scale it by conversion_rate.
188
+ conversion_rate = flt(self.get("conversion_rate")) or 1.0
189
+ sl_entries = []
190
+ for item in self.get("items"):
191
+ warehouse = item.get("warehouse")
192
+ if not warehouse:
193
+ continue
194
+ actual_qty = flt(item["qty"]) # Normal: positive (in). Return: negative (out).
195
+ rate = flt(item.get("rate", 0)) * conversion_rate
196
+ sl_entries.append(_dict(
197
+ item_code=item["item_code"],
198
+ warehouse=warehouse,
199
+ actual_qty=actual_qty,
200
+ incoming_rate=rate if actual_qty > 0 else 0,
201
+ outgoing_rate=rate if actual_qty < 0 else 0,
202
+ voucher_type=self.DOCTYPE,
203
+ voucher_no=self.name,
204
+ voucher_detail_no=item.get("name"),
205
+ posting_date=self.posting_date,
206
+ company=self.company,
207
+ ))
208
+ return sl_entries
209
+
210
+ def _get_gl_entries(self):
211
+ db = get_db()
212
+ gl_entries = []
213
+
214
+ stock_account = db.get_value("Company", self.company, "stock_in_hand_account")
215
+ srbnb_account = db.get_value("Company", self.company, "stock_received_but_not_billed")
216
+
217
+ if not stock_account or not srbnb_account:
218
+ return []
219
+
220
+ total = sum(flt(item.get("amount", 0)) for item in self.get("items"))
221
+ if not total:
222
+ return []
223
+
224
+ # Dr: Stock In Hand
225
+ gl_entries.append(_dict(
226
+ account=stock_account,
227
+ debit=flt(total, 2),
228
+ debit_in_account_currency=flt(total, 2),
229
+ credit=0,
230
+ credit_in_account_currency=0,
231
+ voucher_type=self.DOCTYPE,
232
+ voucher_no=self.name,
233
+ posting_date=self.posting_date,
234
+ company=self.company,
235
+ ))
236
+
237
+ # Cr: Stock Received But Not Billed
238
+ gl_entries.append(_dict(
239
+ account=srbnb_account,
240
+ debit=0,
241
+ debit_in_account_currency=0,
242
+ credit=flt(total, 2),
243
+ credit_in_account_currency=flt(total, 2),
244
+ voucher_type=self.DOCTYPE,
245
+ voucher_no=self.name,
246
+ posting_date=self.posting_date,
247
+ company=self.company,
248
+ ))
249
+
250
+ return gl_entries
251
+
252
+ def _update_purchase_order_received(self, cancel=False):
253
+ db = get_db()
254
+ updated_pos = set()
255
+ po_details = set()
256
+ for item in self.get("items"):
257
+ if item.get("against_purchase_order"):
258
+ updated_pos.add(item["against_purchase_order"])
259
+ if item.get("po_detail"):
260
+ po_details.add(item["po_detail"])
261
+
262
+ for po_detail in po_details:
263
+ result = db.sql(
264
+ """SELECT COALESCE(SUM(qty), 0) as total_received
265
+ FROM "Purchase Receipt Item"
266
+ WHERE po_detail = ?
267
+ AND parent IN (
268
+ SELECT name FROM "Purchase Receipt" WHERE docstatus = 1
269
+ )""",
270
+ [po_detail],
271
+ )
272
+ received = flt(result[0]["total_received"]) if result else 0
273
+ db.set_value("Purchase Order Item", po_detail, "received_qty", received)
274
+
275
+ for po_name in updated_pos:
276
+ from lambda_erp.buying.purchase_order import PurchaseOrder
277
+ po = PurchaseOrder.load(po_name)
278
+ po.update_receipt_status()
279
+
280
+ def make_purchase_receipt(purchase_order_name):
281
+ """Convert a Purchase Order into a Purchase Receipt."""
282
+ from lambda_erp.buying.purchase_order import PurchaseOrder
283
+
284
+ po = PurchaseOrder.load(purchase_order_name)
285
+ if po.docstatus != 1:
286
+ raise ValidationError("Purchase Order must be submitted before creating Purchase Receipt")
287
+
288
+ pr = PurchaseReceipt(
289
+ supplier=po.supplier,
290
+ supplier_name=po.supplier_name,
291
+ company=po.company,
292
+ currency=po.currency,
293
+ conversion_rate=po.conversion_rate,
294
+ posting_date=nowdate(),
295
+ )
296
+
297
+ for item in po.get("items"):
298
+ unreceived = flt(item.get("qty")) - flt(item.get("received_qty"))
299
+ if unreceived <= 0:
300
+ continue
301
+ pr.append("items", _dict(
302
+ item_code=item.get("item_code"),
303
+ item_name=item.get("item_name"),
304
+ description=item.get("description"),
305
+ qty=unreceived,
306
+ uom=item.get("uom"),
307
+ rate=item.get("rate"),
308
+ warehouse=item.get("warehouse"),
309
+ against_purchase_order=po.name,
310
+ po_detail=item.get("name"),
311
+ ))
312
+
313
+ for tax in po.get("taxes") or []:
314
+ pr.append("taxes", _dict(
315
+ charge_type=tax.get("charge_type"),
316
+ account_head=tax.get("account_head"),
317
+ description=tax.get("description"),
318
+ rate=tax.get("rate"),
319
+ tax_amount=0,
320
+ ))
321
+
322
+ return pr
323
+
324
+ def make_purchase_receipt_return(prec_name):
325
+ """Create a return Purchase Receipt (stock back out) from an existing Purchase Receipt."""
326
+ original = PurchaseReceipt.load(prec_name)
327
+
328
+ if original.docstatus != 1:
329
+ raise ValidationError("Purchase Receipt must be submitted before creating a return")
330
+ if original.is_return:
331
+ raise ValidationError("Cannot create a return against a return")
332
+
333
+ return_pr = PurchaseReceipt(
334
+ supplier=original.supplier,
335
+ company=original.company,
336
+ currency=original.get("currency") or "USD",
337
+ conversion_rate=original.get("conversion_rate") or 1.0,
338
+ posting_date=nowdate(),
339
+ is_return=1,
340
+ return_against=original.name,
341
+ )
342
+
343
+ for item in original.get("items"):
344
+ return_pr.append("items", _dict(
345
+ item_code=item.get("item_code"),
346
+ item_name=item.get("item_name"),
347
+ description=item.get("description"),
348
+ qty=-flt(item.get("qty")),
349
+ uom=item.get("uom"),
350
+ rate=flt(item.get("rate")),
351
+ warehouse=item.get("warehouse"),
352
+ against_purchase_order=item.get("against_purchase_order"),
353
+ po_detail=item.get("po_detail"),
354
+ ))
355
+
356
+ return return_pr