From 3ead28906cb849d50b53e2402704121acdfb5b9d Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 22 Aug 2023 14:41:07 +0000 Subject: [PATCH 01/18] feat: item(row) wise tax amount rounding --- .../doctype/accounts_settings/accounts_settings.json | 10 +++++----- .../accounts/doctype/payment_entry/payment_entry.py | 5 +++++ erpnext/controllers/taxes_and_totals.py | 5 +++++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 6857ba343e..570be095ab 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -32,6 +32,7 @@ "column_break_19", "add_taxes_from_item_tax_template", "book_tax_discount_loss", + "round_row_wise_tax", "print_settings", "show_inclusive_tax_in_print", "show_taxes_as_table_in_print", @@ -58,7 +59,6 @@ "closing_settings_tab", "period_closing_settings_section", "acc_frozen_upto", - "ignore_account_closing_balance", "column_break_25", "frozen_accounts_modifier", "tab_break_dpet", @@ -410,10 +410,10 @@ }, { "default": "0", - "description": "Financial reports will be generated using GL Entry doctypes (should be enabled if Period Closing Voucher is not posted for all years sequentially or missing) ", - "fieldname": "ignore_account_closing_balance", + "description": "Tax Amount will be rounded on a row(items) level", + "fieldname": "round_row_wise_tax", "fieldtype": "Check", - "label": "Ignore Account Closing Balance" + "label": "Round Tax Amount Row-wise" } ], "icon": "icon-cog", @@ -421,7 +421,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-07-27 15:05:34.000264", + "modified": "2023-08-22 20:11:00.042840", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index ac31e8a1db..39ee497fda 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1387,6 +1387,9 @@ class PaymentEntry(AccountsController): def calculate_taxes(self): self.total_taxes_and_charges = 0.0 self.base_total_taxes_and_charges = 0.0 + frappe.flags.round_row_wise_tax = ( + frappe.db.get_single_value("Accounts Settings", "round_row_wise_tax") + ) actual_tax_dict = dict( [ @@ -1398,6 +1401,8 @@ class PaymentEntry(AccountsController): for i, tax in enumerate(self.get("taxes")): current_tax_amount = self.get_current_tax_amount(tax) + if frappe.flags.round_row_wise_tax: + current_tax_amount = flt(current_tax_amount, tax.precision("tax_amount")) if tax.charge_type == "Actual": actual_tax_dict[tax.idx] -= current_tax_amount diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 62d4c53868..1cf1788f43 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -25,6 +25,9 @@ class calculate_taxes_and_totals(object): def __init__(self, doc: Document): self.doc = doc frappe.flags.round_off_applicable_accounts = [] + frappe.flags.round_row_wise_tax = ( + frappe.db.get_single_value("Accounts Settings", "round_row_wise_tax") + ) self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items") @@ -368,6 +371,8 @@ class calculate_taxes_and_totals(object): for i, tax in enumerate(self.doc.get("taxes")): # tax_amount represents the amount of tax for the current step current_tax_amount = self.get_current_tax_amount(item, tax, item_tax_map) + if frappe.flags.round_row_wise_tax: + current_tax_amount = flt(current_tax_amount, tax.precision("tax_amount")) # Adjust divisional loss to the last item if tax.charge_type == "Actual": From c20258d2a355388b799b6e9eca710d9254cd0388 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 23 Aug 2023 03:59:08 +0000 Subject: [PATCH 02/18] fix: tax calc changes in js --- .../accounts/doctype/payment_entry/payment_entry.py | 5 ----- erpnext/public/js/controllers/taxes_and_totals.js | 10 +++++++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 39ee497fda..ac31e8a1db 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1387,9 +1387,6 @@ class PaymentEntry(AccountsController): def calculate_taxes(self): self.total_taxes_and_charges = 0.0 self.base_total_taxes_and_charges = 0.0 - frappe.flags.round_row_wise_tax = ( - frappe.db.get_single_value("Accounts Settings", "round_row_wise_tax") - ) actual_tax_dict = dict( [ @@ -1401,8 +1398,6 @@ class PaymentEntry(AccountsController): for i, tax in enumerate(self.get("taxes")): current_tax_amount = self.get_current_tax_amount(tax) - if frappe.flags.round_row_wise_tax: - current_tax_amount = flt(current_tax_amount, tax.precision("tax_amount")) if tax.charge_type == "Actual": actual_tax_dict[tax.idx] -= current_tax_amount diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index eeb09cb8b0..8062ce05cd 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -185,7 +185,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { frappe.flags.round_off_applicable_accounts = []; if (me.frm.doc.company) { - return frappe.call({ + frappe.call({ "method": "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts", "args": { "company": me.frm.doc.company, @@ -198,6 +198,11 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } }); } + + frappe.db.get_single_value("Accounts Settings", "round_row_wise_tax") + .then((round_row_wise_tax) => { + frappe.flags.round_row_wise_tax = round_row_wise_tax; + }) } determine_exclusive_rate() { @@ -338,6 +343,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { $.each(me.frm.doc["taxes"] || [], function(i, tax) { // tax_amount represents the amount of tax for the current step var current_tax_amount = me.get_current_tax_amount(item, tax, item_tax_map); + if (frappe.flags.round_row_wise_tax) { + current_tax_amount = flt(current_tax_amount, precision("tax_amount", tax)); + } // Adjust divisional loss to the last item if (tax.charge_type == "Actual") { From dfb5b88abbd3d00f625f18afeb3e0e57d1fcade6 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 23 Aug 2023 04:01:00 +0000 Subject: [PATCH 03/18] chore: linters --- erpnext/controllers/taxes_and_totals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 1cf1788f43..243b5eb96e 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -25,8 +25,8 @@ class calculate_taxes_and_totals(object): def __init__(self, doc: Document): self.doc = doc frappe.flags.round_off_applicable_accounts = [] - frappe.flags.round_row_wise_tax = ( - frappe.db.get_single_value("Accounts Settings", "round_row_wise_tax") + frappe.flags.round_row_wise_tax = frappe.db.get_single_value( + "Accounts Settings", "round_row_wise_tax" ) self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items") From 0ebcc2cf2c7c3a7c764bdf378822e542ade73253 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 23 Aug 2023 04:51:09 +0000 Subject: [PATCH 04/18] fix: round item_wise_tax_detail in taxes --- erpnext/controllers/taxes_and_totals.py | 9 +++++++-- erpnext/public/js/controllers/taxes_and_totals.js | 11 +++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 243b5eb96e..39d2cf632a 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -483,8 +483,13 @@ class calculate_taxes_and_totals(object): # store tax breakup for each item key = item.item_code or item.item_name item_wise_tax_amount = current_tax_amount * self.doc.conversion_rate - if tax.item_wise_tax_detail.get(key): - item_wise_tax_amount += tax.item_wise_tax_detail[key][1] + if frappe.flags.round_row_wise_tax: + item_wise_tax_amount = flt(item_wise_tax_amount, tax.precision("tax_amount")) + if tax.item_wise_tax_detail.get(key): + item_wise_tax_amount += flt(tax.item_wise_tax_detail[key][1], tax.precision("tax_amount")) + else: + if tax.item_wise_tax_detail.get(key): + item_wise_tax_amount += tax.item_wise_tax_detail[key][1] tax.item_wise_tax_detail[key] = [tax_rate, flt(item_wise_tax_amount)] diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 8062ce05cd..81dcc06471 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -480,8 +480,15 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } let item_wise_tax_amount = current_tax_amount * this.frm.doc.conversion_rate; - if (tax_detail && tax_detail[key]) - item_wise_tax_amount += tax_detail[key][1]; + if (frappe.flags.round_row_wise_tax) { + item_wise_tax_amount = flt(item_wise_tax_amount, precision("tax_amount", tax)); + if (tax_detail && tax_detail[key]) { + item_wise_tax_amount += flt(tax_detail[key][1], precision("tax_amount", tax)); + } + } else { + if (tax_detail && tax_detail[key]) + item_wise_tax_amount += tax_detail[key][1]; + } tax_detail[key] = [tax_rate, flt(item_wise_tax_amount, precision("base_tax_amount", tax))]; } From 9e1b2c9f5799ca5b84bf63f34b64a72d15b99097 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Thu, 24 Aug 2023 05:02:14 +0000 Subject: [PATCH 05/18] fix: item wise split up rounding --- erpnext/controllers/taxes_and_totals.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 39d2cf632a..c1dc316c54 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -487,11 +487,15 @@ class calculate_taxes_and_totals(object): item_wise_tax_amount = flt(item_wise_tax_amount, tax.precision("tax_amount")) if tax.item_wise_tax_detail.get(key): item_wise_tax_amount += flt(tax.item_wise_tax_detail[key][1], tax.precision("tax_amount")) + tax.item_wise_tax_detail[key] = [ + tax_rate, + flt(item_wise_tax_amount, tax.precision("tax_amount")), + ] else: if tax.item_wise_tax_detail.get(key): item_wise_tax_amount += tax.item_wise_tax_detail[key][1] - tax.item_wise_tax_detail[key] = [tax_rate, flt(item_wise_tax_amount)] + tax.item_wise_tax_detail[key] = [tax_rate, flt(item_wise_tax_amount)] def round_off_totals(self, tax): if tax.account_head in frappe.flags.round_off_applicable_accounts: From 89ddf3272e0ce20cb177688d738703e0f0386a1c Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Thu, 24 Aug 2023 05:56:56 +0000 Subject: [PATCH 06/18] fix(regional): item wise tax calc issue --- erpnext/accounts/utils.py | 6 +-- .../regional/united_arab_emirates/utils.py | 38 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 9d6d0f91fb..1aefeaacf7 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -908,9 +908,9 @@ def get_outstanding_invoices( min_outstanding=None, max_outstanding=None, accounting_dimensions=None, - vouchers=None, # list of dicts [{'voucher_type': '', 'voucher_no': ''}] for filtering - limit=None, # passed by reconciliation tool - voucher_no=None, # filter passed by reconciliation tool + vouchers=None, # list of dicts [{'voucher_type': '', 'voucher_no': ''}] for filtering + limit=None, # passed by reconciliation tool + voucher_no=None, # filter passed by reconciliation tool ): ple = qb.DocType("Payment Ledger Entry") diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index a910af6a1d..efeaeed324 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -7,32 +7,32 @@ from erpnext.controllers.taxes_and_totals import get_itemised_tax def update_itemised_tax_data(doc): + # maybe this should be a standard function rather than a regional one if not doc.taxes: return + if not doc.items: + return + + meta = frappe.get_meta(doc.items[0].doctype) + if not meta.has_field("tax_rate"): + return + itemised_tax = get_itemised_tax(doc.taxes) for row in doc.items: - tax_rate = 0.0 - item_tax_rate = 0.0 + tax_rate, tax_amount = 0.0, 0.0 + # dont even bother checking in item tax template as it contains both input and output accounts - double the tax rate + item_code = row.item_code or row.item_name + if itemised_tax.get(item_code): + for tax in itemised_tax.get(row.item_code).values(): + _tax_rate = flt(tax.get("tax_rate", 0), row.precision("tax_rate")) + tax_amount += flt((row.net_amount * _tax_rate) / 100, row.precision("tax_amount")) + tax_rate += _tax_rate - if row.item_tax_rate: - item_tax_rate = frappe.parse_json(row.item_tax_rate) - - # First check if tax rate is present - # If not then look up in item_wise_tax_detail - if item_tax_rate: - for account, rate in item_tax_rate.items(): - tax_rate += rate - elif row.item_code and itemised_tax.get(row.item_code): - tax_rate = sum([tax.get("tax_rate", 0) for d, tax in itemised_tax.get(row.item_code).items()]) - - meta = frappe.get_meta(row.doctype) - - if meta.has_field("tax_rate"): - row.tax_rate = flt(tax_rate, row.precision("tax_rate")) - row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount")) - row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) + row.tax_rate = flt(tax_rate, row.precision("tax_rate")) + row.tax_amount = flt(tax_amount, row.precision("tax_amount")) + row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) def get_account_currency(account): From 159be1d40fbc07b619c27f782d872bf2d1f35a3a Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Sun, 27 Aug 2023 18:43:42 +0000 Subject: [PATCH 07/18] fix: revert `ignore_account_closing_balance` field --- .../doctype/accounts_settings/accounts_settings.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 570be095ab..061bab320e 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -59,6 +59,7 @@ "closing_settings_tab", "period_closing_settings_section", "acc_frozen_upto", + "ignore_account_closing_balance", "column_break_25", "frozen_accounts_modifier", "tab_break_dpet", @@ -408,6 +409,13 @@ "fieldtype": "Check", "label": "Enable Fuzzy Matching" }, + { + "default": "0", + "description": "Financial reports will be generated using GL Entry doctypes (should be enabled if Period Closing Voucher is not posted for all years sequentially or missing) ", + "fieldname": "ignore_account_closing_balance", + "fieldtype": "Check", + "label": "Ignore Account Closing Balance" + }, { "default": "0", "description": "Tax Amount will be rounded on a row(items) level", @@ -421,7 +429,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-08-22 20:11:00.042840", + "modified": "2023-08-28 00:12:02.740633", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", From d3f94a03fc011b8a4ef380497a1e2bc4d54cea57 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Sun, 1 Oct 2023 14:04:34 +0200 Subject: [PATCH 08/18] fix(stock): add delivery user and manager role --- erpnext/setup/doctype/driver/driver.json | 18 +++++++++- erpnext/setup/doctype/vehicle/vehicle.json | 18 +++++++++- .../doctype/delivery_note/delivery_note.json | 30 ++++++++++++++++ .../delivery_settings/delivery_settings.json | 4 +-- .../doctype/delivery_trip/delivery_trip.json | 34 +++++++++++++++++-- 5 files changed, 98 insertions(+), 6 deletions(-) diff --git a/erpnext/setup/doctype/driver/driver.json b/erpnext/setup/doctype/driver/driver.json index 8d426cc29a..2e994b5ff9 100644 --- a/erpnext/setup/doctype/driver/driver.json +++ b/erpnext/setup/doctype/driver/driver.json @@ -157,6 +157,22 @@ "role": "HR Manager", "share": 1, "write": 1 + }, + { + "print": 1, + "read": 1, + "report": 1, + "role": "Delivery User" + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Delivery Manager", + "share": 1, + "write": 1 } ], "quick_entry": 1, @@ -166,4 +182,4 @@ "sort_order": "DESC", "title_field": "full_name", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/setup/doctype/vehicle/vehicle.json b/erpnext/setup/doctype/vehicle/vehicle.json index ed803a763a..b19d45924f 100644 --- a/erpnext/setup/doctype/vehicle/vehicle.json +++ b/erpnext/setup/doctype/vehicle/vehicle.json @@ -860,6 +860,22 @@ "share": 1, "submit": 0, "write": 1 + }, + { + "print": 1, + "read": 1, + "report": 1, + "role": "Delivery User" + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Delivery Manager", + "share": 1, + "write": 1 } ], "quick_entry": 1, @@ -872,4 +888,4 @@ "title_field": "", "track_changes": 1, "track_seen": 0 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index e0d49192eb..b85f296d0b 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -1460,6 +1460,36 @@ "read": 1, "role": "Stock Manager", "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Delivery User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Delivery Manager", + "share": 1, + "submit": 1, + "write": 1 } ], "search_fields": "status,customer,customer_name, territory,base_grand_total", diff --git a/erpnext/stock/doctype/delivery_settings/delivery_settings.json b/erpnext/stock/doctype/delivery_settings/delivery_settings.json index 963403b8f2..ad0ac45851 100644 --- a/erpnext/stock/doctype/delivery_settings/delivery_settings.json +++ b/erpnext/stock/doctype/delivery_settings/delivery_settings.json @@ -239,7 +239,7 @@ "print": 1, "read": 1, "report": 0, - "role": "System Manager", + "role": "Delivery Manager", "set_user_permissions": 0, "share": 1, "submit": 0, @@ -255,4 +255,4 @@ "track_changes": 1, "track_seen": 0, "track_views": 0 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.json b/erpnext/stock/doctype/delivery_trip/delivery_trip.json index 9d8fe46e8c..ec72af8404 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.json +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.json @@ -188,7 +188,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2023-06-27 11:22:27.927637", + "modified": "2023-10-01 07:06:06.314503", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Trip", @@ -224,10 +224,40 @@ "share": 1, "submit": 1, "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Delivery User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Delivery Manager", + "share": 1, + "submit": 1, + "write": 1 } ], "sort_field": "modified", "sort_order": "DESC", "states": [], "title_field": "driver_name" -} \ No newline at end of file +} From b2cee396ac9edea5ba920382bfc27f3736600775 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 14 Oct 2023 20:42:26 +0530 Subject: [PATCH 09/18] fix: consider received qty while creating SO -> MR --- .../doctype/sales_order/sales_order.py | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index aae0fee467..b91002eb86 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -606,29 +606,37 @@ def close_or_unclose_sales_orders(names, status): def get_requested_item_qty(sales_order): - return frappe._dict( - frappe.db.sql( - """ - select sales_order_item, sum(qty) - from `tabMaterial Request Item` - where docstatus = 1 - and sales_order = %s - group by sales_order_item - """, - sales_order, - ) - ) + result = {} + for d in frappe.db.get_all( + "Material Request Item", + filters={"docstatus": 1, "sales_order": sales_order}, + fields=["sales_order_item", "sum(qty) as qty", "sum(received_qty) as received_qty"], + group_by="sales_order_item", + ): + result[d.sales_order_item] = frappe._dict({"qty": d.qty, "received_qty": d.received_qty}) + + return result @frappe.whitelist() def make_material_request(source_name, target_doc=None): requested_item_qty = get_requested_item_qty(source_name) + def get_remaining_qty(so_item): + return flt( + flt(so_item.qty) + - flt(requested_item_qty.get(so_item.name, {}).get("qty")) + - max( + flt(so_item.get("delivered_qty")) + - flt(requested_item_qty.get(so_item.name, {}).get("received_qty")), + 0, + ) + ) + def update_item(source, target, source_parent): # qty is for packed items, because packed items don't have stock_qty field - qty = source.get("qty") target.project = source_parent.project - target.qty = qty - requested_item_qty.get(source.name, 0) - flt(source.get("delivered_qty")) + target.qty = get_remaining_qty(source) target.stock_qty = flt(target.qty) * flt(target.conversion_factor) args = target.as_dict().copy() @@ -661,8 +669,8 @@ def make_material_request(source_name, target_doc=None): "Sales Order Item": { "doctype": "Material Request Item", "field_map": {"name": "sales_order_item", "parent": "sales_order"}, - "condition": lambda doc: not frappe.db.exists("Product Bundle", doc.item_code) - and (doc.stock_qty - flt(doc.get("delivered_qty"))) > requested_item_qty.get(doc.name, 0), + "condition": lambda item: not frappe.db.exists("Product Bundle", item.item_code) + and get_remaining_qty(item) > 0, "postprocess": update_item, }, }, From 46add06a29f8a0a5990dfd3aeda39f01413071bb Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sun, 15 Oct 2023 15:46:29 +0530 Subject: [PATCH 10/18] fix: GL Entries not getting created for PR Return --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 6afa86e34e..de0db1aa8f 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -339,7 +339,7 @@ class PurchaseReceipt(BuyingController): exchange_rate_map, net_rate_map = get_purchase_document_details(self) for d in self.get("items"): - if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty): + if d.item_code in stock_items and flt(d.qty) and (flt(d.valuation_rate) or self.is_return): if warehouse_account.get(d.warehouse): stock_value_diff = frappe.db.get_value( "Stock Ledger Entry", From 253d4782c63963df78216ce51d9f9f9a80791531 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sun, 15 Oct 2023 23:34:58 +0530 Subject: [PATCH 11/18] test: add test case for PR return with zero rate --- .../purchase_receipt/test_purchase_receipt.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index a8ef5e8e48..466e8e7b12 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2086,6 +2086,64 @@ class TestPurchaseReceipt(FrappeTestCase): return_pr.reload() self.assertEqual(return_pr.status, "Completed") + def test_purchase_return_with_zero_rate(self): + company = "_Test Company with perpetual inventory" + + # Step - 1: Create Item + item, warehouse = ( + make_item(properties={"is_stock_item": 1, "valuation_method": "Moving Average"}).name, + "Stores - TCP1", + ) + + # Step - 2: Create Stock Entry (Material Receipt) + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + se = make_stock_entry( + purpose="Material Receipt", + item_code=item, + qty=100, + basic_rate=100, + to_warehouse=warehouse, + company=company, + ) + + # Step - 3: Create Purchase Receipt + pr = make_purchase_receipt( + item_code=item, + qty=5, + rate=0, + warehouse=warehouse, + company=company, + ) + + # Step - 4: Create Purchase Return + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + pr_return = make_return_doc("Purchase Receipt", pr.name) + pr_return.save() + pr_return.submit() + + sl_entries = get_sl_entries(pr_return.doctype, pr_return.name) + gl_entries = get_gl_entries(pr_return.doctype, pr_return.name) + + # Test - 1: SLE Stock Value Difference should be equal to Qty * Average Rate + average_rate = ( + (se.items[0].qty * se.items[0].basic_rate) + (pr.items[0].qty * pr.items[0].rate) + ) / (se.items[0].qty + pr.items[0].qty) + expected_stock_value_difference = pr_return.items[0].qty * average_rate + self.assertEqual( + flt(sl_entries[0].stock_value_difference, 2), flt(expected_stock_value_difference, 2) + ) + + # Test - 2: GL Entries should be created for Stock Value Difference + self.assertEqual(len(gl_entries), 2) + + # Test - 3: SLE Stock Value Difference should be equal to Debit or Credit of GL Entries. + for entry in gl_entries: + self.assertEqual(abs(entry.debit + entry.credit), abs(sl_entries[0].stock_value_difference)) + + self.assertIsNotNone(get_gl_entries(pr_return.doctype, pr_return.name)) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier From 240b161e8161c77ce7e59b50b61d8e7ec9c6cdb7 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 16 Oct 2023 01:08:42 +0530 Subject: [PATCH 12/18] fix: purchase return test case --- erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 466e8e7b12..cdf50532fc 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2142,8 +2142,6 @@ class TestPurchaseReceipt(FrappeTestCase): for entry in gl_entries: self.assertEqual(abs(entry.debit + entry.credit), abs(sl_entries[0].stock_value_difference)) - self.assertIsNotNone(get_gl_entries(pr_return.doctype, pr_return.name)) - def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier From 2790ae07443381ec4b2b23d645bc52e98fdf9ee4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 16 Oct 2023 15:43:14 +0530 Subject: [PATCH 13/18] fix: Update frappe.link_search usage refer https://github.com/frappe/frappe/pull/22745 --- erpnext/public/js/utils/item_selector.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/utils/item_selector.js b/erpnext/public/js/utils/item_selector.js index 9fc264086a..e74d291acd 100644 --- a/erpnext/public/js/utils/item_selector.js +++ b/erpnext/public/js/utils/item_selector.js @@ -97,14 +97,14 @@ erpnext.ItemSelector = class ItemSelector { } var me = this; - frappe.link_search("Item", args, function(r) { - $.each(r.values, function(i, d) { + frappe.link_search("Item", args, function(results) { + $.each(results, function(i, d) { if(!d.image) { d.abbr = frappe.get_abbr(d.item_name); d.color = frappe.get_palette(d.item_name); } }); - me.dialog.results.html(frappe.render_template('item_selector', {'data':r.values})); + me.dialog.results.html(frappe.render_template('item_selector', {'data': results})); }); } }; From 08315522bbc198fce1168a5e8522684cad750276 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 3 Oct 2023 09:53:41 +0530 Subject: [PATCH 14/18] refactor: checkbox to toggle exchange rate inheritence in PO->PI --- .../doctype/purchase_invoice/purchase_invoice.json | 10 +++++++++- .../doctype/buying_settings/buying_settings.json | 10 +++++++++- erpnext/controllers/accounts_controller.py | 11 +++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index e4898826ec..2d1f4451b6 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -36,6 +36,7 @@ "currency_and_price_list", "currency", "conversion_rate", + "use_transaction_date_exchange_rate", "column_break2", "buying_price_list", "price_list_currency", @@ -1588,13 +1589,20 @@ "label": "Repost Required", "options": "Account", "read_only": 1 + }, + { + "default": "0", + "fieldname": "use_transaction_date_exchange_rate", + "fieldtype": "Check", + "label": "Use Transaction Date Exchange Rate", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2023-10-01 21:01:47.282533", + "modified": "2023-10-16 16:24:51.886231", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 8c73e56a99..71cb01b188 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -24,6 +24,7 @@ "bill_for_rejected_quantity_in_purchase_invoice", "disable_last_purchase_rate", "show_pay_button", + "use_transaction_date_exchange_rate", "subcontract", "backflush_raw_materials_of_subcontract_based_on", "column_break_11", @@ -164,6 +165,13 @@ "fieldname": "over_order_allowance", "fieldtype": "Float", "label": "Over Order Allowance (%)" + }, + { + "default": "0", + "description": "While making Purchase Invoice from Purchase Order, use Exchange Rate on Invoice's transaction date rather than inheriting it from Purchase Order. Only applies for Purchase Invoice.", + "fieldname": "use_transaction_date_exchange_rate", + "fieldtype": "Check", + "label": "Use Transaction Date Exchange Rate" } ], "icon": "fa fa-cog", @@ -171,7 +179,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-03-02 17:02:14.404622", + "modified": "2023-10-16 16:22:03.201078", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 6812940ee2..2beee283cb 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -572,6 +572,17 @@ class AccountsController(TransactionBase): self.currency, self.company_currency, transaction_date, args ) + if ( + self.currency + and buying_or_selling == "Buying" + and frappe.db.get_single_value("Buying Settings", "use_transaction_date_exchange_rate") + and self.doctype == "Purchase Invoice" + ): + self.use_transaction_date_exchange_rate = True + self.conversion_rate = get_exchange_rate( + self.currency, self.company_currency, transaction_date, args + ) + def set_missing_item_details(self, for_validate=False): """set missing item values""" from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos From 5b4528e6146aaeb8f86f9fde3f272635d005eeec Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 16 Oct 2023 16:26:03 +0530 Subject: [PATCH 15/18] perf: index `dn_detail` in `Delivery Note Item` --- .../doctype/delivery_note_item/delivery_note_item.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 612d674e01..6148950462 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -725,7 +725,8 @@ "label": "Against Delivery Note Item", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "stock_qty_sec_break", @@ -892,7 +893,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-07-26 12:53:49.357171", + "modified": "2023-10-16 16:18:18.013379", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", @@ -902,4 +903,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} +} \ No newline at end of file From d2096cfdb752bff03f8d3a00262d86c9eeb76c37 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 16 Oct 2023 16:53:43 +0530 Subject: [PATCH 16/18] fix: keep customer/supplier website role by default --- erpnext/setup/install.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 85eaf5fa92..b106cfcc1a 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -33,6 +33,7 @@ def after_install(): add_app_name() setup_log_settings() hide_workspaces() + update_roles() frappe.db.commit() @@ -232,6 +233,12 @@ def hide_workspaces(): frappe.db.set_value("Workspace", ws, "public", 0) +def update_roles(): + website_user_roles = ("Customer", "Supplier") + for role in website_user_roles: + frappe.db.set_value("Role", role, "desk_access", 0) + + def create_default_role_profiles(): for role_profile_name, roles in DEFAULT_ROLE_PROFILES.items(): role_profile = frappe.new_doc("Role Profile") From 27a1e3bf834cb87f322d5943f8ef5405b0c30801 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 19:15:18 +0530 Subject: [PATCH 17/18] feat: validate negative stock for inventory dimension (backport #37373) (#37383) * feat: validate negative stock for inventory dimension (#37373) * feat: validate negative stock for inventory dimension * test: test case for validate negative stock for inv dimension (cherry picked from commit 1480acabb0faeae61c7c055bb7d1e81877b87cfb) # Conflicts: # erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py # erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py # erpnext/stock/stock_ledger.py * chore: fix conflicts * chore: fix conflicts * chore: fix conflicts * chore: fix linter issue * chore: fix linter issue * chore: fix linter issue * chore: fix linter issue * chore: fix linter issue --------- Co-authored-by: rohitwaghchaure --- .../inventory_dimension.js | 2 +- .../inventory_dimension.json | 14 +++- .../inventory_dimension.py | 5 +- .../test_inventory_dimension.py | 67 ++++++++++++++++++ .../stock_ledger_entry/stock_ledger_entry.py | 69 ++++++++++++++++++- .../stock_reconciliation.py | 43 +++++++++++- erpnext/stock/stock_ledger.py | 20 +++++- erpnext/stock/utils.py | 9 ++- 8 files changed, 218 insertions(+), 11 deletions(-) diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js index 0310682a2c..35d1c02719 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js @@ -37,7 +37,7 @@ frappe.ui.form.on('Inventory Dimension', { if (frm.doc.__onload && frm.doc.__onload.has_stock_ledger && frm.doc.__onload.has_stock_ledger.length) { let allow_to_edit_fields = ['disabled', 'fetch_from_parent', - 'type_of_transaction', 'condition', 'mandatory_depends_on']; + 'type_of_transaction', 'condition', 'mandatory_depends_on', 'validate_negative_stock']; frm.fields.forEach((field) => { if (!in_list(allow_to_edit_fields, field.df.fieldname)) { diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json index eb6102a436..0e4055251f 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json @@ -17,6 +17,8 @@ "target_fieldname", "applicable_for_documents_tab", "apply_to_all_doctypes", + "column_break_niy2u", + "validate_negative_stock", "column_break_13", "document_type", "type_of_transaction", @@ -173,11 +175,21 @@ "fieldname": "reqd", "fieldtype": "Check", "label": "Mandatory" + }, + { + "fieldname": "column_break_niy2u", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "validate_negative_stock", + "fieldtype": "Check", + "label": "Validate Negative Stock" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-01-31 13:44:38.507698", + "modified": "2023-10-05 12:52:18.705431", "modified_by": "Administrator", "module": "Stock", "name": "Inventory Dimension", diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py index 8bff4d5147..257d18fc33 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -60,6 +60,7 @@ class InventoryDimension(Document): "fetch_from_parent", "type_of_transaction", "condition", + "validate_negative_stock", ] for field in frappe.get_meta("Inventory Dimension").fields: @@ -160,6 +161,7 @@ class InventoryDimension(Document): insert_after="inventory_dimension", options=self.reference_document, label=label, + search_index=1, reqd=self.reqd, mandatory_depends_on=self.mandatory_depends_on, ), @@ -255,7 +257,7 @@ def field_exists(doctype, fieldname) -> str or None: def get_inventory_documents( doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None ): - and_filters = [["DocField", "parent", "not in", ["Batch", "Serial No"]]] + and_filters = [["DocField", "parent", "not in", ["Batch", "Serial No", "Item Price"]]] or_filters = [ ["DocField", "options", "in", ["Batch", "Serial No"]], ["DocField", "parent", "in", ["Putaway Rule"]], @@ -340,6 +342,7 @@ def get_inventory_dimensions(): fields=[ "distinct target_fieldname as fieldname", "reference_document as doctype", + "validate_negative_stock", ], filters={"disabled": 0}, ) diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index 2d273c66fa..33394e5a11 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -414,6 +414,53 @@ class TestInventoryDimension(FrappeTestCase): else: self.assertEqual(d.store, "Inter Transfer Store 2") + def test_validate_negative_stock_for_inventory_dimension(self): + frappe.local.inventory_dimensions = {} + item_code = "Test Negative Inventory Dimension Item" + frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1) + create_item(item_code) + + inv_dimension = create_inventory_dimension( + apply_to_all_doctypes=1, + dimension_name="Inv Site", + reference_document="Inv Site", + document_type="Inv Site", + validate_negative_stock=1, + ) + + warehouse = create_warehouse("Negative Stock Warehouse") + doc = make_stock_entry(item_code=item_code, target=warehouse, qty=10, do_not_submit=True) + + doc.items[0].to_inv_site = "Site 1" + doc.submit() + + site_name = frappe.get_all( + "Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"] + )[0].inv_site + + self.assertEqual(site_name, "Site 1") + + doc = make_stock_entry(item_code=item_code, source=warehouse, qty=100, do_not_submit=True) + + doc.items[0].inv_site = "Site 1" + self.assertRaises(frappe.ValidationError, doc.submit) + + inv_dimension.reload() + inv_dimension.db_set("validate_negative_stock", 0) + frappe.local.inventory_dimensions = {} + + doc = make_stock_entry(item_code=item_code, source=warehouse, qty=100, do_not_submit=True) + + doc.items[0].inv_site = "Site 1" + doc.submit() + self.assertEqual(doc.docstatus, 1) + + site_name = frappe.get_all( + "Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"] + )[0].inv_site + + self.assertEqual(site_name, "Site 1") + def get_voucher_sl_entries(voucher_no, fields): return frappe.get_all( @@ -504,6 +551,26 @@ def prepare_test_data(): } ).insert(ignore_permissions=True) + if not frappe.db.exists("DocType", "Inv Site"): + frappe.get_doc( + { + "doctype": "DocType", + "name": "Inv Site", + "module": "Stock", + "custom": 1, + "naming_rule": "By fieldname", + "autoname": "field:site_name", + "fields": [{"label": "Site Name", "fieldname": "site_name", "fieldtype": "Data"}], + "permissions": [ + {"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1} + ], + } + ).insert(ignore_permissions=True) + + for site in ["Site 1", "Site 2"]: + if not frappe.db.exists("Inv Site", site): + frappe.get_doc({"doctype": "Inv Site", "site_name": site}).insert(ignore_permissions=True) + def create_inventory_dimension(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 3ca4bad4e4..c1b205132c 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -5,14 +5,16 @@ from datetime import date import frappe -from frappe import _ +from frappe import _, bold from frappe.core.doctype.role.role import get_users from frappe.model.document import Document -from frappe.utils import add_days, cint, formatdate, get_datetime, getdate +from frappe.utils import add_days, cint, flt, formatdate, get_datetime, getdate from erpnext.accounts.utils import get_fiscal_year from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock +from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.serial_batch_bundle import SerialBatchBundle +from erpnext.stock.stock_ledger import get_previous_sle class StockFreezeError(frappe.ValidationError): @@ -48,6 +50,69 @@ class StockLedgerEntry(Document): self.validate_and_set_fiscal_year() self.block_transactions_against_group_warehouse() self.validate_with_last_transaction_posting_time() + self.validate_inventory_dimension_negative_stock() + + def validate_inventory_dimension_negative_stock(self): + extra_cond = "" + kwargs = {} + + dimensions = self._get_inventory_dimensions() + if not dimensions: + return + + for dimension, values in dimensions.items(): + kwargs[dimension] = values.get("value") + extra_cond += f" and {dimension} = %({dimension})s" + + kwargs.update( + { + "item_code": self.item_code, + "warehouse": self.warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "company": self.company, + } + ) + + sle = get_previous_sle(kwargs, extra_cond=extra_cond) + if sle: + flt_precision = cint(frappe.db.get_default("float_precision")) or 2 + diff = sle.qty_after_transaction + flt(self.actual_qty) + diff = flt(diff, flt_precision) + if diff < 0 and abs(diff) > 0.0001: + self.throw_validation_error(diff, dimensions) + + def throw_validation_error(self, diff, dimensions): + dimension_msg = _(", with the inventory {0}: {1}").format( + "dimensions" if len(dimensions) > 1 else "dimension", + ", ".join(f"{bold(d.doctype)} ({d.value})" for k, d in dimensions.items()), + ) + + msg = _( + "{0} units of {1} are required in {2}{3}, on {4} {5} for {6} to complete the transaction." + ).format( + abs(diff), + frappe.get_desk_link("Item", self.item_code), + frappe.get_desk_link("Warehouse", self.warehouse), + dimension_msg, + self.posting_date, + self.posting_time, + frappe.get_desk_link(self.voucher_type, self.voucher_no), + ) + + frappe.throw(msg, title=_("Inventory Dimension Negative Stock")) + + def _get_inventory_dimensions(self): + inv_dimensions = get_inventory_dimensions() + inv_dimension_dict = {} + for dimension in inv_dimensions: + if not dimension.get("validate_negative_stock") or not self.get(dimension.fieldname): + continue + + dimension["value"] = self.get(dimension.fieldname) + inv_dimension_dict.setdefault(dimension.fieldname, dimension) + + return inv_dimension_dict def on_submit(self): self.check_stock_frozen_date() diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index e36d5769bd..98b4ffdfcf 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -12,6 +12,7 @@ import erpnext from erpnext.accounts.utils import get_company_default from erpnext.controllers.stock_controller import StockController from erpnext.stock.doctype.batch.batch import get_available_batches, get_batch_qty +from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( get_available_serial_nos, ) @@ -50,6 +51,7 @@ class StockReconciliation(StockController): self.clean_serial_nos() self.set_total_qty_and_amount() self.validate_putaway_capacity() + self.validate_inventory_dimension() if self._action == "submit": self.validate_reserved_stock() @@ -57,6 +59,17 @@ class StockReconciliation(StockController): def on_update(self): self.set_serial_and_batch_bundle(ignore_validate=True) + def validate_inventory_dimension(self): + dimensions = get_inventory_dimensions() + for dimension in dimensions: + for row in self.items: + if not row.batch_no and row.current_qty and row.get(dimension.get("fieldname")): + frappe.throw( + _( + "Row #{0}: You cannot use the inventory dimension '{1}' in Stock Reconciliation to modify the quantity or valuation rate. Stock reconciliation with inventory dimensions is intended solely for performing opening entries." + ).format(row.idx, bold(dimension.get("doctype"))) + ) + def on_submit(self): self.update_stock_ledger() self.make_gl_entries() @@ -202,8 +215,19 @@ class StockReconciliation(StockController): self.calculate_difference_amount(item, bundle_data) return True + inventory_dimensions_dict = {} + if not item.batch_no and not item.serial_no: + for dimension in get_inventory_dimensions(): + if item.get(dimension.get("fieldname")): + inventory_dimensions_dict[dimension.get("fieldname")] = item.get(dimension.get("fieldname")) + item_dict = get_stock_balance_for( - item.item_code, item.warehouse, self.posting_date, self.posting_time, batch_no=item.batch_no + item.item_code, + item.warehouse, + self.posting_date, + self.posting_time, + batch_no=item.batch_no, + inventory_dimensions_dict=inventory_dimensions_dict, ) if (item.qty is None or item.qty == item_dict.get("qty")) and ( @@ -507,7 +531,13 @@ class StockReconciliation(StockController): if not row.batch_no: data.qty_after_transaction = flt(row.qty, row.precision("qty")) - if self.docstatus == 2: + dimensions = get_inventory_dimensions() + has_dimensions = False + for dimension in dimensions: + if row.get(dimension.get("fieldname")): + has_dimensions = True + + if self.docstatus == 2 and (not row.batch_no or not row.serial_and_batch_bundle): if row.current_qty: data.actual_qty = -1 * row.current_qty data.qty_after_transaction = flt(row.current_qty) @@ -523,6 +553,13 @@ class StockReconciliation(StockController): data.valuation_rate = flt(row.valuation_rate) data.stock_value_difference = -1 * flt(row.amount_difference) + elif ( + self.docstatus == 1 and has_dimensions and (not row.batch_no or not row.serial_and_batch_bundle) + ): + data.actual_qty = row.qty + data.qty_after_transaction = 0.0 + data.incoming_rate = flt(row.valuation_rate) + self.update_inventory_dimensions(row, data) return data @@ -911,6 +948,7 @@ def get_stock_balance_for( posting_time, batch_no: Optional[str] = None, with_valuation_rate: bool = True, + inventory_dimensions_dict=None, ): frappe.has_permission("Stock Reconciliation", "write", throw=True) @@ -939,6 +977,7 @@ def get_stock_balance_for( posting_time, with_valuation_rate=with_valuation_rate, with_serial_no=has_serial_no, + inventory_dimensions_dict=inventory_dimensions_dict, ) if has_serial_no: diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index d3807b0f97..48119b8d1f 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -24,6 +24,7 @@ from frappe.utils import ( import erpnext from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty +from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( get_sre_reserved_qty_for_item_and_warehouse as get_reserved_stock, ) @@ -711,10 +712,17 @@ class update_entries_after(object): ): sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle) + dimensions = get_inventory_dimensions() + has_dimensions = False + if dimensions: + for dimension in dimensions: + if sle.get(dimension.get("fieldname")): + has_dimensions = True + if sle.serial_and_batch_bundle: self.calculate_valuation_for_serial_batch_bundle(sle) else: - if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no: + if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions: # assert self.wh_data.valuation_rate = sle.valuation_rate self.wh_data.qty_after_transaction = sle.qty_after_transaction @@ -1297,7 +1305,7 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc return sle[0] if sle else frappe._dict() -def get_previous_sle(args, for_update=False): +def get_previous_sle(args, for_update=False, extra_cond=None): """ get the last sle on or before the current time-bucket, to get actual qty before transaction, this function @@ -1312,7 +1320,9 @@ def get_previous_sle(args, for_update=False): } """ args["name"] = args.get("sle", None) or "" - sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update) + sle = get_stock_ledger_entries( + args, "<=", "desc", "limit 1", for_update=for_update, extra_cond=extra_cond + ) return sle and sle[0] or {} @@ -1324,6 +1334,7 @@ def get_stock_ledger_entries( for_update=False, debug=False, check_serial_no=True, + extra_cond=None, ): """get stock ledger entries filtered by specific posting datetime conditions""" conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format( @@ -1361,6 +1372,9 @@ def get_stock_ledger_entries( if operator in (">", "<=") and previous_sle.get("name"): conditions += " and name!=%(name)s" + if extra_cond: + conditions += f"{extra_cond}" + return frappe.db.sql( """ select *, timestamp(posting_date, posting_time) as "timestamp" diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 02444064c1..bd0d4697c9 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -95,6 +95,7 @@ def get_stock_balance( posting_time=None, with_valuation_rate=False, with_serial_no=False, + inventory_dimensions_dict=None, ): """Returns stock balance quantity at given warehouse on given posting date or current date. @@ -114,7 +115,13 @@ def get_stock_balance( "posting_time": posting_time, } - last_entry = get_previous_sle(args) + extra_cond = "" + if inventory_dimensions_dict: + for field, value in inventory_dimensions_dict.items(): + args[field] = value + extra_cond += f" and {field} = %({field})s" + + last_entry = get_previous_sle(args, extra_cond=extra_cond) if with_valuation_rate: if with_serial_no: From fd6aee15e6bf3b7ea18487ab1b24b0a77526ac85 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 17 Oct 2023 12:48:07 +0530 Subject: [PATCH 18/18] fix(test): project test case --- erpnext/projects/doctype/task_depends_on/task_depends_on.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/doctype/task_depends_on/task_depends_on.json b/erpnext/projects/doctype/task_depends_on/task_depends_on.json index 5102986f00..3300b7eb90 100644 --- a/erpnext/projects/doctype/task_depends_on/task_depends_on.json +++ b/erpnext/projects/doctype/task_depends_on/task_depends_on.json @@ -24,6 +24,7 @@ }, { "fetch_from": "task.subject", + "fetch_if_empty": 1, "fieldname": "subject", "fieldtype": "Text", "in_list_view": 1, @@ -31,7 +32,6 @@ "read_only": 1 }, { - "fetch_from": "task.project", "fieldname": "project", "fieldtype": "Text", "label": "Project", @@ -40,7 +40,7 @@ ], "istable": 1, "links": [], - "modified": "2023-10-09 11:34:14.335853", + "modified": "2023-10-17 12:45:21.536165", "modified_by": "Administrator", "module": "Projects", "name": "Task Depends On",