From f04542eac9db8a1bf9118f943271fcb3fb72294d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 13 Feb 2023 11:39:30 +0530 Subject: [PATCH 1/4] feat: Setting to allow Sales Order creation against expired quotation (#33952) * feat: Setting to allow Sales Order creation against expired quotation (#33952) * feat: Setting to allow Sales Order creation against expired quotation * chore: linting issues (cherry picked from commit 148703bfc2fa9248c22d69ef92d14697a91a9301) # Conflicts: # erpnext/selling/doctype/selling_settings/selling_settings.json * chore: Resolve conflicts --------- Co-authored-by: Deepesh Garg --- erpnext/selling/doctype/quotation/quotation.js | 14 +++++++++----- erpnext/selling/doctype/quotation/quotation.py | 11 +++++++++++ .../selling/doctype/quotation/test_quotation.py | 10 ++++++++++ .../doctype/selling_settings/selling_settings.json | 11 +++++++++-- erpnext/startup/boot.py | 6 ++++++ 5 files changed, 45 insertions(+), 7 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index e33fa4b1d1a..1f32c60c077 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -84,11 +84,15 @@ erpnext.selling.QuotationController = erpnext.selling.SellingController.extend({ } if (doc.docstatus == 1 && !["Lost", "Ordered"].includes(doc.status)) { - this.frm.add_custom_button( - __("Sales Order"), - this.frm.cscript["Make Sales Order"], - __("Create") - ); + if (frappe.boot.sysdefaults.allow_sales_order_creation_for_expired_quotation + || (!doc.valid_till) + || frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) { + this.frm.add_custom_button( + __("Sales Order"), + this.frm.cscript["Make Sales Order"], + __("Create") + ); + } if(doc.status!=="Ordered") { this.frm.add_custom_button(__('Set as Lost'), () => { diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 3bfe79e3f38..0f316c3bc90 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -192,6 +192,17 @@ def get_list_context(context=None): @frappe.whitelist() def make_sales_order(source_name: str, target_doc=None): + if not frappe.db.get_singles_value( + "Selling Settings", "allow_sales_order_creation_for_expired_quotation" + ): + quotation = frappe.db.get_value( + "Quotation", source_name, ["transaction_date", "valid_till"], as_dict=1 + ) + if quotation.valid_till and ( + quotation.valid_till < quotation.transaction_date or quotation.valid_till < getdate(nowdate()) + ): + frappe.throw(_("Validity period of this quotation has ended.")) + return _make_sales_order(source_name, target_doc) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 6ab4a52d9d2..02604e3ff51 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -126,11 +126,21 @@ class TestQuotation(FrappeTestCase): def test_so_from_expired_quotation(self): from erpnext.selling.doctype.quotation.quotation import make_sales_order + frappe.db.set_single_value( + "Selling Settings", "allow_sales_order_creation_for_expired_quotation", 0 + ) + quotation = frappe.copy_doc(test_records[0]) quotation.valid_till = add_days(nowdate(), -1) quotation.insert() quotation.submit() + self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name) + + frappe.db.set_single_value( + "Selling Settings", "allow_sales_order_creation_for_expired_quotation", 1 + ) + make_sales_order(quotation.name) def test_shopping_cart_without_website_item(self): diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index eabfd7d2e22..ce976547dcd 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -32,7 +32,8 @@ "sales_update_frequency", "allow_multiple_items", "allow_against_multiple_purchase_orders", - "hide_tax_id" + "hide_tax_id", + "allow_sales_order_creation_for_expired_quotation" ], "fields": [ { @@ -199,6 +200,12 @@ "fieldtype": "Select", "label": "Contract Naming By", "options": "Party Name\nNaming Series" + }, + { + "default": "0", + "fieldname": "allow_sales_order_creation_for_expired_quotation", + "fieldtype": "Check", + "label": "Allow Sales Order Creation For Expired Quotation" } ], "icon": "fa fa-cog", @@ -206,7 +213,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-03-28 12:18:06.768403", + "modified": "2023-02-04 12:37:53.380857", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py index 52bd979e884..9790d995477 100644 --- a/erpnext/startup/boot.py +++ b/erpnext/startup/boot.py @@ -26,6 +26,12 @@ def boot_session(bootinfo): frappe.db.get_single_value("Selling Settings", "default_valid_till") ) + bootinfo.sysdefaults.allow_sales_order_creation_for_expired_quotation = cint( + frappe.db.get_single_value( + "Selling Settings", "allow_sales_order_creation_for_expired_quotation" + ) + ) + # if no company, show a dialog box to create a new company bootinfo.customer_count = frappe.db.sql("""SELECT count(*) FROM `tabCustomer`""")[0][0] From c1de4e44207d391237f593c9b81bf0e1de2320f6 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Mon, 13 Feb 2023 18:51:53 +0530 Subject: [PATCH 2/4] fix: LWP calculation --- erpnext/payroll/doctype/salary_slip/salary_slip.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 0563541eaa5..995f4dc0022 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -1,7 +1,6 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt - import datetime import math @@ -316,6 +315,8 @@ class SalarySlip(TransactionBase): ) working_days = date_diff(self.end_date, self.start_date) + 1 + working_days_list = [add_days(self.start_date, i) for i in range(working_days)] + if for_preview: self.total_working_days = working_days self.payment_days = working_days @@ -335,7 +336,7 @@ class SalarySlip(TransactionBase): actual_lwp, absent = self.calculate_lwp_ppl_and_absent_days_based_on_attendance(holidays) self.absent_days = absent else: - actual_lwp = self.calculate_lwp_or_ppl_based_on_leave_application(holidays, working_days) + actual_lwp = self.calculate_lwp_or_ppl_based_on_leave_application(holidays, working_days_list) if not lwp: lwp = actual_lwp @@ -458,16 +459,15 @@ class SalarySlip(TransactionBase): def get_holidays_for_employee(self, start_date, end_date): return get_holiday_dates_for_employee(self.employee, start_date, end_date) - def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days): + def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days_list): lwp = 0 - holidays = "','".join(holidays) + daily_wages_fraction_for_half_day = ( flt(frappe.db.get_value("Payroll Settings", None, "daily_wages_fraction_for_half_day")) or 0.5 ) - for d in range(working_days): - date = add_days(cstr(getdate(self.start_date)), d) - leave = get_lwp_or_ppl_for_date(date, self.employee, holidays) + for d in working_days_list: + leave = get_lwp_or_ppl_for_date(d, self.employee, holidays) if leave: equivalent_lwp_count = 0 From a8ea3efae29200d470435dc665ca00899b356d76 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Tue, 14 Feb 2023 13:14:58 +0530 Subject: [PATCH 3/4] fix: validate working day list against holidays --- erpnext/payroll/doctype/salary_slip/salary_slip.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 995f4dc0022..302c0d2853a 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -326,6 +326,8 @@ class SalarySlip(TransactionBase): if not cint(include_holidays_in_total_working_days): working_days -= len(holidays) + working_days_list = [cstr(day) for day in working_days_list if cstr(day) not in holidays] + if working_days < 0: frappe.throw(_("There are more holidays than working days this month.")) From 2391c37238b55e02afb31c4800fe419825ca0440 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 6 Feb 2023 13:36:25 +0530 Subject: [PATCH 4/4] fix: update `reserved_qty` when `Sales Order` marked as `Hold` (cherry picked from commit d76759e06690a19dc1a8b4fb201d509cee1b47e9) --- erpnext/stock/stock_balance.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index 9a7d8bbfe42..c505cae3813 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -122,7 +122,7 @@ def get_reserved_qty(item_code, warehouse): and parenttype="Sales Order" and item_code != parent_item and exists (select * from `tabSales Order` so - where name = dnpi_in.parent and docstatus = 1 and status != 'Closed') + where name = dnpi_in.parent and docstatus = 1 and status not in ('On Hold', 'Closed')) ) dnpi) union (select stock_qty as dnpi_qty, qty as so_item_qty, @@ -132,7 +132,7 @@ def get_reserved_qty(item_code, warehouse): and (so_item.delivered_by_supplier is null or so_item.delivered_by_supplier = 0) and exists(select * from `tabSales Order` so where so.name = so_item.parent and so.docstatus = 1 - and so.status != 'Closed')) + and so.status not in ('On Hold', 'Closed'))) ) tab where so_item_qty >= so_item_delivered_qty