From f5bb778d3785510c16708ee322df4b33937e0c02 Mon Sep 17 00:00:00 2001 From: Krithi Ramani Date: Wed, 19 Jan 2022 15:27:09 +0530 Subject: [PATCH 001/129] bypass selling price validation for free item (cherry picked from commit 8c536ffb20424627daac287b6a58af6c35536ebb) --- erpnext/controllers/selling_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 4ff851d7f94..75fcaee3832 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -204,7 +204,7 @@ class SellingController(StockController): valuation_rate_map = {} for item in self.items: - if not item.item_code: + if not item.item_code or item.is_free_item: continue last_purchase_rate, is_stock_item = frappe.get_cached_value( @@ -251,7 +251,7 @@ class SellingController(StockController): valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate for item in self.items: - if not item.item_code: + if not item.item_code or item.is_free_item: continue last_valuation_rate = valuation_rate_map.get( From 946e560c55b6cb2dda3ae87da5cb3e519f4412e2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 25 Jan 2022 19:59:33 +0530 Subject: [PATCH 002/129] feat: Refund entry against loans (cherry picked from commit c68c70f8bc88d9b05d64774ba070a34c059b7d30) # Conflicts: # erpnext/loan_management/doctype/loan/loan.json # erpnext/patches.txt --- erpnext/loan_management/doctype/loan/loan.js | 21 +++++++- .../loan_management/doctype/loan/loan.json | 17 ++++++- erpnext/loan_management/doctype/loan/loan.py | 48 ++++++++++++++++--- .../loan_management/doctype/loan/test_loan.py | 24 ++++++++-- .../loan_disbursement/loan_disbursement.py | 4 +- .../doctype/loan_type/loan_type.js | 2 +- .../doctype/loan_type/loan_type.json | 18 +++++-- erpnext/patches.txt | 8 +++- .../v13_0/update_disbursement_account.py | 22 +++++++++ 9 files changed, 142 insertions(+), 22 deletions(-) create mode 100644 erpnext/patches/v13_0/update_disbursement_account.py diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index f9c201ab603..940a1bbc000 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -46,7 +46,7 @@ frappe.ui.form.on('Loan', { }); }); - $.each(["payment_account", "loan_account"], function (i, field) { + $.each(["payment_account", "loan_account", "disbursement_account"], function (i, field) { frm.set_query(field, function () { return { "filters": { @@ -88,6 +88,10 @@ frappe.ui.form.on('Loan', { frm.add_custom_button(__('Loan Write Off'), function() { frm.trigger("make_loan_write_off_entry"); },__('Create')); + + frm.add_custom_button(__('Loan Refund'), function() { + frm.trigger("make_loan_refund"); + },__('Create')); } } frm.trigger("toggle_fields"); @@ -155,6 +159,21 @@ frappe.ui.form.on('Loan', { }) }, + make_loan_refund: function(frm) { + frappe.call({ + args: { + "loan": frm.doc.name + }, + method: "erpnext.loan_management.doctype.loan.loan.make_refund_jv", + callback: function (r) { + if (r.message) { + let doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + } + }) + }, + request_loan_closure: function(frm) { frappe.confirm(__("Do you really want to close this loan"), function() { diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index fe94e2cadd6..c8bd71d19ab 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -2,7 +2,7 @@ "actions": [], "allow_import": 1, "autoname": "ACC-LOAN-.YYYY.-.#####", - "creation": "2019-08-29 17:29:18.176786", + "creation": "2022-01-25 10:30:02.294967", "doctype": "DocType", "document_type": "Document", "editable_grid": 1, @@ -34,6 +34,7 @@ "is_term_loan", "account_info", "mode_of_payment", + "disbursement_account", "payment_account", "column_break_9", "loan_account", @@ -356,12 +357,25 @@ "fieldtype": "Date", "label": "Closure Date", "read_only": 1 + }, + { + "fetch_from": "loan_type.disbursement_account", + "fieldname": "disbursement_account", + "fieldtype": "Link", + "label": "Disbursement Account", + "options": "Account", + "read_only": 1, + "reqd": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2021-10-20 08:28:16.796105", +======= + "modified": "2022-01-25 16:29:16.325501", +>>>>>>> c68c70f8bc (feat: Refund entry against loans) "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", @@ -391,5 +405,6 @@ "search_fields": "posting_date", "sort_field": "creation", "sort_order": "DESC", + "states": [], "track_changes": 1 } diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index 94e0c55a775..f3914d51286 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -11,6 +11,7 @@ from frappe.utils import add_months, flt, get_last_day, getdate, now_datetime, n from six import string_types import erpnext +from erpnext.accounts.doctype.journal_entry.journal_entry import get_payment_entry from erpnext.controllers.accounts_controller import AccountsController from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import ( @@ -234,17 +235,15 @@ def request_loan_closure(loan, posting_date=None): loan_type = frappe.get_value('Loan', loan, 'loan_type') write_off_limit = frappe.get_value('Loan Type', loan_type, 'write_off_amount') - # checking greater than 0 as there may be some minor precision error - if not pending_amount: - frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested') - elif pending_amount < write_off_limit: + if pending_amount and abs(pending_amount) < write_off_limit: # Auto create loan write off and update status as loan closure requested write_off = make_loan_write_off(loan) write_off.submit() - frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested') - else: + elif pending_amount > 0: frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount)) + frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested') + @frappe.whitelist() def get_loan_application(loan_application): loan = frappe.get_doc("Loan Application", loan_application) @@ -401,4 +400,39 @@ def add_single_month(date): if getdate(date) == get_last_day(date): return get_last_day(add_months(date, 1)) else: - return add_months(date, 1) \ No newline at end of file + return add_months(date, 1) + +@frappe.whitelist() +def make_refund_jv(loan, amount=0, reference_number=None, reference_date=None, submit=0): + loan_details = frappe.db.get_value('Loan', loan, ['applicant_type', 'applicant', + 'loan_account', 'payment_account', 'posting_date', 'company', 'name', + 'total_payment', 'total_principal_paid'], as_dict=1) + + loan_details.doctype = 'Loan' + loan_details[loan_details.applicant_type.lower()] = loan_details.applicant + + if not amount: + amount = flt(loan_details.total_principal_paid - loan_details.total_payment) + + if amount < 0: + frappe.throw(_('No excess amount pending for refund')) + + refund_jv = get_payment_entry(loan_details, { + "party_type": loan_details.applicant_type, + "party_account": loan_details.loan_account, + "amount_field_party": 'debit_in_account_currency', + "amount_field_bank": 'credit_in_account_currency', + "amount": amount, + "bank_account": loan_details.payment_account + }) + + if reference_number: + refund_jv.cheque_no = reference_number + + if reference_date: + refund_jv.cheque_date = reference_date + + if submit: + refund_jv.submit() + + return refund_jv \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 1676c218c87..64156897829 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -42,16 +42,17 @@ class TestLoan(unittest.TestCase): create_loan_type("Personal Loan", 500000, 8.4, is_term_loan=1, mode_of_payment='Cash', + disbursement_account='Disbursement Account - _TC', payment_account='Payment Account - _TC', loan_account='Loan Account - _TC', interest_income_account='Interest Income Account - _TC', penalty_income_account='Penalty Income Account - _TC') - create_loan_type("Stock Loan", 2000000, 13.5, 25, 1, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', - 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + create_loan_type("Stock Loan", 2000000, 13.5, 25, 1, 5, 'Cash', 'Disbursement Account - _TC', + 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC') - create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', - 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC', + 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC') create_loan_security_type() create_loan_security() @@ -790,6 +791,18 @@ def create_loan_accounts(): "account_type": "Bank", }).insert(ignore_permissions=True) + if not frappe.db.exists("Account", "Disbursement Account - _TC"): + frappe.get_doc({ + "doctype": "Account", + "company": "_Test Company", + "account_name": "Disbursement Account", + "root_type": "Asset", + "report_type": "Balance Sheet", + "currency": "INR", + "parent_account": "Bank Accounts - _TC", + "account_type": "Bank", + }).insert(ignore_permissions=True) + if not frappe.db.exists("Account", "Interest Income Account - _TC"): frappe.get_doc({ "doctype": "Account", @@ -815,7 +828,7 @@ def create_loan_accounts(): }).insert(ignore_permissions=True) def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_interest_rate=None, is_term_loan=None, grace_period_in_days=None, - mode_of_payment=None, payment_account=None, loan_account=None, interest_income_account=None, penalty_income_account=None, + mode_of_payment=None, disbursement_account=None, payment_account=None, loan_account=None, interest_income_account=None, penalty_income_account=None, repayment_method=None, repayment_periods=None): if not frappe.db.exists("Loan Type", loan_name): @@ -829,6 +842,7 @@ def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_i "penalty_interest_rate": penalty_interest_rate, "grace_period_in_days": grace_period_in_days, "mode_of_payment": mode_of_payment, + "disbursement_account": disbursement_account, "payment_account": payment_account, "loan_account": loan_account, "interest_income_account": interest_income_account, diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index e2d758b1b90..df3aadfb18d 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -122,7 +122,7 @@ class LoanDisbursement(AccountsController): gle_map.append( self.get_gl_dict({ "account": loan_details.loan_account, - "against": loan_details.payment_account, + "against": loan_details.disbursement_account, "debit": self.disbursed_amount, "debit_in_account_currency": self.disbursed_amount, "against_voucher_type": "Loan", @@ -137,7 +137,7 @@ class LoanDisbursement(AccountsController): gle_map.append( self.get_gl_dict({ - "account": loan_details.payment_account, + "account": loan_details.disbursement_account, "against": loan_details.loan_account, "credit": self.disbursed_amount, "credit_in_account_currency": self.disbursed_amount, diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.js b/erpnext/loan_management/doctype/loan_type/loan_type.js index 04c89c45499..9f9137cfbcd 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type.js +++ b/erpnext/loan_management/doctype/loan_type/loan_type.js @@ -15,7 +15,7 @@ frappe.ui.form.on('Loan Type', { }); }); - $.each(["payment_account", "loan_account"], function (i, field) { + $.each(["payment_account", "loan_account", "disbursement_account"], function (i, field) { frm.set_query(field, function () { return { "filters": { diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json index c0a5d2cda12..00337e4b4c3 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type.json +++ b/erpnext/loan_management/doctype/loan_type/loan_type.json @@ -19,9 +19,10 @@ "description", "account_details_section", "mode_of_payment", + "disbursement_account", "payment_account", - "loan_account", "column_break_12", + "loan_account", "interest_income_account", "penalty_income_account", "amended_from" @@ -79,7 +80,7 @@ { "fieldname": "payment_account", "fieldtype": "Link", - "label": "Payment Account", + "label": "Repayment Account", "options": "Account", "reqd": 1 }, @@ -149,15 +150,23 @@ "fieldtype": "Currency", "label": "Auto Write Off Amount ", "options": "Company:company:default_currency" + }, + { + "fieldname": "disbursement_account", + "fieldtype": "Link", + "label": "Disbursement Account", + "options": "Account", + "reqd": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-04-19 18:10:57.368490", + "modified": "2022-01-25 16:23:57.009349", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Type", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -181,5 +190,6 @@ } ], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 7cbb6fa0ae9..255caea5dca 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -342,8 +342,14 @@ erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021 erpnext.patches.v13_0.update_tax_category_for_rcm erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template erpnext.patches.v13_0.agriculture_deprecation_warning +<<<<<<< HEAD erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit erpnext.patches.v13_0.hospitality_deprecation_warning erpnext.patches.v13_0.delete_bank_reconciliation_detail erpnext.patches.v13_0.update_sane_transfer_against -erpnext.patches.v13_0.enable_provisional_accounting \ No newline at end of file +erpnext.patches.v13_0.enable_provisional_accounting +======= +erpnext.patches.v14_0.delete_agriculture_doctypes +erpnext.patches.v13_0.update_exchange_rate_settings +erpnext.patches.v13_0.update_disbursement_account +>>>>>>> c68c70f8bc (feat: Refund entry against loans) diff --git a/erpnext/patches/v13_0/update_disbursement_account.py b/erpnext/patches/v13_0/update_disbursement_account.py new file mode 100644 index 00000000000..c56fa8fdc62 --- /dev/null +++ b/erpnext/patches/v13_0/update_disbursement_account.py @@ -0,0 +1,22 @@ +import frappe + + +def execute(): + + frappe.reload_doc("loan_management", "doctype", "loan_type") + frappe.reload_doc("loan_management", "doctype", "loan") + + loan_type = frappe.qb.DocType("Loan Type") + loan = frappe.qb.DocType("Loan") + + frappe.qb.update( + loan_type + ).set( + loan_type.disbursement_account, loan_type.payment_account + ).run() + + frappe.qb.update( + loan + ).set( + loan.disbursement_account, loan.payment_account + ).run() \ No newline at end of file From f4e2c1fad3e2fdfb7d430ec84755cbbff5092de9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 7 Feb 2022 13:50:31 +0530 Subject: [PATCH 003/129] fix: Add disbursement accounts to tests (cherry picked from commit 8ece2845f2dd6cc382c21c7794ee9d533cf4ea9b) --- .../doctype/loan_application/test_loan_application.py | 2 +- .../doctype/loan_disbursement/test_loan_disbursement.py | 4 ++-- .../loan_interest_accrual/test_loan_interest_accrual.py | 4 ++-- erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py | 1 + erpnext/payroll/doctype/salary_slip/test_salary_slip.py | 1 + 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_application/test_loan_application.py b/erpnext/loan_management/doctype/loan_application/test_loan_application.py index d367e92ac49..640709c095f 100644 --- a/erpnext/loan_management/doctype/loan_application/test_loan_application.py +++ b/erpnext/loan_management/doctype/loan_application/test_loan_application.py @@ -15,7 +15,7 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( class TestLoanApplication(unittest.TestCase): def setUp(self): create_loan_accounts() - create_loan_type("Home Loan", 500000, 9.2, 0, 1, 0, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', + create_loan_type("Home Loan", 500000, 9.2, 0, 1, 0, 'Cash', 'Disbursement Account - _TC', 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC', 'Repay Over Number of Periods', 18) self.applicant = make_employee("kate_loan@loan.com", "_Test Company") make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant, currency='INR') diff --git a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py index 94ec84ea5db..10be750b449 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py @@ -44,8 +44,8 @@ class TestLoanDisbursement(unittest.TestCase): def setUp(self): create_loan_accounts() - create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', - 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC', + 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC') create_loan_security_type() create_loan_security() diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py index 46aaaad9fd2..e8c77506fcb 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py @@ -30,8 +30,8 @@ class TestLoanInterestAccrual(unittest.TestCase): def setUp(self): create_loan_accounts() - create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC', - 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC', + 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC') create_loan_security_type() create_loan_security() diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index c6f38972880..56a8bf727f2 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -197,6 +197,7 @@ class TestPayrollEntry(unittest.TestCase): create_loan_type("Car Loan", 500000, 8.4, is_term_loan=1, mode_of_payment='Cash', + disbursement_account='Disbursement Account - _TC', payment_account='Payment Account - _TC', loan_account='Loan Account - _TC', interest_income_account='Interest Income Account - _TC', diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index cc011a50c6b..8ef4fbb4565 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -374,6 +374,7 @@ class TestSalarySlip(unittest.TestCase): create_loan_type("Car Loan", 500000, 8.4, is_term_loan=1, mode_of_payment='Cash', + disbursement_account='Disbursement Account - _TC', payment_account='Payment Account - _TC', loan_account='Loan Account - _TC', interest_income_account='Interest Income Account - _TC', From 501ce907967228448c098f12a2fa0f2c471d81b3 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 18 Jan 2022 21:15:27 +0100 Subject: [PATCH 004/129] feat: option to disable Item Tax Template (cherry picked from commit f60e040d69aa56f71a632b3d77474f60657be689) --- .../item_tax_template/item_tax_template.json | 22 +++++++++++++++++-- erpnext/controllers/queries.py | 5 +++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json index 77c9e95b759..b42d712d88a 100644 --- a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json +++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json @@ -2,7 +2,7 @@ "actions": [], "allow_import": 1, "allow_rename": 1, - "creation": "2018-11-22 22:45:00.370913", + "creation": "2022-01-19 01:09:13.297137", "doctype": "DocType", "document_type": "Setup", "editable_grid": 1, @@ -10,6 +10,9 @@ "field_order": [ "title", "company", + "column_break_3", + "disabled", + "section_break_5", "taxes" ], "fields": [ @@ -36,10 +39,24 @@ "label": "Company", "options": "Company", "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break" } ], "links": [], - "modified": "2021-03-08 19:50:21.416513", + "modified": "2022-01-18 21:11:23.105589", "modified_by": "Administrator", "module": "Accounts", "name": "Item Tax Template", @@ -82,6 +99,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "title", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 9324f07ec97..f5c566023c6 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -740,6 +740,7 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters): item_doc = frappe.get_cached_doc('Item', filters.get('item_code')) item_group = filters.get('item_group') + company = filters.get('company') taxes = item_doc.taxes or [] while item_group: @@ -748,7 +749,7 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters): item_group = item_group_doc.parent_item_group if not taxes: - return frappe.db.sql(""" SELECT name FROM `tabItem Tax Template` """) + return frappe.get_all('Item Tax Template', filters={'disabled': 0, 'company': company}, as_list=True) else: valid_from = filters.get('valid_from') valid_from = valid_from[1] if isinstance(valid_from, list) else valid_from @@ -757,7 +758,7 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters): 'item_code': filters.get('item_code'), 'posting_date': valid_from, 'tax_category': filters.get('tax_category'), - 'company': filters.get('company') + 'company': company } taxes = _get_item_tax_template(args, taxes, for_validate=True) From ec55251f6aee13876cf39da949b4af27d8246acc Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 18 Jan 2022 21:16:29 +0100 Subject: [PATCH 005/129] feat: option to disable tax category (cherry picked from commit 663c594ead54ced046887bbd94a6a0c88fa7b8e8) --- .../doctype/tax_category/tax_category.json | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/tax_category/tax_category.json b/erpnext/accounts/doctype/tax_category/tax_category.json index f7145af44c3..44a339f31df 100644 --- a/erpnext/accounts/doctype/tax_category/tax_category.json +++ b/erpnext/accounts/doctype/tax_category/tax_category.json @@ -2,12 +2,13 @@ "actions": [], "allow_rename": 1, "autoname": "field:title", - "creation": "2018-11-22 23:38:39.668804", + "creation": "2022-01-19 01:09:28.920486", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "title" + "title", + "disabled" ], "fields": [ { @@ -18,14 +19,21 @@ "label": "Title", "reqd": 1, "unique": 1 + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-03-03 11:50:38.748872", + "modified": "2022-01-18 21:13:41.161017", "modified_by": "Administrator", "module": "Accounts", "name": "Tax Category", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -65,5 +73,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From 9396143009bce58305ae1e5b3affc74c66467b97 Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Sun, 6 Feb 2022 16:55:24 +0530 Subject: [PATCH 006/129] fix: missing key in loan (cherry picked from commit d0043bdbace8cac51527c9998435aa5c4f438b25) --- .../loan_management/doctype/loan_repayment/loan_repayment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 65099da49b4..c79ea30d67b 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -126,7 +126,7 @@ class LoanRepayment(AccountsController): def update_paid_amount(self): loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid', - 'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'total_interest_payable', + 'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'disbursed_amount', 'total_interest_payable', 'written_off_amount'], as_dict=1) loan.update({ From 4196f04f498c38bf2fa4337d4e05af7a4a5bab18 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 7 Feb 2022 17:00:30 +0530 Subject: [PATCH 007/129] test: Add test case for repayment against partially disbursed loans (cherry picked from commit ef69d1fd385bfe740ce1765f1a4998d27456ce79) --- .../loan_management/doctype/loan/test_loan.py | 23 +++++++++++++++++++ .../doctype/loan_repayment/loan_repayment.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 1676c218c87..cb7337e8d62 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -679,6 +679,29 @@ class TestLoan(unittest.TestCase): loan.load_from_db() self.assertEqual(loan.status, "Loan Closure Requested") + def test_loan_repayment_against_partially_disbursed_loan(self): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + create_pledge(loan_application) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan.submit() + + first_date = '2019-10-01' + last_date = '2019-10-30' + + make_loan_disbursement_entry(loan.name, loan.loan_amount/2, disbursement_date=first_date) + + loan.load_from_db() + + self.assertEqual(loan.status, "Partially Disbursed") + create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), + flt(loan.loan_amount/3)) + def test_loan_amount_write_off(self): pledge = [{ "loan_security": "Test Security 1", diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index c79ea30d67b..56ee2c0225c 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -154,7 +154,7 @@ class LoanRepayment(AccountsController): def mark_as_unpaid(self): loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid', - 'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'total_interest_payable', + 'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'disbursed_amount', 'total_interest_payable', 'written_off_amount'], as_dict=1) no_of_repayments = len(self.repayment_details) From 066ceb06cd29642e5c948f074d3fbe68df7a3fb4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 7 Feb 2022 18:46:53 +0530 Subject: [PATCH 008/129] fix: Resolve conflicts --- erpnext/loan_management/doctype/loan/loan.json | 4 ---- erpnext/patches.txt | 7 +------ 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index c8bd71d19ab..dd723f38bdf 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -371,11 +371,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], -<<<<<<< HEAD - "modified": "2021-10-20 08:28:16.796105", -======= "modified": "2022-01-25 16:29:16.325501", ->>>>>>> c68c70f8bc (feat: Refund entry against loans) "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 255caea5dca..0a7eb33f66b 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -342,14 +342,9 @@ erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021 erpnext.patches.v13_0.update_tax_category_for_rcm erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template erpnext.patches.v13_0.agriculture_deprecation_warning -<<<<<<< HEAD erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit erpnext.patches.v13_0.hospitality_deprecation_warning erpnext.patches.v13_0.delete_bank_reconciliation_detail erpnext.patches.v13_0.update_sane_transfer_against erpnext.patches.v13_0.enable_provisional_accounting -======= -erpnext.patches.v14_0.delete_agriculture_doctypes -erpnext.patches.v13_0.update_exchange_rate_settings -erpnext.patches.v13_0.update_disbursement_account ->>>>>>> c68c70f8bc (feat: Refund entry against loans) +erpnext.patches.v13_0.update_disbursement_account \ No newline at end of file From c137b141e8ae3b3dcaf5fed78458fff1baa193df Mon Sep 17 00:00:00 2001 From: Ritwik Puri Date: Mon, 7 Feb 2022 19:43:29 +0530 Subject: [PATCH 009/129] fix: use item_code instead of parent field in bom_stock_calculated report (#29684) (cherry picked from commit 72a812f18bfd27842156d7b1afb1f301fbead7ed) --- .../report/bom_stock_calculated/bom_stock_calculated.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py index 090a3e74fc8..26933523246 100644 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py +++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py @@ -89,10 +89,10 @@ def get_bom_stock(filters): GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1) def get_manufacturer_records(): - details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"]) + details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "item_code"]) manufacture_details = frappe._dict() for detail in details: - dic = manufacture_details.setdefault(detail.get('parent'), {}) + dic = manufacture_details.setdefault(detail.get('item_code'), {}) dic.setdefault('manufacturer', []).append(detail.get('manufacturer')) dic.setdefault('manufacturer_part', []).append(detail.get('manufacturer_part_no')) From bd401810e4eabfd744c81b8e8eb75d92122ab17c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 5 Feb 2022 17:28:21 +0530 Subject: [PATCH 010/129] fix: consider packed items too when reposting (cherry picked from commit c56d07dee3f0c35e211424d82fec628aea22d2e9) --- erpnext/controllers/stock_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 8d17683953e..92f8ff0d20c 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -184,8 +184,8 @@ class StockController(AccountsController): def get_items_and_warehouses(self): items, warehouses = [], [] - if hasattr(self, "items"): - item_doclist = self.get("items") + if hasattr(self, "items") or hasattr(self, "packed_items"): + item_doclist = (self.get("items") or []) + (self.get("packed_items") or []) elif self.doctype == "Stock Reconciliation": item_doclist = [] data = json.loads(self.reconciliation_json) From eeb8f4874d0aa858139fd24bd458d3457b121ad4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 6 Feb 2022 13:02:34 +0530 Subject: [PATCH 011/129] refactor: simplify get_items_and_warehouses Also remove dead code related to stock reconciliation_json. (cherry picked from commit e6ab8df8f2c48932a7368c5ac69ebcac14cf015c) --- erpnext/controllers/stock_controller.py | 40 +++++++++++-------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 92f8ff0d20c..9be5c0d03f1 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -3,6 +3,7 @@ import json from collections import defaultdict +from typing import List, Tuple import frappe from frappe import _ @@ -181,33 +182,28 @@ class StockController(AccountsController): return details - def get_items_and_warehouses(self): - items, warehouses = [], [] + def get_items_and_warehouses(self) -> Tuple[List[str], List[str]]: + """Get list of items and warehouses affected by a transaction""" - if hasattr(self, "items") or hasattr(self, "packed_items"): - item_doclist = (self.get("items") or []) + (self.get("packed_items") or []) - elif self.doctype == "Stock Reconciliation": - item_doclist = [] - data = json.loads(self.reconciliation_json) - for row in data[data.index(self.head_row)+1:]: - d = frappe._dict(zip(["item_code", "warehouse", "qty", "valuation_rate"], row)) - item_doclist.append(d) + if not (hasattr(self, "items") or hasattr(self, "packed_items")): + return [], [] - if item_doclist: - for d in item_doclist: - if d.item_code and d.item_code not in items: - items.append(d.item_code) + item_rows = (self.get("items") or []) + (self.get("packed_items") or []) - if d.get("warehouse") and d.warehouse not in warehouses: - warehouses.append(d.warehouse) + items = {d.item_code for d in item_rows if d.item_code} - if self.doctype == "Stock Entry": - if d.get("s_warehouse") and d.s_warehouse not in warehouses: - warehouses.append(d.s_warehouse) - if d.get("t_warehouse") and d.t_warehouse not in warehouses: - warehouses.append(d.t_warehouse) + warehouses = set() + for d in item_rows: + if d.get("warehouse"): + warehouses.add(d.warehouse) - return items, warehouses + if self.doctype == "Stock Entry": + if d.get("s_warehouse"): + warehouses.add(d.s_warehouse) + if d.get("t_warehouse"): + warehouses.add(d.t_warehouse) + + return list(items), list(warehouses) def get_stock_ledger_details(self): stock_ledger = {} From 6f6b474333da09d11b9e6a3b683da52f6a7c7754 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 6 Feb 2022 13:29:40 +0530 Subject: [PATCH 012/129] fix: merge stock ledger item warehouse with doc's (cherry picked from commit 1022db04745b3c6f16710b43720fef4f0fd0d295) --- .../repost_item_valuation/repost_item_valuation.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 3b76301b4cc..c6baa46c5eb 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -13,7 +13,7 @@ from erpnext.accounts.utils import ( check_if_stock_and_account_balance_synced, update_gl_entries_after, ) -from erpnext.stock.stock_ledger import repost_future_sle +from erpnext.stock.stock_ledger import get_items_to_be_repost, repost_future_sle class RepostItemValuation(Document): @@ -138,13 +138,20 @@ def repost_gl_entries(doc): if doc.based_on == 'Transaction': ref_doc = frappe.get_doc(doc.voucher_type, doc.voucher_no) - items, warehouses = ref_doc.get_items_and_warehouses() + doc_items, doc_warehouses = ref_doc.get_items_and_warehouses() + + sles = get_items_to_be_repost(doc.voucher_type, doc.voucher_no) + sle_items = [sle.item_code for sle in sles] + sle_warehouse = [sle.warehouse for sle in sles] + + items = list(set(doc_items).union(set(sle_items))) + warehouses = list(set(doc_warehouses).union(set(sle_warehouse))) else: items = [doc.item_code] warehouses = [doc.warehouse] update_gl_entries_after(doc.posting_date, doc.posting_time, - warehouses, items, company=doc.company) + for_warehouses=warehouses, for_items=items, company=doc.company) def notify_error_to_stock_managers(doc, traceback): recipients = get_users_with_role("Stock Manager") From 86d30ed583d5269b62008e79b6b90e8038c68ad3 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 6 Feb 2022 13:42:44 +0530 Subject: [PATCH 013/129] test: move bundle info to class variables (cherry picked from commit 853e658dccf1a71fad03e23bf3b7d8f9d0784c37) --- .../doctype/packed_item/test_packed_item.py | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py index 5cbaa1ea669..fcb4727f6b8 100644 --- a/erpnext/stock/doctype/packed_item/test_packed_item.py +++ b/erpnext/stock/doctype/packed_item/test_packed_item.py @@ -5,6 +5,7 @@ from erpnext.selling.doctype.product_bundle.test_product_bundle import make_prod from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.tests.utils import ERPNextTestCase, change_settings @@ -12,31 +13,29 @@ class TestPackedItem(ERPNextTestCase): "Test impact on Packed Items table in various scenarios." @classmethod def setUpClass(cls) -> None: - make_item("_Test Product Bundle X", {"is_stock_item": 0}) - make_item("_Test Bundle Item 1", {"is_stock_item": 1}) - make_item("_Test Bundle Item 2", {"is_stock_item": 1}) + cls.bundle = "_Test Product Bundle X" + cls.bundle_items = ["_Test Bundle Item 1", "_Test Bundle Item 2"] + make_item(cls.bundle, {"is_stock_item": 0}) + for item in cls.bundle_items: + make_item(item, {"is_stock_item": 1}) + make_item("_Test Normal Stock Item", {"is_stock_item": 1}) - make_product_bundle( - "_Test Product Bundle X", - ["_Test Bundle Item 1", "_Test Bundle Item 2"], - qty=2 - ) + make_product_bundle(cls.bundle, cls.bundle_items, qty=2) def test_adding_bundle_item(self): "Test impact on packed items if bundle item row is added." - so = make_sales_order(item_code = "_Test Product Bundle X", qty=1, + so = make_sales_order(item_code = self.bundle, qty=1, do_not_submit=True) self.assertEqual(so.items[0].qty, 1) self.assertEqual(len(so.packed_items), 2) - self.assertEqual(so.packed_items[0].item_code, "_Test Bundle Item 1") + self.assertEqual(so.packed_items[0].item_code, self.bundle_items[0]) self.assertEqual(so.packed_items[0].qty, 2) def test_updating_bundle_item(self): "Test impact on packed items if bundle item row is updated." - so = make_sales_order(item_code = "_Test Product Bundle X", qty=1, - do_not_submit=True) + so = make_sales_order(item_code=self.bundle, qty=1, do_not_submit=True) so.items[0].qty = 2 # change qty so.save() @@ -55,7 +54,7 @@ class TestPackedItem(ERPNextTestCase): so_items = [] for qty in [2, 4, 6, 8]: so_items.append({ - "item_code": "_Test Product Bundle X", + "item_code": self.bundle, "qty": qty, "rate": 400, "warehouse": "_Test Warehouse - _TC" @@ -66,7 +65,7 @@ class TestPackedItem(ERPNextTestCase): # check alternate rows for qty self.assertEqual(len(so.packed_items), 8) - self.assertEqual(so.packed_items[1].item_code, "_Test Bundle Item 2") + self.assertEqual(so.packed_items[1].item_code, self.bundle_items[1]) self.assertEqual(so.packed_items[1].qty, 4) self.assertEqual(so.packed_items[3].qty, 8) self.assertEqual(so.packed_items[5].qty, 12) @@ -94,8 +93,7 @@ class TestPackedItem(ERPNextTestCase): @change_settings("Selling Settings", {"editable_bundle_item_rates": 1}) def test_bundle_item_cumulative_price(self): "Test if Bundle Item rate is cumulative from packed items." - so = make_sales_order(item_code = "_Test Product Bundle X", qty=2, - do_not_submit=True) + so = make_sales_order(item_code=self.bundle, qty=2, do_not_submit=True) so.packed_items[0].rate = 150 so.packed_items[1].rate = 200 @@ -109,7 +107,7 @@ class TestPackedItem(ERPNextTestCase): so_items = [] for qty in [2, 4]: so_items.append({ - "item_code": "_Test Product Bundle X", + "item_code": self.bundle, "qty": qty, "rate": 400, "warehouse": "_Test Warehouse - _TC" @@ -124,4 +122,4 @@ class TestPackedItem(ERPNextTestCase): self.assertEqual(len(dn.packed_items), 4) self.assertEqual(dn.packed_items[2].qty, 6) - self.assertEqual(dn.packed_items[3].qty, 6) \ No newline at end of file + self.assertEqual(dn.packed_items[3].qty, 6) From 19f99a1c5f9b32198d0b8a74ec06b23e52e71ae7 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 6 Feb 2022 14:19:58 +0530 Subject: [PATCH 014/129] test: product bundle reposting (cherry picked from commit 699519f7b69b0b5eaa53a0db2fb35857b080261c) --- .../doctype/packed_item/test_packed_item.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py index fcb4727f6b8..2521ac9fe72 100644 --- a/erpnext/stock/doctype/packed_item/test_packed_item.py +++ b/erpnext/stock/doctype/packed_item/test_packed_item.py @@ -1,10 +1,13 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +from frappe.utils import add_to_date, nowdate + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.tests.utils import ERPNextTestCase, change_settings @@ -13,6 +16,7 @@ class TestPackedItem(ERPNextTestCase): "Test impact on Packed Items table in various scenarios." @classmethod def setUpClass(cls) -> None: + super().setUpClass() cls.bundle = "_Test Product Bundle X" cls.bundle_items = ["_Test Bundle Item 1", "_Test Bundle Item 2"] make_item(cls.bundle, {"is_stock_item": 0}) @@ -123,3 +127,32 @@ class TestPackedItem(ERPNextTestCase): self.assertEqual(len(dn.packed_items), 4) self.assertEqual(dn.packed_items[2].qty, 6) self.assertEqual(dn.packed_items[3].qty, 6) + + def test_reposting_packed_items(self): + warehouse = "Stores - TCP1" + company = "_Test Company with perpetual inventory" + + today = nowdate() + yesterday = add_to_date(today, days=-1, as_string=True) + + for item in self.bundle_items: + make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=100, posting_date=today) + + so = make_sales_order(item_code = self.bundle, qty=1, company=company, warehouse=warehouse) + + dn = make_delivery_note(so.name) + dn.save() + dn.submit() + + gles = get_gl_entries(dn.doctype, dn.name) + credit_before_repost = sum(gle.credit for gle in gles) + + # backdated stock entry + for item in self.bundle_items: + make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=200, posting_date=yesterday) + + # assert correct reposting + gles = get_gl_entries(dn.doctype, dn.name) + credit_after_reposting = sum(gle.credit for gle in gles) + self.assertNotEqual(credit_before_repost, credit_after_reposting) + self.assertAlmostEqual(credit_after_reposting, 2 * credit_before_repost) From 29d3a189cb3eb02ab7249b31a9114ecca386b834 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 6 Feb 2022 14:28:55 +0530 Subject: [PATCH 015/129] chore: drop dead field from stock reconciliation (cherry picked from commit 43f8ee1dd1525b6cf3e88154bec314aca7b23ca5) --- .../stock_reconciliation.json | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json index 3402972bb89..a882a61e5a5 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json @@ -18,7 +18,6 @@ "items", "section_break_9", "expense_account", - "reconciliation_json", "column_break_13", "difference_amount", "amended_from", @@ -111,15 +110,6 @@ "label": "Cost Center", "options": "Cost Center" }, - { - "fieldname": "reconciliation_json", - "fieldtype": "Long Text", - "hidden": 1, - "label": "Reconciliation JSON", - "no_copy": 1, - "print_hide": 1, - "read_only": 1 - }, { "fieldname": "column_break_13", "fieldtype": "Column Break" @@ -155,7 +145,7 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2021-11-30 01:33:51.437194", + "modified": "2022-02-06 14:28:19.043905", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation", @@ -178,5 +168,6 @@ "search_fields": "posting_date", "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file From 8e660890f4e6202e2d336ef89c4d5b1d9a333eae Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 6 Feb 2022 15:18:00 +0530 Subject: [PATCH 016/129] test: commit item/warehouse creation to db (cherry picked from commit f82d7eb73fe6dd70a98fa6656e90ed9c6565be16) --- .../doctype/stock_reconciliation/test_stock_reconciliation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 428370cc758..86af0a0cf3b 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -25,8 +25,8 @@ from erpnext.tests.utils import ERPNextTestCase, change_settings class TestStockReconciliation(ERPNextTestCase): @classmethod def setUpClass(cls): - super().setUpClass() create_batch_or_serial_no_items() + super().setUpClass() frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) def tearDown(self): From 52a74921c0ca8c113a3d71602473aa7c05f3fd05 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 8 Feb 2022 10:49:37 +0530 Subject: [PATCH 017/129] chore: show credit/debit-to account in error message (cherry picked from commit e93bb8a3364c04c8a30b47f681e53747140e4fd9) --- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 4 ++-- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index c9be49f6868..f3452e1cf81 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -176,8 +176,8 @@ class PurchaseInvoice(BuyingController): if self.supplier and account.account_type != "Payable": frappe.throw( - _("Please ensure {} account is a Payable account. Change the account type to Payable or select a different account.") - .format(frappe.bold("Credit To")), title=_("Invalid Account") + _("Please ensure {} account {} is a Payable account. Change the account type to Payable or select a different account.") + .format(frappe.bold("Credit To"), frappe.bold(self.credit_to)), title=_("Invalid Account") ) self.party_account_currency = account.account_currency diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 8927ce3d6ac..0317656e83b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -587,7 +587,10 @@ class SalesInvoice(SellingController): frappe.throw(msg, title=_("Invalid Account")) if self.customer and account.account_type != "Receivable": - msg = _("Please ensure {} account is a Receivable account.").format(frappe.bold("Debit To")) + " " + msg = _("Please ensure {} account {} is a Receivable account.").format( + frappe.bold("Debit To"), + frappe.bold(self.debit_to) + ) + " " msg += _("Change the account type to Receivable or select a different account.") frappe.throw(msg, title=_("Invalid Account")) From c494ed087a2aa4cbefbee2caf1491fae8aa5ce4c Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Wed, 2 Feb 2022 20:13:33 +0530 Subject: [PATCH 018/129] fix: Coupon code item pricing dynamic updation issue (cherry picked from commit ccf63124d62139c586a3d6737460a67a942956b1) --- erpnext/public/js/controllers/transaction.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 47454b9a789..3730f15078e 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2269,7 +2269,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ () => this.frm.doc.ignore_pricing_rule=1, () => me.ignore_pricing_rule(), () => this.frm.doc.ignore_pricing_rule=0, - () => me.apply_pricing_rule() + () => me.apply_pricing_rule(), + () => this.frm.save() ]); } else { frappe.run_serially([ From cc5107eab312d7854dbcf9b803b79d997aca91db Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 8 Feb 2022 13:52:28 +0530 Subject: [PATCH 019/129] fix: ignore cancelled SLEs (#29679) (#29698) (cherry picked from commit 0ca60afc3fb7190cbba58ef42b84c51bffb9d660) Co-authored-by: Ankush Menat --- erpnext/controllers/stock_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 9be5c0d03f1..c8e5eddfeac 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -215,7 +215,7 @@ class StockController(AccountsController): from `tabStock Ledger Entry` where - voucher_type=%s and voucher_no=%s + voucher_type=%s and voucher_no=%s and is_cancelled = 0 """, (self.doctype, self.name), as_dict=True) for sle in stock_ledger_entries: From bd9a07ed4ce1d79d5100f6c6414b009574acb02f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 8 Feb 2022 13:52:41 +0530 Subject: [PATCH 020/129] fix: dont ignore items that dont have SVD (backport #29656) (#29694) * fix: dont ignore items that dont have SVD When items go from negative to positive stock value diff can be zero but item might have taxes / need divisional loss adjustment. (cherry picked from commit c88c368880a134b12ad82d7674cb5e12d5a858ba) * test: check when PR moves stock from neg to pos (cherry picked from commit d1f57538855eaa8c6598a553a8b776b3ab511171) Co-authored-by: Ankush Menat --- .../purchase_receipt/purchase_receipt.py | 3 -- .../purchase_receipt/test_purchase_receipt.py | 33 ++++++++++++++++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index efac6e4dc44..afaa8b02a89 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -283,9 +283,6 @@ class PurchaseReceipt(BuyingController): {"voucher_type": "Purchase Receipt", "voucher_no": self.name, "voucher_detail_no": d.name, "warehouse": d.warehouse, "is_cancelled": 0}, "stock_value_difference") - if not stock_value_diff: - continue - warehouse_account_name = warehouse_account[d.warehouse]["account"] warehouse_account_currency = warehouse_account[d.warehouse]["account_currency"] supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account") diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 556535317d4..89f11ca78d4 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -4,6 +4,7 @@ import json import unittest +from collections import defaultdict import frappe from frappe.utils import add_days, cint, cstr, flt, today @@ -17,7 +18,7 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchas from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction -from erpnext.tests.utils import ERPNextTestCase +from erpnext.tests.utils import ERPNextTestCase, change_settings class TestPurchaseReceipt(ERPNextTestCase): @@ -1367,6 +1368,36 @@ class TestPurchaseReceipt(ERPNextTestCase): automatically_fetch_payment_terms(enable=0) + @change_settings("Stock Settings", {"allow_negative_stock": 1}) + def test_neg_to_positive(self): + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + item_code = "_TestNegToPosItem" + warehouse = "Stores - TCP1" + company = "_Test Company with perpetual inventory" + account = "Stock Received But Not Billed - TCP1" + + make_item(item_code) + se = make_stock_entry(item_code=item_code, from_warehouse=warehouse, qty=50, do_not_save=True, rate=0) + se.items[0].allow_zero_valuation_rate = 1 + se.save() + se.submit() + + pr = make_purchase_receipt( + qty=50, + rate=1, + item_code=item_code, + warehouse=warehouse, + get_taxes_and_charges=True, + company=company, + ) + gles = get_gl_entries(pr.doctype, pr.name) + + for gle in gles: + if gle.account == account: + self.assertEqual(gle.credit, 50) + + def get_sl_entries(voucher_type, voucher_no): return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s From 08b9ee7b369bd665712c446e32c92159a9c2f668 Mon Sep 17 00:00:00 2001 From: Bhavesh Maheshwari <34086262+bhavesh95863@users.noreply.github.com> Date: Tue, 8 Feb 2022 14:01:20 +0530 Subject: [PATCH 021/129] fix: ignore rate validation for work order (#29690) * fix: ignore rate validation for work order * test: validate on save instead of on creation Co-authored-by: Ankush Menat --- erpnext/manufacturing/doctype/work_order/test_work_order.py | 3 ++- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index f3beabddcf9..0e150fee23a 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -700,7 +700,8 @@ class TestWorkOrder(ERPNextTestCase): wo = make_wo_order_test_record(item=item_name, qty=1, source_warehouse=source_warehouse, company=company) - self.assertRaises(frappe.ValidationError, make_stock_entry, wo.name, 'Material Transfer for Manufacture') + stock_entry = frappe.get_doc(make_stock_entry(wo.name, 'Material Transfer for Manufacture')) + self.assertRaises(frappe.ValidationError, stock_entry.save) def test_wo_completion_with_pl_bom(self): from erpnext.manufacturing.doctype.bom.test_bom import ( diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index dc49f984f13..f7109ab6b0d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1116,7 +1116,7 @@ class StockEntry(StockController): self.set_actual_qty() self.update_items_for_process_loss() self.validate_customer_provided_item() - self.calculate_rate_and_amount() + self.calculate_rate_and_amount(raise_error_if_no_rate=False) def set_scrap_items(self): if self.purpose != "Send to Subcontractor" and self.purpose in ["Manufacture", "Repack"]: From e5112e16a85c6ede892025477b3a279227eba9ea Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 1 Feb 2022 14:14:04 +0530 Subject: [PATCH 022/129] fix: typeerror on invoice creation from SO/PO (cherry picked from commit 9bd56b0f79af4970ce6c1762d647725fba4ebbf9) --- erpnext/controllers/accounts_controller.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index b7b198eac4c..582818406b9 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1319,6 +1319,9 @@ class AccountsController(TransactionBase): payment_schedule['discount_type'] = schedule.discount_type payment_schedule['discount'] = schedule.discount + if not schedule.invoice_portion: + payment_schedule['payment_amount'] = schedule.payment_amount + self.append("payment_schedule", payment_schedule) def set_due_date(self): From 5363b1922ac2812ed2920f106ce1ed9c13cd2768 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 5 Feb 2022 16:05:46 +0530 Subject: [PATCH 023/129] fix: earned leaves not allocated if assignment is created on month-end (cherry picked from commit 25c7f850b14f1f423631225725ad7d2e9647049f) # Conflicts: # erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py --- .../leave_policy_assignment.py | 23 +++++++++++++++++-- erpnext/hr/utils.py | 22 ++++++++++++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index c79216a275d..0ae368f9df4 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -8,8 +8,12 @@ from math import ceil import frappe from frappe import _, bold from frappe.model.document import Document +<<<<<<< HEAD from frappe.utils import date_diff, flt, formatdate, get_datetime, getdate from six import string_types +======= +from frappe.utils import date_diff, flt, formatdate, get_datetime, get_last_day, getdate +>>>>>>> 25c7f850b1 (fix: earned leaves not allocated if assignment is created on month-end) class LeavePolicyAssignment(Document): @@ -109,8 +113,8 @@ class LeavePolicyAssignment(Document): def get_leaves_for_passed_months(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining): from erpnext.hr.utils import get_monthly_earned_leave - current_month = get_datetime().month - current_year = get_datetime().year + current_month = get_datetime(frappe.flags.current_date).month or get_datetime().month + current_year = get_datetime(frappe.flags.current_date).year or get_datetime().year from_date = frappe.db.get_value("Leave Period", self.leave_period, "from_date") if getdate(date_of_joining) > getdate(from_date): @@ -120,10 +124,14 @@ class LeavePolicyAssignment(Document): from_date_year = get_datetime(from_date).year months_passed = 0 + if current_year == from_date_year and current_month > from_date_month: months_passed = current_month - from_date_month + months_passed = add_current_month_if_applicable(months_passed) + elif current_year > from_date_year: months_passed = (12 - from_date_month) + current_month + months_passed = add_current_month_if_applicable(months_passed) if months_passed > 0: monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated, @@ -135,6 +143,17 @@ class LeavePolicyAssignment(Document): return new_leaves_allocated +def add_current_month_if_applicable(months_passed): + date = getdate(frappe.flags.current_date) or getdate() + last_day_of_month = get_last_day(date) + + # if its the last day of the month, then that month should also be considered + if last_day_of_month == date: + months_passed += 1 + + return months_passed + + @frappe.whitelist() def create_assignment_for_multiple_employees(employees, data): diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 0b2f99c358e..6cf7df371e7 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -393,9 +393,12 @@ def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type new_allocation = e_leave_type.max_leaves_allowed if new_allocation != allocation.total_leaves_allocated: - allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) today_date = today() - create_additional_leave_ledger_entry(allocation, earned_leaves, today_date) + + if not is_earned_leave_already_allocated(allocation, annual_allocation): + allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) + create_additional_leave_ledger_entry(allocation, earned_leaves, today_date) + def get_monthly_earned_leave(annual_leaves, frequency, rounding): earned_leaves = 0.0 @@ -413,6 +416,21 @@ def get_monthly_earned_leave(annual_leaves, frequency, rounding): return earned_leaves +def is_earned_leave_already_allocated(allocation, annual_allocation): + from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import get_leave_type_details + + leave_type_details = get_leave_type_details() + date_of_joining = frappe.db.get_value("Employee", allocation.employee, "date_of_joining") + + assignment = frappe.get_doc("Leave Policy Assignment", allocation.leave_policy_assignment) + leaves_for_passed_months = assignment.get_leaves_for_passed_months(allocation.leave_type, + annual_allocation, leave_type_details, date_of_joining) + + if allocation.total_leaves_allocated >= leaves_for_passed_months: + return True + return False + + def get_leave_allocations(date, leave_type): return frappe.db.sql("""select name, employee, from_date, to_date, leave_policy_assignment, leave_policy from `tabLeave Allocation` From d26a8d47e4a4ad6faa47bcf67700dba28399efae Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 5 Feb 2022 16:05:54 +0530 Subject: [PATCH 024/129] test: earned leave allocation for passed months and allocation on month-end (cherry picked from commit 63ee4f1b64b0110d6d97f4114605db2732dcb224) --- .../test_leave_policy_assignment.py | 73 +++++++++++++++++-- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py index 8953a51e8bb..6dd589182fc 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py @@ -4,7 +4,7 @@ import unittest import frappe -from frappe.utils import add_months, get_first_day, getdate +from frappe.utils import add_months, get_first_day, get_last_day, getdate from erpnext.hr.doctype.leave_application.test_leave_application import ( get_employee, @@ -124,6 +124,69 @@ class TestLeavePolicyAssignment(unittest.TestCase): }, "total_leaves_allocated") self.assertEqual(leaves_allocated, 0) + def test_earned_leave_allocation_for_passed_months(self): + employee = get_employee() + leave_type = create_earned_leave_type("Test Earned Leave") + leave_period = create_leave_period("Test Earned Leave Period", + start_date=get_first_day(add_months(getdate(), -1))) + leave_policy = frappe.get_doc({ + "doctype": "Leave Policy", + "title": "Test Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] + }).insert() + + # Case 1: assignment created one month after the leave period, should allocate 1 leave + frappe.flags.current_date = get_first_day(getdate()) + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": leave_period.name + } + leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + + leaves_allocated = frappe.db.get_value("Leave Allocation", { + "leave_policy_assignment": leave_policy_assignments[0] + }, "total_leaves_allocated") + self.assertEqual(leaves_allocated, 1) + + def test_earned_leave_allocation_for_passed_months_on_month_end(self): + employee = get_employee() + leave_type = create_earned_leave_type("Test Earned Leave") + leave_period = create_leave_period("Test Earned Leave Period", + start_date=get_first_day(add_months(getdate(), -2))) + leave_policy = frappe.get_doc({ + "doctype": "Leave Policy", + "title": "Test Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] + }).insert() + + # Case 2: assignment created on the last day of the leave period's latter month + # should allocate 1 leave for current month even though the month has not ended + # since the daily job might have already executed + frappe.flags.current_date = get_last_day(getdate()) + + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": leave_period.name + } + leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + + leaves_allocated = frappe.db.get_value("Leave Allocation", { + "leave_policy_assignment": leave_policy_assignments[0] + }, "total_leaves_allocated") + self.assertEqual(leaves_allocated, 3) + + # if the daily job is not completed yet, there is another check present + # to ensure leave is not already allocated to avoid duplication + from erpnext.hr.utils import allocate_earned_leaves + allocate_earned_leaves() + + leaves_allocated = frappe.db.get_value("Leave Allocation", { + "leave_policy_assignment": leave_policy_assignments[0] + }, "total_leaves_allocated") + self.assertEqual(leaves_allocated, 3) + def tearDown(self): frappe.db.rollback() @@ -136,14 +199,14 @@ def create_earned_leave_type(leave_type): doctype="Leave Type", is_earned_leave=1, earned_leave_frequency="Monthly", - rounding=0.5, - max_leaves_allowed=6 + rounding=0.5 )).insert() -def create_leave_period(name): +def create_leave_period(name, start_date=None): frappe.delete_doc_if_exists("Leave Period", name, force=1) - start_date = get_first_day(getdate()) + if not start_date: + start_date = get_first_day(getdate()) return frappe.get_doc(dict( name=name, From ce26759a95b21d776e3cf24efd92d3b99ca776ff Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 5 Feb 2022 16:40:55 +0530 Subject: [PATCH 025/129] fix: linter (cherry picked from commit a52ba0a5447df1998b7900230ce1cdb4a0a3dace) --- erpnext/hr/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 6cf7df371e7..4ddfaafd104 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -417,7 +417,9 @@ def get_monthly_earned_leave(annual_leaves, frequency, rounding): def is_earned_leave_already_allocated(allocation, annual_allocation): - from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import get_leave_type_details + from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import ( + get_leave_type_details, + ) leave_type_details = get_leave_type_details() date_of_joining = frappe.db.get_value("Employee", allocation.employee, "date_of_joining") From aa04e027107a2a5652a6b2b61fc54ca137b767f5 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 6 Feb 2022 20:30:46 +0530 Subject: [PATCH 026/129] fix(test): add ignore duplicates flag to allocation function (cherry picked from commit e25544f94e20f675befd71e58df8156649cbf1f0) --- .../doctype/leave_application/test_leave_application.py | 4 ++-- erpnext/hr/utils.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 93f4176bd5f..39356bdcf18 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -545,7 +545,7 @@ class TestLeaveApplication(unittest.TestCase): from erpnext.hr.utils import allocate_earned_leaves i = 0 while(i<14): - allocate_earned_leaves() + allocate_earned_leaves(ignore_duplicates=True) i += 1 self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6) @@ -553,7 +553,7 @@ class TestLeaveApplication(unittest.TestCase): frappe.db.set_value('Leave Type', leave_type, 'max_leaves_allowed', 0) i = 0 while(i<6): - allocate_earned_leaves() + allocate_earned_leaves(ignore_duplicates=True) i += 1 self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9) diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 4ddfaafd104..622ee3f9389 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -353,7 +353,7 @@ def generate_leave_encashment(): create_leave_encashment(leave_allocation=leave_allocation) -def allocate_earned_leaves(): +def allocate_earned_leaves(ignore_duplicates=False): '''Allocate earned leaves to Employees''' e_leave_types = get_earned_leaves() today = getdate() @@ -381,9 +381,9 @@ def allocate_earned_leaves(): from_date = frappe.db.get_value("Employee", allocation.employee, "date_of_joining") if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining_date): - update_previous_leave_allocation(allocation, annual_allocation, e_leave_type) + update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates) -def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type): +def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates=False): earned_leaves = get_monthly_earned_leave(annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding) allocation = frappe.get_doc('Leave Allocation', allocation.name) @@ -395,7 +395,7 @@ def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type if new_allocation != allocation.total_leaves_allocated: today_date = today() - if not is_earned_leave_already_allocated(allocation, annual_allocation): + if ignore_duplicates or not is_earned_leave_already_allocated(allocation, annual_allocation): allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) create_additional_leave_ledger_entry(allocation, earned_leaves, today_date) From a5f016a7cd7ac952fde70f7127d52a61703bb22f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 8 Feb 2022 14:36:31 +0530 Subject: [PATCH 027/129] fix: handle carry forwarded leaves while checking for duplicate allocation (cherry picked from commit bd1555bd230c0932bc0b7476f1ca68092a697e51) --- .../test_leave_policy_assignment.py | 55 ++++++++++++++++++- erpnext/hr/utils.py | 7 ++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py index 6dd589182fc..26821f58e2d 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py @@ -187,6 +187,58 @@ class TestLeavePolicyAssignment(unittest.TestCase): }, "total_leaves_allocated") self.assertEqual(leaves_allocated, 3) + def test_earned_leave_allocation_for_passed_months_with_carry_forwarded_leaves(self): + from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation + + employee = get_employee() + leave_type = create_earned_leave_type("Test Earned Leave") + leave_period = create_leave_period("Test Earned Leave Period", + start_date=get_first_day(add_months(getdate(), -2))) + leave_policy = frappe.get_doc({ + "doctype": "Leave Policy", + "title": "Test Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] + }).insert() + + # initial leave allocation = 5 + leave_allocation = create_leave_allocation( + employee=employee.name, + employee_name=employee.employee_name, + leave_type=leave_type.name, + from_date=add_months(getdate(), -12), + to_date=add_months(getdate(), -3), + new_leaves_allocated=5, + carry_forward=0) + leave_allocation.submit() + + # Case 3: assignment created on the last day of the leave period's latter month with carry forwarding + frappe.flags.current_date = get_last_day(add_months(getdate(), -1)) + + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": leave_period.name, + "carry_forward": 1 + } + # carry forwarded leaves = 5, 3 leaves allocated for passed months + leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + + details = frappe.db.get_value("Leave Allocation", { + "leave_policy_assignment": leave_policy_assignments[0] + }, ["total_leaves_allocated", "new_leaves_allocated", "unused_leaves", "name"], as_dict=True) + self.assertEqual(details.new_leaves_allocated, 2) + self.assertEqual(details.unused_leaves, 5) + self.assertEqual(details.total_leaves_allocated, 7) + + # if the daily job is not completed yet, there is another check present + # to ensure leave is not already allocated to avoid duplication + from erpnext.hr.utils import is_earned_leave_already_allocated + frappe.flags.current_date = get_last_day(getdate()) + + allocation = frappe.get_doc('Leave Allocation', details.name) + # 1 leave is still pending to be allocated, irrespective of carry forwarded leaves + self.assertFalse(is_earned_leave_already_allocated(allocation, leave_policy.leave_policy_details[0].annual_allocation)) + def tearDown(self): frappe.db.rollback() @@ -199,7 +251,8 @@ def create_earned_leave_type(leave_type): doctype="Leave Type", is_earned_leave=1, earned_leave_frequency="Monthly", - rounding=0.5 + rounding=0.5, + is_carry_forward=1 )).insert() diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 622ee3f9389..6032d0c1bd3 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -428,7 +428,12 @@ def is_earned_leave_already_allocated(allocation, annual_allocation): leaves_for_passed_months = assignment.get_leaves_for_passed_months(allocation.leave_type, annual_allocation, leave_type_details, date_of_joining) - if allocation.total_leaves_allocated >= leaves_for_passed_months: + # exclude carry-forwarded leaves while checking for leave allocation for passed months + num_allocations = allocation.total_leaves_allocated + if allocation.unused_leaves: + num_allocations -= allocation.unused_leaves + + if num_allocations >= leaves_for_passed_months: return True return False From cc867d6338e42c76c976e0632d926ec7eda2b024 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 8 Feb 2022 15:55:44 +0530 Subject: [PATCH 028/129] fix: conflicts --- .../leave_policy_assignment/leave_policy_assignment.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index 0ae368f9df4..fe96f3db4f2 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -8,12 +8,7 @@ from math import ceil import frappe from frappe import _, bold from frappe.model.document import Document -<<<<<<< HEAD -from frappe.utils import date_diff, flt, formatdate, get_datetime, getdate -from six import string_types -======= from frappe.utils import date_diff, flt, formatdate, get_datetime, get_last_day, getdate ->>>>>>> 25c7f850b1 (fix: earned leaves not allocated if assignment is created on month-end) class LeavePolicyAssignment(Document): From e3246efa5db6c9de199083f6973c1aabf64d1f9f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 8 Feb 2022 16:12:24 +0530 Subject: [PATCH 029/129] fix: conflicts --- .../doctype/leave_policy_assignment/leave_policy_assignment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index fe96f3db4f2..2115b05e92f 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -9,6 +9,7 @@ import frappe from frappe import _, bold from frappe.model.document import Document from frappe.utils import date_diff, flt, formatdate, get_datetime, get_last_day, getdate +from six import string_types class LeavePolicyAssignment(Document): From df870e4b22e1217d6ed2960c4304cddb70581855 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 8 Feb 2022 15:50:41 +0530 Subject: [PATCH 030/129] fix: currency in bank reconciliation chart (cherry picked from commit 0fc5d2278d2785385051d576e895c8286ad7a3a2) --- .../bank_reconciliation_tool/bank_reconciliation_tool.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js index dd7409f4b01..398e4576a68 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js @@ -14,6 +14,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", { }); }, + onload: function (frm) { + frm.trigger('bank_account'); + }, + refresh: function (frm) { frappe.require("assets/js/bank-reconciliation-tool.min.js", () => frm.trigger("make_reconciliation_tool") @@ -51,7 +55,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", { bank_account: function (frm) { frappe.db.get_value( "Bank Account", - frm.bank_account, + frm.doc.bank_account, "account", (r) => { frappe.db.get_value( From e69f70f264e35f38dfa2d8c70f7a29d1b0dcf4ce Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 8 Feb 2022 21:54:07 +0530 Subject: [PATCH 031/129] fix: fetch image form item (backport #29523) (#29571) * fix: fetch image form item (#29523) (cherry picked from commit 319322228aeb39e2d76c8b7bfd23a335f725579a) # Conflicts: # erpnext/assets/doctype/asset/asset.json * chore: resolve merge conflicts Co-authored-by: Himanshu Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/assets/doctype/asset/asset.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index de060757e2e..411e1efe8ea 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -141,6 +141,7 @@ }, { "allow_on_submit": 1, + "fetch_from": "item_code.image", "fieldname": "image", "fieldtype": "Attach Image", "hidden": 1, @@ -502,7 +503,7 @@ "link_fieldname": "asset" } ], - "modified": "2021-06-24 14:58:51.097908", + "modified": "2022-01-30 20:19:24.680027", "modified_by": "Administrator", "module": "Assets", "name": "Asset", @@ -544,4 +545,4 @@ "sort_order": "DESC", "title_field": "asset_name", "track_changes": 1 -} \ No newline at end of file +} From 2572480554db265e2e93a5dfba75749675b46d14 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 8 Feb 2022 22:56:14 +0530 Subject: [PATCH 032/129] fix: Loan repayment via Salary Slip --- .../doctype/loan_repayment/loan_repayment.py | 6 ++++-- erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py | 2 +- erpnext/payroll/doctype/salary_slip/test_salary_slip.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 65099da49b4..46c8b4ac548 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -346,7 +346,7 @@ class LoanRepayment(AccountsController): gle_map.append( self.get_gl_dict({ "account": loan_details.penalty_income_account, - "against": payment_account, + "against": loan_details.loan_account, "credit": self.total_penalty_paid, "credit_in_account_currency": self.total_penalty_paid, "against_voucher_type": "Loan", @@ -368,7 +368,9 @@ class LoanRepayment(AccountsController): "against_voucher": self.against_loan, "remarks": remarks, "cost_center": self.cost_center, - "posting_date": getdate(self.posting_date) + "posting_date": getdate(self.posting_date), + "party_type": loan_details.applicant_type if self.repay_from_salary else '', + "party": loan_details.applicant if self.repay_from_salary else '' }) ) diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index c6f38972880..f7e5442ee0e 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -125,7 +125,7 @@ class TestPayrollEntry(unittest.TestCase): if not frappe.db.exists("Account", "_Test Payroll Payable - _TC"): create_account(account_name="_Test Payroll Payable", - company="_Test Company", parent_account="Current Liabilities - _TC") + company="_Test Company", parent_account="Current Liabilities - _TC", account_type=None) if not frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") or \ frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC": diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index cc011a50c6b..314f74edf70 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -730,7 +730,7 @@ def get_salary_component_account(sal_comp, company_list=None): }) sal_comp.save() -def create_account(account_name, company, parent_account): +def create_account(account_name, company, parent_account, account_type=None): company_abbr = frappe.get_cached_value('Company', company, 'abbr') account = frappe.db.get_value("Account", account_name + " - " + company_abbr) if not account: From bc80f34d92bd5d8335243813d907f4b2919602c4 Mon Sep 17 00:00:00 2001 From: aaronmenezes Date: Tue, 8 Feb 2022 19:25:49 +0530 Subject: [PATCH 033/129] fix: Reserved for Production calculation considered closed work orders (cherry picked from commit 6a8b7eeffecba15e8a664449b6d92f5a8aa8d2cf) # Conflicts: # erpnext/stock/doctype/bin/bin.py --- .../doctype/work_order/test_work_order.py | 13 ++++++++++ erpnext/stock/doctype/bin/bin.py | 25 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 0e150fee23a..7be816062ec 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -198,6 +198,19 @@ class TestWorkOrder(ERPNextTestCase): self.assertEqual(cint(bin1_on_end_production.reserved_qty_for_production), cint(bin1_on_start_production.reserved_qty_for_production)) + def test_reserved_qty_for_production(self): + self.bin1_at_start = get_bin(self.item, self.warehouse) + self.bin1_at_start.update_reserved_qty_for_production() + self.test_reserved_qty_for_production_submit() + self.test_reserved_qty_for_production_cancel() + self.test_close_work_order() + self.wo_order = make_wo_order_test_record(item="_Test FG Item", qty=2, + source_warehouse=self.warehouse) + self.bin1_on_submit = get_bin(self.item, self.warehouse) + bin1_on_end_production = get_bin(self.item, self.warehouse) + self.assertEqual(cint(bin1_on_end_production.reserved_qty_for_production), + cint(self.bin1_at_start.reserved_qty_for_production) + 2) + def test_backflush_qty_for_overpduction_manufacture(self): cancel_stock_entry = [] allow_overproduction("overproduction_percentage_for_work_order", 30) diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 1d874cd06fb..e6412e9426c 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -31,6 +31,7 @@ class Bin(Document): def update_reserved_qty_for_production(self): '''Update qty reserved for production from Production Item tables in open work orders''' +<<<<<<< HEAD self.reserved_qty_for_production = frappe.db.sql(''' SELECT SUM(CASE WHEN ifnull(skip_transfer, 0) = 0 THEN @@ -47,6 +48,30 @@ class Bin(Document): and pro.status not in ("Stopped", "Completed") and (item.required_qty > item.transferred_qty or item.required_qty > item.consumed_qty) ''', (self.item_code, self.warehouse))[0][0] +======= + + wo = frappe.qb.DocType("Work Order") + wo_item = frappe.qb.DocType("Work Order Item") + + self.reserved_qty_for_production = ( + frappe.qb + .from_(wo) + .from_(wo_item) + .select(Sum(Case() + .when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty) + .else_(wo_item.required_qty - wo_item.consumed_qty)) + ) + .where( + (wo_item.item_code == self.item_code) + & (wo_item.parent == wo.name) + & (wo.docstatus == 1) + & (wo_item.source_warehouse == self.warehouse) + & (wo.status.notin(["Stopped", "Completed", "Closed"])) + & ((wo_item.required_qty > wo_item.transferred_qty) + | (wo_item.required_qty > wo_item.consumed_qty)) + ) + ).run()[0][0] or 0.0 +>>>>>>> 6a8b7eeffe (fix: Reserved for Production calculation considered closed work orders) self.set_projected_qty() From bde3bece36e3753cf0386fc9a043b13bfe2a974d Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 9 Feb 2022 11:19:01 +0530 Subject: [PATCH 034/129] test: remove dependency on other tests (cherry picked from commit d2cc5f2482727ee82ef914ea768311a5c1f94996) --- .../doctype/work_order/test_work_order.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 7be816062ec..975216d1bd9 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -198,18 +198,20 @@ class TestWorkOrder(ERPNextTestCase): self.assertEqual(cint(bin1_on_end_production.reserved_qty_for_production), cint(bin1_on_start_production.reserved_qty_for_production)) - def test_reserved_qty_for_production(self): - self.bin1_at_start = get_bin(self.item, self.warehouse) - self.bin1_at_start.update_reserved_qty_for_production() - self.test_reserved_qty_for_production_submit() - self.test_reserved_qty_for_production_cancel() - self.test_close_work_order() - self.wo_order = make_wo_order_test_record(item="_Test FG Item", qty=2, + def test_reserved_qty_for_production_closed(self): + + wo1 = make_wo_order_test_record(item="_Test FG Item", qty=2, source_warehouse=self.warehouse) - self.bin1_on_submit = get_bin(self.item, self.warehouse) - bin1_on_end_production = get_bin(self.item, self.warehouse) - self.assertEqual(cint(bin1_on_end_production.reserved_qty_for_production), - cint(self.bin1_at_start.reserved_qty_for_production) + 2) + item = wo1.required_items[0].item_code + bin_before = get_bin(item, self.warehouse) + bin_before.update_reserved_qty_for_production() + + make_wo_order_test_record(item="_Test FG Item", qty=2, + source_warehouse=self.warehouse) + close_work_order(wo1.name, "Closed") + + bin_after = get_bin(item, self.warehouse) + self.assertEqual(bin_before.reserved_qty_for_production, bin_after.reserved_qty_for_production) def test_backflush_qty_for_overpduction_manufacture(self): cancel_stock_entry = [] From 197bf8fbec45c9856d8bf120210cd01605cb5ebd Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 9 Feb 2022 11:26:03 +0530 Subject: [PATCH 035/129] refactor: move reserve quantity computation to work order (cherry picked from commit a8bf3a3f0d21ba8b841b69b2185c9d2bd46cd3f2) # Conflicts: # erpnext/stock/doctype/bin/bin.py --- .../doctype/work_order/work_order.py | 26 +++++++++++++++++++ erpnext/stock/doctype/bin/bin.py | 6 +++++ 2 files changed, 32 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index c5daba58c3d..46e02b0cdd4 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -8,6 +8,8 @@ from dateutil.relativedelta import relativedelta from frappe import _ from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc +from frappe.query_builder import Case +from frappe.query_builder.functions import Sum from frappe.utils import ( cint, date_diff, @@ -1171,3 +1173,27 @@ def create_pick_list(source_name, target_doc=None, for_qty=None): doc.set_item_locations() return doc + +def get_reserved_qty_for_production(item_code: str, warehouse: str) -> float: + """Get total reserved quantity for any item in specified warehouse""" + wo = frappe.qb.DocType("Work Order") + wo_item = frappe.qb.DocType("Work Order Item") + + return ( + frappe.qb + .from_(wo) + .from_(wo_item) + .select(Sum(Case() + .when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty) + .else_(wo_item.required_qty - wo_item.consumed_qty)) + ) + .where( + (wo_item.item_code == item_code) + & (wo_item.parent == wo.name) + & (wo.docstatus == 1) + & (wo_item.source_warehouse == warehouse) + & (wo.status.notin(["Stopped", "Completed", "Closed"])) + & ((wo_item.required_qty > wo_item.transferred_qty) + | (wo_item.required_qty > wo_item.consumed_qty)) + ) + ).run()[0][0] or 0.0 diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index e6412e9426c..fe897bc0168 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -31,6 +31,7 @@ class Bin(Document): def update_reserved_qty_for_production(self): '''Update qty reserved for production from Production Item tables in open work orders''' +<<<<<<< HEAD <<<<<<< HEAD self.reserved_qty_for_production = frappe.db.sql(''' SELECT @@ -72,6 +73,11 @@ class Bin(Document): ) ).run()[0][0] or 0.0 >>>>>>> 6a8b7eeffe (fix: Reserved for Production calculation considered closed work orders) +======= + from erpnext.manufacturing.doctype.work_order.work_order import get_reserved_qty_for_production + + self.reserved_qty_for_production = get_reserved_qty_for_production(self.item_code, self.warehouse) +>>>>>>> a8bf3a3f0d (refactor: move reserve quantity computation to work order) self.set_projected_qty() From 0ac16f9e63a6daa9299d16021af9863259697c6c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 9 Feb 2022 11:38:52 +0530 Subject: [PATCH 036/129] fix: patch existing bins (cherry picked from commit e134524532db0473bef48b7e558e5d7a289f090e) # Conflicts: # erpnext/patches.txt --- erpnext/patches.txt | 8 +++++- .../v13_0/update_reserved_qty_closed_wo.py | 28 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v13_0/update_reserved_qty_closed_wo.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 0a7eb33f66b..3e91e9de4c9 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -341,10 +341,16 @@ erpnext.patches.v13_0.wipe_serial_no_field_for_0_qty erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021 erpnext.patches.v13_0.update_tax_category_for_rcm erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template +<<<<<<< HEAD erpnext.patches.v13_0.agriculture_deprecation_warning erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit erpnext.patches.v13_0.hospitality_deprecation_warning erpnext.patches.v13_0.delete_bank_reconciliation_detail erpnext.patches.v13_0.update_sane_transfer_against erpnext.patches.v13_0.enable_provisional_accounting -erpnext.patches.v13_0.update_disbursement_account \ No newline at end of file +erpnext.patches.v13_0.update_disbursement_account +======= +erpnext.patches.v13_0.shopping_cart_to_ecommerce +erpnext.patches.v13_0.update_disbursement_account +erpnext.patches.v13_0.update_reserved_qty_closed_wo +>>>>>>> e134524532 (fix: patch existing bins) diff --git a/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py b/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py new file mode 100644 index 00000000000..62f774ae06f --- /dev/null +++ b/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py @@ -0,0 +1,28 @@ +import frappe + +from erpnext.stock.utils import get_bin + + +def execute(self): + + wo = frappe.qb.DocType("Work Order") + wo_item = frappe.qb.DocType("Work Order Item") + + incorrect_item_wh = ( + frappe.qb + .from_(wo) + .join(wo_item).on(wo.name == wo_item.parent) + .select(wo_item.item_code, wo.source_warehouse).distinct() + .where( + (wo.status == "Closed") + & (wo.docstatus == 1) + & (wo.source_warehouse.notnull()) + ) + ).run(debug=True) + + for item_code, warehouse in incorrect_item_wh: + if not (item_code and warehouse): + continue + + bin = get_bin(item_code, warehouse) + bin.update_reserved_qty_for_production() From 94fc5f95db39fd2caa1bbc8fa6e0a6c4677dad4f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 9 Feb 2022 11:53:14 +0530 Subject: [PATCH 037/129] fix: concflicts --- erpnext/patches.txt | 5 --- .../v13_0/update_reserved_qty_closed_wo.py | 4 +- erpnext/stock/doctype/bin/bin.py | 45 ------------------- 3 files changed, 2 insertions(+), 52 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 3e91e9de4c9..4bbeb64ce1c 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -341,7 +341,6 @@ erpnext.patches.v13_0.wipe_serial_no_field_for_0_qty erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021 erpnext.patches.v13_0.update_tax_category_for_rcm erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template -<<<<<<< HEAD erpnext.patches.v13_0.agriculture_deprecation_warning erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit erpnext.patches.v13_0.hospitality_deprecation_warning @@ -349,8 +348,4 @@ erpnext.patches.v13_0.delete_bank_reconciliation_detail erpnext.patches.v13_0.update_sane_transfer_against erpnext.patches.v13_0.enable_provisional_accounting erpnext.patches.v13_0.update_disbursement_account -======= -erpnext.patches.v13_0.shopping_cart_to_ecommerce -erpnext.patches.v13_0.update_disbursement_account erpnext.patches.v13_0.update_reserved_qty_closed_wo ->>>>>>> e134524532 (fix: patch existing bins) diff --git a/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py b/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py index 62f774ae06f..00926b09241 100644 --- a/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py +++ b/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py @@ -3,7 +3,7 @@ import frappe from erpnext.stock.utils import get_bin -def execute(self): +def execute(): wo = frappe.qb.DocType("Work Order") wo_item = frappe.qb.DocType("Work Order Item") @@ -18,7 +18,7 @@ def execute(self): & (wo.docstatus == 1) & (wo.source_warehouse.notnull()) ) - ).run(debug=True) + ).run() for item_code, warehouse in incorrect_item_wh: if not (item_code and warehouse): diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index fe897bc0168..b2ec15690c2 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -31,54 +31,9 @@ class Bin(Document): def update_reserved_qty_for_production(self): '''Update qty reserved for production from Production Item tables in open work orders''' -<<<<<<< HEAD -<<<<<<< HEAD - self.reserved_qty_for_production = frappe.db.sql(''' - SELECT - SUM(CASE WHEN ifnull(skip_transfer, 0) = 0 THEN - item.required_qty - item.transferred_qty - ELSE - item.required_qty - item.consumed_qty END) - END - FROM `tabWork Order` pro, `tabWork Order Item` item - WHERE - item.item_code = %s - and item.parent = pro.name - and pro.docstatus = 1 - and item.source_warehouse = %s - and pro.status not in ("Stopped", "Completed") - and (item.required_qty > item.transferred_qty or item.required_qty > item.consumed_qty) - ''', (self.item_code, self.warehouse))[0][0] -======= - - wo = frappe.qb.DocType("Work Order") - wo_item = frappe.qb.DocType("Work Order Item") - - self.reserved_qty_for_production = ( - frappe.qb - .from_(wo) - .from_(wo_item) - .select(Sum(Case() - .when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty) - .else_(wo_item.required_qty - wo_item.consumed_qty)) - ) - .where( - (wo_item.item_code == self.item_code) - & (wo_item.parent == wo.name) - & (wo.docstatus == 1) - & (wo_item.source_warehouse == self.warehouse) - & (wo.status.notin(["Stopped", "Completed", "Closed"])) - & ((wo_item.required_qty > wo_item.transferred_qty) - | (wo_item.required_qty > wo_item.consumed_qty)) - ) - ).run()[0][0] or 0.0 ->>>>>>> 6a8b7eeffe (fix: Reserved for Production calculation considered closed work orders) -======= from erpnext.manufacturing.doctype.work_order.work_order import get_reserved_qty_for_production self.reserved_qty_for_production = get_reserved_qty_for_production(self.item_code, self.warehouse) ->>>>>>> a8bf3a3f0d (refactor: move reserve quantity computation to work order) - self.set_projected_qty() self.db_set('reserved_qty_for_production', flt(self.reserved_qty_for_production)) From 78c5f34286a21c548489bac406337b015118cc3c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 20 Jan 2022 12:22:56 +0530 Subject: [PATCH 038/129] fix: ignore pricing rule in all transactions (cherry picked from commit f6dda738dc99060090e703b21f7a77692887605b) --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 4 +++- erpnext/public/js/controllers/transaction.js | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 23606cec53f..41faa2a509e 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -250,7 +250,8 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa "free_item_data": [], "parent": args.parent, "parenttype": args.parenttype, - "child_docname": args.get('child_docname') + "child_docname": args.get('child_docname'), + "price_list_rate": args.get('price_list_rate') }) if args.ignore_pricing_rule or not args.item_code: @@ -404,6 +405,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None): if pricing_rule.rate_or_discount == 'Discount Percentage': item_details.discount_percentage = 0.0 item_details.discount_amount = 0.0 + item_details.rate = item_details.get('price_list_rate', 0) if pricing_rule.rate_or_discount == 'Discount Amount': item_details.discount_amount = 0.0 diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 3730f15078e..76de31770a5 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1443,7 +1443,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ "item_code": d.item_code, "pricing_rules": d.pricing_rules, "parenttype": d.parenttype, - "parent": d.parent + "parent": d.parent, + "price_list_rate": d.price_list_rate }) } }); From fc67f02ffca032eb9e1e4f74746b2de040def9ce Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 20 Jan 2022 13:06:08 +0530 Subject: [PATCH 039/129] test: item price on remove pricing rule (cherry picked from commit b8c41e303035993d98aeb406865052a968335afe) # Conflicts: # erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py --- .../doctype/pricing_rule/pricing_rule.py | 1 + .../doctype/pricing_rule/test_pricing_rule.py | 43 +++++++++++++++++++ erpnext/controllers/accounts_controller.py | 8 ++++ 3 files changed, 52 insertions(+) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 41faa2a509e..b5021ddfcea 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -424,6 +424,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None): item_details.applied_on_items = ','.join(items) item_details.pricing_rules = '' + item_details.pricing_rule_removed = True return item_details diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 6571e1674c2..48f92589e40 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -630,6 +630,7 @@ class TestPricingRule(unittest.TestCase): for doc in [si, si1]: doc.delete() +<<<<<<< HEAD def test_multiple_pricing_rules_with_min_qty(self): make_pricing_rule(discount_percentage=20, selling=1, priority=1, min_qty=4, apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 1") @@ -649,6 +650,48 @@ class TestPricingRule(unittest.TestCase): frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1") frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2") +======= + def test_remove_pricing_rule(self): + item = make_item("Water Flask") + make_item_price("Water Flask", "_Test Price List", 100) + + pricing_rule_record = { + "doctype": "Pricing Rule", + "title": "_Test Water Flask Rule", + "apply_on": "Item Code", + "price_or_product_discount": "Price", + "items": [{ + "item_code": "Water Flask", + }], + "selling": 1, + "currency": "INR", + "rate_or_discount": "Discount Percentage", + "discount_percentage": 20, + "company": "_Test Company" + } + rule = frappe.get_doc(pricing_rule_record) + rule.insert() + + si = create_sales_invoice(do_not_save=True, item_code="Water Flask") + si.selling_price_list = "_Test Price List" + si.save() + + self.assertEqual(si.items[0].price_list_rate, 100) + self.assertEqual(si.items[0].discount_percentage, 20) + self.assertEqual(si.items[0].rate, 80) + + si.ignore_pricing_rule = 1 + si.save() + + self.assertEqual(si.items[0].discount_percentage, 0) + self.assertEqual(si.items[0].rate, 100) + + si.delete() + rule.delete() + frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete() + item.delete() + +>>>>>>> b8c41e3030 (test: item price on remove pricing rule) test_dependencies = ["Campaign"] diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 582818406b9..b8306518b25 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -408,6 +408,14 @@ class AccountsController(TransactionBase): if item_qty != len(get_serial_nos(item.get('serial_no'))): item.set(fieldname, value) + elif ret.get("pricing_rule_removed") and value is not None \ + and fieldname in [ + 'discount_percentage', 'discount_amount', 'rate', + 'margin_rate_or_amount', 'margin_type', 'remove_free_item' + ]: + # reset pricing rule fields if pricing_rule_removed + item.set(fieldname, value) + if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field('is_fixed_asset'): item.set('is_fixed_asset', ret.get('is_fixed_asset', 0)) From 91f65da4ece6340b9e67915364c90ca4f1785721 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 8 Feb 2022 16:32:08 +0530 Subject: [PATCH 040/129] fix(test): pass price_list_rate only if pricing rule has to be removed (cherry picked from commit 6fa406dd04d7538b38e076cb4636b5713994456d) --- .../doctype/pos_invoice/test_pos_invoice.py | 36 +++++++++++-------- .../doctype/pricing_rule/pricing_rule.py | 30 ++++++++++------ 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index ba751c081bb..cf8affdd010 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -586,23 +586,29 @@ class TestPOSInvoice(unittest.TestCase): item_price.insert() pr = make_pricing_rule(selling=1, priority=5, discount_percentage=10) pr.save() - pos_inv = create_pos_invoice(qty=1, do_not_submit=1) - pos_inv.items[0].rate = 300 - pos_inv.save() - self.assertEquals(pos_inv.items[0].discount_percentage, 10) - # rate shouldn't change - self.assertEquals(pos_inv.items[0].rate, 405) - pos_inv.ignore_pricing_rule = 1 - pos_inv.items[0].rate = 300 - pos_inv.save() - self.assertEquals(pos_inv.ignore_pricing_rule, 1) - # rate should change since pricing rules are ignored - self.assertEquals(pos_inv.items[0].rate, 300) + try: + pos_inv = create_pos_invoice(qty=1, do_not_submit=1) + pos_inv.items[0].rate = 300 + pos_inv.save() + self.assertEquals(pos_inv.items[0].discount_percentage, 10) + # rate shouldn't change + self.assertEquals(pos_inv.items[0].rate, 405) - item_price.delete() - pos_inv.delete() - pr.delete() + pos_inv.ignore_pricing_rule = 1 + pos_inv.save() + self.assertEquals(pos_inv.ignore_pricing_rule, 1) + # rate should reset since pricing rules are ignored + self.assertEquals(pos_inv.items[0].rate, 450) + + pos_inv.items[0].rate = 300 + pos_inv.save() + self.assertEquals(pos_inv.items[0].rate, 300) + + finally: + item_price.delete() + pos_inv.delete() + pr.delete() def create_pos_invoice(**args): diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index b5021ddfcea..ad60bbad950 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -251,13 +251,16 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa "parent": args.parent, "parenttype": args.parenttype, "child_docname": args.get('child_docname'), - "price_list_rate": args.get('price_list_rate') }) if args.ignore_pricing_rule or not args.item_code: if frappe.db.exists(args.doctype, args.name) and args.get("pricing_rules"): - item_details = remove_pricing_rule_for_item(args.get("pricing_rules"), - item_details, args.get('item_code')) + item_details = remove_pricing_rule_for_item( + args.get("pricing_rules"), + item_details, + item_code=args.get("item_code"), + rate=args.get("price_list_rate"), + ) return item_details update_args_for_pricing_rule(args) @@ -310,8 +313,12 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa if not doc: return item_details elif args.get("pricing_rules"): - item_details = remove_pricing_rule_for_item(args.get("pricing_rules"), - item_details, args.get('item_code')) + item_details = remove_pricing_rule_for_item( + args.get("pricing_rules"), + item_details, + item_code=args.get("item_code"), + rate=args.get("price_list_rate"), + ) return item_details @@ -392,7 +399,7 @@ def apply_price_discount_rule(pricing_rule, item_details, args): item_details[field] += (pricing_rule.get(field, 0) if pricing_rule else args.get(field, 0)) -def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None): +def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, rate=None): from erpnext.accounts.doctype.pricing_rule.utils import ( get_applied_pricing_rules, get_pricing_rule_items, @@ -405,7 +412,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None): if pricing_rule.rate_or_discount == 'Discount Percentage': item_details.discount_percentage = 0.0 item_details.discount_amount = 0.0 - item_details.rate = item_details.get('price_list_rate', 0) + item_details.rate = rate or 0.0 if pricing_rule.rate_or_discount == 'Discount Amount': item_details.discount_amount = 0.0 @@ -436,9 +443,12 @@ def remove_pricing_rules(item_list): out = [] for item in item_list: item = frappe._dict(item) - if item.get('pricing_rules'): - out.append(remove_pricing_rule_for_item(item.get("pricing_rules"), - item, item.item_code)) + if item.get("pricing_rules"): + out.append( + remove_pricing_rule_for_item( + item.get("pricing_rules"), item, item.item_code, item.get("price_list_rate") + ) + ) return out From 23a5b3df182e0e62d83dfc0edfd54c3ce0556c58 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 9 Feb 2022 10:10:17 +0530 Subject: [PATCH 041/129] fix: ignore pricing rule in all transactions (cherry picked from commit ab36b27a94f5f88a71358292ef7b76103c7080b7) --- erpnext/controllers/accounts_controller.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index b8306518b25..c5d354e445d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -408,11 +408,19 @@ class AccountsController(TransactionBase): if item_qty != len(get_serial_nos(item.get('serial_no'))): item.set(fieldname, value) - elif ret.get("pricing_rule_removed") and value is not None \ - and fieldname in [ - 'discount_percentage', 'discount_amount', 'rate', - 'margin_rate_or_amount', 'margin_type', 'remove_free_item' - ]: + elif ( + ret.get("pricing_rule_removed") + and value is not None + and fieldname + in [ + "discount_percentage", + "discount_amount", + "rate", + "margin_rate_or_amount", + "margin_type", + "remove_free_item", + ] + ): # reset pricing rule fields if pricing_rule_removed item.set(fieldname, value) From a54e0fe42b38f571396e88b3ebcc46fd3b389301 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Wed, 9 Feb 2022 13:44:14 +0530 Subject: [PATCH 042/129] test: Update account type in payroll payable account --- erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index f7e5442ee0e..2e806f3c057 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -125,7 +125,7 @@ class TestPayrollEntry(unittest.TestCase): if not frappe.db.exists("Account", "_Test Payroll Payable - _TC"): create_account(account_name="_Test Payroll Payable", - company="_Test Company", parent_account="Current Liabilities - _TC", account_type=None) + company="_Test Company", parent_account="Current Liabilities - _TC", account_type="Payable") if not frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") or \ frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC": From 32b55e715a4589e03e15dd46a5e958860b1268da Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 8 Feb 2022 11:26:23 +0530 Subject: [PATCH 043/129] fix(pos): incorrect grand_total in case of inclusive taxes on item (cherry picked from commit 0452d7de20a8eddc1403d20b5f6cfba12eb63e82) --- .../pos_invoice_merge_log.py | 26 +++- .../test_pos_invoice_merge_log.py | 115 ++++++++++++++++++ erpnext/controllers/taxes_and_totals.py | 3 + 3 files changed, 140 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 0cd19549f60..5ec0d0a51e6 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -85,12 +85,21 @@ class POSInvoiceMergeLog(Document): sales_invoice.set_posting_time = 1 sales_invoice.posting_date = getdate(self.posting_date) sales_invoice.save() + self.write_off_fractional_amount(sales_invoice, data) sales_invoice.submit() self.consolidated_invoice = sales_invoice.name return sales_invoice.name + def write_off_fractional_amount(self, invoice, data): + pos_invoice_grand_total = sum(d.grand_total for d in data) + + if abs(pos_invoice_grand_total - invoice.grand_total) < 1: + + invoice.write_off_amount += -1 * (pos_invoice_grand_total - invoice.grand_total) + invoice.save() + def process_merging_into_credit_note(self, data): credit_note = self.get_new_sales_invoice() credit_note.is_return = 1 @@ -103,6 +112,7 @@ class POSInvoiceMergeLog(Document): # TODO: return could be against multiple sales invoice which could also have been consolidated? # credit_note.return_against = self.consolidated_invoice credit_note.save() + self.write_off_fractional_amount(credit_note, data) credit_note.submit() self.consolidated_credit_note = credit_note.name @@ -136,9 +146,15 @@ class POSInvoiceMergeLog(Document): i.uom == item.uom and i.net_rate == item.net_rate and i.warehouse == item.warehouse): found = True i.qty = i.qty + item.qty + i.amount = i.amount + item.net_amount + i.net_amount = i.amount + i.base_amount = i.base_amount + item.base_net_amount + i.base_net_amount = i.base_amount if not found: item.rate = item.net_rate + item.amount = item.net_amount + item.base_amount = item.base_net_amount item.price_list_rate = 0 si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"}) items.append(si_item) @@ -170,10 +186,12 @@ class POSInvoiceMergeLog(Document): found = True if not found: payments.append(payment) - rounding_adjustment += doc.rounding_adjustment - rounded_total += doc.rounded_total - base_rounding_adjustment += doc.base_rounding_adjustment - base_rounded_total += doc.base_rounded_total + + if doc.rounding_adjustment or doc.base_rounding_adjustment: + rounding_adjustment += doc.rounding_adjustment + rounded_total += doc.rounded_total + base_rounding_adjustment += doc.base_rounding_adjustment + base_rounded_total += doc.base_rounded_total if loyalty_points_sum: diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index 3555da83a40..928d26676dc 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py @@ -150,3 +150,118 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") + + + def test_consolidation_round_off_error_1(self): + ''' + Test case for bug: + Round off error in consolidated invoice creation if POS Invoice has inclusive tax + ''' + frappe.db.sql("delete from `tabPOS Invoice`") + + try: + init_user_and_profile() + + inv = create_pos_invoice(qty=3, rate=10000, do_not_save=True) + inv.append("taxes", { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 7.5, + "included_in_print_rate": 1 + }) + inv.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 30000 + }) + inv.insert() + inv.submit() + + inv2 = create_pos_invoice(qty=3, rate=10000, do_not_save=True) + inv2.append("taxes", { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 7.5, + "included_in_print_rate": 1 + }) + inv2.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 30000 + }) + inv2.insert() + inv2.submit() + + consolidate_pos_invoices() + + inv.load_from_db() + consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice) + self.assertEqual(consolidated_invoice.outstanding_amount, 0) + self.assertEqual(consolidated_invoice.status, 'Paid') + + finally: + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") + + def test_consolidation_round_off_error_2(self): + ''' + Test the same case as above but with an Unpaid POS Invoice + ''' + frappe.db.sql("delete from `tabPOS Invoice`") + + try: + init_user_and_profile() + + inv = create_pos_invoice(qty=6, rate=10000, do_not_save=True) + inv.append("taxes", { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 7.5, + "included_in_print_rate": 1 + }) + inv.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60000 + }) + inv.insert() + inv.submit() + + inv2 = create_pos_invoice(qty=6, rate=10000, do_not_save=True) + inv2.append("taxes", { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 7.5, + "included_in_print_rate": 1 + }) + inv2.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60000 + }) + inv2.insert() + inv2.submit() + + inv3 = create_pos_invoice(qty=3, rate=600, do_not_save=True) + inv3.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000 + }) + inv3.insert() + inv3.submit() + + consolidate_pos_invoices() + + inv.load_from_db() + consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice) + self.assertEqual(consolidated_invoice.outstanding_amount, 800) + self.assertEqual(consolidated_invoice.status, 'Unpaid') + + finally: + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 02b1b3b1734..f2fe00151d1 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -106,6 +106,9 @@ class calculate_taxes_and_totals(object): self.doc.conversion_rate = flt(self.doc.conversion_rate) def calculate_item_values(self): + if self.doc.get('is_consolidated'): + return + if not self.discount_amount_applied: for item in self.doc.get("items"): self.doc.round_floats_in(item) From 96034c4396519de168080829d6d95c576bf6410a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 8 Feb 2022 16:04:08 +0530 Subject: [PATCH 044/129] fix(test): ignore stock validation (cherry picked from commit afc5c26d1c7ba2973f8e74d57029e78db550946b) --- .../pos_invoice_merge_log/test_pos_invoice_merge_log.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index 928d26676dc..fd1aaab2649 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py @@ -158,6 +158,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): Round off error in consolidated invoice creation if POS Invoice has inclusive tax ''' frappe.db.sql("delete from `tabPOS Invoice`") + frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1) try: init_user_and_profile() @@ -205,12 +206,14 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") + frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 0) def test_consolidation_round_off_error_2(self): ''' Test the same case as above but with an Unpaid POS Invoice ''' frappe.db.sql("delete from `tabPOS Invoice`") + frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1) try: init_user_and_profile() @@ -265,3 +268,4 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") + frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 0) From e2d8fce3c52b6f87d19a0b0e90294d00707bcd0e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 8 Feb 2022 17:07:51 +0530 Subject: [PATCH 045/129] fix(test): case if write off is calculated as negative amount (cherry picked from commit c2b83a02837e8ab9c2e23596f22b7f75e420003f) --- .../pos_invoice_merge_log/pos_invoice_merge_log.py | 1 - .../test_pos_invoice_merge_log.py | 11 ++++++----- erpnext/controllers/taxes_and_totals.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 5ec0d0a51e6..08c3bc71553 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -96,7 +96,6 @@ class POSInvoiceMergeLog(Document): pos_invoice_grand_total = sum(d.grand_total for d in data) if abs(pos_invoice_grand_total - invoice.grand_total) < 1: - invoice.write_off_amount += -1 * (pos_invoice_grand_total - invoice.grand_total) invoice.save() diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index fd1aaab2649..fc14161456c 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py @@ -154,10 +154,10 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): def test_consolidation_round_off_error_1(self): ''' - Test case for bug: - Round off error in consolidated invoice creation if POS Invoice has inclusive tax + Test round off error in consolidated invoice creation if POS Invoice has inclusive tax ''' frappe.db.sql("delete from `tabPOS Invoice`") + allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock') frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1) try: @@ -206,13 +206,14 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") - frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 0) + frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', allow_negative_stock) def test_consolidation_round_off_error_2(self): ''' Test the same case as above but with an Unpaid POS Invoice ''' frappe.db.sql("delete from `tabPOS Invoice`") + allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock') frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1) try: @@ -262,10 +263,10 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): inv.load_from_db() consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice) self.assertEqual(consolidated_invoice.outstanding_amount, 800) - self.assertEqual(consolidated_invoice.status, 'Unpaid') + self.assertNotEqual(consolidated_invoice.status, 'Paid') finally: frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") - frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 0) + frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', allow_negative_stock) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index f2fe00151d1..e3e7fdde973 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -649,12 +649,12 @@ class calculate_taxes_and_totals(object): def calculate_change_amount(self): self.doc.change_amount = 0.0 self.doc.base_change_amount = 0.0 + grand_total = self.doc.rounded_total or self.doc.grand_total + base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total if self.doc.doctype == "Sales Invoice" \ - and self.doc.paid_amount > self.doc.grand_total and not self.doc.is_return \ + and self.doc.paid_amount > grand_total and not self.doc.is_return \ and any(d.type == "Cash" for d in self.doc.payments): - grand_total = self.doc.rounded_total or self.doc.grand_total - base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total self.doc.change_amount = flt(self.doc.paid_amount - grand_total + self.doc.write_off_amount, self.doc.precision("change_amount")) From 7835e86da00bf1630b6063c51b473b8ecc76457c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 9 Feb 2022 10:05:06 +0530 Subject: [PATCH 046/129] fix(test): do not enable negative stock (cherry picked from commit 75256863c6e3ed917d3ff00a9435da9fa7115cbb) --- .../test_pos_invoice_merge_log.py | 22 ++++++++++++++----- erpnext/controllers/taxes_and_totals.py | 2 +- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index fc14161456c..5930aa097f7 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py @@ -12,6 +12,7 @@ from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_inv from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import ( consolidate_pos_invoices, ) +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry class TestPOSInvoiceMergeLog(unittest.TestCase): @@ -156,11 +157,17 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): ''' Test round off error in consolidated invoice creation if POS Invoice has inclusive tax ''' + frappe.db.sql("delete from `tabPOS Invoice`") - allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock') - frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1) try: + make_stock_entry( + to_warehouse="_Test Warehouse - _TC", + item_code="_Test Item", + rate=8000, + qty=10, + ) + init_user_and_profile() inv = create_pos_invoice(qty=3, rate=10000, do_not_save=True) @@ -206,17 +213,21 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") - frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', allow_negative_stock) def test_consolidation_round_off_error_2(self): ''' Test the same case as above but with an Unpaid POS Invoice ''' frappe.db.sql("delete from `tabPOS Invoice`") - allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock') - frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1) try: + make_stock_entry( + to_warehouse="_Test Warehouse - _TC", + item_code="_Test Item", + rate=8000, + qty=10, + ) + init_user_and_profile() inv = create_pos_invoice(qty=6, rate=10000, do_not_save=True) @@ -269,4 +280,3 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") - frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', allow_negative_stock) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index e3e7fdde973..08d1dcea7dc 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -653,7 +653,7 @@ class calculate_taxes_and_totals(object): base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total if self.doc.doctype == "Sales Invoice" \ - and self.doc.paid_amount > grand_total and not self.doc.is_return \ + and self.doc.paid_amount > grand_total and not self.doc.is_return \ and any(d.type == "Cash" for d in self.doc.payments): self.doc.change_amount = flt(self.doc.paid_amount - grand_total + From 02cf4e9f25ecfa4aba51aad896e46252689bc162 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 9 Feb 2022 12:06:59 +0530 Subject: [PATCH 047/129] fix: flaky point of sale test (cherry picked from commit 4bb557dbd84b109b83de12b2e77a60d953c292ea) --- erpnext/tests/test_point_of_sale.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/erpnext/tests/test_point_of_sale.py b/erpnext/tests/test_point_of_sale.py index df2dc8b99a1..3299c8885f2 100644 --- a/erpnext/tests/test_point_of_sale.py +++ b/erpnext/tests/test_point_of_sale.py @@ -1,15 +1,25 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt +import unittest + +import frappe from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.selling.page.point_of_sale.point_of_sale import get_items from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -from erpnext.tests.utils import ERPNextTestCase -class TestPointOfSale(ERPNextTestCase): +class TestPointOfSale(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + frappe.db.savepoint('before_test_point_of_sale') + + @classmethod + def tearDownClass(cls) -> None: + frappe.db.rollback(save_point='before_test_point_of_sale') + def test_item_search(self): """ Test Stock and Service Item Search. From 41b7c5f92d0b828515868367537776f96385739e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 9 Feb 2022 16:08:28 +0530 Subject: [PATCH 048/129] fix: cancelling of consolidated sales invoice that doesn't have closing entry (cherry picked from commit 0ebd32dc630daf03dc77f81a93944a1919f0c016) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 0317656e83b..0c22a0e092b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -294,7 +294,7 @@ class SalesInvoice(SellingController): filters={ invoice_or_credit_note: self.name }, pluck="pos_closing_entry" ) - if pos_closing_entry: + if pos_closing_entry and pos_closing_entry[0]: msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format( frappe.bold("Consolidated Sales Invoice"), get_link_to_form("POS Closing Entry", pos_closing_entry[0]) From d97fa58e4f5bac774811c1362325d3395721fbcc Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 9 Feb 2022 16:44:13 +0530 Subject: [PATCH 049/129] fix: merge conflicts --- erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 48f92589e40..f3b3cd4df77 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -630,7 +630,6 @@ class TestPricingRule(unittest.TestCase): for doc in [si, si1]: doc.delete() -<<<<<<< HEAD def test_multiple_pricing_rules_with_min_qty(self): make_pricing_rule(discount_percentage=20, selling=1, priority=1, min_qty=4, apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 1") @@ -650,7 +649,7 @@ class TestPricingRule(unittest.TestCase): frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1") frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2") -======= + def test_remove_pricing_rule(self): item = make_item("Water Flask") make_item_price("Water Flask", "_Test Price List", 100) @@ -691,7 +690,6 @@ class TestPricingRule(unittest.TestCase): frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete() item.delete() ->>>>>>> b8c41e3030 (test: item price on remove pricing rule) test_dependencies = ["Campaign"] From 503cc65db4ed9cf4b920971195bb32109d7d92d2 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 10 Feb 2022 10:28:51 +0530 Subject: [PATCH 050/129] fix: restore missing change from develop branch --- .../pos_invoice_merge_log/pos_invoice_merge_log.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 08c3bc71553..40ab0c50deb 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -186,11 +186,10 @@ class POSInvoiceMergeLog(Document): if not found: payments.append(payment) - if doc.rounding_adjustment or doc.base_rounding_adjustment: - rounding_adjustment += doc.rounding_adjustment - rounded_total += doc.rounded_total - base_rounding_adjustment += doc.base_rounding_adjustment - base_rounded_total += doc.base_rounded_total + rounding_adjustment += doc.rounding_adjustment + rounded_total += doc.rounded_total + base_rounding_adjustment += doc.base_rounding_adjustment + base_rounded_total += doc.base_rounded_total if loyalty_points_sum: From d0625045934479f28afde8093bebe7697ed038aa Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 10 Feb 2022 10:20:14 +0530 Subject: [PATCH 051/129] fix: restrict filetypes to csv for rename tool (cherry picked from commit c371b52d279c02af0632c9e783e45c13e30ebaac) --- erpnext/utilities/doctype/rename_tool/rename_tool.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/utilities/doctype/rename_tool/rename_tool.js b/erpnext/utilities/doctype/rename_tool/rename_tool.js index 7823055e523..5553e44ef81 100644 --- a/erpnext/utilities/doctype/rename_tool/rename_tool.js +++ b/erpnext/utilities/doctype/rename_tool/rename_tool.js @@ -13,6 +13,12 @@ frappe.ui.form.on("Rename Tool", { }, refresh: function(frm) { frm.disable_save(); + + frm.get_field("file_to_rename").df.options = { + restrictions: { + allowed_file_types: [".csv"], + }, + }; if (!frm.doc.file_to_rename) { frm.get_field("rename_log").$wrapper.html(""); } From ffd5efe669e3a086dd76613bc629c1061c2bb4ea Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 10 Feb 2022 10:52:13 +0530 Subject: [PATCH 052/129] ci: bump ci python version for patch test --- .github/workflows/patch.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index 8d29057b487..54b381d7f89 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -41,7 +41,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v2 with: - python-version: 3.6 + python-version: 3.7 - name: Setup Node uses: actions/setup-node@v2 From 5a771dca4632713b38620694082a3117f1ac8be3 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 11 Feb 2022 11:29:37 +0530 Subject: [PATCH 053/129] fix: update bin modified timestamp when updating qty These timestamps are used for writing integrations hence whenever bin is updated timestamp should update to reliabily use Bin for integration logic. (cherry picked from commit 77be98295c836d6fba02ae34f91f36cd99c625a4) --- erpnext/buying/doctype/purchase_order/test_purchase_order.py | 5 +++-- erpnext/stock/stock_balance.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 9a63afc1303..645e97ee7c8 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -682,17 +682,18 @@ class TestPurchaseOrder(unittest.TestCase): bin1 = frappe.db.get_value("Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname=["reserved_qty_for_sub_contract", "projected_qty"], as_dict=1) + fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], as_dict=1) # Submit PO po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes") bin2 = frappe.db.get_value("Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname=["reserved_qty_for_sub_contract", "projected_qty"], as_dict=1) + fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], as_dict=1) self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10) + self.assertNotEqual(bin1.modified, bin2.modified) # Create stock transfer rm_item = [{"item_code":"_Test FG Item","rm_item_code":"_Test Item","item_name":"_Test Item", diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index 6663458e651..35cad2ba305 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -3,7 +3,7 @@ import frappe -from frappe.utils import cstr, flt, nowdate, nowtime +from frappe.utils import cstr, flt, now, nowdate, nowtime from erpnext.controllers.stock_controller import create_repost_item_valuation_entry from erpnext.stock.utils import update_bin @@ -175,6 +175,7 @@ def update_bin_qty(item_code, warehouse, qty_dict=None): bin.set(field, flt(value)) mismatch = True + bin.modified = now() if mismatch: bin.set_projected_qty() bin.db_update() From 7c5480d729ec9ca9e1f4fafabc0774c14fd29b69 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 11 Feb 2022 12:32:45 +0530 Subject: [PATCH 054/129] fix: cannot jump to sales invoice in gross profit report (cherry picked from commit 78dd364b0be9913208d61c402a6c858eb578e210) --- erpnext/accounts/report/gross_profit/gross_profit.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js index 685f2d6176b..2ba649da07f 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.js +++ b/erpnext/accounts/report/gross_profit/gross_profit.js @@ -42,6 +42,11 @@ frappe.query_reports["Gross Profit"] = { "parent_field": "parent_invoice", "initial_depth": 3, "formatter": function(value, row, column, data, default_formatter) { + if (column.fieldname == "sales_invoice" && column.options == "Item" && data.indent == 0) { + column._options = "Sales Invoice"; + } else { + column._options = "Item"; + } value = default_formatter(value, row, column, data); if (data && (data.indent == 0.0 || row[1].content == "Total")) { From 032e4e4486369af2e48793a51065631063b51ee6 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 9 Feb 2022 17:53:33 +0100 Subject: [PATCH 055/129] fix: encode filters for URI (cherry picked from commit 5811d9e318de46095f85fb183583e61d14aff7ef) --- erpnext/regional/report/datev/datev.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/report/datev/datev.js b/erpnext/regional/report/datev/datev.js index 4124e3df190..03c729e6df4 100644 --- a/erpnext/regional/report/datev/datev.js +++ b/erpnext/regional/report/datev/datev.js @@ -40,7 +40,11 @@ frappe.query_reports["DATEV"] = { }); query_report.page.add_menu_item(__("Download DATEV File"), () => { - const filters = JSON.stringify(query_report.get_values()); + const filters = encodeURIComponent( + JSON.stringify( + query_report.get_values() + ) + ); window.open(`/api/method/erpnext.regional.report.datev.datev.download_datev_csv?filters=${filters}`); }); From adaa8bd32d1406619a38dc5a1dace6fea039fbbb Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 10 Feb 2022 12:30:41 +0530 Subject: [PATCH 056/129] fix: time out error while making work orders from prodcution plan (cherry picked from commit eec2f87088e630a7ef2a918d64dd3cf2b78787d3) --- .../production_plan/production_plan.py | 79 +++++++++++-------- .../doctype/work_order/work_order.py | 5 +- 2 files changed, 47 insertions(+), 37 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index f06624fe92c..f6d2a14a074 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -342,6 +342,7 @@ class ProductionPlan(Document): def get_production_items(self): item_dict = {} + for d in self.po_items: item_details = { "production_item" : d.item_code, @@ -358,12 +359,12 @@ class ProductionPlan(Document): "production_plan" : self.name, "production_plan_item" : d.name, "product_bundle_item" : d.product_bundle_item, - "planned_start_date" : d.planned_start_date + "planned_start_date" : d.planned_start_date, + "project" : self.project } - item_details.update({ - "project": self.project or frappe.db.get_value("Sales Order", d.sales_order, "project") - }) + if not item_details['project'] and d.sales_order: + item_details['project'] = frappe.get_cached_value("Sales Order", d.sales_order, "project") if self.get_items_from == "Material Request": item_details.update({ @@ -381,39 +382,59 @@ class ProductionPlan(Document): @frappe.whitelist() def make_work_order(self): + from erpnext.manufacturing.doctype.work_order.work_order import get_default_warehouse + wo_list, po_list = [], [] subcontracted_po = {} + default_warehouses = get_default_warehouse() - self.validate_data() - self.make_work_order_for_finished_goods(wo_list) - self.make_work_order_for_subassembly_items(wo_list, subcontracted_po) + self.make_work_order_for_finished_goods(wo_list, default_warehouses) + self.make_work_order_for_subassembly_items(wo_list, subcontracted_po, default_warehouses) self.make_subcontracted_purchase_order(subcontracted_po, po_list) self.show_list_created_message('Work Order', wo_list) self.show_list_created_message('Purchase Order', po_list) - def make_work_order_for_finished_goods(self, wo_list): + def make_work_order_for_finished_goods(self, wo_list, default_warehouses): items_data = self.get_production_items() for key, item in items_data.items(): if self.sub_assembly_items: item['use_multi_level_bom'] = 0 + set_default_warehouses(item, default_warehouses) work_order = self.create_work_order(item) if work_order: wo_list.append(work_order) - def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po): + def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po, default_warehouses): for row in self.sub_assembly_items: if row.type_of_manufacturing == 'Subcontract': subcontracted_po.setdefault(row.supplier, []).append(row) continue - args = {} - self.prepare_args_for_sub_assembly_items(row, args) - work_order = self.create_work_order(args) + work_order_data = { + 'wip_warehouse': default_warehouses.get('wip_warehouse'), + 'fg_warehouse': default_warehouses.get('fg_warehouse') + } + + self.prepare_data_for_sub_assembly_items(row, work_order_data) + work_order = self.create_work_order(work_order_data) if work_order: wo_list.append(work_order) + def prepare_data_for_sub_assembly_items(self, row, wo_data): + for field in ["production_item", "item_name", "qty", "fg_warehouse", + "description", "bom_no", "stock_uom", "bom_level", + "production_plan_item", "schedule_date"]: + if row.get(field): + wo_data[field] = row.get(field) + + wo_data.update({ + "use_multi_level_bom": 0, + "production_plan": self.name, + "production_plan_sub_assembly_item": row.name + }) + def make_subcontracted_purchase_order(self, subcontracted_po, purchase_orders): if not subcontracted_po: return @@ -424,7 +445,7 @@ class ProductionPlan(Document): po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate() po.is_subcontracted = 'Yes' for row in po_list: - args = { + po_data = { 'item_code': row.production_item, 'warehouse': row.fg_warehouse, 'production_plan_sub_assembly_item': row.name, @@ -434,9 +455,9 @@ class ProductionPlan(Document): for field in ['schedule_date', 'qty', 'uom', 'stock_uom', 'item_name', 'description', 'production_plan_item']: - args[field] = row.get(field) + po_data[field] = row.get(field) - po.append('items', args) + po.append('items', po_data) po.set_missing_values() po.flags.ignore_mandatory = True @@ -453,24 +474,9 @@ class ProductionPlan(Document): doc_list = [get_link_to_form(doctype, p) for p in doc_list] msgprint(_("{0} created").format(comma_and(doc_list))) - def prepare_args_for_sub_assembly_items(self, row, args): - for field in ["production_item", "item_name", "qty", "fg_warehouse", - "description", "bom_no", "stock_uom", "bom_level", - "production_plan_item", "schedule_date"]: - args[field] = row.get(field) - - args.update({ - "use_multi_level_bom": 0, - "production_plan": self.name, - "production_plan_sub_assembly_item": row.name - }) - def create_work_order(self, item): - from erpnext.manufacturing.doctype.work_order.work_order import ( - OverProductionError, - get_default_warehouse, - ) - warehouse = get_default_warehouse() + from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError + wo = frappe.new_doc("Work Order") wo.update(item) wo.planned_start_date = item.get('planned_start_date') or item.get('schedule_date') @@ -479,11 +485,11 @@ class ProductionPlan(Document): wo.fg_warehouse = item.get("warehouse") wo.set_work_order_operations() + wo.set_required_items() - if not wo.fg_warehouse: - wo.fg_warehouse = warehouse.get('fg_warehouse') try: wo.flags.ignore_mandatory = True + wo.flags.ignore_validate = True wo.insert() return wo.name except OverProductionError: @@ -1024,3 +1030,8 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0): if d.value: get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent+1) + +def set_default_warehouses(row, default_warehouses): + for field in ['wip_warehouse', 'fg_warehouse']: + if not row.get(field): + row[field] = default_warehouses.get(field) \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 46e02b0cdd4..8a08c2c6248 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -76,7 +76,6 @@ class WorkOrder(Document): self.set_required_items(reset_only_qty = len(self.get("required_items"))) - def validate_sales_order(self): if self.sales_order: self.check_sales_order_on_hold_or_close() @@ -543,7 +542,7 @@ class WorkOrder(Document): if node.is_bom: operations.extend(_get_operations(node.name, qty=node.exploded_qty)) - bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") + bom_qty = frappe.get_cached_value("BOM", self.bom_no, "quantity") operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty)) for correct_index, operation in enumerate(operations, start=1): @@ -623,7 +622,7 @@ class WorkOrder(Document): frappe.delete_doc("Job Card", d.name) def validate_production_item(self): - if frappe.db.get_value("Item", self.production_item, "has_variants"): + if frappe.get_cached_value("Item", self.production_item, "has_variants"): frappe.throw(_("Work Order cannot be raised against a Item Template"), ItemHasVariantError) if self.production_item: From 87decb3734fdc3fd3c03c28dc5054bb861fc9111 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Fri, 11 Feb 2022 17:38:37 +0530 Subject: [PATCH 057/129] Revert "fix(India): Tax calculation for overseas suppliers" (cherry picked from commit ea20c63182ba0b380aa46bab438ed45db0f19e8a) --- erpnext/regional/india/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 0126b090fca..2287714a008 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -221,7 +221,6 @@ def get_regional_address_details(party_details, doctype, company): if not party_details.place_of_supply: return party_details if not party_details.company_gstin: return party_details - if not party_details.supplier_gstin: return party_details if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice", From 2b4e53222676cb805324da394bc21928caff1d63 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 8 Feb 2022 17:14:11 +0530 Subject: [PATCH 058/129] fix: Earned Leave allocation based on joining date not working (cherry picked from commit 7326d57966d09ababc9fd02d32980dae8d51dc3c) --- erpnext/hr/utils.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 6032d0c1bd3..416366de9c0 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -377,10 +377,10 @@ def allocate_earned_leaves(ignore_duplicates=False): from_date=allocation.from_date - if e_leave_type.based_on_date_of_joining_date: + if e_leave_type.based_on_date_of_joining: from_date = frappe.db.get_value("Employee", allocation.employee, "date_of_joining") - if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining_date): + if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining): update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates) def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates=False): @@ -421,10 +421,13 @@ def is_earned_leave_already_allocated(allocation, annual_allocation): get_leave_type_details, ) + assignment = frappe.get_doc("Leave Policy Assignment", allocation.leave_policy_assignment) + if assignment.assignment_based_on == "Joining Date": + return False + leave_type_details = get_leave_type_details() date_of_joining = frappe.db.get_value("Employee", allocation.employee, "date_of_joining") - assignment = frappe.get_doc("Leave Policy Assignment", allocation.leave_policy_assignment) leaves_for_passed_months = assignment.get_leaves_for_passed_months(allocation.leave_type, annual_allocation, leave_type_details, date_of_joining) @@ -459,7 +462,7 @@ def create_additional_leave_ledger_entry(allocation, leaves, date): allocation.unused_leaves = 0 allocation.create_leave_ledger_entry() -def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining_date): +def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining): import calendar from dateutil import relativedelta @@ -470,7 +473,7 @@ def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining #last day of month last_day = calendar.monthrange(to_date.year, to_date.month)[1] - if (from_date.day == to_date.day and based_on_date_of_joining_date) or (not based_on_date_of_joining_date and to_date.day == last_day): + if (from_date.day == to_date.day and based_on_date_of_joining) or (not based_on_date_of_joining and to_date.day == last_day): if frequency == "Monthly": return True elif frequency == "Quarterly" and rd.months % 3: From 3fea6fd9e87aa170eba925c4d8f9af9af8c3c729 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 9 Feb 2022 11:28:14 +0530 Subject: [PATCH 059/129] fix: consider based on DOJ config while calculating leaves for passed months (cherry picked from commit 89fa0bb73f1a192c2bfe8bc0a87956cb12ff6352) --- .../leave_policy_assignment.py | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index 2115b05e92f..0618b9de14b 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -120,14 +120,15 @@ class LeavePolicyAssignment(Document): from_date_year = get_datetime(from_date).year months_passed = 0 + based_on_doj = leave_type_details.get(leave_type).based_on_date_of_joining - if current_year == from_date_year and current_month > from_date_month: + if current_year == from_date_year and current_month >= from_date_month: months_passed = current_month - from_date_month - months_passed = add_current_month_if_applicable(months_passed) + months_passed = add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj) elif current_year > from_date_year: months_passed = (12 - from_date_month) + current_month - months_passed = add_current_month_if_applicable(months_passed) + months_passed = add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj) if months_passed > 0: monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated, @@ -139,13 +140,20 @@ class LeavePolicyAssignment(Document): return new_leaves_allocated -def add_current_month_if_applicable(months_passed): +def add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj): date = getdate(frappe.flags.current_date) or getdate() - last_day_of_month = get_last_day(date) - # if its the last day of the month, then that month should also be considered - if last_day_of_month == date: - months_passed += 1 + if based_on_doj: + # if leave type allocation is based on DOJ, + # and the date of assignment creation is same as DOJ, + # then the month should be considered + if date == date_of_joining: + months_passed += 1 + else: + last_day_of_month = get_last_day(date) + # if its the last day of the month, then that month should be considered + if last_day_of_month == date: + months_passed += 1 return months_passed @@ -184,7 +192,7 @@ def create_assignment_for_multiple_employees(employees, data): def get_leave_type_details(): leave_type_details = frappe._dict() leave_types = frappe.get_all("Leave Type", - fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", + fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "based_on_date_of_joining", "is_carry_forward", "expire_carry_forwarded_leaves_after_days", "earned_leave_frequency", "rounding"]) for d in leave_types: leave_type_details.setdefault(d.name, d) From 9b6040979a4131c63a98d38eb0fa4f0ca0653d53 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 11 Feb 2022 13:52:46 +0530 Subject: [PATCH 060/129] fix: consider leaves for past months if assignment is based on joining date too (cherry picked from commit c7be9ef5d24a3e03efde64a45302baca76e8107f) # Conflicts: # erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py --- .../leave_policy_assignment.py | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index 0618b9de14b..158b576bdb6 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -8,8 +8,12 @@ from math import ceil import frappe from frappe import _, bold from frappe.model.document import Document +<<<<<<< HEAD from frappe.utils import date_diff, flt, formatdate, get_datetime, get_last_day, getdate from six import string_types +======= +from frappe.utils import date_diff, flt, formatdate, get_last_day, getdate +>>>>>>> c7be9ef5d2 (fix: consider leaves for past months if assignment is based on joining date too) class LeavePolicyAssignment(Document): @@ -95,10 +99,12 @@ class LeavePolicyAssignment(Document): new_leaves_allocated = 0 elif leave_type_details.get(leave_type).is_earned_leave == 1: - if self.assignment_based_on == "Leave Period": - new_leaves_allocated = self.get_leaves_for_passed_months(leave_type, new_leaves_allocated, leave_type_details, date_of_joining) - else: + if not self.assignment_based_on: new_leaves_allocated = 0 + else: + # get leaves for past months if assignment is based on Leave Period / Joining Date + new_leaves_allocated = self.get_leaves_for_passed_months(leave_type, new_leaves_allocated, leave_type_details, date_of_joining) + # Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period elif getdate(date_of_joining) > getdate(self.effective_from): remaining_period = ((date_diff(self.effective_to, date_of_joining) + 1) / (date_diff(self.effective_to, self.effective_from) + 1)) @@ -109,25 +115,23 @@ class LeavePolicyAssignment(Document): def get_leaves_for_passed_months(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining): from erpnext.hr.utils import get_monthly_earned_leave - current_month = get_datetime(frappe.flags.current_date).month or get_datetime().month - current_year = get_datetime(frappe.flags.current_date).year or get_datetime().year + current_date = frappe.flags.current_date or getdate() + if current_date > getdate(self.effective_to): + current_date = getdate(self.effective_to) - from_date = frappe.db.get_value("Leave Period", self.leave_period, "from_date") - if getdate(date_of_joining) > getdate(from_date): - from_date = date_of_joining - - from_date_month = get_datetime(from_date).month - from_date_year = get_datetime(from_date).year + from_date = getdate(self.effective_from) + if getdate(date_of_joining) > from_date: + from_date = getdate(date_of_joining) months_passed = 0 based_on_doj = leave_type_details.get(leave_type).based_on_date_of_joining - if current_year == from_date_year and current_month >= from_date_month: - months_passed = current_month - from_date_month + if current_date.year == from_date.year and current_date.month >= from_date.month: + months_passed = current_date.month - from_date.month months_passed = add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj) - elif current_year > from_date_year: - months_passed = (12 - from_date_month) + current_month + elif current_date.year > from_date.year: + months_passed = (12 - from_date.month) + current_date.month months_passed = add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj) if months_passed > 0: @@ -144,8 +148,7 @@ def add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj date = getdate(frappe.flags.current_date) or getdate() if based_on_doj: - # if leave type allocation is based on DOJ, - # and the date of assignment creation is same as DOJ, + # if leave type allocation is based on DOJ, and the date of assignment creation is same as DOJ, # then the month should be considered if date == date_of_joining: months_passed += 1 From c0ea6d07214acca4473014f6c813d17267b7658b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 11 Feb 2022 17:40:20 +0530 Subject: [PATCH 061/129] chore: clean-up leave policy assignment tests (cherry picked from commit 51e608682934610d3414e40f9524e529c4a36f49) --- .../leave_policy_assignment.py | 1 - .../test_leave_policy_assignment.py | 116 ++++++------------ 2 files changed, 36 insertions(+), 81 deletions(-) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index 158b576bdb6..81d06062602 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -17,7 +17,6 @@ from frappe.utils import date_diff, flt, formatdate, get_last_day, getdate class LeavePolicyAssignment(Document): - def validate(self): self.validate_policy_assignment_overlap() self.set_dates() diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py index 26821f58e2d..2d4fcf91f72 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py @@ -22,34 +22,27 @@ class TestLeavePolicyAssignment(unittest.TestCase): for doctype in ["Leave Period", "Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]: frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec + self.employee = get_employee() + def test_grant_leaves(self): leave_period = get_leave_period() - employee = get_employee() - - # create the leave policy with leave type "_Test Leave Type", allocation = 10 + # allocation = 10 leave_policy = create_leave_policy() leave_policy.submit() - data = { "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, "leave_period": leave_period.name } - - leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) - - leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]) - leave_policy_assignment_doc.reload() - - self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1) + leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 1) leave_allocation = frappe.get_list("Leave Allocation", filters={ - "employee": employee.name, + "employee": self.employee.name, "leave_policy":leave_policy.name, "leave_policy_assignment": leave_policy_assignments[0], "docstatus": 1})[0] - leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation) self.assertEqual(leave_alloc_doc.new_leaves_allocated, 10) @@ -61,49 +54,32 @@ class TestLeavePolicyAssignment(unittest.TestCase): def test_allow_to_grant_all_leave_after_cancellation_of_every_leave_allocation(self): leave_period = get_leave_period() - employee = get_employee() - # create the leave policy with leave type "_Test Leave Type", allocation = 10 leave_policy = create_leave_policy() leave_policy.submit() - data = { "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, "leave_period": leave_period.name } - - leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) - - leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]) - leave_policy_assignment_doc.reload() - + leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) # every leave is allocated no more leave can be granted now - self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1) - + self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 1) leave_allocation = frappe.get_list("Leave Allocation", filters={ - "employee": employee.name, + "employee": self.employee.name, "leave_policy":leave_policy.name, "leave_policy_assignment": leave_policy_assignments[0], "docstatus": 1})[0] leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation) - - # User all allowed to grant leave when there is no allocation against assignment leave_alloc_doc.cancel() leave_alloc_doc.delete() - - leave_policy_assignment_doc.reload() - - - # User are now allowed to grant leave - self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 0) + self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 0) def test_earned_leave_allocation(self): leave_period = create_leave_period("Test Earned Leave Period") - employee = get_employee() leave_type = create_earned_leave_type("Test Earned Leave") leave_policy = frappe.get_doc({ @@ -116,7 +92,7 @@ class TestLeavePolicyAssignment(unittest.TestCase): "leave_policy": leave_policy.name, "leave_period": leave_period.name } - leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) # leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency leaves_allocated = frappe.db.get_value("Leave Allocation", { @@ -124,16 +100,8 @@ class TestLeavePolicyAssignment(unittest.TestCase): }, "total_leaves_allocated") self.assertEqual(leaves_allocated, 0) - def test_earned_leave_allocation_for_passed_months(self): - employee = get_employee() - leave_type = create_earned_leave_type("Test Earned Leave") - leave_period = create_leave_period("Test Earned Leave Period", - start_date=get_first_day(add_months(getdate(), -1))) - leave_policy = frappe.get_doc({ - "doctype": "Leave Policy", - "title": "Test Leave Policy", - "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] - }).insert() + def test_earned_leave_alloc_for_passed_months_based_on_leave_period(self): + leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -1))) # Case 1: assignment created one month after the leave period, should allocate 1 leave frappe.flags.current_date = get_first_day(getdate()) @@ -142,24 +110,15 @@ class TestLeavePolicyAssignment(unittest.TestCase): "leave_policy": leave_policy.name, "leave_period": leave_period.name } - leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) leaves_allocated = frappe.db.get_value("Leave Allocation", { "leave_policy_assignment": leave_policy_assignments[0] }, "total_leaves_allocated") self.assertEqual(leaves_allocated, 1) - def test_earned_leave_allocation_for_passed_months_on_month_end(self): - employee = get_employee() - leave_type = create_earned_leave_type("Test Earned Leave") - leave_period = create_leave_period("Test Earned Leave Period", - start_date=get_first_day(add_months(getdate(), -2))) - leave_policy = frappe.get_doc({ - "doctype": "Leave Policy", - "title": "Test Leave Policy", - "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] - }).insert() - + def test_earned_leave_alloc_for_passed_months_based_on_leave_period(self): + leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2))) # Case 2: assignment created on the last day of the leave period's latter month # should allocate 1 leave for current month even though the month has not ended # since the daily job might have already executed @@ -170,7 +129,7 @@ class TestLeavePolicyAssignment(unittest.TestCase): "leave_policy": leave_policy.name, "leave_period": leave_period.name } - leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) leaves_allocated = frappe.db.get_value("Leave Allocation", { "leave_policy_assignment": leave_policy_assignments[0] @@ -187,33 +146,17 @@ class TestLeavePolicyAssignment(unittest.TestCase): }, "total_leaves_allocated") self.assertEqual(leaves_allocated, 3) - def test_earned_leave_allocation_for_passed_months_with_carry_forwarded_leaves(self): + def test_earned_leave_alloc_for_passed_months_with_cf_leaves_based_on_leave_period(self): from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation - employee = get_employee() - leave_type = create_earned_leave_type("Test Earned Leave") - leave_period = create_leave_period("Test Earned Leave Period", - start_date=get_first_day(add_months(getdate(), -2))) - leave_policy = frappe.get_doc({ - "doctype": "Leave Policy", - "title": "Test Leave Policy", - "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] - }).insert() - + leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2))) # initial leave allocation = 5 - leave_allocation = create_leave_allocation( - employee=employee.name, - employee_name=employee.employee_name, - leave_type=leave_type.name, - from_date=add_months(getdate(), -12), - to_date=add_months(getdate(), -3), - new_leaves_allocated=5, - carry_forward=0) + leave_allocation = create_leave_allocation(employee=self.employee.name, employee_name=self.employee.employee_name, leave_type="Test Earned Leave", + from_date=add_months(getdate(), -12), to_date=add_months(getdate(), -3), new_leaves_allocated=5, carry_forward=0) leave_allocation.submit() # Case 3: assignment created on the last day of the leave period's latter month with carry forwarding frappe.flags.current_date = get_last_day(add_months(getdate(), -1)) - data = { "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, @@ -221,7 +164,7 @@ class TestLeavePolicyAssignment(unittest.TestCase): "carry_forward": 1 } # carry forwarded leaves = 5, 3 leaves allocated for passed months - leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) details = frappe.db.get_value("Leave Allocation", { "leave_policy_assignment": leave_policy_assignments[0] @@ -268,4 +211,17 @@ def create_leave_period(name, start_date=None): to_date=add_months(start_date, 12), company="_Test Company", is_active=1 - )).insert() \ No newline at end of file + )).insert() + + +def setup_leave_period_and_policy(start_date): + leave_type = create_earned_leave_type("Test Earned Leave") + leave_period = create_leave_period("Test Earned Leave Period", + start_date=start_date) + leave_policy = frappe.get_doc({ + "doctype": "Leave Policy", + "title": "Test Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] + }).insert() + + return leave_period, leave_policy \ No newline at end of file From c261621dcaecf026f38c85b309ea968f3e7dd4c7 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 11 Feb 2022 20:08:01 +0530 Subject: [PATCH 062/129] test: earned leave allocations based on DOJ (cherry picked from commit 9b0f9c344282c9cad5334c6e3b46aa1c74826f9b) --- .../leave_policy_assignment.py | 2 +- .../test_leave_policy_assignment.py | 134 ++++++++++++++++-- erpnext/hr/utils.py | 5 +- 3 files changed, 128 insertions(+), 13 deletions(-) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index 81d06062602..b70b59fe1b0 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -149,7 +149,7 @@ def add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj if based_on_doj: # if leave type allocation is based on DOJ, and the date of assignment creation is same as DOJ, # then the month should be considered - if date == date_of_joining: + if date.day == date_of_joining.day: months_passed += 1 else: last_day_of_month = get_last_day(date) diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py index 2d4fcf91f72..678468ffc2d 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py @@ -20,7 +20,7 @@ test_dependencies = ["Employee"] class TestLeavePolicyAssignment(unittest.TestCase): def setUp(self): for doctype in ["Leave Period", "Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]: - frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec + frappe.db.delete(doctype) self.employee = get_employee() @@ -85,7 +85,7 @@ class TestLeavePolicyAssignment(unittest.TestCase): leave_policy = frappe.get_doc({ "doctype": "Leave Policy", "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 6}] - }).insert() + }).submit() data = { "assignment_based_on": "Leave Period", @@ -117,7 +117,7 @@ class TestLeavePolicyAssignment(unittest.TestCase): }, "total_leaves_allocated") self.assertEqual(leaves_allocated, 1) - def test_earned_leave_alloc_for_passed_months_based_on_leave_period(self): + def test_earned_leave_alloc_for_passed_months_on_month_end_based_on_leave_period(self): leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2))) # Case 2: assignment created on the last day of the leave period's latter month # should allocate 1 leave for current month even though the month has not ended @@ -178,15 +178,132 @@ class TestLeavePolicyAssignment(unittest.TestCase): from erpnext.hr.utils import is_earned_leave_already_allocated frappe.flags.current_date = get_last_day(getdate()) - allocation = frappe.get_doc('Leave Allocation', details.name) + allocation = frappe.get_doc("Leave Allocation", details.name) # 1 leave is still pending to be allocated, irrespective of carry forwarded leaves self.assertFalse(is_earned_leave_already_allocated(allocation, leave_policy.leave_policy_details[0].annual_allocation)) + def test_earned_leave_alloc_for_passed_months_based_on_joining_date(self): + # tests leave alloc for earned leaves for assignment based on joining date in policy assignment + leave_type = create_earned_leave_type("Test Earned Leave") + leave_policy = frappe.get_doc({ + "doctype": "Leave Policy", + "title": "Test Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] + }).submit() + + # joining date set to 2 months back + doj = self.employee.date_of_joining + self.employee.date_of_joining = get_first_day(add_months(getdate(), -2)) + self.employee.save() + + # assignment created on the last day of the current month + frappe.flags.current_date = get_last_day(getdate()) + data = { + "assignment_based_on": "Joining Date", + "leave_policy": leave_policy.name + } + leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated") + effective_from = frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "effective_from") + self.assertEqual(effective_from, self.employee.date_of_joining) + self.assertEqual(leaves_allocated, 3) + + # to ensure leave is not already allocated to avoid duplication + from erpnext.hr.utils import allocate_earned_leaves + frappe.flags.current_date = get_last_day(getdate()) + allocate_earned_leaves() + + leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated") + self.assertEqual(leaves_allocated, 3) + + # reset DOJ + frappe.db.set_value("Employee", self.employee.name, "date_of_joining", doj) + + def test_grant_leaves_on_doj_for_earned_leaves_based_on_leave_period(self): + # tests leave alloc based on leave period for earned leaves with "based on doj" configuration in leave type + leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2)), based_on_doj=True) + + # joining date set to 2 months back + doj = self.employee.date_of_joining + self.employee.date_of_joining = get_first_day(add_months(getdate(), -2)) + self.employee.save() + + # assignment created on the same day of the current month, should allocate leaves including the current month + frappe.flags.current_date = get_first_day(getdate()) + + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": leave_period.name + } + leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + + leaves_allocated = frappe.db.get_value("Leave Allocation", { + "leave_policy_assignment": leave_policy_assignments[0] + }, "total_leaves_allocated") + self.assertEqual(leaves_allocated, 3) + + # if the daily job is not completed yet, there is another check present + # to ensure leave is not already allocated to avoid duplication + from erpnext.hr.utils import allocate_earned_leaves + frappe.flags.current_date = get_first_day(getdate()) + allocate_earned_leaves() + + leaves_allocated = frappe.db.get_value("Leave Allocation", { + "leave_policy_assignment": leave_policy_assignments[0] + }, "total_leaves_allocated") + self.assertEqual(leaves_allocated, 3) + + # reset DOJ + frappe.db.set_value("Employee", self.employee.name, "date_of_joining", doj) + + def test_grant_leaves_on_doj_for_earned_leaves_based_on_joining_date(self): + # tests leave alloc based on joining date for earned leaves with "based on doj" configuration in leave type + leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj=True) + leave_policy = frappe.get_doc({ + "doctype": "Leave Policy", + "title": "Test Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] + }).submit() + + # joining date set to 2 months back + # leave should be allocated for current month too since this day is same as the joining day + doj = self.employee.date_of_joining + self.employee.date_of_joining = get_first_day(add_months(getdate(), -2)) + self.employee.save() + + # assignment created on the first day of the current month + frappe.flags.current_date = get_first_day(getdate()) + data = { + "assignment_based_on": "Joining Date", + "leave_policy": leave_policy.name + } + leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated") + effective_from = frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "effective_from") + self.assertEqual(effective_from, self.employee.date_of_joining) + self.assertEqual(leaves_allocated, 3) + + # to ensure leave is not already allocated to avoid duplication + from erpnext.hr.utils import allocate_earned_leaves + frappe.flags.current_date = get_first_day(getdate()) + allocate_earned_leaves() + + leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated") + self.assertEqual(leaves_allocated, 3) + + # reset DOJ + frappe.db.set_value("Employee", self.employee.name, "date_of_joining", doj) + def tearDown(self): frappe.db.rollback() -def create_earned_leave_type(leave_type): +def create_earned_leave_type(leave_type, based_on_doj=False): frappe.delete_doc_if_exists("Leave Type", leave_type, force=1) return frappe.get_doc(dict( @@ -195,7 +312,8 @@ def create_earned_leave_type(leave_type): is_earned_leave=1, earned_leave_frequency="Monthly", rounding=0.5, - is_carry_forward=1 + is_carry_forward=1, + based_on_date_of_joining=based_on_doj )).insert() @@ -214,8 +332,8 @@ def create_leave_period(name, start_date=None): )).insert() -def setup_leave_period_and_policy(start_date): - leave_type = create_earned_leave_type("Test Earned Leave") +def setup_leave_period_and_policy(start_date, based_on_doj=False): + leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj) leave_period = create_leave_period("Test Earned Leave Period", start_date=start_date) leave_policy = frappe.get_doc({ diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 416366de9c0..46bcadcf536 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -421,13 +421,10 @@ def is_earned_leave_already_allocated(allocation, annual_allocation): get_leave_type_details, ) - assignment = frappe.get_doc("Leave Policy Assignment", allocation.leave_policy_assignment) - if assignment.assignment_based_on == "Joining Date": - return False - leave_type_details = get_leave_type_details() date_of_joining = frappe.db.get_value("Employee", allocation.employee, "date_of_joining") + assignment = frappe.get_doc("Leave Policy Assignment", allocation.leave_policy_assignment) leaves_for_passed_months = assignment.get_leaves_for_passed_months(allocation.leave_type, annual_allocation, leave_type_details, date_of_joining) From 6a1c27a18f6747822b6352d865c961e969131e73 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 11 Feb 2022 20:59:19 +0530 Subject: [PATCH 063/129] fix(test): reset test setup (cherry picked from commit cbaadcf1138cba113cc18c6d2bc2690e144cf9d0) --- .../test_leave_policy_assignment.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py index 678468ffc2d..8d7b27ee5af 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py @@ -22,7 +22,9 @@ class TestLeavePolicyAssignment(unittest.TestCase): for doctype in ["Leave Period", "Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]: frappe.db.delete(doctype) - self.employee = get_employee() + employee = get_employee() + self.original_doj = employee.date_of_joining + self.employee = employee def test_grant_leaves(self): leave_period = get_leave_period() @@ -192,7 +194,6 @@ class TestLeavePolicyAssignment(unittest.TestCase): }).submit() # joining date set to 2 months back - doj = self.employee.date_of_joining self.employee.date_of_joining = get_first_day(add_months(getdate(), -2)) self.employee.save() @@ -218,15 +219,11 @@ class TestLeavePolicyAssignment(unittest.TestCase): "total_leaves_allocated") self.assertEqual(leaves_allocated, 3) - # reset DOJ - frappe.db.set_value("Employee", self.employee.name, "date_of_joining", doj) - def test_grant_leaves_on_doj_for_earned_leaves_based_on_leave_period(self): # tests leave alloc based on leave period for earned leaves with "based on doj" configuration in leave type leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2)), based_on_doj=True) # joining date set to 2 months back - doj = self.employee.date_of_joining self.employee.date_of_joining = get_first_day(add_months(getdate(), -2)) self.employee.save() @@ -256,9 +253,6 @@ class TestLeavePolicyAssignment(unittest.TestCase): }, "total_leaves_allocated") self.assertEqual(leaves_allocated, 3) - # reset DOJ - frappe.db.set_value("Employee", self.employee.name, "date_of_joining", doj) - def test_grant_leaves_on_doj_for_earned_leaves_based_on_joining_date(self): # tests leave alloc based on joining date for earned leaves with "based on doj" configuration in leave type leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj=True) @@ -270,7 +264,6 @@ class TestLeavePolicyAssignment(unittest.TestCase): # joining date set to 2 months back # leave should be allocated for current month too since this day is same as the joining day - doj = self.employee.date_of_joining self.employee.date_of_joining = get_first_day(add_months(getdate(), -2)) self.employee.save() @@ -296,11 +289,10 @@ class TestLeavePolicyAssignment(unittest.TestCase): "total_leaves_allocated") self.assertEqual(leaves_allocated, 3) - # reset DOJ - frappe.db.set_value("Employee", self.employee.name, "date_of_joining", doj) - def tearDown(self): frappe.db.rollback() + frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj) + frappe.flags.current_date = None def create_earned_leave_type(leave_type, based_on_doj=False): From 0e3788e34656fc0d41cf47161282aace4ec2591a Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 11 Feb 2022 21:27:44 +0530 Subject: [PATCH 064/129] fix: conflicts --- .../leave_policy_assignment/leave_policy_assignment.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index b70b59fe1b0..6e6943f71aa 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -8,12 +8,8 @@ from math import ceil import frappe from frappe import _, bold from frappe.model.document import Document -<<<<<<< HEAD -from frappe.utils import date_diff, flt, formatdate, get_datetime, get_last_day, getdate -from six import string_types -======= from frappe.utils import date_diff, flt, formatdate, get_last_day, getdate ->>>>>>> c7be9ef5d2 (fix: consider leaves for past months if assignment is based on joining date too) +from six import string_types class LeavePolicyAssignment(Document): From b7b5531ed030de9596f8a7edfc20cf664bb6425a Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Fri, 11 Feb 2022 15:12:25 +0530 Subject: [PATCH 065/129] chore: remove deprecated print format (cherry picked from commit d93d2a80b10c94cc2d7f8b5a3601d0efec8cbf2d) --- .../print_format/gst_pos_invoice/__init__.py | 0 .../gst_pos_invoice/gst_pos_invoice.json | 23 ------------------- 2 files changed, 23 deletions(-) delete mode 100644 erpnext/accounts/print_format/gst_pos_invoice/__init__.py delete mode 100644 erpnext/accounts/print_format/gst_pos_invoice/gst_pos_invoice.json diff --git a/erpnext/accounts/print_format/gst_pos_invoice/__init__.py b/erpnext/accounts/print_format/gst_pos_invoice/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/accounts/print_format/gst_pos_invoice/gst_pos_invoice.json b/erpnext/accounts/print_format/gst_pos_invoice/gst_pos_invoice.json deleted file mode 100644 index 1aa1c02968f..00000000000 --- a/erpnext/accounts/print_format/gst_pos_invoice/gst_pos_invoice.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "align_labels_right": 0, - "creation": "2017-08-08 12:33:04.773099", - "custom_format": 1, - "disabled": 0, - "doc_type": "Sales Invoice", - "docstatus": 0, - "doctype": "Print Format", - "font": "Default", - "html": "\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n

\n\t{{ doc.company }}
\n\t{% if doc.company_address_display %}\n\t\t{% set company_address = doc.company_address_display.replace(\"\\n\", \" \").replace(\"
\", \" \") %}\n\t\t{% if \"GSTIN\" not in company_address %}\n\t\t\t{{ company_address }}\n\t\t\t{{ _(\"GSTIN\") }}:{{ doc.company_gstin }}\n\t\t{% else %}\n\t\t\t{{ company_address.replace(\"GSTIN\", \"
GSTIN\") }}\n\t\t{% endif %}\n\t{% endif %}\n\t
\n\t{% if doc.docstatus == 0 %}\n\t\t{{ doc.status + \" \"+ (doc.select_print_heading or _(\"Invoice\")) }}
\n\t{% else %}\n\t\t{{ doc.select_print_heading or _(\"Invoice\") }}
\n\t{% endif %}\n

\n

\n\t{{ _(\"Receipt No\") }}: {{ doc.name }}
\n\t{{ _(\"Date\") }}: {{ doc.get_formatted(\"posting_date\") }}
\n\t{% if doc.grand_total > 50000 %}\n\t\t{% set customer_address = doc.address_display.replace(\"\\n\", \" \").replace(\"
\", \" \") %}\n\t\t{{ _(\"Customer\") }}:
\n\t\t{{ doc.customer_name }}
\n\t\t{{ customer_address }}\n\t{% endif %}\n

\n\n
\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\t{%- for item in doc.items -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endfor -%}\n\t\n
{{ _(\"Item\") }}{{ _(\"Qty\") }}{{ _(\"Amount\") }}
\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t
{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.gst_hsn_code -%}\n\t\t\t\t\t
{{ _(\"HSN/SAC\") }}: {{ item.gst_hsn_code }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t
{{ _(\"Serial No\") }}: {{ item.serial_no }}\n\t\t\t\t{%- endif -%}\n\t\t\t
{{ item.qty }}
@ {{ item.rate }}
{{ item.get_formatted(\"amount\") }}
\n\n\t\n\t\t\n\t\t\t{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t\n\t\t\t\t\n\t\t\t{% else %}\n\t\t\t\t\n\t\t\t\t\n\t\t\t{% endif %}\n\t\t\n\t\t{%- for row in doc.taxes -%}\n\t\t {%- if (not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) and row.tax_amount != 0 -%}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t {%- endif -%}\n\t\t{%- endfor -%}\n\t\t{%- if doc.discount_amount -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endif -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- if doc.rounded_total -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{%- endif -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t{%- if doc.change_amount -%}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t{%- endif -%}\n\t\n
\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t
\n\t\t\t\t\t{{ row.description }}\n\t\t\t\t\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t
\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Grand Total\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Rounded Total\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Paid Amount\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t
\n\t\t\t\t{{ _(\"Change Amount\") }}\n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t
\n

{{ doc.terms or \"\" }}

\n

{{ _(\"Thank you, please visit again.\") }}

", - "idx": 0, - "line_breaks": 0, - "modified": "2020-04-29 16:39:12.936215", - "modified_by": "Administrator", - "module": "Accounts", - "name": "GST POS Invoice", - "owner": "Administrator", - "print_format_builder": 0, - "print_format_type": "Jinja", - "raw_printing": 0, - "show_section_headings": 0, - "standard": "Yes" -} \ No newline at end of file From 894a406ed406f8e6fa3efed9315609ffc33075f6 Mon Sep 17 00:00:00 2001 From: Govind S Menokee Date: Tue, 8 Feb 2022 23:53:20 +0530 Subject: [PATCH 066/129] Bug: fix list mutation within loop Prevent list mutation within loop leading to incorrect data --- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 99dfc231c74..4ef29848bc6 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -479,11 +479,12 @@ def get_emp_list(sal_struct, cond, end_date, payroll_payable_account): """ % cond, {"sal_struct": tuple(sal_struct), "from_date": end_date, "payroll_payable_account": payroll_payable_account}, as_dict=True) def remove_payrolled_employees(emp_list, start_date, end_date): + new_emp_list = [] for employee_details in emp_list: - if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": start_date, "end_date": end_date, "docstatus": 1}): - emp_list.remove(employee_details) + if not frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": start_date, "end_date": end_date, "docstatus": 1}): + new_emp_list.append(employee_details) - return emp_list + return new_emp_list @frappe.whitelist() def get_start_end_dates(payroll_frequency, start_date=None, company=None): From c7fbeb3aac3724f991b2e3819ea87adcd55b81b3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 12 Feb 2022 20:55:27 +0530 Subject: [PATCH 067/129] fix(India): Report GSTR-1 minor fixes (#29768) * fix(India): Report GSTR-1 minor fixes (cherry picked from commit 6e679a5ad2f82f6c97deb4446590abe0d5c3ab46) * fix: cleaner implementation for `get_invoice_type` (cherry picked from commit 2bc157a95cff5d13f492fddf7c177b3e67fe62a8) Co-authored-by: Smit Vora Co-authored-by: Sagar Vora --- erpnext/regional/report/gstr_1/gstr_1.py | 58 ++++++++++-------------- 1 file changed, 25 insertions(+), 33 deletions(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 17b11073346..8805678dc7d 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -29,7 +29,7 @@ class Gstr1Report(object): posting_date, base_grand_total, base_rounded_total, - COALESCE(NULLIF(customer_gstin,''), NULLIF(billing_address_gstin, '')) as customer_gstin, + NULLIF(billing_address_gstin, '') as billing_address_gstin, place_of_supply, ecommerce_gstin, reverse_charge, @@ -260,7 +260,7 @@ class Gstr1Report(object): if self.filters.get("type_of_business") == "B2B": - conditions += "AND IFNULL(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') AND is_return != 1 AND is_debit_note !=1" + conditions += "AND IFNULL(gst_category, '') in ('Registered Regular', 'Registered Composition', 'Deemed Export', 'SEZ') AND is_return != 1 AND is_debit_note !=1" if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"): b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit') @@ -384,7 +384,7 @@ class Gstr1Report(object): for invoice, items in iteritems(self.invoice_items): if invoice not in self.items_based_on_tax_rate and invoice not in unidentified_gst_accounts_invoice \ and self.invoices.get(invoice, {}).get('export_type') == "Without Payment of Tax" \ - and self.invoices.get(invoice, {}).get('gst_category') == "Overseas": + and self.invoices.get(invoice, {}).get('gst_category') in ("Overseas", "SEZ"): self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) def get_columns(self): @@ -410,7 +410,7 @@ class Gstr1Report(object): if self.filters.get("type_of_business") == "B2B": self.invoice_columns = [ { - "fieldname": "customer_gstin", + "fieldname": "billing_address_gstin", "label": "GSTIN/UIN of Recipient", "fieldtype": "Data", "width": 150 @@ -517,7 +517,7 @@ class Gstr1Report(object): elif self.filters.get("type_of_business") == "CDNR-REG": self.invoice_columns = [ { - "fieldname": "customer_gstin", + "fieldname": "billing_address_gstin", "label": "GSTIN/UIN of Recipient", "fieldtype": "Data", "width": 150 @@ -818,7 +818,7 @@ def get_json(filters, report_name, data): res = {} if filters["type_of_business"] == "B2B": for item in report_data[:-1]: - res.setdefault(item["customer_gstin"], {}).setdefault(item["invoice_number"],[]).append(item) + res.setdefault(item["billing_address_gstin"], {}).setdefault(item["invoice_number"],[]).append(item) out = get_b2b_json(res, gstin) gst_json["b2b"] = out @@ -842,7 +842,7 @@ def get_json(filters, report_name, data): gst_json["exp"] = out elif filters["type_of_business"] == "CDNR-REG": for item in report_data[:-1]: - res.setdefault(item["customer_gstin"], {}).setdefault(item["invoice_number"],[]).append(item) + res.setdefault(item["billing_address_gstin"], {}).setdefault(item["invoice_number"],[]).append(item) out = get_cdnr_reg_json(res, gstin) gst_json["cdnr"] = out @@ -876,7 +876,7 @@ def get_json(filters, report_name, data): } def get_b2b_json(res, gstin): - inv_type, out = {"Registered Regular": "R", "Deemed Export": "DE", "URD": "URD", "SEZ": "SEZ"}, [] + out = [] for gst_in in res: b2b_item, inv = {"ctin": gst_in, "inv": []}, [] if not gst_in: continue @@ -890,7 +890,7 @@ def get_b2b_json(res, gstin): inv_item = get_basic_invoice_detail(invoice[0]) inv_item["pos"] = "%02d" % int(invoice[0]["place_of_supply"].split('-')[0]) inv_item["rchrg"] = invoice[0]["reverse_charge"] - inv_item["inv_typ"] = inv_type.get(invoice[0].get("gst_category", ""),"") + inv_item["inv_typ"] = get_invoice_type(invoice[0]) if inv_item["pos"]=="00": continue inv_item["itms"] = [] @@ -1045,7 +1045,7 @@ def get_cdnr_reg_json(res, gstin): "ntty": invoice[0]["document_type"], "pos": "%02d" % int(invoice[0]["place_of_supply"].split('-')[0]), "rchrg": invoice[0]["reverse_charge"], - "inv_typ": get_invoice_type_for_cdnr(invoice[0]) + "inv_typ": get_invoice_type(invoice[0]) } inv_item["itms"] = [] @@ -1070,7 +1070,7 @@ def get_cdnr_unreg_json(res, gstin): "val": abs(flt(items[0]["invoice_value"])), "ntty": items[0]["document_type"], "pos": "%02d" % int(items[0]["place_of_supply"].split('-')[0]), - "typ": get_invoice_type_for_cdnrur(items[0]) + "typ": get_invoice_type(items[0]) } inv_item["itms"] = [] @@ -1111,29 +1111,21 @@ def get_exempted_json(data): return out -def get_invoice_type_for_cdnr(row): - if row.get('gst_category') == 'SEZ': - if row.get('export_type') == 'WPAY': - invoice_type = 'SEWP' - else: - invoice_type = 'SEWOP' - elif row.get('gst_category') == 'Deemed Export': - invoice_type = 'DE' - elif row.get('gst_category') == 'Registered Regular': - invoice_type = 'R' +def get_invoice_type(row): + gst_category = row.get('gst_category') - return invoice_type + if gst_category == 'SEZ': + return 'SEWP' if row.get('export_type') == 'WPAY' else 'SEWOP' -def get_invoice_type_for_cdnrur(row): - if row.get('gst_category') == 'Overseas': - if row.get('export_type') == 'WPAY': - invoice_type = 'EXPWP' - else: - invoice_type = 'EXPWOP' - elif row.get('gst_category') == 'Unregistered': - invoice_type = 'B2CL' + if gst_category == 'Overseas': + return 'EXPWP' if row.get('export_type') == 'WPAY' else 'EXPWOP' - return invoice_type + return ({ + 'Deemed Export': 'DE', + 'Registered Regular': 'R', + 'Registered Composition': 'R', + 'Unregistered': 'B2CL' + }).get(gst_category) def get_basic_invoice_detail(row): return { @@ -1155,7 +1147,7 @@ def get_rate_and_tax_details(row, gstin): # calculate tax amount added tax = flt((row["taxable_value"]*rate)/100.0, 2) frappe.errprint([tax, tax/2]) - if row.get("customer_gstin") and gstin[0:2] == row["customer_gstin"][0:2]: + if row.get("billing_address_gstin") and gstin[0:2] == row["billing_address_gstin"][0:2]: itm_det.update({"camt": flt(tax/2.0, 2), "samt": flt(tax/2.0, 2)}) else: itm_det.update({"iamt": tax}) @@ -1200,4 +1192,4 @@ def is_inter_state(invoice_detail): if invoice_detail.place_of_supply.split("-")[0] != invoice_detail.company_gstin[:2]: return True else: - return False \ No newline at end of file + return False From e5b50557e64628937d9a8fbfaa875cfc5b36a3cf Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 10:09:39 +0530 Subject: [PATCH 068/129] fix: show user id in emp group table (backport #29776) (#29777) Co-authored-by: Dany Robert --- .../doctype/employee_group_table/employee_group_table.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/employee_group_table/employee_group_table.json b/erpnext/hr/doctype/employee_group_table/employee_group_table.json index 4e0045cdeb8..54eb8c6da91 100644 --- a/erpnext/hr/doctype/employee_group_table/employee_group_table.json +++ b/erpnext/hr/doctype/employee_group_table/employee_group_table.json @@ -27,12 +27,13 @@ "fetch_from": "employee.user_id", "fieldname": "user_id", "fieldtype": "Data", + "in_list_view": 1, "label": "ERPNext User ID", "read_only": 1 } ], "istable": 1, - "modified": "2019-06-06 10:41:20.313756", + "modified": "2022-02-13 19:44:21.302938", "modified_by": "Administrator", "module": "HR", "name": "Employee Group Table", @@ -42,4 +43,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} From 47f8b44cfe442abdd6605b2c51b0c7fd06de1d3a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 14 Feb 2022 11:33:34 +0530 Subject: [PATCH 069/129] fix: incorrect pricing rule filtering on selecting first item (cherry picked from commit 3713ae75ab16ea7ca469ab82d529da571583cea2) --- erpnext/stock/get_item_details.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 339d1b60839..e7b4ca2de38 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -344,6 +344,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): args.conversion_factor = out.conversion_factor out.stock_qty = out.qty * out.conversion_factor + args.stock_qty = out.stock_qty # calculate last purchase rate if args.get('doctype') in purchase_doctypes: From 7fa46f77e0bdbc516b3c0cb0fb20594ee7fa398b Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Mon, 14 Feb 2022 19:54:29 +0530 Subject: [PATCH 070/129] revert: "Merge pull request #29290 from s-aga-r/fix/delivery-note/billed-amount" (#29782) * Revert "Merge pull request #29290 from s-aga-r/fix/delivery-note/billed-amount" This reverts commit 038f94955006c88209f9df28e3a785c59a4ddb28, reversing changes made to c7b491843476bca89be02851ccafb7e409876609. * fix: linter --- .../doctype/sales_invoice/sales_invoice.py | 6 +++--- erpnext/patches.txt | 2 +- .../stock/doctype/delivery_note/delivery_note.py | 16 ++++------------ 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 0c22a0e092b..42da6b7708f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1263,14 +1263,14 @@ class SalesInvoice(SellingController): def update_billing_status_in_dn(self, update_modified=True): updated_delivery_notes = [] for d in self.get("items"): - if d.so_detail: - updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified) - elif d.dn_detail: + if d.dn_detail: billed_amt = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item` where dn_detail=%s and docstatus=1""", d.dn_detail) billed_amt = billed_amt and billed_amt[0][0] or 0 frappe.db.set_value("Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified) updated_delivery_notes.append(d.delivery_note) + elif d.so_detail: + updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified) for dn in set(updated_delivery_notes): frappe.get_doc("Delivery Note", dn).update_billing_percentage(update_modified=update_modified) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 4bbeb64ce1c..f4a6e089637 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -348,4 +348,4 @@ erpnext.patches.v13_0.delete_bank_reconciliation_detail erpnext.patches.v13_0.update_sane_transfer_against erpnext.patches.v13_0.enable_provisional_accounting erpnext.patches.v13_0.update_disbursement_account -erpnext.patches.v13_0.update_reserved_qty_closed_wo +erpnext.patches.v13_0.update_reserved_qty_closed_wo \ No newline at end of file diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index c3247fbe3e8..00836fc8157 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -342,25 +342,21 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True): from frappe.query_builder.functions import Sum # Billed against Sales Order directly - si = frappe.qb.DocType("Sales Invoice").as_("si") si_item = frappe.qb.DocType("Sales Invoice Item").as_("si_item") sum_amount = Sum(si_item.amount).as_("amount") - billed_against_so = frappe.qb.from_(si).from_(si_item).select(sum_amount).where( - (si_item.parent == si.name) & + billed_against_so = frappe.qb.from_(si_item).select(sum_amount).where( (si_item.so_detail == so_detail) & ((si_item.dn_detail.isnull()) | (si_item.dn_detail == '')) & - (si_item.docstatus == 1) & - (si.update_stock == 0) + (si_item.docstatus == 1) ).run() billed_against_so = billed_against_so and billed_against_so[0][0] or 0 # Get all Delivery Note Item rows against the Sales Order Item row - dn = frappe.qb.DocType("Delivery Note").as_("dn") dn_item = frappe.qb.DocType("Delivery Note Item").as_("dn_item") - dn_details = frappe.qb.from_(dn).from_(dn_item).select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent, dn_item.stock_qty, dn_item.returned_qty).where( + dn_details = frappe.qb.from_(dn).from_(dn_item).select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent).where( (dn.name == dn_item.parent) & (dn_item.so_detail == so_detail) & (dn.docstatus == 1) & @@ -385,11 +381,7 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True): # Distribute billed amount directly against SO between DNs based on FIFO if billed_against_so and billed_amt_agianst_dn < dnd.amount: - if dnd.returned_qty: - pending_to_bill = flt(dnd.amount) * (dnd.stock_qty - dnd.returned_qty) / dnd.stock_qty - else: - pending_to_bill = flt(dnd.amount) - pending_to_bill -= billed_amt_agianst_dn + pending_to_bill = flt(dnd.amount) - billed_amt_agianst_dn if pending_to_bill <= billed_against_so: billed_amt_agianst_dn += pending_to_bill billed_against_so -= pending_to_bill From c7b9df5aee0992ffd45ca8b2c4e2e7308e92b36e Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 8 Feb 2022 12:40:33 +0530 Subject: [PATCH 071/129] fix: Set Pending Qty in Prod Plan after updating Work Order (cherry picked from commit 7116d7ae0eabe9c31e03b84466ba74751e9479a9) --- erpnext/manufacturing/doctype/production_plan/production_plan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index f6d2a14a074..c7e5d5a8614 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -280,6 +280,7 @@ class ProductionPlan(Document): for data in self.po_items: if data.name == production_plan_item: data.produced_qty = produced_qty + data.pending_qty = flt(data.planned_qty - produced_qty) data.db_update() self.calculate_total_produced_qty() From 6800c3efaeb58c9898faf362788ae71ba298fd3a Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 8 Feb 2022 17:55:38 +0530 Subject: [PATCH 072/129] fix: Initialise pending qty as planned qty for independent item rows in Prod Plan - Rows that are not fetched from MR or SO, had pending qty 0 throughout - Initialise pending qty on save only. - After submit this field will be updated by work order/stock entry - Bring functions in `validate()` closer to the top (cherry picked from commit eaccef6116f051bfa8c65934c1b45767e7465aaa) --- .../production_plan/production_plan.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index c7e5d5a8614..d3a228d7aeb 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -29,9 +29,24 @@ from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults class ProductionPlan(Document): def validate(self): + self.set_pending_qty_in_row_without_reference() self.calculate_total_planned_qty() self.set_status() + def set_pending_qty_in_row_without_reference(self): + "Set Pending Qty in independent rows (not from SO or MR)." + if self.docstatus > 0: # set only to initialise value before submit + return + + for item in self.po_items: + if not item.get("sales_order") or not item.get("material_request"): + item.pending_qty = item.planned_qty + + def calculate_total_planned_qty(self): + self.total_planned_qty = 0 + for d in self.po_items: + self.total_planned_qty += flt(d.planned_qty) + def validate_data(self): for d in self.get('po_items'): if not d.bom_no: @@ -264,11 +279,6 @@ class ProductionPlan(Document): 'qty': so_detail['qty'] }) - def calculate_total_planned_qty(self): - self.total_planned_qty = 0 - for d in self.po_items: - self.total_planned_qty += flt(d.planned_qty) - def calculate_total_produced_qty(self): self.total_produced_qty = 0 for d in self.po_items: From 1df4e2c44a27b22f308935d64700f7abcd468a6a Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 10 Feb 2022 20:14:28 +0530 Subject: [PATCH 073/129] test: Production Plan Pending Qty impact tests - Two tests to check impact on pending qty: From SO and independent Prod Plan - Added docstring to each test case for brief summary - Changed helper function args to fallback to 0 instead of 1 if no arg is passed - Removed unnecessary `get_doc()` - Made helper function actions optional depending on args passed (cherry picked from commit 86ca41b14af45f44ec63a27ed10580b161a33b4c) --- .../production_plan/production_plan.py | 2 +- .../production_plan/test_production_plan.py | 253 ++++++++++++++---- .../doctype/work_order/work_order.py | 2 +- 3 files changed, 209 insertions(+), 48 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index d3a228d7aeb..1ccd9dc7ad7 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -286,7 +286,7 @@ class ProductionPlan(Document): self.db_set("total_produced_qty", self.total_produced_qty, update_modified=False) - def update_produced_qty(self, produced_qty, production_plan_item): + def update_produced_pending_qty(self, produced_qty, production_plan_item): for data in self.po_items: if data.name == production_plan_item: data.produced_qty = produced_qty diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 276e70859e6..3aa5c9f008d 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -11,6 +11,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import ( ) from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, ) @@ -36,15 +37,21 @@ class TestProductionPlan(ERPNextTestCase): if not frappe.db.get_value('BOM', {'item': item}): make_bom(item = item, raw_materials = raw_materials) - def test_production_plan(self): + def test_production_plan_mr_creation(self): + "Test if MRs are created for unavailable raw materials." pln = create_production_plan(item_code='Test Production Item 1') self.assertTrue(len(pln.mr_items), 2) - pln.make_material_request() - pln = frappe.get_doc('Production Plan', pln.name) + pln.make_material_request() + pln.reload() self.assertTrue(pln.status, 'Material Requested') - material_requests = frappe.get_all('Material Request Item', fields = ['distinct parent'], - filters = {'production_plan': pln.name}, as_list=1) + + material_requests = frappe.get_all( + 'Material Request Item', + fields = ['distinct parent'], + filters = {'production_plan': pln.name}, + as_list=1 + ) self.assertTrue(len(material_requests), 2) @@ -66,27 +73,42 @@ class TestProductionPlan(ERPNextTestCase): pln.cancel() def test_production_plan_start_date(self): + "Test if Work Order has same Planned Start Date as Prod Plan." planned_date = add_to_date(date=None, days=3) - plan = create_production_plan(item_code='Test Production Item 1', planned_start_date=planned_date) + plan = create_production_plan( + item_code='Test Production Item 1', + planned_start_date=planned_date + ) plan.make_work_order() - work_orders = frappe.get_all('Work Order', fields = ['name', 'planned_start_date'], - filters = {'production_plan': plan.name}) + work_orders = frappe.get_all( + 'Work Order', + fields = ['name', 'planned_start_date'], + filters = {'production_plan': plan.name} + ) self.assertEqual(work_orders[0].planned_start_date, planned_date) for wo in work_orders: frappe.delete_doc('Work Order', wo.name) - frappe.get_doc('Production Plan', plan.name).cancel() + plan.reload() + plan.cancel() def test_production_plan_for_existing_ordered_qty(self): + """ + - Enable 'ignore_existing_ordered_qty'. + - Test if MR Planning table pulls Raw Material Qty even if it is in stock. + """ sr1 = create_stock_reconciliation(item_code="Raw Material Item 1", target="_Test Warehouse - _TC", qty=1, rate=110) sr2 = create_stock_reconciliation(item_code="Raw Material Item 2", target="_Test Warehouse - _TC", qty=1, rate=120) - pln = create_production_plan(item_code='Test Production Item 1', ignore_existing_ordered_qty=0) + pln = create_production_plan( + item_code='Test Production Item 1', + ignore_existing_ordered_qty=1 + ) self.assertTrue(len(pln.mr_items), 1) self.assertTrue(flt(pln.mr_items[0].quantity), 1.0) @@ -95,23 +117,39 @@ class TestProductionPlan(ERPNextTestCase): pln.cancel() def test_production_plan_with_non_stock_item(self): - pln = create_production_plan(item_code='Test Production Item 1', include_non_stock_items=0) + "Test if MR Planning table includes Non Stock RM." + pln = create_production_plan( + item_code='Test Production Item 1', + include_non_stock_items=1 + ) self.assertTrue(len(pln.mr_items), 3) pln.cancel() def test_production_plan_without_multi_level(self): - pln = create_production_plan(item_code='Test Production Item 1', use_multi_level_bom=0) + "Test MR Planning table for non exploded BOM." + pln = create_production_plan( + item_code='Test Production Item 1', + use_multi_level_bom=0 + ) self.assertTrue(len(pln.mr_items), 2) pln.cancel() def test_production_plan_without_multi_level_for_existing_ordered_qty(self): + """ + - Disable 'ignore_existing_ordered_qty'. + - Test if MR Planning table avoids pulling Raw Material Qty as it is in stock for + non exploded BOM. + """ sr1 = create_stock_reconciliation(item_code="Raw Material Item 1", target="_Test Warehouse - _TC", qty=1, rate=130) sr2 = create_stock_reconciliation(item_code="Subassembly Item 1", target="_Test Warehouse - _TC", qty=1, rate=140) - pln = create_production_plan(item_code='Test Production Item 1', - use_multi_level_bom=0, ignore_existing_ordered_qty=0) + pln = create_production_plan( + item_code='Test Production Item 1', + use_multi_level_bom=0, + ignore_existing_ordered_qty=0 + ) self.assertTrue(len(pln.mr_items), 0) sr1.cancel() @@ -119,6 +157,7 @@ class TestProductionPlan(ERPNextTestCase): pln.cancel() def test_production_plan_sales_orders(self): + "Test if previously fulfilled SO (with WO) is pulled into Prod Plan." item = 'Test Production Item 1' so = make_sales_order(item_code=item, qty=1) sales_order = so.name @@ -166,24 +205,25 @@ class TestProductionPlan(ERPNextTestCase): self.assertEqual(sales_orders, []) def test_production_plan_combine_items(self): + "Test combining FG items in Production Plan." item = 'Test Production Item 1' - so = make_sales_order(item_code=item, qty=1) + so1 = make_sales_order(item_code=item, qty=1) pln = frappe.new_doc('Production Plan') - pln.company = so.company + pln.company = so1.company pln.get_items_from = 'Sales Order' pln.append('sales_orders', { - 'sales_order': so.name, - 'sales_order_date': so.transaction_date, - 'customer': so.customer, - 'grand_total': so.grand_total + 'sales_order': so1.name, + 'sales_order_date': so1.transaction_date, + 'customer': so1.customer, + 'grand_total': so1.grand_total }) - so = make_sales_order(item_code=item, qty=2) + so2 = make_sales_order(item_code=item, qty=2) pln.append('sales_orders', { - 'sales_order': so.name, - 'sales_order_date': so.transaction_date, - 'customer': so.customer, - 'grand_total': so.grand_total + 'sales_order': so2.name, + 'sales_order_date': so2.transaction_date, + 'customer': so2.customer, + 'grand_total': so2.grand_total }) pln.combine_items = 1 pln.get_items() @@ -214,28 +254,37 @@ class TestProductionPlan(ERPNextTestCase): so_wo_qty = frappe.db.get_value('Sales Order Item', so_item, 'work_order_qty') self.assertEqual(so_wo_qty, 0.0) - latest_plan = frappe.get_doc('Production Plan', pln.name) - latest_plan.cancel() + pln.reload() + pln.cancel() def test_pp_to_mr_customer_provided(self): - #Material Request from Production Plan for Customer Provided + " Test Material Request from Production Plan for Customer Provided Item." create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) create_item('Production Item CUST') + for item, raw_materials in {'Production Item CUST': ['Raw Material Item 1', 'CUST-0987']}.items(): if not frappe.db.get_value('BOM', {'item': item}): make_bom(item = item, raw_materials = raw_materials) production_plan = create_production_plan(item_code = 'Production Item CUST') production_plan.make_material_request() - material_request = frappe.db.get_value('Material Request Item', {'production_plan': production_plan.name, 'item_code': 'CUST-0987'}, 'parent') + + material_request = frappe.db.get_value( + 'Material Request Item', + {'production_plan': production_plan.name, 'item_code': 'CUST-0987'}, + 'parent' + ) mr = frappe.get_doc('Material Request', material_request) + self.assertTrue(mr.material_request_type, 'Customer Provided') self.assertTrue(mr.customer, '_Test Customer') def test_production_plan_with_multi_level_bom(self): - #|Item Code | Qty | - #|Test BOM 1 | 1 | - #| Test BOM 2 | 2 | - #| Test BOM 3 | 3 | + """ + Item Code | Qty | + |Test BOM 1 | 1 | + |Test BOM 2 | 2 | + |Test BOM 3 | 3 | + """ for item_code in ["Test BOM 1", "Test BOM 2", "Test BOM 3", "Test RM BOM 1"]: create_item(item_code, is_stock_item=1) @@ -264,15 +313,18 @@ class TestProductionPlan(ERPNextTestCase): pln.make_work_order() #last level sub-assembly work order produce qty - to_produce_qty = frappe.db.get_value("Work Order", - {"production_plan": pln.name, "production_item": "Test BOM 3"}, "qty") + to_produce_qty = frappe.db.get_value( + "Work Order", + {"production_plan": pln.name, "production_item": "Test BOM 3"}, + "qty" + ) self.assertEqual(to_produce_qty, 18.0) pln.cancel() frappe.delete_doc("Production Plan", pln.name) def test_get_warehouse_list_group(self): - """Check if required warehouses are returned""" + "Check if required child warehouses are returned." warehouse_json = '[{\"warehouse\":\"_Test Warehouse Group - _TC\"}]' warehouses = set(get_warehouse_list(warehouse_json)) @@ -284,6 +336,7 @@ class TestProductionPlan(ERPNextTestCase): msg=f"Following warehouses were expected {', '.join(missing_warehouse)}") def test_get_warehouse_list_single(self): + "Check if same warehouse is returned in absence of child warehouses." warehouse_json = '[{\"warehouse\":\"_Test Scrap Warehouse - _TC\"}]' warehouses = set(get_warehouse_list(warehouse_json)) @@ -292,6 +345,7 @@ class TestProductionPlan(ERPNextTestCase): self.assertEqual(warehouses, expected_warehouses) def test_get_sales_order_with_variant(self): + "Check if Template BOM is fetched in absence of Variant BOM." rm_item = create_item('PIV_RM', valuation_rate = 100) if not frappe.db.exists('Item', {"item_code": 'PIV'}): item = create_item('PIV', valuation_rate = 100) @@ -348,7 +402,7 @@ class TestProductionPlan(ERPNextTestCase): frappe.db.rollback() def test_subassmebly_sorting(self): - """ Test subassembly sorting in case of multiple items with nested BOMs""" + "Test subassembly sorting in case of multiple items with nested BOMs." from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom prefix = "_TestLevel_" @@ -386,6 +440,7 @@ class TestProductionPlan(ERPNextTestCase): self.assertIn("SuperSecret", plan.sub_assembly_items[0].production_item) def test_multiple_work_order_for_production_plan_item(self): + "Test producing Prod Plan (making WO) in parts." def create_work_order(item, pln, qty): # Get Production Items items_data = pln.get_production_items() @@ -441,7 +496,98 @@ class TestProductionPlan(ERPNextTestCase): pln.reload() self.assertEqual(pln.po_items[0].ordered_qty, 0) + def test_production_plan_pending_qty_with_sales_order(self): + """ + Test Prod Plan impact via: SO -> Prod Plan -> WO -> SE -> SE (cancel) + """ + from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record + from erpnext.manufacturing.doctype.work_order.work_order import ( + make_stock_entry as make_se_from_wo, + ) + + make_stock_entry(item_code="Raw Material Item 1", + target="_Test Warehouse - _TC", + qty=2, basic_rate=100 + ) + make_stock_entry(item_code="Raw Material Item 2", + target="_Test Warehouse - _TC", + qty=2, basic_rate=100 + ) + + item = 'Test Production Item 1' + so = make_sales_order(item_code=item, qty=1) + + pln = create_production_plan( + company=so.company, + get_items_from="Sales Order", + sales_order=so, + skip_getting_mr_items=True + ) + self.assertEqual(pln.po_items[0].pending_qty, 1) + + wo = make_wo_order_test_record( + item_code=item, qty=1, + company=so.company, + wip_warehouse='Work In Progress - _TC', + fg_warehouse='Finished Goods - _TC', + skip_transfer=1, + do_not_submit=True + ) + wo.production_plan = pln.name + wo.production_plan_item = pln.po_items[0].name + wo.submit() + + se = frappe.get_doc(make_se_from_wo(wo.name, "Manufacture", 1)) + se.submit() + + pln.reload() + self.assertEqual(pln.po_items[0].pending_qty, 0) + + se.cancel() + pln.reload() + self.assertEqual(pln.po_items[0].pending_qty, 1) + + def test_production_plan_pending_qty_independent_items(self): + "Test Prod Plan impact if items are added independently (no from SO or MR)." + from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record + from erpnext.manufacturing.doctype.work_order.work_order import ( + make_stock_entry as make_se_from_wo, + ) + + pln = create_production_plan( + item_code='Test Production Item 1', + skip_getting_mr_items=True + ) + self.assertEqual(pln.po_items[0].pending_qty, 1) + + wo = make_wo_order_test_record( + item_code='Test Production Item 1', qty=1, + company=pln.company, + wip_warehouse='Work In Progress - _TC', + fg_warehouse='Finished Goods - _TC', + skip_transfer=1, + do_not_submit=True + ) + wo.production_plan = pln.name + wo.production_plan_item = pln.po_items[0].name + wo.submit() + + se = frappe.get_doc(make_se_from_wo(wo.name, "Manufacture", 1)) + se.submit() + + pln.reload() + self.assertEqual(pln.po_items[0].pending_qty, 0) + + se.cancel() + pln.reload() + self.assertEqual(pln.po_items[0].pending_qty, 1) + def create_production_plan(**args): + """ + sales_order (obj): Sales Order Doc Object + get_items_from (str): Sales Order/Material Request + skip_getting_mr_items (bool): Whether or not to plan for new MRs + """ args = frappe._dict(args) pln = frappe.get_doc({ @@ -449,20 +595,35 @@ def create_production_plan(**args): 'company': args.company or '_Test Company', 'customer': args.customer or '_Test Customer', 'posting_date': nowdate(), - 'include_non_stock_items': args.include_non_stock_items or 1, - 'include_subcontracted_items': args.include_subcontracted_items or 1, - 'ignore_existing_ordered_qty': args.ignore_existing_ordered_qty or 1, - 'po_items': [{ + 'include_non_stock_items': args.include_non_stock_items or 0, + 'include_subcontracted_items': args.include_subcontracted_items or 0, + 'ignore_existing_ordered_qty': args.ignore_existing_ordered_qty or 0, + 'get_items_from': 'Sales Order' + }) + + if not args.get("sales_order"): + pln.append('po_items', { 'use_multi_level_bom': args.use_multi_level_bom or 1, 'item_code': args.item_code, 'bom_no': frappe.db.get_value('Item', args.item_code, 'default_bom'), 'planned_qty': args.planned_qty or 1, 'planned_start_date': args.planned_start_date or now_datetime() - }] - }) - mr_items = get_items_for_material_requests(pln.as_dict()) - for d in mr_items: - pln.append('mr_items', d) + }) + + if args.get("get_items_from") == "Sales Order" and args.get("sales_order"): + so = args.get("sales_order") + pln.append('sales_orders', { + 'sales_order': so.name, + 'sales_order_date': so.transaction_date, + 'customer': so.customer, + 'grand_total': so.grand_total + }) + pln.get_items() + + if not args.get("skip_getting_mr_items"): + mr_items = get_items_for_material_requests(pln.as_dict()) + for d in mr_items: + pln.append('mr_items', d) if not args.do_not_save: pln.insert() diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 8a08c2c6248..f50c82c66b6 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -272,7 +272,7 @@ class WorkOrder(Document): produced_qty = total_qty[0][0] if total_qty else 0 - production_plan.run_method("update_produced_qty", produced_qty, self.production_plan_item) + production_plan.run_method("update_produced_pending_qty", produced_qty, self.production_plan_item) def before_submit(self): self.create_serial_no_batch_no() From 698f910a300f283a3cfa332ef491fba2e44110f6 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 14 Feb 2022 21:00:51 +0530 Subject: [PATCH 074/129] fix: Server Tests and sider (cherry picked from commit e46a1bc80fd2aaf01be4298af0d2b9e93fbdcd24) --- .../production_plan/test_production_plan.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 3aa5c9f008d..afa1501efcd 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -138,7 +138,7 @@ class TestProductionPlan(ERPNextTestCase): """ - Disable 'ignore_existing_ordered_qty'. - Test if MR Planning table avoids pulling Raw Material Qty as it is in stock for - non exploded BOM. + non exploded BOM. """ sr1 = create_stock_reconciliation(item_code="Raw Material Item 1", target="_Test Warehouse - _TC", qty=1, rate=130) @@ -506,11 +506,11 @@ class TestProductionPlan(ERPNextTestCase): ) make_stock_entry(item_code="Raw Material Item 1", - target="_Test Warehouse - _TC", + target="Work In Progress - _TC", qty=2, basic_rate=100 ) make_stock_entry(item_code="Raw Material Item 2", - target="_Test Warehouse - _TC", + target="Work In Progress - _TC", qty=2, basic_rate=100 ) @@ -554,6 +554,15 @@ class TestProductionPlan(ERPNextTestCase): make_stock_entry as make_se_from_wo, ) + make_stock_entry(item_code="Raw Material Item 1", + target="Work In Progress - _TC", + qty=2, basic_rate=100 + ) + make_stock_entry(item_code="Raw Material Item 2", + target="Work In Progress - _TC", + qty=2, basic_rate=100 + ) + pln = create_production_plan( item_code='Test Production Item 1', skip_getting_mr_items=True From 3d45ea0c1f9842620222ed4f1f3b2d134d3492d8 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 11 Feb 2022 18:14:28 +0530 Subject: [PATCH 075/129] fix: Generate Wh wise FIFO Queue and later aggregate if required - Back to back stock recos cause incorrect qty calculation across warehouses - Hard to differentiate how much of the qty is reset by the reco - Maintain Queue and balances warehouse wise and later aggregate for accurate values (cherry picked from commit f62b3207ff4c947f2f45006755134761c30bec96) --- .../stock/report/stock_ageing/stock_ageing.py | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index e6dfc97a998..a89a4038c20 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -252,6 +252,7 @@ class FIFOSlots: key, fifo_queue, transferred_item_key = self.__init_key_stores(d) if d.voucher_type == "Stock Reconciliation": + # get difference in qty shift as actual qty prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0) d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty) @@ -264,12 +265,16 @@ class FIFOSlots: self.__update_balances(d, key) + if not self.filters.get("show_warehouse_wise_stock"): + # (Item 1, WH 1), (Item 1, WH 2) => (Item 1) + self.item_details = self.__aggregate_details_by_item(self.item_details) + return self.item_details def __init_key_stores(self, row: Dict) -> Tuple: "Initialise keys and FIFO Queue." - key = (row.name, row.warehouse) if self.filters.get('show_warehouse_wise_stock') else row.name + key = (row.name, row.warehouse) self.item_details.setdefault(key, {"details": row, "fifo_queue": []}) fifo_queue = self.item_details[key]["fifo_queue"] @@ -338,6 +343,27 @@ class FIFOSlots: self.item_details[key]["has_serial_no"] = row.has_serial_no + def __aggregate_details_by_item(self, wh_wise_data: Dict) -> Dict: + "Aggregate Item-Wh wise data into single Item entry." + item_aggregated_data = {} + for key,row in wh_wise_data.items(): + item = key[0] + if not item_aggregated_data.get(item): + item_aggregated_data.setdefault(item, { + "details": frappe._dict(), + "fifo_queue": [], + "qty_after_transaction": 0.0, + "total_qty": 0.0 + }) + item_row = item_aggregated_data.get(item) + item_row["details"].update(row["details"]) + item_row["fifo_queue"].extend(row["fifo_queue"]) + item_row["qty_after_transaction"] += flt(row["qty_after_transaction"]) + item_row["total_qty"] += flt(row["total_qty"]) + item_row["has_serial_no"] = row["has_serial_no"] + + return item_aggregated_data + def __get_stock_ledger_entries(self) -> List[Dict]: sle = frappe.qb.DocType("Stock Ledger Entry") item = self.__get_item_query() # used as derived table in sle query From e9ef1cbd3db30b7db912240aa31c7d63f79377c2 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 14 Feb 2022 20:14:14 +0530 Subject: [PATCH 076/129] test: Cover back to back recos from different warehouses (cherry picked from commit f221a0d253c6c4a2dc1faf4b41f371bf5a7e86ad) --- .../stock_ageing/stock_ageing_fifo_logic.md | 1 + .../report/stock_ageing/test_stock_ageing.py | 127 +++++++++++++++++- 2 files changed, 125 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md b/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md index 5ffe97fd742..9e9bed48e3e 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md +++ b/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md @@ -15,6 +15,7 @@ Here, the balance qty is 70. 50 qty is (today-the 1st) days old 20 qty is (today-the 2nd) days old +> Note: We generate FIFO slots warehouse wise as stock reconciliations from different warehouses can cause incorrect values. ### Calculation of FIFO Slots #### Case 1: Outward from sufficient balance qty diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 949bb7c15a8..66d2f6b7539 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -15,11 +15,12 @@ class TestStockAgeing(ERPNextTestCase): ) def test_normal_inward_outward_queue(self): - "Reference: Case 1 in stock_ageing_fifo_logic.md" + "Reference: Case 1 in stock_ageing_fifo_logic.md (same wh)" sle = [ frappe._dict( name="Flask Item", actual_qty=30, qty_after_transaction=30, + warehouse="WH 1", posting_date="2021-12-01", voucher_type="Stock Entry", voucher_no="001", has_serial_no=False, serial_no=None @@ -27,6 +28,7 @@ class TestStockAgeing(ERPNextTestCase): frappe._dict( name="Flask Item", actual_qty=20, qty_after_transaction=50, + warehouse="WH 1", posting_date="2021-12-02", voucher_type="Stock Entry", voucher_no="002", has_serial_no=False, serial_no=None @@ -34,6 +36,7 @@ class TestStockAgeing(ERPNextTestCase): frappe._dict( name="Flask Item", actual_qty=(-10), qty_after_transaction=40, + warehouse="WH 1", posting_date="2021-12-03", voucher_type="Stock Entry", voucher_no="003", has_serial_no=False, serial_no=None @@ -50,11 +53,12 @@ class TestStockAgeing(ERPNextTestCase): self.assertEqual(queue[0][0], 20.0) def test_insufficient_balance(self): - "Reference: Case 3 in stock_ageing_fifo_logic.md" + "Reference: Case 3 in stock_ageing_fifo_logic.md (same wh)" sle = [ frappe._dict( name="Flask Item", actual_qty=(-30), qty_after_transaction=(-30), + warehouse="WH 1", posting_date="2021-12-01", voucher_type="Stock Entry", voucher_no="001", has_serial_no=False, serial_no=None @@ -62,6 +66,7 @@ class TestStockAgeing(ERPNextTestCase): frappe._dict( name="Flask Item", actual_qty=20, qty_after_transaction=(-10), + warehouse="WH 1", posting_date="2021-12-02", voucher_type="Stock Entry", voucher_no="002", has_serial_no=False, serial_no=None @@ -69,6 +74,7 @@ class TestStockAgeing(ERPNextTestCase): frappe._dict( name="Flask Item", actual_qty=20, qty_after_transaction=10, + warehouse="WH 1", posting_date="2021-12-03", voucher_type="Stock Entry", voucher_no="003", has_serial_no=False, serial_no=None @@ -76,6 +82,7 @@ class TestStockAgeing(ERPNextTestCase): frappe._dict( name="Flask Item", actual_qty=10, qty_after_transaction=20, + warehouse="WH 1", posting_date="2021-12-03", voucher_type="Stock Entry", voucher_no="004", has_serial_no=False, serial_no=None @@ -91,11 +98,16 @@ class TestStockAgeing(ERPNextTestCase): self.assertEqual(queue[0][0], 10.0) self.assertEqual(queue[1][0], 10.0) - def test_stock_reconciliation(self): + def test_basic_stock_reconciliation(self): + """ + Ledger (same wh): [+30, reco reset >> 50, -10] + Bal: 40 + """ sle = [ frappe._dict( name="Flask Item", actual_qty=30, qty_after_transaction=30, + warehouse="WH 1", posting_date="2021-12-01", voucher_type="Stock Entry", voucher_no="001", has_serial_no=False, serial_no=None @@ -103,6 +115,7 @@ class TestStockAgeing(ERPNextTestCase): frappe._dict( name="Flask Item", actual_qty=0, qty_after_transaction=50, + warehouse="WH 1", posting_date="2021-12-02", voucher_type="Stock Reconciliation", voucher_no="002", has_serial_no=False, serial_no=None @@ -110,6 +123,7 @@ class TestStockAgeing(ERPNextTestCase): frappe._dict( name="Flask Item", actual_qty=(-10), qty_after_transaction=40, + warehouse="WH 1", posting_date="2021-12-03", voucher_type="Stock Entry", voucher_no="003", has_serial_no=False, serial_no=None @@ -122,5 +136,112 @@ class TestStockAgeing(ERPNextTestCase): queue = result["fifo_queue"] self.assertEqual(result["qty_after_transaction"], result["total_qty"]) + self.assertEqual(result["total_qty"], 40.0) self.assertEqual(queue[0][0], 20.0) self.assertEqual(queue[1][0], 20.0) + + def test_sequential_stock_reco_same_warehouse(self): + """ + Test back to back stock recos (same warehouse). + Ledger: [reco opening >> +1000, reco reset >> 400, -10] + Bal: 390 + """ + sle = [ + frappe._dict( + name="Flask Item", + actual_qty=0, qty_after_transaction=1000, + warehouse="WH 1", + posting_date="2021-12-01", voucher_type="Stock Reconciliation", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=0, qty_after_transaction=400, + warehouse="WH 1", + posting_date="2021-12-02", voucher_type="Stock Reconciliation", + voucher_no="003", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-10), qty_after_transaction=390, + warehouse="WH 1", + posting_date="2021-12-03", voucher_type="Stock Entry", + voucher_no="003", + has_serial_no=False, serial_no=None + ) + ] + slots = FIFOSlots(self.filters, sle).generate() + + result = slots["Flask Item"] + queue = result["fifo_queue"] + + self.assertEqual(result["qty_after_transaction"], result["total_qty"]) + self.assertEqual(result["total_qty"], 390.0) + self.assertEqual(queue[0][0], 390.0) + + def test_sequential_stock_reco_different_warehouse(self): + """ + Ledger: + WH | Voucher | Qty + ------------------- + WH1 | Reco | 1000 + WH2 | Reco | 400 + WH1 | SE | -10 + + Bal: WH1 bal + WH2 bal = 990 + 400 = 1390 + """ + sle = [ + frappe._dict( + name="Flask Item", + actual_qty=0, qty_after_transaction=1000, + warehouse="WH 1", + posting_date="2021-12-01", voucher_type="Stock Reconciliation", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=0, qty_after_transaction=400, + warehouse="WH 2", + posting_date="2021-12-02", voucher_type="Stock Reconciliation", + voucher_no="003", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-10), qty_after_transaction=990, + warehouse="WH 1", + posting_date="2021-12-03", voucher_type="Stock Entry", + voucher_no="004", + has_serial_no=False, serial_no=None + ) + ] + + item_wise_slots, item_wh_wise_slots = generate_item_and_item_wh_wise_slots( + filters=self.filters,sle=sle + ) + + # test without 'show_warehouse_wise_stock' + item_result = item_wise_slots["Flask Item"] + queue = item_result["fifo_queue"] + + self.assertEqual(item_result["qty_after_transaction"], item_result["total_qty"]) + self.assertEqual(item_result["total_qty"], 1390.0) + self.assertEqual(queue[0][0], 990.0) + self.assertEqual(queue[1][0], 400.0) + + # test with 'show_warehouse_wise_stock' checked + item_wh_balances = [item_wh_wise_slots.get(i).get("qty_after_transaction") for i in item_wh_wise_slots] + self.assertEqual(sum(item_wh_balances), item_result["qty_after_transaction"]) + +def generate_item_and_item_wh_wise_slots(filters, sle): + "Return results with and without 'show_warehouse_wise_stock'" + item_wise_slots = FIFOSlots(filters, sle).generate() + + filters.show_warehouse_wise_stock = True + item_wh_wise_slots = FIFOSlots(filters, sle).generate() + filters.show_warehouse_wise_stock = False + + return item_wise_slots, item_wh_wise_slots \ No newline at end of file From 5d608edc54553a13ac3b90d95dc3c4cc8060cc57 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 14 Feb 2022 18:38:02 +0530 Subject: [PATCH 077/129] fix: disable rounded total in opening invoice creation tool (cherry picked from commit 18d0a59a9d4d4fd35ce997f2d23aa7ced930b00e) --- .../opening_invoice_creation_tool.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index 00eecd3a4f4..5ba0131f6c0 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -167,7 +167,8 @@ class OpeningInvoiceCreationTool(Document): "is_pos": 0, "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice", "update_stock": 0, - "invoice_number": row.invoice_number + "invoice_number": row.invoice_number, + "disable_rounded_total": 1 }) accounting_dimension = get_accounting_dimensions() From 83ab63ba814fd2e622f74e567f8401a3591e1351 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Tue, 15 Feb 2022 11:37:29 +0530 Subject: [PATCH 078/129] chore: warning for Amazon MWS integration deprecation (#29792) * chore: warning for amazon mws integration deprecation * style: spaces to tabs [skip ci] --- .../amazon_mws_settings/amazon_mws_settings.js | 8 ++++++++ erpnext/patches.txt | 3 ++- .../v13_0/amazon_mws_deprecation_warning.py | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v13_0/amazon_mws_deprecation_warning.py diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.js b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.js index f5ea8047c6a..a15558bc2b6 100644 --- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.js +++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.js @@ -1,2 +1,10 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt + + +frappe.ui.form.on('Amazon MWS Settings', { + refresh: function (frm) { + let app_link = "Ecommerce Integrations" + frm.dashboard.add_comment(__("Amazon MWS Integration will be removed from ERPNext in Version 14. Please install {0} app to continue using it.", [app_link]), "yellow", true); + } +}); diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f4a6e089637..b3366f3cba7 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -348,4 +348,5 @@ erpnext.patches.v13_0.delete_bank_reconciliation_detail erpnext.patches.v13_0.update_sane_transfer_against erpnext.patches.v13_0.enable_provisional_accounting erpnext.patches.v13_0.update_disbursement_account -erpnext.patches.v13_0.update_reserved_qty_closed_wo \ No newline at end of file +erpnext.patches.v13_0.update_reserved_qty_closed_wo +erpnext.patches.v13_0.amazon_mws_deprecation_warning \ No newline at end of file diff --git a/erpnext/patches/v13_0/amazon_mws_deprecation_warning.py b/erpnext/patches/v13_0/amazon_mws_deprecation_warning.py new file mode 100644 index 00000000000..5eb6ff44702 --- /dev/null +++ b/erpnext/patches/v13_0/amazon_mws_deprecation_warning.py @@ -0,0 +1,15 @@ +import click +import frappe + + +def execute(): + + frappe.reload_doc("erpnext_integrations", "doctype", "amazon_mws_settings") + if not frappe.db.get_single_value("Amazon MWS Settings", "enable_amazon"): + return + + click.secho( + "Amazon MWS Integration is moved to a separate app and will be removed from ERPNext in version-14.\n" + "Please install the app to continue using the integration: https://github.com/frappe/ecommerce_integrations", + fg="yellow", + ) \ No newline at end of file From ea3acb09beac8f4f86dff90dcab466052e4dda4f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 14 Feb 2022 19:51:58 +0530 Subject: [PATCH 079/129] fix: Fixes in TDS payable monthly report (cherry picked from commit 2ff6b3560e6ec8820a6ba8cccba24945e089d7d2) --- .../tds_payable_monthly/tds_payable_monthly.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index caee1a10bbb..9eeeb3a6804 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -23,7 +23,7 @@ def validate_filters(filters): def get_result(filters, tds_docs, tds_accounts, tax_category_map): supplier_map = get_supplier_pan_map() tax_rate_map = get_tax_rate_map(filters) - gle_map = get_gle_map(filters, tds_docs) + gle_map = get_gle_map(tds_docs) out = [] for name, details in gle_map.items(): @@ -78,7 +78,7 @@ def get_supplier_pan_map(): return supplier_map -def get_gle_map(filters, documents): +def get_gle_map(documents): # create gle_map of the form # {"purchase_invoice": list of dict of all gle created for this invoice} gle_map = {} @@ -86,7 +86,7 @@ def get_gle_map(filters, documents): gle = frappe.db.get_all('GL Entry', { "voucher_no": ["in", documents], - "credit": (">", 0) + "is_cancelled": 0 }, ["credit", "debit", "account", "voucher_no", "posting_date", "voucher_type", "against", "party"], ) @@ -184,21 +184,25 @@ def get_tds_docs(filters): payment_entries = [] journal_entries = [] tax_category_map = {} + or_filters={} tds_accounts = frappe.get_all("Tax Withholding Account", {'company': filters.get('company')}, pluck="account") query_filters = { - "credit": ('>', 0), "account": ("in", tds_accounts), "posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]), "is_cancelled": 0 } - if filters.get('supplier'): - query_filters.update({'against': filters.get('supplier')}) + if filters.get("supplier"): + del query_filters["account"] + or_filters = { + "against": filters.get('supplier'), + "party": filters.get('supplier') + } - tds_docs = frappe.get_all("GL Entry", query_filters, ["voucher_no", "voucher_type", "against", "party"]) + tds_docs = frappe.get_all("GL Entry", filters=query_filters, or_filters=or_filters, fields=["voucher_no", "voucher_type", "against", "party"]) for d in tds_docs: if d.voucher_type == "Purchase Invoice": From 038d729462303ddfa52a7a2c0505fdd3a1424dc4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 14 Feb 2022 20:38:04 +0530 Subject: [PATCH 080/129] fix: Filter out bank payment entries (cherry picked from commit 04cbde2e52bc9839b8ce3d6446c870f9957b614d) --- .../report/tds_payable_monthly/tds_payable_monthly.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index 9eeeb3a6804..57f79748f0a 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -184,7 +184,8 @@ def get_tds_docs(filters): payment_entries = [] journal_entries = [] tax_category_map = {} - or_filters={} + or_filters = {} + bank_accounts = frappe.get_all('Account', {'is_group': 0, 'account_type': 'Bank'}, pluck="name") tds_accounts = frappe.get_all("Tax Withholding Account", {'company': filters.get('company')}, pluck="account") @@ -192,11 +193,13 @@ def get_tds_docs(filters): query_filters = { "account": ("in", tds_accounts), "posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]), - "is_cancelled": 0 + "is_cancelled": 0, + "against": ("not in", bank_accounts) } if filters.get("supplier"): del query_filters["account"] + del query_filters["against"] or_filters = { "against": filters.get('supplier'), "party": filters.get('supplier') From fd5ce9ca10e8da650455a4eb62a620b7f8729ef7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 15 Feb 2022 16:12:03 +0530 Subject: [PATCH 081/129] fix: dont attempt to set batch number if item doesn't have batch no (#29812) (#29813) This causes other triggers and unnecessary changes (e.g. price list) (cherry picked from commit f89a64db486b46ac756d5ba62faee87f28baf889) Co-authored-by: Ankush Menat --- erpnext/selling/sales_common.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index e2c752cecfa..8ef71ca86a1 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -227,11 +227,11 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ }, callback:function(r){ if (in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) { - if (doc.doctype === 'Sales Invoice' && (!doc.update_stock)) return; - - me.set_batch_number(cdt, cdn); - me.batch_no(doc, cdt, cdn); + if (has_batch_no) { + me.set_batch_number(cdt, cdn); + me.batch_no(doc, cdt, cdn); + } } } }); From 3b4e4955f72c4803f99fda70b6b38faca85f25b3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 16 Feb 2022 11:32:40 +0530 Subject: [PATCH 082/129] fix: task status loop (backport #26006) (#29821) Co-authored-by: Rucha Mahabal Co-authored-by: Jannat Patel <31363128+pateljannat@users.noreply.github.com> --- erpnext/projects/doctype/task/task.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 6d3f20f271a..8fa0538f360 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -76,9 +76,6 @@ class Task(NestedSet): if flt(self.progress or 0) > 100: frappe.throw(_("Progress % for a task cannot be more than 100.")) - if flt(self.progress) == 100: - self.status = 'Completed' - if self.status == 'Completed': self.progress = 100 From dccf2a3dec89af28c3bdb143f6761d1985ab4719 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 16 Feb 2022 11:52:03 +0530 Subject: [PATCH 083/129] test: set correct DocType (#29819) (#29820) (cherry picked from commit 08a391fa88d97ab003a00e58eb47fb263923adc1) Co-authored-by: Sagar Vora --- .../test_supplier_scorecard.py | 8 ++-- .../doctype/salary_slip/test_salary_slip.py | 38 +++++++++++-------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py index 49e33517e6f..7908c35cbbe 100644 --- a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py +++ b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py @@ -49,7 +49,7 @@ valid_scorecard = [ "min_grade":0.0,"name":"Very Poor", "prevent_rfqs":1, "notify_supplier":0, - "doctype":"Supplier Scorecard Standing", + "doctype":"Supplier Scorecard Scoring Standing", "max_grade":30.0, "prevent_pos":1, "warn_pos":0, @@ -65,7 +65,7 @@ valid_scorecard = [ "name":"Poor", "prevent_rfqs":1, "notify_supplier":0, - "doctype":"Supplier Scorecard Standing", + "doctype":"Supplier Scorecard Scoring Standing", "max_grade":50.0, "prevent_pos":0, "warn_pos":0, @@ -81,7 +81,7 @@ valid_scorecard = [ "name":"Average", "prevent_rfqs":0, "notify_supplier":0, - "doctype":"Supplier Scorecard Standing", + "doctype":"Supplier Scorecard Scoring Standing", "max_grade":80.0, "prevent_pos":0, "warn_pos":0, @@ -97,7 +97,7 @@ valid_scorecard = [ "name":"Excellent", "prevent_rfqs":0, "notify_supplier":0, - "doctype":"Supplier Scorecard Standing", + "doctype":"Supplier Scorecard Scoring Standing", "max_grade":100.0, "prevent_pos":0, "warn_pos":0, diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 60fc1f00dd5..4249fa76c71 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -6,6 +6,7 @@ import random import unittest import frappe +from frappe.model.document import Document from frappe.utils import ( add_days, add_months, @@ -692,20 +693,25 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): def make_salary_component(salary_components, test_tax, company_list=None): for salary_component in salary_components: - if not frappe.db.exists('Salary Component', salary_component["salary_component"]): - if test_tax: - if salary_component["type"] == "Earning": - salary_component["is_tax_applicable"] = 1 - elif salary_component["salary_component"] == "TDS": - salary_component["variable_based_on_taxable_salary"] = 1 - salary_component["amount_based_on_formula"] = 0 - salary_component["amount"] = 0 - salary_component["formula"] = "" - salary_component["condition"] = "" - salary_component["doctype"] = "Salary Component" - salary_component["salary_component_abbr"] = salary_component["abbr"] - frappe.get_doc(salary_component).insert() - get_salary_component_account(salary_component["salary_component"], company_list) + if frappe.db.exists('Salary Component', salary_component["salary_component"]): + continue + + if test_tax: + if salary_component["type"] == "Earning": + salary_component["is_tax_applicable"] = 1 + elif salary_component["salary_component"] == "TDS": + salary_component["variable_based_on_taxable_salary"] = 1 + salary_component["amount_based_on_formula"] = 0 + salary_component["amount"] = 0 + salary_component["formula"] = "" + salary_component["condition"] = "" + + salary_component["salary_component_abbr"] = salary_component["abbr"] + doc = frappe.new_doc("Salary Component") + doc.update(salary_component) + doc.insert() + + get_salary_component_account(doc, company_list) def get_salary_component_account(sal_comp, company_list=None): company = erpnext.get_default_company() @@ -713,7 +719,9 @@ def get_salary_component_account(sal_comp, company_list=None): if company_list and company not in company_list: company_list.append(company) - sal_comp = frappe.get_doc("Salary Component", sal_comp) + if not isinstance(sal_comp, Document): + sal_comp = frappe.get_doc("Salary Component", sal_comp) + if not sal_comp.get("accounts"): for d in company_list: company_abbr = frappe.get_cached_value('Company', d, 'abbr') From 765ade74e6c77a74e0cc8e670b7b06e518a12646 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 16 Feb 2022 13:04:20 +0530 Subject: [PATCH 084/129] fix: add supported currencies (#29805) (#29822) (cherry picked from commit a26183e205effa11d1fae7a3d6cb96c7db100e07) Co-authored-by: Kenneth Sequeira <33246109+kennethsequeira@users.noreply.github.com> --- .../doctype/gocardless_settings/gocardless_settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py index e242ace60f7..1499d258fe6 100644 --- a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py +++ b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py @@ -12,7 +12,7 @@ from six.moves.urllib.parse import urlencode class GoCardlessSettings(Document): - supported_currencies = ["EUR", "DKK", "GBP", "SEK"] + supported_currencies = ["EUR", "DKK", "GBP", "SEK", "AUD", "NZD", "CAD", "USD"] def validate(self): self.initialize_client() @@ -79,7 +79,7 @@ class GoCardlessSettings(Document): def validate_transaction_currency(self, currency): if currency not in self.supported_currencies: - frappe.throw(_("Please select another payment method. Stripe does not support transactions in currency '{0}'").format(currency)) + frappe.throw(_("Please select another payment method. Go Cardless does not support transactions in currency '{0}'").format(currency)) def get_payment_url(self, **kwargs): return get_url("./integrations/gocardless_checkout?{0}".format(urlencode(kwargs))) From 251656ab52f200517ee7a8c8157bba7ac86d3f7e Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 4 Feb 2022 17:34:56 +0530 Subject: [PATCH 085/129] perf: Weed out disabled variants via sql query instead of pythonic looping separately - If the number of variants are large (almost 2lakhs), the query to get variants and attribute data takes time - If the no.of disabled attributes is large as well, the list comprehension weeding out disabled variants takes forever - We dont need to loop over the variants data so many times - Avoid any `if a in list(b)` is best when the iterables have tremendous data (cherry picked from commit 26bd3053d190df07e8b75e0e86203050047b25cf) --- .../variant_selector/item_variants_cache.py | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/erpnext/e_commerce/variant_selector/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py index bb6b3ef37fe..9b22255d9aa 100644 --- a/erpnext/e_commerce/variant_selector/item_variants_cache.py +++ b/erpnext/e_commerce/variant_selector/item_variants_cache.py @@ -66,25 +66,39 @@ class ItemVariantsCacheManager: ) ] - # join with Website Item - item_variants_data = frappe.get_all( - 'Item Variant Attribute', - {'variant_of': parent_item_code}, - ['parent', 'attribute', 'attribute_value'], - order_by='name', - as_list=1 + # Get Variants and tehir Attributes that are not disabled + iva = frappe.qb.DocType("Item Variant Attribute") + item = frappe.qb.DocType("Item") + query = ( + frappe.qb.from_(iva) + .join(item).on(item.name == iva.parent) + .select( + iva.parent, iva.attribute, iva.attribute_value + ).where( + (iva.variant_of == parent_item_code) + & (item.disabled == 0) + ).orderby(iva.name) ) + item_variants_data = query.run() - disabled_items = set( - [i.name for i in frappe.db.get_all('Item', {'disabled': 1})] - ) + # item_variants_data = frappe.get_all( + # 'Item Variant Attribute', + # {'variant_of': parent_item_code}, + # ['parent', 'attribute', 'attribute_value'], + # order_by='name', + # as_list=1 + # ) + + # disabled_items = set( + # [i.name for i in frappe.db.get_all('Item', {'disabled': 1})] + # ) attribute_value_item_map = frappe._dict() item_attribute_value_map = frappe._dict() # dont consider variants that are disabled # pull all other variants - item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items] + # item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items] for row in item_variants_data: item_code, attribute, attribute_value = row From ca8986028d32f1653bda91b224b0aa3533f61284 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 8 Feb 2022 11:15:19 +0530 Subject: [PATCH 086/129] fix: Trim spaces from attributes (multi-variant creation) & explicit method for building cache - Multiple Item Variants creation fails due to extra spaces in attributes from popup. Clean them before passing to server side - Mention explicit method to build variants cache to avoid ambiguity between old method path (pre-refactor) (cherry picked from commit a64228741d065f7ac33b3208d3a704616250f925) --- erpnext/e_commerce/variant_selector/item_variants_cache.py | 5 ++++- erpnext/stock/doctype/item/item.js | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/e_commerce/variant_selector/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py index 9b22255d9aa..3aefc446c2c 100644 --- a/erpnext/e_commerce/variant_selector/item_variants_cache.py +++ b/erpnext/e_commerce/variant_selector/item_variants_cache.py @@ -138,4 +138,7 @@ def build_cache(item_code): def enqueue_build_cache(item_code): if frappe.cache().hget('item_cache_build_in_progress', item_code): return - frappe.enqueue(build_cache, item_code=item_code, queue='long') + frappe.enqueue( + "erpnext.e_commerce.variant_selector.item_variants_cache.build_cache", + item_code=item_code, queue='long' + ) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index da371d968c9..954e061caa6 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -545,7 +545,7 @@ $.extend(erpnext.item, { let selected_attributes = {}; me.multiple_variant_dialog.$wrapper.find('.form-column').each((i, col) => { if(i===0) return; - let attribute_name = $(col).find('label').html(); + let attribute_name = $(col).find('label').html().trim(); selected_attributes[attribute_name] = []; let checked_opts = $(col).find('.checkbox input'); checked_opts.each((i, opt) => { From b75e98228611d1afd1fa5f6aefd557211ef6a8be Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 8 Feb 2022 12:02:02 +0530 Subject: [PATCH 087/129] chore: Fix flaky test `test_exact_match_with_price` - Clear cart settings in cache to avoid stale values (cherry picked from commit 4f5a0b8941101f759f2d1af33d952a1bfdfd3cf4) --- erpnext/e_commerce/variant_selector/test_variant_selector.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/e_commerce/variant_selector/test_variant_selector.py b/erpnext/e_commerce/variant_selector/test_variant_selector.py index 0412abb4d9f..967be838e67 100644 --- a/erpnext/e_commerce/variant_selector/test_variant_selector.py +++ b/erpnext/e_commerce/variant_selector/test_variant_selector.py @@ -106,6 +106,8 @@ class TestVariantSelector(ERPNextTestCase): }) make_web_item_price(item_code="Test-Tshirt-Temp-S-R", price_list_rate=100) + + frappe.local.shopping_cart_settings = None # clear cached settings values next_values = get_next_attribute_and_values( "Test-Tshirt-Temp", selected_attributes={"Test Size": "Small", "Test Colour": "Red"} From de834a83627893fa3ba19572c577aa621bbf8816 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 16 Feb 2022 12:41:39 +0530 Subject: [PATCH 088/129] chore: Remove commented out code (cherry picked from commit 29c576e144489072c992e9b5bdfe4c9359639ef8) --- .../variant_selector/item_variants_cache.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/erpnext/e_commerce/variant_selector/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py index 3aefc446c2c..3107c019e62 100644 --- a/erpnext/e_commerce/variant_selector/item_variants_cache.py +++ b/erpnext/e_commerce/variant_selector/item_variants_cache.py @@ -81,25 +81,9 @@ class ItemVariantsCacheManager: ) item_variants_data = query.run() - # item_variants_data = frappe.get_all( - # 'Item Variant Attribute', - # {'variant_of': parent_item_code}, - # ['parent', 'attribute', 'attribute_value'], - # order_by='name', - # as_list=1 - # ) - - # disabled_items = set( - # [i.name for i in frappe.db.get_all('Item', {'disabled': 1})] - # ) - attribute_value_item_map = frappe._dict() item_attribute_value_map = frappe._dict() - # dont consider variants that are disabled - # pull all other variants - # item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items] - for row in item_variants_data: item_code, attribute, attribute_value = row # (attr, value) => [item1, item2] From bb119c8e52ea5e58bd1969e6c062706ce1fafa6f Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Feb 2022 12:40:39 +0530 Subject: [PATCH 089/129] fix: Update SO via Work Order made from MR (attached to SO) - Add SO Item reference in WO from MR (that was made from SO) (cherry picked from commit 18731622c43f3b8f7d792d4bb4139eb7cdda39d9) --- erpnext/stock/doctype/material_request/material_request.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index d85970665e1..bdbf5e47f03 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -534,6 +534,7 @@ def raise_work_orders(material_request): "stock_uom": d.stock_uom, "expected_delivery_date": d.schedule_date, "sales_order": d.sales_order, + "sales_order_item": d.get("sales_order_item"), "bom_no": get_item_details(d.item_code).bom_no, "material_request": mr.name, "material_request_item": d.name, From 2ce07eff7135af17bc773da08a48e1e37697fa0e Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Feb 2022 13:38:15 +0530 Subject: [PATCH 090/129] test: SO > MR > WO flow (cherry picked from commit f9d52e73469ea298e3a2d39d893f2da5e6baf9aa) --- .../doctype/sales_order/test_sales_order.py | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index c9d857d951f..41ba1126dea 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -6,7 +6,7 @@ import json import frappe import frappe.permissions from frappe.core.doctype.user_permission.test_user_permission import create_user -from frappe.utils import add_days, flt, getdate, nowdate +from frappe.utils import add_days, flt, getdate, nowdate, today from erpnext.controllers.accounts_controller import update_child_qty_rate from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order @@ -1295,6 +1295,48 @@ class TestSalesOrder(ERPNextTestCase): so.load_from_db() self.assertEqual(so.billing_status, 'Fully Billed') + def test_so_back_updated_from_wo_via_mr(self): + "SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO." + from erpnext.stock.doctype.material_request.material_request import raise_work_orders + from erpnext.manufacturing.doctype.work_order.work_order import ( + make_stock_entry as make_se_from_wo, + ) + + so = make_sales_order(item_list=[{"item_code": "_Test FG Item","qty": 2, "rate":100}]) + + mr = make_material_request(so.name) + mr.material_request_type = "Manufacture" + mr.schedule_date = today() + mr.submit() + + # WO from MR + wo_name = raise_work_orders(mr.name)[0] + wo = frappe.get_doc("Work Order", wo_name) + wo.wip_warehouse = "Work In Progress - _TC" + wo.skip_transfer = True + + self.assertEqual(wo.sales_order, so.name) + self.assertEqual(wo.sales_order_item, so.items[0].name) + + wo.submit() + make_stock_entry(item_code="_Test Item", # Stock RM + target="Work In Progress - _TC", + qty=4, basic_rate=100 + ) + make_stock_entry(item_code="_Test Item Home Desktop 100", # Stock RM + target="Work In Progress - _TC", + qty=4, basic_rate=100 + ) + + se = frappe.get_doc(make_se_from_wo(wo.name, "Manufacture", 2)) + se.submit() # Finish WO + + mr.reload() + wo.reload() + so.reload() + self.assertEqual(so.items[0].work_order_qty, wo.produced_qty) + self.assertEqual(mr.status, "Manufactured") + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") accounts_settings.automatically_fetch_payment_terms = enable From 98ce6a1c8de11cbde34ced2ac12df0824fa9a034 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Feb 2022 14:20:54 +0530 Subject: [PATCH 091/129] chore: Patch to update SO work_order_qty and Linter fix (cherry picked from commit 0ca58d762715fd10c751c4497f3037908f4dfb20) # Conflicts: # erpnext/patches.txt --- erpnext/patches.txt | 7 +++- .../v14_0/set_work_order_qty_in_so_from_mr.py | 36 +++++++++++++++++++ .../doctype/sales_order/test_sales_order.py | 2 +- 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index b3366f3cba7..013031cf913 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -349,4 +349,9 @@ erpnext.patches.v13_0.update_sane_transfer_against erpnext.patches.v13_0.enable_provisional_accounting erpnext.patches.v13_0.update_disbursement_account erpnext.patches.v13_0.update_reserved_qty_closed_wo -erpnext.patches.v13_0.amazon_mws_deprecation_warning \ No newline at end of file +<<<<<<< HEAD +erpnext.patches.v13_0.amazon_mws_deprecation_warning +======= +erpnext.patches.v14_0.delete_amazon_mws_doctype +erpnext.patches.v14_0.set_work_order_qty_in_so_from_mr +>>>>>>> 0ca58d7627 (chore: Patch to update SO work_order_qty and Linter fix) diff --git a/erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py b/erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py new file mode 100644 index 00000000000..f097ab9297f --- /dev/null +++ b/erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py @@ -0,0 +1,36 @@ +import frappe + + +def execute(): + """ + 1. Get submitted Work Orders with MR, MR Item and SO set + 2. Get SO Item detail from MR Item detail in WO, and set in WO + 3. Update work_order_qty in SO + """ + work_order = frappe.qb.DocType("Work Order") + query = ( + frappe.qb.from_(work_order) + .select( + work_order.name, work_order.produced_qty, + work_order.material_request, + work_order.material_request_item, + work_order.sales_order + ).where( + (work_order.material_request.isnotnull()) + & (work_order.material_request_item.isnotnull()) + & (work_order.sales_order.isnotnull()) + & (work_order.docstatus == 1) + & (work_order.produced_qty > 0) + ) + ) + results = query.run(as_dict=True) + + for row in results: + so_item = frappe.get_value( + "Material Request Item", row.material_request_item, "sales_order_item" + ) + frappe.db.set_value("Work Order", row.name, "sales_order_item", so_item) + + if so_item: + wo = frappe.get_doc("Work Order", row.name) + wo.update_work_order_qty_in_so() diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 41ba1126dea..788a8350caa 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1297,10 +1297,10 @@ class TestSalesOrder(ERPNextTestCase): def test_so_back_updated_from_wo_via_mr(self): "SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO." - from erpnext.stock.doctype.material_request.material_request import raise_work_orders from erpnext.manufacturing.doctype.work_order.work_order import ( make_stock_entry as make_se_from_wo, ) + from erpnext.stock.doctype.material_request.material_request import raise_work_orders so = make_sales_order(item_list=[{"item_code": "_Test FG Item","qty": 2, "rate":100}]) From bc2b4411ee2dea9afd527117f80f4d2a71bcdd47 Mon Sep 17 00:00:00 2001 From: ruthra Date: Tue, 4 Jan 2022 15:53:41 +0530 Subject: [PATCH 092/129] feat: Payment Terms Status report - calculate status at runtime for payment terms based on invoices - invoices are used in FIFO method (cherry picked from commit 1bac7930834d6f688950e836c45305a62e7ecb3f) --- .../__init__.py | 0 .../payment_terms_status_for_sales_order.js | 84 +++++++ .../payment_terms_status_for_sales_order.json | 38 ++++ .../payment_terms_status_for_sales_order.py | 211 ++++++++++++++++++ ...st_payment_terms_status_for_sales_order.py | 119 ++++++++++ 5 files changed, 452 insertions(+) create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/__init__.py create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/__init__.py b/erpnext/selling/report/payment_terms_status_for_sales_order/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js new file mode 100644 index 00000000000..0450631a3be --- /dev/null +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js @@ -0,0 +1,84 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +function get_filters() { + let filters = [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + }, + { + "fieldname":"period_start_date", + "label": __("Start Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1) + }, + { + "fieldname":"period_end_date", + "label": __("End Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.get_today() + }, + { + "fieldname":"sales_order", + "label": __("Sales Order"), + "fieldtype": "MultiSelectList", + "width": 100, + "options": "Sales Order", + "get_data": function(txt) { + return frappe.db.get_link_options("Sales Order", txt, this.filters()); + }, + "filters": () => { + return { + docstatus: 1, + payment_terms_template: ['not in', ['']], + company: frappe.query_report.get_filter_value("company"), + transaction_date: ['between', [frappe.query_report.get_filter_value("period_start_date"), frappe.query_report.get_filter_value("period_end_date")]] + } + }, + on_change: function(){ + frappe.query_report.refresh(); + } + } + ] + + return filters; +} + +frappe.query_reports["Payment Terms Status for Sales Order"] = { + "filters": get_filters(), + "formatter": function(value, row, column, data, default_formatter){ + if(column.fieldname == 'invoices' && value) { + invoices = value.split(','); + const invoice_formatter = (prev_value, curr_value) => { + if(prev_value != "") { + return prev_value + ", " + default_formatter(curr_value, row, column, data); + } + else { + return default_formatter(curr_value, row, column, data); + } + } + return invoices.reduce(invoice_formatter, "") + } + else if (column.fieldname == 'paid_amount' && value){ + formatted_value = default_formatter(value, row, column, data); + if(value > 0) { + formatted_value = "" + formatted_value + "" + } + return formatted_value; + } + else if (column.fieldname == 'status' && value == 'Completed'){ + return "" + default_formatter(value, row, column, data) + ""; + } + + return default_formatter(value, row, column, data); + }, + +}; diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json new file mode 100644 index 00000000000..850fa4dc47a --- /dev/null +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json @@ -0,0 +1,38 @@ +{ + "add_total_row": 1, + "columns": [], + "creation": "2021-12-28 10:39:34.533964", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-12-30 10:42:06.058457", + "modified_by": "Administrator", + "module": "Selling", + "name": "Payment Terms Status for Sales Order", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Sales Order", + "report_name": "Payment Terms Status for Sales Order", + "report_type": "Script Report", + "roles": [ + { + "role": "Sales User" + }, + { + "role": "Sales Manager" + }, + { + "role": "Maintenance User" + }, + { + "role": "Accounts User" + }, + { + "role": "Stock User" + } + ] +} \ No newline at end of file diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py new file mode 100644 index 00000000000..aa2f757218e --- /dev/null +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -0,0 +1,211 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# License: MIT. See LICENSE + +import frappe +from frappe import _, qb, query_builder +from frappe.query_builder import functions + + +def get_columns(): + columns = [ + { + "label": _("Sales Order"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Sales Order", + "read_only": 1, + }, + { + "label": _("Submitted"), + "fieldname": "submitted", + "fieldtype": "Date", + "read_only": 1 + }, + { + "label": _("Payment Term"), + "fieldname": "payment_term", + "fieldtype": "Data", + "read_only": 1 + }, + { + "label": _("Description"), + "fieldname": "description", + "fieldtype": "Data", + "read_only": 1 + }, + { + "label": _("Due Date"), + "fieldname": "due_date", + "fieldtype": "Date", + "read_only": 1 + }, + { + "label": _("Invoice Portion"), + "fieldname": "invoice_portion", + "fieldtype": "Percent", + "read_only": 1, + }, + { + "label": _("Payment Amount"), + "fieldname": "payment_amount", + "fieldtype": "Currency", + "read_only": 1, + }, + { + "label": _("Paid Amount"), + "fieldname": "paid_amount", + "fieldtype": "Currency", + "read_only": 1 + }, + { + "label": _("Invoices"), + "fieldname": "invoices", + "fieldtype": "Link", + "options": "Sales Invoice", + "read_only": 1, + }, + { + "label": _("Status"), + "fieldname": "status", + "fieldtype": "Data", + "read_only": 1 + } + ] + return columns + + +def get_conditions(filters): + """ + Convert filter options to conditions used in query + """ + filters = frappe._dict(filters) if filters else frappe._dict({}) + conditions = frappe._dict({}) + + conditions.company = filters.company or frappe.defaults.get_user_default("company") + conditions.end_date = filters.period_end_date or frappe.utils.today() + conditions.start_date = filters.period_start_date or frappe.utils.add_months( + conditions.end_date, -1 + ) + conditions.sales_order = filters.sales_order or [] + + return conditions + + +def get_so_with_invoices(filters): + """ + Get Sales Order with payment terms template with their associated Invoices + """ + sorders = [] + + so = qb.DocType("Sales Order") + ps = qb.DocType("Payment Schedule") + datediff = query_builder.CustomFunction("DATEDIFF", ["cur_date", "due_date"]) + ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"]) + + conditions = get_conditions(filters) + query_so = ( + qb.from_(so) + .join(ps) + .on(ps.parent == so.name) + .select( + so.name, + so.transaction_date.as_("submitted"), + ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"), + ps.payment_term, + ps.description, + ps.due_date, + ps.invoice_portion, + ps.payment_amount, + ps.paid_amount, + ) + .where( + (so.docstatus == 1) + & (so.payment_terms_template != "NULL") + & (so.company == conditions.company) + & (so.transaction_date[conditions.start_date : conditions.end_date]) + ) + .orderby(so.name, so.transaction_date, ps.due_date) + ) + + if conditions.sales_order != []: + query_so = query_so.where(so.name.isin(conditions.sales_order)) + + sorders = query_so.run(as_dict=True) + + invoices = [] + if sorders != []: + soi = qb.DocType("Sales Order Item") + si = qb.DocType("Sales Invoice") + sii = qb.DocType("Sales Invoice Item") + query_inv = ( + qb.from_(sii) + .right_join(si) + .on(si.name == sii.parent) + .inner_join(soi) + .on(soi.name == sii.so_detail) + .select(sii.sales_order, sii.parent.as_("invoice"), si.base_net_total.as_("invoice_amount")) + .where((sii.sales_order.isin([x.name for x in sorders])) & (si.docstatus == 1)) + .groupby(sii.parent) + ) + invoices = query_inv.run(as_dict=True) + + return sorders, invoices + + +def set_payment_terms_statuses(sales_orders, invoices): + """ + compute status for payment terms with associated sales invoice using FIFO + """ + + for so in sales_orders: + for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]: + if so.payment_amount - so.paid_amount > 0: + amount = so.payment_amount - so.paid_amount + if inv.invoice_amount >= amount: + inv.invoice_amount -= amount + so.paid_amount += amount + if so.invoices: + so.invoices = so.invoices + "," + inv.invoice + else: + so.invoices = inv.invoice + so.status = "Completed" + break + else: + so.paid_amount += inv.invoice_amount + inv.invoice_amount = 0 + if so.invoices: + so.invoices = so.invoices + "," + inv.invoice + else: + so.invoices = inv.invoice + so.status = "Partly Paid" + + return sales_orders, invoices + + +def prepare_chart(s_orders): + if len(set([x.name for x in s_orders])) == 1: + chart = { + "data": { + "labels": [term.payment_term for term in s_orders], + "datasets": [ + {"name": "Payment Amount", "values": [x.payment_amount for x in s_orders],}, + {"name": "Paid Amount", "values": [x.paid_amount for x in s_orders],}, + ], + }, + "type": "bar", + } + return chart + + +def execute(filters=None): + columns = get_columns() + sales_orders, so_invoices = get_so_with_invoices(filters) + sales_orders, so_invoices = set_payment_terms_statuses(sales_orders, so_invoices) + + prepare_chart(sales_orders) + + data = sales_orders + message = [] + chart = prepare_chart(sales_orders) + + return columns, data, message, chart diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py new file mode 100644 index 00000000000..e9dba84f3aa --- /dev/null +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -0,0 +1,119 @@ +import datetime +import unittest + +import frappe +from frappe import qb +from frappe.utils import add_days + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order import ( + execute, +) +from erpnext.stock.doctype.item.test_item import create_item +from erpnext.tests.utils import ERPNextTestCase + +test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template"] + + +class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): + def test_payment_terms_status(self): + # disable Must be a whole number + nos = frappe.get_doc("UOM", "Nos") + nos.must_be_whole_number = 0 + nos.save() + + template = None + if frappe.db.exists("Payment Terms Template", "_Test 50-50"): + template = frappe.get_doc("Payment Terms Template", "_Test 50-50") + else: + template = frappe.get_doc( + { + "doctype": "Payment Terms Template", + "template_name": "_Test 50-50", + "terms": [ + { + "doctype": "Payment Terms Template Detail", + "due_date_based_on": "Day(s) after invoice date", + "payment_term_name": "_Test 50% on 15 Days", + "description": "_Test 50-50", + "invoice_portion": 50, + "credit_days": 15, + }, + { + "doctype": "Payment Terms Template Detail", + "due_date_based_on": "Day(s) after invoice date", + "payment_term_name": "_Test 50% on 30 Days", + "description": "_Test 50-50", + "invoice_portion": 50, + "credit_days": 30, + }, + ], + } + ) + template.insert() + + # item = create_item(item_code="_Test Excavator", is_stock_item=0, valuation_rate=1000000) + item = create_item(item_code="_Test Excavator", is_stock_item=0) + so = make_sales_order( + transaction_date="2021-06-15", + delivery_date=add_days("2021-06-15", -30), + item=item.item_code, + qty=1, + rate=1000000, + po_no=54321, + do_not_save=True, + ) + so.payment_terms_template = template.name + so.save() + so.submit() + + # make invoice with 60% of the total sales order value + sinv = make_sales_invoice(so.name) + # sinv.posting_date = "2021-06-29" + sinv.items[0].qty *= 0.60 + sinv.insert() + sinv.submit() + + columns, data, message, chart = execute( + { + "company": "_Test Company", + "period_start_date": "2021-06-01", + "period_end_date": "2021-06-30", + "sales_order": [so.name], + } + ) + + # revert changes to Nos + nos.must_be_whole_number = 1 + nos.save() + + expected_value = [ + { + "name": so.name, + "submitted": datetime.date(2021, 6, 15), + "status": "Completed", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 6, 30), + "invoice_portion": 50.0, + "payment_amount": 500000.0, + "paid_amount": 500000.0, + "invoices": sinv.name, + }, + { + "name": so.name, + "submitted": datetime.date(2021, 6, 15), + "status": "Partly Paid", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 7, 15), + "invoice_portion": 50.0, + "payment_amount": 500000.0, + "paid_amount": 100000.0, + "invoices": sinv.name, + }, + ] + + self.assertEqual(data, expected_value) From 408704e269e2070636d16a427eda4b83dafd0cac Mon Sep 17 00:00:00 2001 From: ruthra Date: Wed, 5 Jan 2022 10:11:19 +0530 Subject: [PATCH 093/129] test: fix failing test case payment terms status (cherry picked from commit 9f1e68801d527628551984402fd0c06e401084d8) --- .../test_payment_terms_status_for_sales_order.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index e9dba84f3aa..19c01f2d43b 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -21,8 +21,7 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): def test_payment_terms_status(self): # disable Must be a whole number nos = frappe.get_doc("UOM", "Nos") - nos.must_be_whole_number = 0 - nos.save() + nos.db_set("must_be_whole_number", 0, commit=True) template = None if frappe.db.exists("Payment Terms Template", "_Test 50-50"): @@ -62,9 +61,9 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): item=item.item_code, qty=1, rate=1000000, - po_no=54321, do_not_save=True, ) + so.po_no = "" so.payment_terms_template = template.name so.save() so.submit() @@ -86,8 +85,7 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): ) # revert changes to Nos - nos.must_be_whole_number = 1 - nos.save() + nos.db_set("must_be_whole_number", 1, commit=True) expected_value = [ { From c6fb52a31a01376140ec973f76f95e10573941ee Mon Sep 17 00:00:00 2001 From: ruthra Date: Wed, 5 Jan 2022 10:43:20 +0530 Subject: [PATCH 094/129] refactor: remove unused imports (cherry picked from commit edd980acdc9e51f74eb6b70a793ae17b2e827710) --- .../test_payment_terms_status_for_sales_order.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index 19c01f2d43b..4f27a5683de 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -1,11 +1,8 @@ import datetime -import unittest import frappe -from frappe import qb from frappe.utils import add_days -from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order import ( From b6910912a93cb9adaa27ef41beb7e99e7019334c Mon Sep 17 00:00:00 2001 From: ruthra Date: Wed, 5 Jan 2022 10:46:32 +0530 Subject: [PATCH 095/129] test: qty and rate changed to remove need for fractional Nos (cherry picked from commit 4535a7a301f76fa3b867902f19e806dcb01bdb75) --- .../test_payment_terms_status_for_sales_order.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index 4f27a5683de..5d6e91e8a50 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -16,9 +16,6 @@ test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Temp class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): def test_payment_terms_status(self): - # disable Must be a whole number - nos = frappe.get_doc("UOM", "Nos") - nos.db_set("must_be_whole_number", 0, commit=True) template = None if frappe.db.exists("Payment Terms Template", "_Test 50-50"): @@ -56,8 +53,8 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): transaction_date="2021-06-15", delivery_date=add_days("2021-06-15", -30), item=item.item_code, - qty=1, - rate=1000000, + qty=10, + rate=100000, do_not_save=True, ) so.po_no = "" @@ -67,8 +64,7 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): # make invoice with 60% of the total sales order value sinv = make_sales_invoice(so.name) - # sinv.posting_date = "2021-06-29" - sinv.items[0].qty *= 0.60 + sinv.items[0].qty = 6 sinv.insert() sinv.submit() @@ -81,9 +77,6 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): } ) - # revert changes to Nos - nos.db_set("must_be_whole_number", 1, commit=True) - expected_value = [ { "name": so.name, From b249478fcc0e22f58377596401cd974e948646c3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 7 Feb 2022 12:26:01 +0530 Subject: [PATCH 096/129] fix: Copyright info (cherry picked from commit 4284017e9de3b033156ca6947665bc99f0daefc3) --- .../payment_terms_status_for_sales_order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js index 0450631a3be..0e36b3fe3d2 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js @@ -1,4 +1,4 @@ -// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt /* eslint-disable */ From 2614de9ab316b66984a14457b2ad0a3a271c8aec Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 8 Feb 2022 18:53:08 +0530 Subject: [PATCH 097/129] refactor: currency field and code cleanup (cherry picked from commit bc244d074062d23be99922a370564bba13e15890) --- .../payment_terms_status_for_sales_order.py | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index aa2f757218e..4eafa9b2efc 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -13,62 +13,60 @@ def get_columns(): "fieldname": "name", "fieldtype": "Link", "options": "Sales Order", - "read_only": 1, }, { - "label": _("Submitted"), + "label": _("Posting Date"), "fieldname": "submitted", "fieldtype": "Date", - "read_only": 1 }, { "label": _("Payment Term"), "fieldname": "payment_term", "fieldtype": "Data", - "read_only": 1 }, { "label": _("Description"), "fieldname": "description", "fieldtype": "Data", - "read_only": 1 }, { "label": _("Due Date"), "fieldname": "due_date", "fieldtype": "Date", - "read_only": 1 }, { "label": _("Invoice Portion"), "fieldname": "invoice_portion", "fieldtype": "Percent", - "read_only": 1, }, { "label": _("Payment Amount"), "fieldname": "payment_amount", "fieldtype": "Currency", - "read_only": 1, + "options": "currency", }, { "label": _("Paid Amount"), "fieldname": "paid_amount", "fieldtype": "Currency", - "read_only": 1 + "options": "currency", }, { "label": _("Invoices"), "fieldname": "invoices", "fieldtype": "Link", "options": "Sales Invoice", - "read_only": 1, }, { "label": _("Status"), "fieldname": "status", "fieldtype": "Data", - "read_only": 1 + }, + { + "label": _("Currency"), + "fieldname": "currency", + "fieldtype": "Currency", + "hidden": 1 } ] return columns @@ -152,12 +150,13 @@ def get_so_with_invoices(filters): return sorders, invoices -def set_payment_terms_statuses(sales_orders, invoices): +def set_payment_terms_statuses(sales_orders, invoices, filters): """ compute status for payment terms with associated sales invoice using FIFO """ for so in sales_orders: + so.currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency') for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]: if so.payment_amount - so.paid_amount > 0: amount = so.payment_amount - so.paid_amount @@ -200,7 +199,7 @@ def prepare_chart(s_orders): def execute(filters=None): columns = get_columns() sales_orders, so_invoices = get_so_with_invoices(filters) - sales_orders, so_invoices = set_payment_terms_statuses(sales_orders, so_invoices) + sales_orders, so_invoices = set_payment_terms_statuses(sales_orders, so_invoices, filters) prepare_chart(sales_orders) From 63c73e6eaf2e7add08acfaa35c385b9dbeec6a0e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 15 Feb 2022 12:20:06 +0530 Subject: [PATCH 098/129] fix: default to company currency in report output (cherry picked from commit 85ed0fb8d6ef45197bfef4a71cb8f02355d61930) --- .../payment_terms_status_for_sales_order.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index 4eafa9b2efc..d0902e111a0 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -41,7 +41,7 @@ def get_columns(): }, { "label": _("Payment Amount"), - "fieldname": "payment_amount", + "fieldname": "base_payment_amount", "fieldtype": "Currency", "options": "currency", }, @@ -113,7 +113,7 @@ def get_so_with_invoices(filters): ps.description, ps.due_date, ps.invoice_portion, - ps.payment_amount, + ps.base_payment_amount, ps.paid_amount, ) .where( @@ -141,7 +141,7 @@ def get_so_with_invoices(filters): .on(si.name == sii.parent) .inner_join(soi) .on(soi.name == sii.so_detail) - .select(sii.sales_order, sii.parent.as_("invoice"), si.base_net_total.as_("invoice_amount")) + .select(sii.sales_order, sii.parent.as_("invoice"), si.base_grand_total.as_("invoice_amount")) .where((sii.sales_order.isin([x.name for x in sorders])) & (si.docstatus == 1)) .groupby(sii.parent) ) @@ -158,8 +158,8 @@ def set_payment_terms_statuses(sales_orders, invoices, filters): for so in sales_orders: so.currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency') for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]: - if so.payment_amount - so.paid_amount > 0: - amount = so.payment_amount - so.paid_amount + if so.base_payment_amount - so.paid_amount > 0: + amount = so.base_payment_amount - so.paid_amount if inv.invoice_amount >= amount: inv.invoice_amount -= amount so.paid_amount += amount @@ -187,7 +187,7 @@ def prepare_chart(s_orders): "data": { "labels": [term.payment_term for term in s_orders], "datasets": [ - {"name": "Payment Amount", "values": [x.payment_amount for x in s_orders],}, + {"name": "Payment Amount", "values": [x.base_payment_amount for x in s_orders],}, {"name": "Paid Amount", "values": [x.paid_amount for x in s_orders],}, ], }, From 07c094152e46761d8fb847fd14c5502860af2a29 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 15 Feb 2022 12:20:41 +0530 Subject: [PATCH 099/129] refactor: create invoices list without if else (cherry picked from commit a4b8d673232fd313396788ef745e67572c235dcc) --- .../payment_terms_status_for_sales_order.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index d0902e111a0..e6a56eea310 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -157,25 +157,20 @@ def set_payment_terms_statuses(sales_orders, invoices, filters): for so in sales_orders: so.currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency') + so.invoices = "" for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]: if so.base_payment_amount - so.paid_amount > 0: amount = so.base_payment_amount - so.paid_amount if inv.invoice_amount >= amount: inv.invoice_amount -= amount so.paid_amount += amount - if so.invoices: - so.invoices = so.invoices + "," + inv.invoice - else: - so.invoices = inv.invoice + so.invoices += "," + inv.invoice so.status = "Completed" break else: so.paid_amount += inv.invoice_amount inv.invoice_amount = 0 - if so.invoices: - so.invoices = so.invoices + "," + inv.invoice - else: - so.invoices = inv.invoice + so.invoices += "," + inv.invoice so.status = "Partly Paid" return sales_orders, invoices From fa03246a947c74ebec47e84e3cdcf325fa9819e6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 15 Feb 2022 17:20:29 +0530 Subject: [PATCH 100/129] test: refactor and fix failing test case (cherry picked from commit 49fdc6c52e9752362b754f1615ca77ac9e09b418) --- ...st_payment_terms_status_for_sales_order.py | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index 5d6e91e8a50..ee6cee3be8e 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -15,8 +15,8 @@ test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Temp class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): - def test_payment_terms_status(self): - + def create_payment_terms_template(self): + # create template for 50-50 payments template = None if frappe.db.exists("Payment Terms Template", "_Test 50-50"): template = frappe.get_doc("Payment Terms Template", "_Test 50-50") @@ -46,8 +46,10 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): } ) template.insert() + self.template = template - # item = create_item(item_code="_Test Excavator", is_stock_item=0, valuation_rate=1000000) + def test_payment_terms_status(self): + self.create_payment_terms_template() item = create_item(item_code="_Test Excavator", is_stock_item=0) so = make_sales_order( transaction_date="2021-06-15", @@ -58,16 +60,19 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): do_not_save=True, ) so.po_no = "" - so.payment_terms_template = template.name + so.taxes_and_charges = "" + so.taxes = "" + so.payment_terms_template = self.template.name so.save() so.submit() # make invoice with 60% of the total sales order value sinv = make_sales_invoice(so.name) + sinv.taxes_and_charges = "" + sinv.taxes = "" sinv.items[0].qty = 6 sinv.insert() sinv.submit() - columns, data, message, chart = execute( { "company": "_Test Company", @@ -86,9 +91,10 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): "description": "_Test 50-50", "due_date": datetime.date(2021, 6, 30), "invoice_portion": 50.0, - "payment_amount": 500000.0, + "currency": "INR", + "base_payment_amount": 500000.0, "paid_amount": 500000.0, - "invoices": sinv.name, + "invoices": ","+sinv.name, }, { "name": so.name, @@ -98,10 +104,13 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): "description": "_Test 50-50", "due_date": datetime.date(2021, 7, 15), "invoice_portion": 50.0, - "payment_amount": 500000.0, + "currency": "INR", + "base_payment_amount": 500000.0, "paid_amount": 100000.0, - "invoices": sinv.name, + "invoices": ","+sinv.name, }, ] + self.assertEqual(data, expected_value) + self.assertEqual(data, expected_value) From 2ca25fc9f14ec58d67daa195709e59aac3036ce5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 15 Feb 2022 17:21:08 +0530 Subject: [PATCH 101/129] test: added test for alternate currency - Sales Order and Invoice will be submitted in USD with exchange rate of 70 with the default company currency - Report will display in defauly company currency (cherry picked from commit 48f37c76594fad1cd64cd44b7126d6ef1ddd5bd1) --- ...st_payment_terms_status_for_sales_order.py | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index ee6cee3be8e..cad41e1dc03 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -112,5 +112,87 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): ] self.assertEqual(data, expected_value) + def create_exchange_rate(self, date): + # make an entry in Currency Exchange list. serves as a static exchange rate + if frappe.db.exists({'doctype': "Currency Exchange",'date': date,'from_currency': 'USD', 'to_currency':'INR'}): + return + else: + doc = frappe.get_doc({ + 'doctype': "Currency Exchange", + 'date': date, + 'from_currency': 'USD', + 'to_currency': frappe.get_cached_value("Company", '_Test Company','default_currency'), + 'exchange_rate': 70, + 'for_buying': True, + 'for_selling': True + }) + doc.insert() + def test_alternate_currency(self): + transaction_date = "2021-06-15" + self.create_payment_terms_template() + self.create_exchange_rate(transaction_date) + item = create_item(item_code="_Test Excavator", is_stock_item=0) + so = make_sales_order( + transaction_date=transaction_date, + currency="USD", + delivery_date=add_days(transaction_date, -30), + item=item.item_code, + qty=10, + rate=10000, + do_not_save=True, + ) + so.po_no = "" + so.taxes_and_charges = "" + so.taxes = "" + so.payment_terms_template = self.template.name + so.save() + so.submit() + + # make invoice with 60% of the total sales order value + sinv = make_sales_invoice(so.name) + sinv.currency = "USD" + sinv.taxes_and_charges = "" + sinv.taxes = "" + sinv.items[0].qty = 6 + sinv.insert() + sinv.submit() + columns, data, message, chart = execute( + { + "company": "_Test Company", + "period_start_date": "2021-06-01", + "period_end_date": "2021-06-30", + "sales_order": [so.name], + } + ) + + # report defaults to company currency. + expected_value = [ + { + "name": so.name, + "submitted": datetime.date(2021, 6, 15), + "status": "Completed", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 6, 30), + "invoice_portion": 50.0, + "currency": frappe.get_cached_value("Company", '_Test Company','default_currency'), + "base_payment_amount": 3500000.0, + "paid_amount": 3500000.0, + "invoices": ","+sinv.name, + }, + { + "name": so.name, + "submitted": datetime.date(2021, 6, 15), + "status": "Partly Paid", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 7, 15), + "invoice_portion": 50.0, + "currency": frappe.get_cached_value("Company", '_Test Company','default_currency'), + "base_payment_amount": 3500000.0, + "paid_amount": 700000.0, + "invoices": ","+sinv.name, + }, + ] self.assertEqual(data, expected_value) From 94a101662304bedd0914ba53d56889954a1db287 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 16 Feb 2022 17:40:16 +0530 Subject: [PATCH 102/129] chore: Move patch that updates SO from WO to v13 --- erpnext/patches.txt | 6 +----- .../{v14_0 => v13_0}/set_work_order_qty_in_so_from_mr.py | 0 2 files changed, 1 insertion(+), 5 deletions(-) rename erpnext/patches/{v14_0 => v13_0}/set_work_order_qty_in_so_from_mr.py (100%) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 013031cf913..6aaf9aa33aa 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -349,9 +349,5 @@ erpnext.patches.v13_0.update_sane_transfer_against erpnext.patches.v13_0.enable_provisional_accounting erpnext.patches.v13_0.update_disbursement_account erpnext.patches.v13_0.update_reserved_qty_closed_wo -<<<<<<< HEAD erpnext.patches.v13_0.amazon_mws_deprecation_warning -======= -erpnext.patches.v14_0.delete_amazon_mws_doctype -erpnext.patches.v14_0.set_work_order_qty_in_so_from_mr ->>>>>>> 0ca58d7627 (chore: Patch to update SO work_order_qty and Linter fix) +erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr diff --git a/erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py b/erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py similarity index 100% rename from erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py rename to erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py From 8953794cda16b24c2271e3d929128599bb7e391f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 16 Feb 2022 17:53:00 +0530 Subject: [PATCH 103/129] fix: allow renaming and merging (backport #29830) (#29832) Co-authored-by: Wolfram Schmidt --- .../opportunity_lost_reason/opportunity_lost_reason.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json b/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json index 8a8d4252daa..0cfcf0e0ea4 100644 --- a/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json +++ b/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json @@ -3,7 +3,7 @@ "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 0, - "allow_rename": 0, + "allow_rename": 1, "autoname": "field:lost_reason", "beta": 0, "creation": "2018-12-28 14:48:51.044975", @@ -57,7 +57,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-12-28 14:49:43.336437", + "modified": "2022-02-16 10:49:43.336437", "modified_by": "Administrator", "module": "CRM", "name": "Opportunity Lost Reason", @@ -150,4 +150,4 @@ "track_changes": 0, "track_seen": 0, "track_views": 0 -} \ No newline at end of file +} From 90f2cfd1029901e34adcf36eed7812143404a81f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 16 Feb 2022 20:04:45 +0530 Subject: [PATCH 104/129] fix: avoid updating items table if no change due to putaway (cherry picked from commit b000e93744c2730517172717ed63048bab50d62f) --- .../doctype/putaway_rule/putaway_rule.py | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 25330b71833..4e472a92dc1 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -9,8 +9,7 @@ from collections import defaultdict import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cint, floor, flt, nowdate -from six import string_types +from frappe.utils import cint, cstr, floor, flt, nowdate from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.utils import get_stock_balance @@ -75,7 +74,7 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): purpose: Purpose of Stock Entry sync (optional): Sync with client side only for client side calls """ - if isinstance(items, string_types): + if isinstance(items, str): items = json.loads(items) items_not_accomodated, updated_table = [], [] @@ -143,11 +142,44 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): if items_not_accomodated: show_unassigned_items_message(items_not_accomodated) - items[:] = updated_table if updated_table else items # modify items table + if updated_table and _items_changed(items, updated_table, doctype): + items[:] = updated_table + frappe.msgprint(_("Applied putaway rules."), alert=True) if sync and json.loads(sync): # sync with client side return items +def _items_changed(old, new, doctype: str) -> bool: + """ Check if any items changed by application of putaway rules. + + If not, changing item table can have side effects since `name` items also changes. + """ + if len(old) != len(new): + return True + + old = [frappe._dict(item) if isinstance(item, dict) else item for item in old] + + if doctype == "Stock Entry": + compare_keys = ("item_code", "t_warehouse", "transfer_qty", "serial_no") + sort_key = lambda item: (item.item_code, cstr(item.t_warehouse), # noqa + flt(item.transfer_qty), cstr(item.serial_no)) + else: + # purchase receipt / invoice + compare_keys = ("item_code", "warehouse", "stock_qty", "received_qty", "serial_no") + sort_key = lambda item: (item.item_code, cstr(item.warehouse), # noqa + flt(item.stock_qty), flt(item.received_qty), cstr(item.serial_no)) + + old_sorted = sorted(old, key=sort_key) + new_sorted = sorted(new, key=sort_key) + + # Once sorted by all relevant keys both tables should align if they are same. + for old_item, new_item in zip(old_sorted, new_sorted): + for key in compare_keys: + if old_item.get(key) != new_item.get(key): + return True + return False + + def get_ordered_putaway_rules(item_code, company, source_warehouse=None): """Returns an ordered list of putaway rules to apply on an item.""" filters = { From fb0167ffc6cc005aa47f1a1717f4f768049a10c8 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 17 Feb 2022 11:08:50 +0530 Subject: [PATCH 105/129] test: putaway rule re-application shouldn't do anything (cherry picked from commit d9fc3f3d902a98dc9b1c1ab6814c66b170e18a04) --- .../doctype/putaway_rule/test_putaway_rule.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py index bd4d811e76c..ff1c19a8275 100644 --- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py @@ -35,6 +35,18 @@ class TestPutawayRule(ERPNextTestCase): new_uom.uom_name = "Bag" new_uom.save() + def assertUnchangedItemsOnResave(self, doc): + """ Check if same items remain even after reapplication of rules. + + This is required since some business logic like subcontracting + depends on `name` of items to be same if item isn't changed. + """ + doc.reload() + old_items = {d.name for d in doc.items} + doc.save() + new_items = {d.name for d in doc.items} + self.assertSetEqual(old_items, new_items) + def test_putaway_rules_priority(self): """Test if rule is applied by priority, irrespective of free space.""" rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, @@ -50,6 +62,8 @@ class TestPutawayRule(ERPNextTestCase): self.assertEqual(pr.items[1].qty, 100) self.assertEqual(pr.items[1].warehouse, self.warehouse_2) + self.assertUnchangedItemsOnResave(pr) + pr.delete() rule_1.delete() rule_2.delete() @@ -162,6 +176,8 @@ class TestPutawayRule(ERPNextTestCase): # leftover space was for 500 kg (0.5 Bag) # Since Bag is a whole UOM, 1(out of 2) Bag will be unassigned + self.assertUnchangedItemsOnResave(pr) + pr.delete() rule_1.delete() rule_2.delete() @@ -196,6 +212,8 @@ class TestPutawayRule(ERPNextTestCase): self.assertEqual(pr.items[1].warehouse, self.warehouse_1) self.assertEqual(pr.items[1].putaway_rule, rule_1.name) + self.assertUnchangedItemsOnResave(pr) + pr.delete() rule_1.delete() @@ -239,6 +257,8 @@ class TestPutawayRule(ERPNextTestCase): self.assertEqual(stock_entry_item.qty, 100) # unassigned 100 out of 200 Kg self.assertEqual(stock_entry_item.putaway_rule, rule_2.name) + self.assertUnchangedItemsOnResave(stock_entry) + stock_entry.delete() rule_1.delete() rule_2.delete() @@ -294,6 +314,8 @@ class TestPutawayRule(ERPNextTestCase): self.assertEqual(stock_entry.items[2].qty, 200) self.assertEqual(stock_entry.items[2].putaway_rule, rule_2.name) + self.assertUnchangedItemsOnResave(stock_entry) + stock_entry.delete() rule_1.delete() rule_2.delete() @@ -344,6 +366,8 @@ class TestPutawayRule(ERPNextTestCase): self.assertEqual(stock_entry.items[1].serial_no, "\n".join(serial_nos[3:])) self.assertEqual(stock_entry.items[1].batch_no, "BOTTL-BATCH-1") + self.assertUnchangedItemsOnResave(stock_entry) + stock_entry.delete() pr.cancel() rule_1.delete() @@ -366,6 +390,8 @@ class TestPutawayRule(ERPNextTestCase): self.assertEqual(stock_entry_item.qty, 100) self.assertEqual(stock_entry_item.putaway_rule, rule_1.name) + self.assertUnchangedItemsOnResave(stock_entry) + stock_entry.delete() rule_1.delete() rule_2.delete() From 97505fdc82d87739829d7ceef96cbd3271626f4f Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 17 Feb 2022 14:14:47 +0530 Subject: [PATCH 106/129] fix: currency in bank reconciliation tool (cherry picked from commit 60674e52b8a08dc5785da73e9ce418fad00d836c) --- .../bank_reconciliation_tool/bank_reconciliation_tool.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js index 398e4576a68..cf52471912d 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js @@ -64,6 +64,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", { "account_currency", (r) => { frm.currency = r.account_currency; + frm.trigger("render_chart"); } ); } @@ -128,7 +129,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", { } }, - render_chart(frm) { + render_chart: frappe.utils.debounce((frm) => { frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager( { $reconciliation_tool_cards: frm.get_field( @@ -140,7 +141,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", { currency: frm.currency, } ); - }, + }, 500), render(frm) { if (frm.doc.bank_account) { From 20f7db98cbb7af50100c93e18867e1c328485694 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 17 Feb 2022 14:24:52 +0530 Subject: [PATCH 107/129] fix: production plan status should consider qty + WO status (cherry picked from commit db93f26f20fe315e46324bfb36de759637f918bc) --- .../production_plan/production_plan.py | 24 +++++++++++++------ .../production_plan/test_production_plan.py | 17 ++++++++++--- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 1ccd9dc7ad7..562c47fcf17 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -320,7 +320,7 @@ class ProductionPlan(Document): if self.total_produced_qty > 0: self.status = "In Process" - if self.check_have_work_orders_completed(): + if self.all_items_completed(): self.status = "Completed" if self.status != 'Completed': @@ -592,14 +592,24 @@ class ProductionPlan(Document): self.append("sub_assembly_items", data) - def check_have_work_orders_completed(self): - wo_status = frappe.db.get_list( + def all_items_completed(self): + all_items_produced = all(flt(d.planned_qty) - flt(d.produced_qty) < 0.000001 + for d in self.po_items) + if not all_items_produced: + return False + + wo_status = frappe.get_all( "Work Order", - filters={"production_plan": self.name}, + filters={ + "production_plan": self.name, + "status": ("not in", ["Closed", "Stopped"]), + "docstatus": ("<", 2), + }, fields="status", - pluck="status" + pluck="status", ) - return all(s == "Completed" for s in wo_status) + all_work_orders_completed = all(s == "Completed" for s in wo_status) + return all_work_orders_completed @frappe.whitelist() def download_raw_materials(doc, warehouses=None): @@ -1045,4 +1055,4 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0): def set_default_warehouses(row, default_warehouses): for field in ['wip_warehouse', 'fg_warehouse']: if not row.get(field): - row[field] = default_warehouses.get(field) \ No newline at end of file + row[field] = default_warehouses.get(field) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index afa1501efcd..d88e10a564c 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -409,9 +409,6 @@ class TestProductionPlan(ERPNextTestCase): boms = { "Assembly": { "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},}, - "SubAssembly2": {"ChildPart3": {}}, - "SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}}, - "ChildPart5": {}, "ChildPart6": {}, "SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}}, }, @@ -591,6 +588,20 @@ class TestProductionPlan(ERPNextTestCase): pln.reload() self.assertEqual(pln.po_items[0].pending_qty, 1) + def test_qty_based_status(self): + pp = frappe.new_doc("Production Plan") + pp.po_items = [ + frappe._dict(planned_qty=5, produce_qty=4) + ] + self.assertFalse(pp.all_items_completed()) + + pp.po_items = [ + frappe._dict(planned_qty=5, produce_qty=10), + frappe._dict(planned_qty=5, produce_qty=4) + ] + self.assertFalse(pp.all_items_completed()) + + def create_production_plan(**args): """ sales_order (obj): Sales Order Doc Object From 63079f3e0771c445c7c7bf21171ddb380e34df7a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 17 Feb 2022 15:59:12 +0530 Subject: [PATCH 108/129] fix: coupon code is applied even if ignore_pricing_rule is enabled (cherry picked from commit 274399978572b1f2e80fd2a1db2663efa544fcf7) --- erpnext/public/js/controllers/transaction.js | 20 +++++----------- .../selling/page/point_of_sale/pos_payment.js | 23 +++++++++++++++++++ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 76de31770a5..e742ae9a890 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2265,20 +2265,12 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ coupon_code: function() { var me = this; - if (this.frm.doc.coupon_code) { - frappe.run_serially([ - () => this.frm.doc.ignore_pricing_rule=1, - () => me.ignore_pricing_rule(), - () => this.frm.doc.ignore_pricing_rule=0, - () => me.apply_pricing_rule(), - () => this.frm.save() - ]); - } else { - frappe.run_serially([ - () => this.frm.doc.ignore_pricing_rule=1, - () => me.ignore_pricing_rule() - ]); - } + frappe.run_serially([ + () => this.frm.doc.ignore_pricing_rule=1, + () => me.ignore_pricing_rule(), + () => this.frm.doc.ignore_pricing_rule=0, + () => me.apply_pricing_rule() + ]); } }); diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index b9b65591dc7..9650bc88a42 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -169,6 +169,29 @@ erpnext.PointOfSale.Payment = class { } }); + frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => { + if (!frm.doc.ignore_pricing_rule) { + if (frm.doc.coupon_code) { + frappe.run_serially([ + () => frm.doc.ignore_pricing_rule=1, + () => frm.trigger('ignore_pricing_rule'), + () => frm.doc.ignore_pricing_rule=0, + () => frm.trigger('apply_pricing_rule'), + () => frm.save(), + () => this.update_totals_section(frm.doc) + ]); + } else { + frappe.run_serially([ + () => frm.doc.ignore_pricing_rule=1, + () => frm.trigger('ignore_pricing_rule'), + () => frm.doc.ignore_pricing_rule=0, + () => frm.save(), + () => this.update_totals_section(frm.doc) + ]); + } + } + }); + this.setup_listener_for_payments(); this.$payment_modes.on('click', '.shortcut', function() { From 6db0ea8be84f8a5479cbc23acf1cf77fa3603ac4 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 17 Feb 2022 12:00:19 +0530 Subject: [PATCH 109/129] fix(Timesheet): convert time logs to datetime while checking for overlap (cherry picked from commit e2e998fbd9baa6015bc9c376dd5b6db7ae6cae49) --- .../projects/doctype/timesheet/timesheet.py | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index dd0b5f90f4d..fa0411e0f8a 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -7,7 +7,7 @@ import json import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import add_to_date, flt, getdate, time_diff_in_hours +from frappe.utils import add_to_date, flt, get_datetime, getdate, time_diff_in_hours from erpnext.controllers.queries import get_match_cond from erpnext.hr.utils import validate_active_employee @@ -145,7 +145,7 @@ class Timesheet(Document): if not (data.from_time and data.hours): return - _to_time = add_to_date(data.from_time, hours=data.hours, as_datetime=True) + _to_time = get_datetime(add_to_date(data.from_time, hours=data.hours, as_datetime=True)) if data.to_time != _to_time: data.to_time = _to_time @@ -186,24 +186,37 @@ class Timesheet(Document): and ts.docstatus < 2""".format(cond), { "val": value, - "from_time": args.from_time, - "to_time": args.to_time, + "from_time": get_datetime(args.from_time), + "to_time": get_datetime(args.to_time), "name": args.name or "No Name", "parent": args.parent or "No Name" }, as_dict=True) - # check internal overlap - for time_log in self.time_logs: - if not (time_log.from_time and time_log.to_time - and args.from_time and args.to_time): continue - if (fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and \ - args.idx != time_log.idx and ((args.from_time > time_log.from_time and args.from_time < time_log.to_time) or - (args.to_time > time_log.from_time and args.to_time < time_log.to_time) or - (args.from_time <= time_log.from_time and args.to_time >= time_log.to_time)): - return self + if self.check_internal_overlap(fieldname, args): + return self return existing[0] if existing else None + def check_internal_overlap(self, fieldname, args): + for time_log in self.time_logs: + if not (time_log.from_time and time_log.to_time + and args.from_time and args.to_time): + continue + + from_time = get_datetime(time_log.from_time) + to_time = get_datetime(time_log.to_time) + args_from_time = get_datetime(args.from_time) + args_to_time = get_datetime(args.to_time) + + if (fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and \ + args.idx != time_log.idx and ( + (args_from_time > from_time and args_from_time < to_time) + or (args_to_time > from_time and args_to_time < to_time) + or (args_from_time <= from_time and args_to_time >= to_time) + ): + return True + return False + def update_cost(self): for data in self.time_logs: if data.activity_type or data.is_billable: From 91e6fea2cdb56d984bbafd3b11d9b4c1ec3d631e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 17 Feb 2022 14:39:17 +0530 Subject: [PATCH 110/129] fix: convert overlap raw query to frappe.qb (cherry picked from commit 3ec9acf8f7c8fd08e5709ac0f352728f6a9d6cfa) --- .../projects/doctype/timesheet/timesheet.py | 52 +++++++++++-------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index fa0411e0f8a..c43be8cbd8b 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -171,26 +171,35 @@ class Timesheet(Document): .format(args.idx, self.name, existing.name), OverlapError) def get_overlap_for(self, fieldname, args, value): - cond = "ts.`{0}`".format(fieldname) - if fieldname == 'workstation': - cond = "tsd.`{0}`".format(fieldname) + timesheet = frappe.qb.DocType("Timesheet") + timelog = frappe.qb.DocType("Timesheet Detail") - existing = frappe.db.sql("""select ts.name as name, tsd.from_time as from_time, tsd.to_time as to_time from - `tabTimesheet Detail` tsd, `tabTimesheet` ts where {0}=%(val)s and tsd.parent = ts.name and - ( - (%(from_time)s > tsd.from_time and %(from_time)s < tsd.to_time) or - (%(to_time)s > tsd.from_time and %(to_time)s < tsd.to_time) or - (%(from_time)s <= tsd.from_time and %(to_time)s >= tsd.to_time)) - and tsd.name!=%(name)s - and ts.name!=%(parent)s - and ts.docstatus < 2""".format(cond), - { - "val": value, - "from_time": get_datetime(args.from_time), - "to_time": get_datetime(args.to_time), - "name": args.name or "No Name", - "parent": args.parent or "No Name" - }, as_dict=True) + from_time = get_datetime(args.from_time) + to_time = get_datetime(args.to_time) + + query = ( + frappe.qb.from_(timesheet) + .join(timelog) + .on(timelog.parent == timesheet.name) + .select(timesheet.name.as_('name'), timelog.from_time.as_('from_time'), timelog.to_time.as_('to_time')) + .where( + (timelog.name != (args.name or "No Name")) + & (timesheet.name != (args.parent or "No Name")) + & (timesheet.docstatus < 2) + & ( + ((from_time > timelog.from_time) & (from_time < timelog.to_time)) + | ((to_time > timelog.from_time) & (to_time < timelog.to_time)) + | ((from_time <= timelog.from_time) & (to_time >= timelog.to_time)) + ) + ) + ) + + if fieldname == "workstation": + query = query.where(timelog[fieldname] == value) + else: + query = query.where(timesheet[fieldname] == value) + + existing = query.run(as_dict=True) if self.check_internal_overlap(fieldname, args): return self @@ -208,12 +217,13 @@ class Timesheet(Document): args_from_time = get_datetime(args.from_time) args_to_time = get_datetime(args.to_time) - if (fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and \ + if ((fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and args.idx != time_log.idx and ( (args_from_time > from_time and args_from_time < to_time) or (args_to_time > from_time and args_to_time < to_time) or (args_from_time <= from_time and args_to_time >= to_time) - ): + ) + ): return True return False From 98bafc589828268954f70ed75f636d390251c97f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 17 Feb 2022 14:39:26 +0530 Subject: [PATCH 111/129] test: timesheet not overlapping with continuous timelogs (cherry picked from commit 47ff968253ff7c4e7ca4e7769ccc29d93a8f71f2) --- .../doctype/timesheet/test_timesheet.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index 989bcd1670d..8b603570217 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -151,6 +151,35 @@ class TestTimesheet(unittest.TestCase): settings.ignore_employee_time_overlap = initial_setting settings.save() + def test_timesheet_not_overlapping_with_continuous_timelogs(self): + emp = make_employee("test_employee_6@salary.com") + + update_activity_type("_Test Activity Type") + timesheet = frappe.new_doc("Timesheet") + timesheet.employee = emp + timesheet.append( + 'time_logs', + { + "billable": 1, + "activity_type": "_Test Activity Type", + "from_time": now_datetime(), + "to_time": now_datetime() + datetime.timedelta(hours=3), + "company": "_Test Company" + } + ) + timesheet.append( + 'time_logs', + { + "billable": 1, + "activity_type": "_Test Activity Type", + "from_time": now_datetime() + datetime.timedelta(hours=3), + "to_time": now_datetime() + datetime.timedelta(hours=4), + "company": "_Test Company" + } + ) + + timesheet.save() # should not throw an error + def test_to_time(self): emp = make_employee("test_employee_6@salary.com") from_time = now_datetime() From ddfdb445fa509f523f7d904ff7eea8120a0f1434 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 17 Feb 2022 16:59:14 +0530 Subject: [PATCH 112/129] chore: remove unused code and fields related to workstation from Timesheet Detail (cherry picked from commit bef46e2b645f17eca8c1cd6ebe74e2845f6ea64f) --- .../projects/doctype/timesheet/timesheet.py | 22 +++------ .../timesheet_detail/timesheet_detail.json | 48 ++----------------- 2 files changed, 10 insertions(+), 60 deletions(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index c43be8cbd8b..b44d5017431 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -177,7 +177,7 @@ class Timesheet(Document): from_time = get_datetime(args.from_time) to_time = get_datetime(args.to_time) - query = ( + existing = ( frappe.qb.from_(timesheet) .join(timelog) .on(timelog.parent == timesheet.name) @@ -186,20 +186,14 @@ class Timesheet(Document): (timelog.name != (args.name or "No Name")) & (timesheet.name != (args.parent or "No Name")) & (timesheet.docstatus < 2) + & (timesheet[fieldname] == value) & ( ((from_time > timelog.from_time) & (from_time < timelog.to_time)) | ((to_time > timelog.from_time) & (to_time < timelog.to_time)) | ((from_time <= timelog.from_time) & (to_time >= timelog.to_time)) ) ) - ) - - if fieldname == "workstation": - query = query.where(timelog[fieldname] == value) - else: - query = query.where(timesheet[fieldname] == value) - - existing = query.run(as_dict=True) + ).run(as_dict=True) if self.check_internal_overlap(fieldname, args): return self @@ -217,12 +211,10 @@ class Timesheet(Document): args_from_time = get_datetime(args.from_time) args_to_time = get_datetime(args.to_time) - if ((fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and - args.idx != time_log.idx and ( - (args_from_time > from_time and args_from_time < to_time) - or (args_to_time > from_time and args_to_time < to_time) - or (args_from_time <= from_time and args_to_time >= to_time) - ) + if (args.get(fieldname) == time_log.get(fieldname)) and (args.idx != time_log.idx) and ( + (args_from_time > from_time and args_from_time < to_time) + or (args_to_time > from_time and args_to_time < to_time) + or (args_from_time <= from_time and args_to_time >= to_time) ): return True return False diff --git a/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json b/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json index ee04c612c9a..90fdb833315 100644 --- a/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json +++ b/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json @@ -14,12 +14,6 @@ "to_time", "hours", "completed", - "section_break_7", - "completed_qty", - "workstation", - "column_break_12", - "operation", - "operation_id", "project_details", "project", "project_name", @@ -83,43 +77,6 @@ "fieldtype": "Check", "label": "Completed" }, - { - "fieldname": "section_break_7", - "fieldtype": "Section Break" - }, - { - "depends_on": "eval:parent.work_order", - "fieldname": "completed_qty", - "fieldtype": "Float", - "label": "Completed Qty" - }, - { - "depends_on": "eval:parent.work_order", - "fieldname": "workstation", - "fieldtype": "Link", - "label": "Workstation", - "options": "Workstation", - "read_only": 1 - }, - { - "fieldname": "column_break_12", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval:parent.work_order", - "fieldname": "operation", - "fieldtype": "Link", - "label": "Operation", - "options": "Operation", - "read_only": 1 - }, - { - "depends_on": "eval:parent.work_order", - "fieldname": "operation_id", - "fieldtype": "Data", - "hidden": 1, - "label": "Operation Id" - }, { "fieldname": "project_details", "fieldtype": "Section Break" @@ -267,7 +224,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-05-18 12:19:33.205940", + "modified": "2022-02-17 16:53:34.878798", "modified_by": "Administrator", "module": "Projects", "name": "Timesheet Detail", @@ -275,5 +232,6 @@ "permissions": [], "quick_entry": 1, "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "states": [] } \ No newline at end of file From 90ebe6f266cbeced28daa7dbf2cb83e3ab74bcd3 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 17 Feb 2022 19:25:00 +0530 Subject: [PATCH 113/129] fix: avoid creating bins without item-wh Co-Authored-By: Shadrak Gurupnor <30501401+shadrak98@users.noreply.github.com> Co-Authored-By: Saurabh (cherry picked from commit c36bd7e1a6fe48c5fff4765e843571a0d6560dd1) --- erpnext/controllers/accounts_controller.py | 3 ++- erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py | 2 ++ erpnext/patches/v4_2/repost_reserved_qty.py | 8 +++++--- erpnext/patches/v4_2/update_requested_and_ordered_qty.py | 2 ++ 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index c5d354e445d..3513b0ad663 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1956,7 +1956,8 @@ def update_bin_on_delete(row, doctype): qty_dict["ordered_qty"] = get_ordered_qty(row.item_code, row.warehouse) - update_bin_qty(row.item_code, row.warehouse, qty_dict) + if row.warehouse: + update_bin_qty(row.item_code, row.warehouse, qty_dict) def validate_and_delete_children(parent, data): deleted_children = [] diff --git a/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py b/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py index c89e4bb9eae..50d97c4830d 100644 --- a/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py +++ b/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py @@ -10,6 +10,8 @@ def execute(): FROM `tabBin`""",as_dict=1) for entry in bin_details: + if not (entry.item_code and entry.warehouse): + continue update_bin_qty(entry.get("item_code"), entry.get("warehouse"), { "indented_qty": get_indented_qty(entry.get("item_code"), entry.get("warehouse")) }) diff --git a/erpnext/patches/v4_2/repost_reserved_qty.py b/erpnext/patches/v4_2/repost_reserved_qty.py index c2ca9be64aa..ed4b19d07d3 100644 --- a/erpnext/patches/v4_2/repost_reserved_qty.py +++ b/erpnext/patches/v4_2/repost_reserved_qty.py @@ -29,9 +29,11 @@ def execute(): """) for item_code, warehouse in repost_for: - update_bin_qty(item_code, warehouse, { - "reserved_qty": get_reserved_qty(item_code, warehouse) - }) + if not (item_code and warehouse): + continue + update_bin_qty(item_code, warehouse, { + "reserved_qty": get_reserved_qty(item_code, warehouse) + }) frappe.db.sql("""delete from tabBin where exists( diff --git a/erpnext/patches/v4_2/update_requested_and_ordered_qty.py b/erpnext/patches/v4_2/update_requested_and_ordered_qty.py index 42b0b04076f..dd79410ba58 100644 --- a/erpnext/patches/v4_2/update_requested_and_ordered_qty.py +++ b/erpnext/patches/v4_2/update_requested_and_ordered_qty.py @@ -14,6 +14,8 @@ def execute(): union select item_code, warehouse from `tabStock Ledger Entry`) a"""): try: + if not (item_code and warehouse): + continue count += 1 update_bin_qty(item_code, warehouse, { "indented_qty": get_indented_qty(item_code, warehouse), From 58aedaa0f7206cd9d505653a945f4d3fc46cac5b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 16 Feb 2022 18:15:57 +0530 Subject: [PATCH 114/129] fix: added item name in the excel sheet (cherry picked from commit 02e77029faed67ffff3e395c1de132cf15a14a03) --- .../doctype/production_plan/production_plan.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 562c47fcf17..60771592da5 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -616,7 +616,8 @@ def download_raw_materials(doc, warehouses=None): if isinstance(doc, str): doc = frappe._dict(json.loads(doc)) - item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM', + item_list = [['Item Code', 'Item Name', 'Description', + 'Stock UOM', 'Warehouse', 'Required Qty as per BOM', 'Projected Qty', 'Available Qty In Hand', 'Ordered Qty', 'Planned Qty', 'Reserved Qty for Production', 'Safety Stock', 'Required Qty']] @@ -625,7 +626,8 @@ def download_raw_materials(doc, warehouses=None): items = get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=True) for d in items: - item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('warehouse'), + item_list.append([d.get('item_code'), d.get('item_name'), + d.get('description'), d.get('stock_uom'), d.get('warehouse'), d.get('required_bom_qty'), d.get('projected_qty'), d.get('actual_qty'), d.get('ordered_qty'), d.get('planned_qty'), d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')]) From d86e63e22d530f4ac63044475bb4576e2fae0235 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Feb 2022 18:10:57 +0530 Subject: [PATCH 115/129] fix: Transfer Bucket logic for Repack Entry with split batch rows (cherry picked from commit 799671c7482fa8bca12a24636ea0000579ca9537) --- .../stock/report/stock_ageing/stock_ageing.py | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index a89a4038c20..9866e63fb5a 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -286,10 +286,11 @@ class FIFOSlots: def __compute_incoming_stock(self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List): "Update FIFO Queue on inward stock." - if self.transferred_item_details.get(transfer_key): - # inward/outward from same voucher, item & warehouse - slot = self.transferred_item_details[transfer_key].pop(0) - fifo_queue.append(slot) + transfer_data = self.transferred_item_details.get(transfer_key) + if transfer_data: + # [Repack] inward/outward from same voucher, item & warehouse + # consume transfer data and add stock to fifo queue + self.__adjust_incoming_transfer_qty(transfer_data, fifo_queue, row) else: if not serial_nos: if fifo_queue and flt(fifo_queue[0][0]) < 0: @@ -333,6 +334,27 @@ class FIFOSlots: self.transferred_item_details[transfer_key].append([qty_to_pop, slot[1]]) qty_to_pop = 0 + def __adjust_incoming_transfer_qty(self, transfer_data: Dict, fifo_queue: List, row: Dict): + "Add previously removed stock back to FIFO Queue." + transfer_qty_to_pop = flt(row.actual_qty) + first_bucket_qty = transfer_data[0][0] + first_bucket_date = transfer_data[0][1] + + while transfer_qty_to_pop: + if transfer_data and 0 > first_bucket_qty <= transfer_qty_to_pop: + # bucket qty is not enough, consume whole + transfer_qty_to_pop -= first_bucket_qty + slot = transfer_data.pop(0) + fifo_queue.append(slot) + elif not transfer_data: + # transfer bucket is empty, extra incoming qty + fifo_queue.append([transfer_qty_to_pop, row.posting_date]) + else: + # ample bucket qty to consume + first_bucket_qty -= transfer_qty_to_pop + fifo_queue.append([transfer_qty_to_pop, first_bucket_date]) + transfer_qty_to_pop = 0 + def __update_balances(self, row: Dict, key: Union[Tuple, str]): self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction From 690ae52a8e7a201da8ad7cb59e1f009c56c64f6a Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Feb 2022 18:41:42 +0530 Subject: [PATCH 116/129] test: Stock Ageing FIFO buckets for Repack entry with same item (cherry picked from commit ea3b7de867fdcc565567ec9ca1b7925116e16f2f) --- .../report/stock_ageing/test_stock_ageing.py | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 66d2f6b7539..3055332540f 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -236,6 +236,159 @@ class TestStockAgeing(ERPNextTestCase): item_wh_balances = [item_wh_wise_slots.get(i).get("qty_after_transaction") for i in item_wh_wise_slots] self.assertEqual(sum(item_wh_balances), item_result["qty_after_transaction"]) + def test_repack_entry_same_item_split_rows(self): + """ + Split consumption rows and have single repacked item row (same warehouse). + Ledger: + Item | Qty | Voucher + ------------------------ + Item 1 | 500 | 001 + Item 1 | -50 | 002 (repack) + Item 1 | -50 | 002 (repack) + Item 1 | 100 | 002 (repack) + + Case most likely for batch items. Test time bucket computation. + """ + sle = [ + frappe._dict( # stock up item + name="Flask Item", + actual_qty=500, qty_after_transaction=500, + warehouse="WH 1", + posting_date="2021-12-03", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-50), qty_after_transaction=450, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-50), qty_after_transaction=400, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=100, qty_after_transaction=500, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + ] + slots = FIFOSlots(self.filters, sle).generate() + item_result = slots["Flask Item"] + queue = item_result["fifo_queue"] + + self.assertEqual(item_result["total_qty"], 500.0) + self.assertEqual(queue[0][0], 400.0) + self.assertEqual(queue[1][0], 100.0) + # check if time buckets add up to balance qty + self.assertEqual(sum([i[0] for i in queue]), 500.0) + + def test_repack_entry_same_item_overconsume(self): + """ + Over consume item and have less repacked item qty (same warehouse). + Ledger: + Item | Qty | Voucher + ------------------------ + Item 1 | 500 | 001 + Item 1 | -100 | 002 (repack) + Item 1 | 50 | 002 (repack) + + Case most likely for batch items. Test time bucket computation. + """ + sle = [ + frappe._dict( # stock up item + name="Flask Item", + actual_qty=500, qty_after_transaction=500, + warehouse="WH 1", + posting_date="2021-12-03", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-100), qty_after_transaction=400, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=50, qty_after_transaction=450, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + ] + slots = FIFOSlots(self.filters, sle).generate() + item_result = slots["Flask Item"] + queue = item_result["fifo_queue"] + + self.assertEqual(item_result["total_qty"], 450.0) + self.assertEqual(queue[0][0], 400.0) + self.assertEqual(queue[1][0], 50.0) + # check if time buckets add up to balance qty + self.assertEqual(sum([i[0] for i in queue]), 450.0) + + def test_repack_entry_same_item_overproduce(self): + """ + Under consume item and have more repacked item qty (same warehouse). + Ledger: + Item | Qty | Voucher + ------------------------ + Item 1 | 500 | 001 + Item 1 | -50 | 002 (repack) + Item 1 | 100 | 002 (repack) + + Case most likely for batch items. Test time bucket computation. + """ + sle = [ + frappe._dict( # stock up item + name="Flask Item", + actual_qty=500, qty_after_transaction=500, + warehouse="WH 1", + posting_date="2021-12-03", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-50), qty_after_transaction=450, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=100, qty_after_transaction=550, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + ] + slots = FIFOSlots(self.filters, sle).generate() + item_result = slots["Flask Item"] + queue = item_result["fifo_queue"] + + self.assertEqual(item_result["total_qty"], 550.0) + self.assertEqual(queue[0][0], 450.0) + self.assertEqual(queue[1][0], 100.0) + # check if time buckets add up to balance qty + self.assertEqual(sum([i[0] for i in queue]), 550.0) + def generate_item_and_item_wh_wise_slots(filters, sle): "Return results with and without 'show_warehouse_wise_stock'" item_wise_slots = FIFOSlots(filters, sle).generate() From cab05dbefb79ce48ac00d7c942824ff9c7f4f5af Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Feb 2022 20:30:16 +0530 Subject: [PATCH 117/129] chore: Add transfer bucket working to .md file (cherry picked from commit f6233e77c6c2cbfeec6aeb82a73c1bbcbaa8f5da) --- .../stock_ageing/stock_ageing_fifo_logic.md | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md b/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md index 9e9bed48e3e..3d759dd9989 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md +++ b/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md @@ -71,4 +71,39 @@ Date | Qty | Queue 2nd | -60 | [[-10, 1-12-2021]] 3rd | +5 | [[-5, 3-12-2021]] 4th | +10 | [[5, 4-12-2021]] -4th | +20 | [[5, 4-12-2021], [20, 4-12-2021]] \ No newline at end of file +4th | +20 | [[5, 4-12-2021], [20, 4-12-2021]] + +### Concept of Transfer Qty Bucket +In the case of **Repack**, Quantity that comes in, isn't really incoming. It is just new stock repurposed from old stock, due to incoming-outgoing of the same warehouse. + +Here, stock is consumed from the FIFO Queue. It is then re-added back to the queue. +While adding stock back to the queue we need to know how much to add. +For this we need to keep track of how much was previously consumed. +Hence we use **Transfer Qty Bucket**. + +While re-adding stock, we try to add buckets that were consumed earlier (date intact), to maintain correctness. + +#### Case 1: Same Item-Warehouse in Repack +Eg: +------------------------------------------------------------------------------------- +Date | Qty | Voucher | FIFO Queue | Transfer Qty Buckets +------------------------------------------------------------------------------------- +1st | +500 | PR | [[500, 1-12-2021]] | +2nd | -50 | Repack | [[450, 1-12-2021]] | [[50, 1-12-2021]] +2nd | +50 | Repack | [[450, 1-12-2021], [50, 1-12-2021]] | [] + +- The balance at the end is restored back to 500 +- However, the initial 500 qty bucket is now split into 450 and 50, with the same date +- The net effect is the same as that before the Repack + +#### Case 2: Same Item-Warehouse in Repack with Split Consumption rows +Eg: +------------------------------------------------------------------------------------- +Date | Qty | Voucher | FIFO Queue | Transfer Qty Buckets +------------------------------------------------------------------------------------- +1st | +500 | PR | [[500, 1-12-2021]] | +2nd | -50 | Repack | [[450, 1-12-2021]] | [[50, 1-12-2021]] +2nd | -50 | Repack | [[400, 1-12-2021]] | [[50, 1-12-2021], +- | | | |[50, 1-12-2021]] +2nd | +100 | Repack | [[400, 1-12-2021], [50, 1-12-2021], | [] +- | | | [50, 1-12-2021]] | From c66b5752e286a927e479a393d406940cb51824d5 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 17 Feb 2022 14:30:00 +0530 Subject: [PATCH 118/129] fix: Precision of available qty and negative stock in transfer bucket - Maintain only positive values in transfer bucket - Use it to neutralize/add stock to fifo queue (cherry picked from commit d3fbbcfed39570fbad52a77b2533c2b72da8679f) --- .../stock/report/stock_ageing/stock_ageing.py | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 9866e63fb5a..60f9e959c89 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -28,6 +28,7 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li "Returns ordered, formatted data with ranges." _func = itemgetter(1) data = [] + precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) for item, item_dict in item_details.items(): earliest_age, latest_age = 0, 0 @@ -48,10 +49,13 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li if filters.get("show_warehouse_wise_stock"): row.append(details.warehouse) - row.extend([item_dict.get("total_qty"), average_age, + row.extend([ + flt(item_dict.get("total_qty"), precision), + average_age, range1, range2, range3, above_range3, earliest_age, latest_age, - details.stock_uom]) + details.stock_uom + ]) data.append(row) @@ -288,13 +292,14 @@ class FIFOSlots: transfer_data = self.transferred_item_details.get(transfer_key) if transfer_data: - # [Repack] inward/outward from same voucher, item & warehouse + # inward/outward from same voucher, item & warehouse + # eg: Repack with same item, Stock reco for batch item # consume transfer data and add stock to fifo queue self.__adjust_incoming_transfer_qty(transfer_data, fifo_queue, row) else: if not serial_nos: - if fifo_queue and flt(fifo_queue[0][0]) < 0: - # neutralize negative stock by adding positive stock + if fifo_queue and flt(fifo_queue[0][0]) <= 0: + # neutralize 0/negative stock by adding positive stock fifo_queue[0][0] += flt(row.actual_qty) fifo_queue[0][1] = row.posting_date else: @@ -325,7 +330,7 @@ class FIFOSlots: elif not fifo_queue: # negative stock, no balance but qty yet to consume fifo_queue.append([-(qty_to_pop), row.posting_date]) - self.transferred_item_details[transfer_key].append([row.actual_qty, row.posting_date]) + self.transferred_item_details[transfer_key].append([qty_to_pop, row.posting_date]) qty_to_pop = 0 else: # qty to pop < slot qty, ample balance @@ -337,22 +342,28 @@ class FIFOSlots: def __adjust_incoming_transfer_qty(self, transfer_data: Dict, fifo_queue: List, row: Dict): "Add previously removed stock back to FIFO Queue." transfer_qty_to_pop = flt(row.actual_qty) - first_bucket_qty = transfer_data[0][0] - first_bucket_date = transfer_data[0][1] + + def add_to_fifo_queue(slot): + if fifo_queue and flt(fifo_queue[0][0]) <= 0: + # neutralize 0/negative stock by adding positive stock + fifo_queue[0][0] += flt(slot[0]) + fifo_queue[0][1] = slot[1] + else: + fifo_queue.append(slot) while transfer_qty_to_pop: - if transfer_data and 0 > first_bucket_qty <= transfer_qty_to_pop: + if transfer_data and 0 < transfer_data[0][0] <= transfer_qty_to_pop: # bucket qty is not enough, consume whole - transfer_qty_to_pop -= first_bucket_qty - slot = transfer_data.pop(0) - fifo_queue.append(slot) + transfer_qty_to_pop -= transfer_data[0][0] + add_to_fifo_queue(transfer_data.pop(0)) elif not transfer_data: # transfer bucket is empty, extra incoming qty - fifo_queue.append([transfer_qty_to_pop, row.posting_date]) + add_to_fifo_queue([transfer_qty_to_pop, row.posting_date]) + transfer_qty_to_pop = 0 else: # ample bucket qty to consume - first_bucket_qty -= transfer_qty_to_pop - fifo_queue.append([transfer_qty_to_pop, first_bucket_date]) + transfer_data[0][0] -= transfer_qty_to_pop + add_to_fifo_queue([transfer_qty_to_pop, transfer_data[0][1]]) transfer_qty_to_pop = 0 def __update_balances(self, row: Dict, key: Union[Tuple, str]): From fde5a9e6f78f1cb4cc30ed4fca48f28775023e4a Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 18 Feb 2022 18:52:42 +0530 Subject: [PATCH 119/129] fix: Range Qty precision (cherry picked from commit ed4a6c6cc63ca37a6033f9f87c35cd26aaa2cb43) --- erpnext/stock/report/stock_ageing/stock_ageing.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 60f9e959c89..97a740e1844 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -12,6 +12,7 @@ from frappe.utils import cint, date_diff, flt from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos Filters = frappe._dict +precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) def execute(filters: Filters = None) -> Tuple: to_date = filters["to_date"] @@ -28,7 +29,6 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li "Returns ordered, formatted data with ranges." _func = itemgetter(1) data = [] - precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) for item, item_dict in item_details.items(): earliest_age, latest_age = 0, 0 @@ -83,13 +83,13 @@ def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: D qty = flt(item[0]) if not item_dict["has_serial_no"] else 1.0 if age <= filters.range1: - range1 += qty + range1 = flt(range1 + qty, precision) elif age <= filters.range2: - range2 += qty + range2 = flt(range2 + qty, precision) elif age <= filters.range3: - range3 += qty + range3 = flt(range3 + qty, precision) else: - above_range3 += qty + above_range3 = flt(above_range3 + qty, precision) return range1, range2, range3, above_range3 From 88ca371887f9cf10069184e220a9410fe4a48842 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 18 Feb 2022 18:53:05 +0530 Subject: [PATCH 120/129] test: Negative Stock, over consumption & over production with split rows, balance precision (cherry picked from commit d5be536740642d0bef9ea23151a41ce2657b9cd2) --- .../report/stock_ageing/test_stock_ageing.py | 221 +++++++++++++++++- 1 file changed, 217 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 3055332540f..3fc357e8d4f 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -3,7 +3,7 @@ import frappe -from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots +from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, format_report_data from erpnext.tests.utils import ERPNextTestCase @@ -11,7 +11,8 @@ class TestStockAgeing(ERPNextTestCase): def setUp(self) -> None: self.filters = frappe._dict( company="_Test Company", - to_date="2021-12-10" + to_date="2021-12-10", + range1=30, range2=60, range3=90 ) def test_normal_inward_outward_queue(self): @@ -289,7 +290,8 @@ class TestStockAgeing(ERPNextTestCase): self.assertEqual(item_result["total_qty"], 500.0) self.assertEqual(queue[0][0], 400.0) - self.assertEqual(queue[1][0], 100.0) + self.assertEqual(queue[1][0], 50.0) + self.assertEqual(queue[2][0], 50.0) # check if time buckets add up to balance qty self.assertEqual(sum([i[0] for i in queue]), 500.0) @@ -341,6 +343,63 @@ class TestStockAgeing(ERPNextTestCase): # check if time buckets add up to balance qty self.assertEqual(sum([i[0] for i in queue]), 450.0) + def test_repack_entry_same_item_overconsume_with_split_rows(self): + """ + Over consume item and have less repacked item qty (same warehouse). + Ledger: + Item | Qty | Voucher + ------------------------ + Item 1 | 20 | 001 + Item 1 | -50 | 002 (repack) + Item 1 | -50 | 002 (repack) + Item 1 | 50 | 002 (repack) + """ + sle = [ + frappe._dict( # stock up item + name="Flask Item", + actual_qty=20, qty_after_transaction=20, + warehouse="WH 1", + posting_date="2021-12-03", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-50), qty_after_transaction=(-30), + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-50), qty_after_transaction=(-80), + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=50, qty_after_transaction=(-30), + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + ] + fifo_slots = FIFOSlots(self.filters, sle) + slots = fifo_slots.generate() + item_result = slots["Flask Item"] + queue = item_result["fifo_queue"] + + self.assertEqual(item_result["total_qty"], -30.0) + self.assertEqual(queue[0][0], -30.0) + + # check transfer bucket + transfer_bucket = fifo_slots.transferred_item_details[('002', 'Flask Item', 'WH 1')] + self.assertEqual(transfer_bucket[0][0], 50) + def test_repack_entry_same_item_overproduce(self): """ Under consume item and have more repacked item qty (same warehouse). @@ -385,10 +444,164 @@ class TestStockAgeing(ERPNextTestCase): self.assertEqual(item_result["total_qty"], 550.0) self.assertEqual(queue[0][0], 450.0) - self.assertEqual(queue[1][0], 100.0) + self.assertEqual(queue[1][0], 50.0) + self.assertEqual(queue[2][0], 50.0) # check if time buckets add up to balance qty self.assertEqual(sum([i[0] for i in queue]), 550.0) + def test_repack_entry_same_item_overproduce_with_split_rows(self): + """ + Over consume item and have less repacked item qty (same warehouse). + Ledger: + Item | Qty | Voucher + ------------------------ + Item 1 | 20 | 001 + Item 1 | -50 | 002 (repack) + Item 1 | 50 | 002 (repack) + Item 1 | 50 | 002 (repack) + """ + sle = [ + frappe._dict( # stock up item + name="Flask Item", + actual_qty=20, qty_after_transaction=20, + warehouse="WH 1", + posting_date="2021-12-03", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-50), qty_after_transaction=(-30), + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=50, qty_after_transaction=20, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=50, qty_after_transaction=70, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + ] + fifo_slots = FIFOSlots(self.filters, sle) + slots = fifo_slots.generate() + item_result = slots["Flask Item"] + queue = item_result["fifo_queue"] + + self.assertEqual(item_result["total_qty"], 70.0) + self.assertEqual(queue[0][0], 20.0) + self.assertEqual(queue[1][0], 50.0) + + # check transfer bucket + transfer_bucket = fifo_slots.transferred_item_details[('002', 'Flask Item', 'WH 1')] + self.assertFalse(transfer_bucket) + + def test_negative_stock_same_voucher(self): + """ + Test negative stock scenario in transfer bucket via repack entry (same wh). + Ledger: + Item | Qty | Voucher + ------------------------ + Item 1 | -50 | 001 + Item 1 | -50 | 001 + Item 1 | 30 | 001 + Item 1 | 80 | 001 + """ + sle = [ + frappe._dict( # stock up item + name="Flask Item", + actual_qty=(-50), qty_after_transaction=(-50), + warehouse="WH 1", + posting_date="2021-12-01", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( # stock up item + name="Flask Item", + actual_qty=(-50), qty_after_transaction=(-100), + warehouse="WH 1", + posting_date="2021-12-01", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( # stock up item + name="Flask Item", + actual_qty=30, qty_after_transaction=(-70), + warehouse="WH 1", + posting_date="2021-12-01", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + ] + fifo_slots = FIFOSlots(self.filters, sle) + slots = fifo_slots.generate() + item_result = slots["Flask Item"] + + # check transfer bucket + transfer_bucket = fifo_slots.transferred_item_details[('001', 'Flask Item', 'WH 1')] + self.assertEqual(transfer_bucket[0][0], 20) + self.assertEqual(transfer_bucket[1][0], 50) + self.assertEqual(item_result["fifo_queue"][0][0], -70.0) + + sle.append(frappe._dict( + name="Flask Item", + actual_qty=80, qty_after_transaction=10, + warehouse="WH 1", + posting_date="2021-12-01", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + )) + + fifo_slots = FIFOSlots(self.filters, sle) + slots = fifo_slots.generate() + item_result = slots["Flask Item"] + + transfer_bucket = fifo_slots.transferred_item_details[('001', 'Flask Item', 'WH 1')] + self.assertFalse(transfer_bucket) + self.assertEqual(item_result["fifo_queue"][0][0], 10.0) + + def test_precision(self): + "Test if final balance qty is rounded off correctly." + sle = [ + frappe._dict( # stock up item + name="Flask Item", + actual_qty=0.3, qty_after_transaction=0.3, + warehouse="WH 1", + posting_date="2021-12-01", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( # stock up item + name="Flask Item", + actual_qty=0.6, qty_after_transaction=0.9, + warehouse="WH 1", + posting_date="2021-12-01", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + ] + + slots = FIFOSlots(self.filters, sle).generate() + report_data = format_report_data(self.filters, slots, self.filters["to_date"]) + row = report_data[0] # first row in report + bal_qty = row[5] + range_qty_sum = sum([i for i in row[7:11]]) # get sum of range balance + + # check if value of Available Qty column matches with range bucket post format + self.assertEqual(bal_qty, 0.9) + self.assertEqual(bal_qty, range_qty_sum) + def generate_item_and_item_wh_wise_slots(filters, sle): "Return results with and without 'show_warehouse_wise_stock'" item_wise_slots = FIFOSlots(filters, sle).generate() From 41b82145ea10e97b730685654f743218b5b8f47e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Feb 2022 22:01:00 +0530 Subject: [PATCH 121/129] fix: GSTIN filter for GSTR-1 report (cherry picked from commit 87b074ac0966ab26bf776c720fcb96b92a451d55) --- erpnext/regional/report/gstr_1/gstr_1.js | 23 ++++++++++++++++++++--- erpnext/regional/report/gstr_1/gstr_1.py | 23 ++++++++++++++++++++++- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js index 4b98978f130..1766fdb2ecd 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.js +++ b/erpnext/regional/report/gstr_1/gstr_1.js @@ -17,7 +17,7 @@ frappe.query_reports["GSTR-1"] = { "fieldtype": "Link", "options": "Address", "get_query": function () { - var company = frappe.query_report.get_filter_value('company'); + let company = frappe.query_report.get_filter_value('company'); if (company) { return { "query": 'frappe.contacts.doctype.address.address.address_query', @@ -26,6 +26,11 @@ frappe.query_reports["GSTR-1"] = { } } }, + { + "fieldname": "company_gstin", + "label": __("Company GSTIN"), + "fieldtype": "Select" + }, { "fieldname": "from_date", "label": __("From Date"), @@ -60,10 +65,22 @@ frappe.query_reports["GSTR-1"] = { } ], onload: function (report) { + let filters = report.get_values(); + + frappe.call({ + method: 'erpnext.regional.report.gstr_1.gstr_1.get_company_gstins', + args: { + company: filters.company + }, + callback: function(r) { + console.log(r.message); + frappe.query_report.page.fields_dict.company_gstin.df.options = r.message; + frappe.query_report.page.fields_dict.company_gstin.refresh(); + } + }); + report.page.add_inner_button(__("Download as JSON"), function () { - var filters = report.get_values(); - frappe.call({ method: 'erpnext.regional.report.gstr_1.gstr_1.get_json', args: { diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 8805678dc7d..1ba3d20bdbb 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -254,7 +254,8 @@ class Gstr1Report(object): for opts in (("company", " and company=%(company)s"), ("from_date", " and posting_date>=%(from_date)s"), ("to_date", " and posting_date<=%(to_date)s"), - ("company_address", " and company_address=%(company_address)s")): + ("company_address", " and company_address=%(company_address)s"), + ("company_gstin", " and company_gstin=%(company_gstin)s")): if self.filters.get(opts[0]): conditions += opts[1] @@ -1193,3 +1194,23 @@ def is_inter_state(invoice_detail): return True else: return False + + +@frappe.whitelist() +def get_company_gstins(company): + address = frappe.qb.DocType("Address") + links = frappe.qb.DocType("Dynamic Link") + + addresses = frappe.qb.from_(address).inner_join(links).on( + address.name == links.parent + ).select( + address.gstin + ).where( + links.link_doctype == 'Company' + ).where( + links.link_name == company + ).run(as_dict=1) + + address_list = [''] + [d.gstin for d in addresses] + + return address_list \ No newline at end of file From 3a98cc351c92374424ef5a8c75068b300a2e6f29 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Sat, 19 Feb 2022 19:35:57 +0530 Subject: [PATCH 122/129] Update gstr_1.js (cherry picked from commit a28ec89507fd42bf100b6a64c6bcdeef55f4b032) --- erpnext/regional/report/gstr_1/gstr_1.js | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js index 1766fdb2ecd..9999a6d167b 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.js +++ b/erpnext/regional/report/gstr_1/gstr_1.js @@ -73,7 +73,6 @@ frappe.query_reports["GSTR-1"] = { company: filters.company }, callback: function(r) { - console.log(r.message); frappe.query_report.page.fields_dict.company_gstin.df.options = r.message; frappe.query_report.page.fields_dict.company_gstin.refresh(); } From 02b7541fb9fc20631f3a3362ad061c48dc9e1d0b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 12 Feb 2022 21:54:22 +0530 Subject: [PATCH 124/129] fix: Error in consolidated financial statements (cherry picked from commit ae613008be59334e5ff72882ef9d70355f56805e) --- .../consolidated_financial_statement.py | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 758e3e93379..62bf156219a 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -367,7 +367,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies): accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0) def get_account_heads(root_type, companies, filters): - accounts = get_accounts(root_type, filters) + accounts = get_accounts(root_type, companies) if not accounts: return None, None, None @@ -396,7 +396,7 @@ def update_parent_account_names(accounts): for account in accounts: if account.parent_account: - account["parent_account_name"] = name_to_account_map[account.parent_account] + account["parent_account_name"] = name_to_account_map.get(account.parent_account) return accounts @@ -419,12 +419,21 @@ def get_subsidiary_companies(company): return frappe.db.sql_list("""select name from `tabCompany` where lft >= {0} and rgt <= {1} order by lft, rgt""".format(lft, rgt)) -def get_accounts(root_type, filters): - return frappe.db.sql(""" select name, is_group, company, - parent_account, lft, rgt, root_type, report_type, account_name, account_number - from - `tabAccount` where company = %s and root_type = %s - """ , (filters.get('company'), root_type), as_dict=1) +def get_accounts(root_type, companies): + accounts = [] + added_accounts = [] + + for company in companies: + for account in frappe.db.sql(""" select name, is_group, company, + parent_account, lft, rgt, root_type, report_type, account_name, account_number + from + `tabAccount` where company = %s and root_type = %s + """ , (company, root_type), as_dict=1): + if account.account_name not in added_accounts: + accounts.append(account) + added_accounts.append(account.account_name) + + return accounts def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters): data = [] From b99a95b280b328a75a0a64bda9e3900d257bd782 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 15 Feb 2022 12:05:51 +0530 Subject: [PATCH 125/129] fix: Remove commented out code (cherry picked from commit 42cdd6d2379d68efb592a5c8a8148979dce8cf1e) --- .../consolidated_financial_statement.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 62bf156219a..dad7384feaf 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -354,9 +354,6 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies): if d.parent_account: account = d.parent_account_name - # if not accounts_by_name.get(account): - # continue - for company in companies: accounts_by_name[account][company] = \ accounts_by_name[account].get(company, 0.0) + d.get(company, 0.0) From 232ba3580b9cd61fd609db55571d7c0ccff14248 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 15 Feb 2022 12:15:35 +0530 Subject: [PATCH 126/129] fix: Linting issues (cherry picked from commit fec40aac7a25c383e384f29471f9ea82382524b2) --- .../consolidated_financial_statement.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index dad7384feaf..1e20f7be3e4 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -421,11 +421,9 @@ def get_accounts(root_type, companies): added_accounts = [] for company in companies: - for account in frappe.db.sql(""" select name, is_group, company, - parent_account, lft, rgt, root_type, report_type, account_name, account_number - from - `tabAccount` where company = %s and root_type = %s - """ , (company, root_type), as_dict=1): + for account in frappe.get_all("Account", fields=["name", "is_group", "company", + "parent_account", "lft", "rgt", "root_type", "report_type", "account_name", "account_number"], + filters={"company": company, "root_type": root_type}): if account.account_name not in added_accounts: accounts.append(account) added_accounts.append(account.account_name) From 8d24cb44a814e25497390c302dd09ca08aa98ac9 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Feb 2022 11:38:16 +0530 Subject: [PATCH 127/129] fix(pos): removal of coupon code (cherry picked from commit fa38c291bd577b40f0d5007470108596d392f89b) --- erpnext/selling/page/point_of_sale/pos_payment.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 9650bc88a42..4d75e6ef1bf 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -180,14 +180,6 @@ erpnext.PointOfSale.Payment = class { () => frm.save(), () => this.update_totals_section(frm.doc) ]); - } else { - frappe.run_serially([ - () => frm.doc.ignore_pricing_rule=1, - () => frm.trigger('ignore_pricing_rule'), - () => frm.doc.ignore_pricing_rule=0, - () => frm.save(), - () => this.update_totals_section(frm.doc) - ]); } } }); From 14c5be7c27c90d0de1247ff83ee5075a16f78b08 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 14:23:52 +0530 Subject: [PATCH 128/129] chore: Show 'Produced Qty' field in Sales Order Item (#29903) (#29904) (cherry picked from commit e952cce17d8931054575de2e430f6000ae80ef9f) Co-authored-by: Marica --- .../selling/doctype/sales_order_item/sales_order_item.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 95f6c4e96df..080d517d131 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -83,8 +83,8 @@ "planned_qty", "column_break_69", "work_order_qty", - "delivered_qty", "produced_qty", + "delivered_qty", "returned_qty", "shopping_cart_section", "additional_notes", @@ -701,10 +701,8 @@ "width": "50px" }, { - "description": "For Production", "fieldname": "produced_qty", "fieldtype": "Float", - "hidden": 1, "label": "Produced Quantity", "oldfieldname": "produced_qty", "oldfieldtype": "Currency", @@ -802,7 +800,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-10-05 12:27:25.014789", + "modified": "2022-02-21 13:55:08.883104", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", @@ -811,5 +809,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From 130f0395496c4019f3e52cee1aa2a761e3134414 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 19 Feb 2022 19:19:32 +0530 Subject: [PATCH 129/129] fix: Ledger entries on LIA for term loans (cherry picked from commit 1aa12fb3f1bee18a8a58d11954acd8112e96261d) --- .../loan_interest_accrual.py | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index 0de073f85da..1c800a06da0 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -74,39 +74,6 @@ class LoanInterestAccrual(AccountsController): }) ) - if self.payable_principal_amount: - gle_map.append( - self.get_gl_dict({ - "account": self.loan_account, - "party_type": self.applicant_type, - "party": self.applicant, - "against": self.interest_income_account, - "debit": self.payable_principal_amount, - "debit_in_account_currency": self.interest_amount, - "against_voucher_type": "Loan", - "against_voucher": self.loan, - "remarks": _("Interest accrued from {0} to {1} against loan: {2}").format( - self.last_accrual_date, self.posting_date, self.loan), - "cost_center": erpnext.get_default_cost_center(self.company), - "posting_date": self.posting_date - }) - ) - - gle_map.append( - self.get_gl_dict({ - "account": self.interest_income_account, - "against": self.loan_account, - "credit": self.payable_principal_amount, - "credit_in_account_currency": self.interest_amount, - "against_voucher_type": "Loan", - "against_voucher": self.loan, - "remarks": ("Interest accrued from {0} to {1} against loan: {2}").format( - self.last_accrual_date, self.posting_date, self.loan), - "cost_center": erpnext.get_default_cost_center(self.company), - "posting_date": self.posting_date - }) - ) - if gle_map: make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj)