From f60e040d69aa56f71a632b3d77474f60657be689 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 01/31] feat: option to disable Item Tax Template --- .../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 77c9e95b75..b42d712d88 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 dc04dab84c..d0a3d4aaac 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -707,6 +707,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: @@ -715,7 +716,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 @@ -724,7 +725,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 663c594ead54ced046887bbd94a6a0c88fa7b8e8 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 02/31] feat: option to disable tax category --- .../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 f7145af44c..44a339f31d 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 c68c70f8bc88d9b05d64774ba070a34c059b7d30 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 25 Jan 2022 19:59:33 +0530 Subject: [PATCH 03/31] feat: Refund entry against loans --- erpnext/loan_management/doctype/loan/loan.js | 21 +++++++- .../loan_management/doctype/loan/loan.json | 15 +++++- 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 | 1 + .../v13_0/update_disbursement_account.py | 22 +++++++++ 9 files changed, 133 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 f9c201ab60..940a1bbc00 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 af26f7bc5c..196f36f0f4 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,21 @@ "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": [], - "modified": "2021-10-12 18:10:32.360818", + "modified": "2022-01-25 16:29:16.325501", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", @@ -391,5 +401,6 @@ "search_fields": "posting_date", "sort_field": "creation", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index f660a24a6d..b798e088b4 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -10,6 +10,7 @@ from frappe import _ from frappe.utils import add_months, flt, get_last_day, getdate, now_datetime, nowdate 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 ( @@ -233,17 +234,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) @@ -400,4 +399,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 1676c218c8..6415689782 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 e2d758b1b9..df3aadfb18 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 04c89c4549..9f9137cfbc 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 c0a5d2cda1..00337e4b4c 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 5190f9f8c6..1683dabccc 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -325,3 +325,4 @@ erpnext.patches.v14_0.set_payroll_cost_centers erpnext.patches.v13_0.agriculture_deprecation_warning erpnext.patches.v14_0.delete_agriculture_doctypes erpnext.patches.v13_0.update_exchange_rate_settings +erpnext.patches.v13_0.update_disbursement_account 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 0000000000..c56fa8fdc6 --- /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 b50036c04a116b2a3aa1784daf161a2f618765a8 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Wed, 12 Jan 2022 16:10:57 +0530 Subject: [PATCH 04/31] fix: consider returned_qty while updating billed_amt (cherry picked from commit 63aaa1e357280b24c537a502a479f7bb7a6654e4) --- .../doctype/delivery_note/delivery_note.py | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index d1e22440b9..9ee1802917 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -340,15 +340,25 @@ class DeliveryNote(SellingController): def update_billed_amount_based_on_so(so_detail, update_modified=True): # Billed against Sales Order directly - billed_against_so = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item` - where so_detail=%s and (dn_detail is null or dn_detail = '') and docstatus=1""", so_detail) + billed_against_so = frappe.db.sql("""select sum(si_item.amount) + from `tabSales Invoice Item` si_item, `tabSales Invoice` si + where + si_item.parent = si.name + and si_item.so_detail=%s + and (si_item.dn_detail is null or si_item.dn_detail = '') + and si_item.docstatus=1 + and si.update_stock = 0 + """, so_detail) 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_details = frappe.db.sql("""select dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent + dn_details = frappe.db.sql("""select dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent, dn_item.stock_qty, dn_item.returned_qty from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn - where dn.name=dn_item.parent and dn_item.so_detail=%s - and dn.docstatus=1 and dn.is_return = 0 + where + dn.name = dn_item.parent + and dn_item.so_detail=%s + and dn.docstatus=1 + and dn.is_return = 0 order by dn.posting_date asc, dn.posting_time asc, dn.name asc""", so_detail, as_dict=1) updated_dn = [] @@ -367,7 +377,11 @@ 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: - pending_to_bill = flt(dnd.amount) - billed_amt_agianst_dn + 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 if pending_to_bill <= billed_against_so: billed_amt_agianst_dn += pending_to_bill billed_against_so -= pending_to_bill From 1fd85398733f3f802b629e04c89dbf2c45237982 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Wed, 12 Jan 2022 16:13:06 +0530 Subject: [PATCH 05/31] fix: check so_detail before dn_detail (cherry picked from commit 5de6b8dc4df407fd953efe69640e22bd4ea90b6e) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index bc443581e4..4fa77186a0 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1249,14 +1249,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.dn_detail: + if d.so_detail: + updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified) + elif 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) From edbf52ab951f80e09b17d5cf348e5b2c91ed87d9 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 14 Jan 2022 16:15:26 +0530 Subject: [PATCH 06/31] feat: add patch (cherry picked from commit fc65a3d9895c8ba9de957da820ed6b59c6c1bcbd) # Conflicts: # erpnext/patches.txt --- erpnext/patches.txt | 4 ++++ .../v13_0/set_billed_amount_in_returned_dn.py | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 erpnext/patches/v13_0/set_billed_amount_in_returned_dn.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 5a8f8ef6f8..5f95e13089 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -324,6 +324,7 @@ erpnext.patches.v13_0.update_tax_category_for_rcm execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings') erpnext.patches.v14_0.set_payroll_cost_centers erpnext.patches.v13_0.agriculture_deprecation_warning +<<<<<<< HEAD erpnext.patches.v13_0.hospitality_deprecation_warning erpnext.patches.v13_0.update_exchange_rate_settings erpnext.patches.v13_0.update_asset_quantity_field @@ -342,3 +343,6 @@ erpnext.patches.v14_0.restore_einvoice_fields erpnext.patches.v13_0.update_sane_transfer_against erpnext.patches.v12_0.add_company_link_to_einvoice_settings erpnext.patches.v14_0.migrate_cost_center_allocations +======= +erpnext.patches.v13_0.set_billed_amount_in_returned_dn +>>>>>>> fc65a3d989 (feat: add patch) diff --git a/erpnext/patches/v13_0/set_billed_amount_in_returned_dn.py b/erpnext/patches/v13_0/set_billed_amount_in_returned_dn.py new file mode 100644 index 0000000000..1f86c76d14 --- /dev/null +++ b/erpnext/patches/v13_0/set_billed_amount_in_returned_dn.py @@ -0,0 +1,22 @@ +# Copyright (c) 2022, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe + +from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so + + +def execute(): + dn_item = frappe.qb.DocType('Delivery Note Item') + + so_detail_list = (frappe.qb.from_(dn_item) + .select(dn_item.so_detail) + .where( + (dn_item.so_detail.notnull()) & + (dn_item.so_detail != '') & + (dn_item.docstatus == 1) & + (dn_item.returned_qty > 0) + )).run() + + for so_detail in so_detail_list: + update_billed_amount_based_on_so(so_detail[0], False) \ No newline at end of file From ce0b84f54d495fc78a6792a9b05d0eb1dc799ed2 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 14 Jan 2022 19:21:52 +0530 Subject: [PATCH 07/31] refactor: use frappe.qb instead of sql (cherry picked from commit 0a9ec9f591f8b4d0e630a3c902b69c9996f080dd) --- .../doctype/delivery_note/delivery_note.py | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 9ee1802917..c3247fbe3e 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -339,27 +339,35 @@ class DeliveryNote(SellingController): frappe.throw(_("Could not create Credit Note automatically, please uncheck 'Issue Credit Note' and submit again")) def update_billed_amount_based_on_so(so_detail, update_modified=True): + from frappe.query_builder.functions import Sum + # Billed against Sales Order directly - billed_against_so = frappe.db.sql("""select sum(si_item.amount) - from `tabSales Invoice Item` si_item, `tabSales Invoice` si - where - si_item.parent = si.name - and si_item.so_detail=%s - and (si_item.dn_detail is null or si_item.dn_detail = '') - and si_item.docstatus=1 - and si.update_stock = 0 - """, so_detail) + 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) & + (si_item.so_detail == so_detail) & + ((si_item.dn_detail.isnull()) | (si_item.dn_detail == '')) & + (si_item.docstatus == 1) & + (si.update_stock == 0) + ).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_details = frappe.db.sql("""select dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent, dn_item.stock_qty, dn_item.returned_qty - from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn - where - dn.name = dn_item.parent - and dn_item.so_detail=%s - and dn.docstatus=1 - and dn.is_return = 0 - order by dn.posting_date asc, dn.posting_time asc, dn.name asc""", so_detail, as_dict=1) + + 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.name == dn_item.parent) & + (dn_item.so_detail == so_detail) & + (dn.docstatus == 1) & + (dn.is_return == 0) + ).orderby( + dn.posting_date, dn.posting_time, dn.name + ).run(as_dict=True) updated_dn = [] for dnd in dn_details: From c53cdce1c08e137bc9e06417ad57f0365e388b62 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Mon, 31 Jan 2022 19:07:07 +0530 Subject: [PATCH 08/31] chore: remove patch (cherry picked from commit fedeb2a70f80a39d31cb928d9876fbc94f27561c) # Conflicts: # erpnext/patches.txt --- erpnext/patches.txt | 4 ++++ .../v13_0/set_billed_amount_in_returned_dn.py | 22 ------------------- 2 files changed, 4 insertions(+), 22 deletions(-) delete mode 100644 erpnext/patches/v13_0/set_billed_amount_in_returned_dn.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 5f95e13089..ffd85a6351 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -325,6 +325,10 @@ execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings' erpnext.patches.v14_0.set_payroll_cost_centers erpnext.patches.v13_0.agriculture_deprecation_warning <<<<<<< HEAD +<<<<<<< HEAD +======= +erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit +>>>>>>> fedeb2a70f (chore: remove patch) erpnext.patches.v13_0.hospitality_deprecation_warning erpnext.patches.v13_0.update_exchange_rate_settings erpnext.patches.v13_0.update_asset_quantity_field diff --git a/erpnext/patches/v13_0/set_billed_amount_in_returned_dn.py b/erpnext/patches/v13_0/set_billed_amount_in_returned_dn.py deleted file mode 100644 index 1f86c76d14..0000000000 --- a/erpnext/patches/v13_0/set_billed_amount_in_returned_dn.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) 2022, Frappe and Contributors -# License: GNU General Public License v3. See license.txt - -import frappe - -from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so - - -def execute(): - dn_item = frappe.qb.DocType('Delivery Note Item') - - so_detail_list = (frappe.qb.from_(dn_item) - .select(dn_item.so_detail) - .where( - (dn_item.so_detail.notnull()) & - (dn_item.so_detail != '') & - (dn_item.docstatus == 1) & - (dn_item.returned_qty > 0) - )).run() - - for so_detail in so_detail_list: - update_billed_amount_based_on_so(so_detail[0], False) \ No newline at end of file From ccf63124d62139c586a3d6737460a67a942956b1 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Wed, 2 Feb 2022 20:13:33 +0530 Subject: [PATCH 09/31] fix: Coupon code item pricing dynamic updation issue --- 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 3791741663..ab3e802051 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2288,7 +2288,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe () => 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 c88c368880a134b12ad82d7674cb5e12d5a858ba Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 5 Feb 2022 22:54:18 +0530 Subject: [PATCH 10/31] 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. --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index ffdf8c420c..33e40c85f1 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -288,9 +288,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") From d1f57538855eaa8c6598a553a8b776b3ab511171 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 5 Feb 2022 23:35:28 +0530 Subject: [PATCH 11/31] test: check when PR moves stock from neg to pos --- .../purchase_receipt/test_purchase_receipt.py | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index b87d9205e0..5ab7929a2a 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 @@ -16,7 +17,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): @@ -1387,6 +1388,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 d0043bdbace8cac51527c9998435aa5c4f438b25 Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Sun, 6 Feb 2022 16:55:24 +0530 Subject: [PATCH 12/31] fix: missing key in loan --- .../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 7e997e87c3..204fce797d 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -125,7 +125,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 36f4fb05850614589c72818be21fb75761ea66cf Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 4 Feb 2022 12:01:07 +0530 Subject: [PATCH 13/31] fix: Incorrect tax template in Sales Invocie via data import (cherry picked from commit 20f321a88980d231f52702e3d32e122022316152) --- erpnext/controllers/selling_controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 75fcaee383..31b2209399 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -74,7 +74,8 @@ class SellingController(StockController): doctype=self.doctype, company=self.company, posting_date=self.get('posting_date'), fetch_payment_terms_template=fetch_payment_terms_template, - party_address=self.customer_address, shipping_address=self.shipping_address_name) + party_address=self.customer_address, shipping_address=self.shipping_address_name, + company_address=self.get('company_address')) if not self.meta.get_field("sales_team"): party_details.pop("sales_team") self.update_if_missing(party_details) From 104a55aff1092dadc1f76803d394bda7f86b3b84 Mon Sep 17 00:00:00 2001 From: Umair Sayed Date: Mon, 7 Feb 2022 10:42:55 +0530 Subject: [PATCH 14/31] feat: Tab views in Stocks and Accounts Settings (#29638) * tab views in Stocks and Accounts Settings. Clean-up in Selling and Buying Settings. * chore: undo changes to creation timestamp happened because of a bug in fw Co-authored-by: Umair Sayed --- .../accounts_settings/accounts_settings.json | 129 ++++++++++++++---- .../buying_settings/buying_settings.json | 32 ++++- .../selling_settings/selling_settings.json | 5 +- .../stock_settings/stock_settings.json | 72 ++++++++-- 4 files changed, 186 insertions(+), 52 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 55ea571ebf..9a35a247dd 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -7,35 +7,30 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "accounts_transactions_settings_section", - "over_billing_allowance", - "role_allowed_to_over_bill", - "credit_controller", - "make_payment_via_journal_entry", - "column_break_11", - "check_supplier_invoice_uniqueness", + "invoice_and_billing_tab", + "enable_features_section", "unlink_payment_on_cancellation_of_invoice", - "automatically_fetch_payment_terms", - "delete_linked_ledger_entries", - "book_asset_depreciation_entry_automatically", "unlink_advance_payment_on_cancelation_of_order", + "column_break_13", + "delete_linked_ledger_entries", + "invoicing_features_section", + "check_supplier_invoice_uniqueness", + "automatically_fetch_payment_terms", + "column_break_17", "enable_common_party_accounting", - "post_change_gl_entries", "enable_discount_accounting", - "tax_settings_section", - "determine_address_tax_category_from", - "column_break_19", - "add_taxes_from_item_tax_template", - "period_closing_settings_section", - "acc_frozen_upto", - "frozen_accounts_modifier", - "column_break_4", + "report_setting_section", + "use_custom_cash_flow", "deferred_accounting_settings_section", "book_deferred_entries_based_on", "column_break_18", "automatically_process_deferred_accounting_entry", "book_deferred_entries_via_journal_entry", "submit_journal_entries", + "tax_settings_section", + "determine_address_tax_category_from", + "column_break_19", + "add_taxes_from_item_tax_template", "print_settings", "show_inclusive_tax_in_print", "column_break_12", @@ -43,8 +38,25 @@ "currency_exchange_section", "allow_stale", "stale_days", - "report_settings_sb", - "use_custom_cash_flow" + "invoicing_settings_tab", + "accounts_transactions_settings_section", + "over_billing_allowance", + "column_break_11", + "role_allowed_to_over_bill", + "credit_controller", + "make_payment_via_journal_entry", + "pos_tab", + "pos_setting_section", + "post_change_gl_entries", + "assets_tab", + "asset_settings_section", + "book_asset_depreciation_entry_automatically", + "closing_settings_tab", + "period_closing_settings_section", + "acc_frozen_upto", + "column_break_25", + "frozen_accounts_modifier", + "report_settings_sb" ], "fields": [ { @@ -70,10 +82,6 @@ "label": "Determine Address Tax Category From", "options": "Billing Address\nShipping Address" }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - }, { "fieldname": "credit_controller", "fieldtype": "Link", @@ -83,6 +91,7 @@ }, { "default": "0", + "description": "Enabling ensure each Sales Invoice has a unique value in Supplier Invoice No. field", "fieldname": "check_supplier_invoice_uniqueness", "fieldtype": "Check", "label": "Check Supplier Invoice Number Uniqueness" @@ -168,7 +177,7 @@ "description": "Only select this if you have set up the Cash Flow Mapper documents", "fieldname": "use_custom_cash_flow", "fieldtype": "Check", - "label": "Use Custom Cash Flow Format" + "label": "Enable Custom Cash Flow Format" }, { "default": "0", @@ -241,7 +250,7 @@ { "fieldname": "accounts_transactions_settings_section", "fieldtype": "Section Break", - "label": "Transactions Settings" + "label": "Credit Limit Settings" }, { "fieldname": "column_break_11", @@ -272,9 +281,72 @@ }, { "default": "0", + "description": "Learn about Common Party", "fieldname": "enable_common_party_accounting", "fieldtype": "Check", "label": "Enable Common Party Accounting" + }, + { + "fieldname": "enable_features_section", + "fieldtype": "Section Break", + "label": "Invoice Cancellation" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_25", + "fieldtype": "Column Break" + }, + { + "fieldname": "asset_settings_section", + "fieldtype": "Section Break", + "label": "Asset Settings" + }, + { + "fieldname": "invoicing_settings_tab", + "fieldtype": "Tab Break", + "label": "Credit Limits" + }, + { + "fieldname": "assets_tab", + "fieldtype": "Tab Break", + "label": "Assets" + }, + { + "fieldname": "closing_settings_tab", + "fieldtype": "Tab Break", + "label": "Accounts Closing" + }, + { + "fieldname": "pos_setting_section", + "fieldtype": "Section Break", + "label": "POS Setting" + }, + { + "fieldname": "invoice_and_billing_tab", + "fieldtype": "Tab Break", + "label": "Invoice and Billing" + }, + { + "fieldname": "invoicing_features_section", + "fieldtype": "Section Break", + "label": "Invoicing Features" + }, + { + "fieldname": "column_break_17", + "fieldtype": "Column Break" + }, + { + "fieldname": "pos_tab", + "fieldtype": "Tab Break", + "label": "POS" + }, + { + "fieldname": "report_setting_section", + "fieldtype": "Section Break", + "label": "Report Setting" } ], "icon": "icon-cog", @@ -282,7 +354,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-10-11 17:42:36.427699", + "modified": "2022-02-04 12:32:36.805652", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", @@ -309,5 +381,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "ASC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index b828a43d3c..50321baa2e 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -6,14 +6,17 @@ "document_type": "Other", "engine": "InnoDB", "field_order": [ + "supplier_and_price_defaults_section", "supp_master_name", "supplier_group", + "column_break_4", "buying_price_list", "maintain_same_rate_action", "role_to_override_stop_action", - "column_break_3", + "transaction_settings_section", "po_required", "pr_required", + "column_break_12", "maintain_same_rate", "allow_multiple_items", "bill_for_rejected_quantity_in_purchase_invoice", @@ -42,10 +45,6 @@ "label": "Default Buying Price List", "options": "Price List" }, - { - "fieldname": "column_break_3", - "fieldtype": "Column Break" - }, { "fieldname": "po_required", "fieldtype": "Select", @@ -73,7 +72,7 @@ { "fieldname": "subcontract", "fieldtype": "Section Break", - "label": "Subcontract" + "label": "Subcontracting Settings" }, { "default": "Material Transferred for Subcontract", @@ -116,6 +115,24 @@ "fieldname": "bill_for_rejected_quantity_in_purchase_invoice", "fieldtype": "Check", "label": "Bill for Rejected Quantity in Purchase Invoice" + }, + { + "fieldname": "supplier_and_price_defaults_section", + "fieldtype": "Section Break", + "label": "Supplier and Price Defaults" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "transaction_settings_section", + "fieldtype": "Section Break", + "label": "Transaction Settings" + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" } ], "icon": "fa fa-cog", @@ -123,7 +140,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-09-08 19:26:23.548837", + "modified": "2022-01-27 17:57:58.367048", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", @@ -141,5 +158,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 27bc541d62..7c4a3f63dc 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -80,7 +80,7 @@ "description": "How often should Project and Company be updated based on Sales Transactions?", "fieldname": "sales_update_frequency", "fieldtype": "Select", - "label": "Sales Update Frequency", + "label": "Sales Update Frequency in Company and Project", "options": "Each Transaction\nDaily\nMonthly", "reqd": 1 }, @@ -171,7 +171,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-09-13 12:32:17.004404", + "modified": "2022-02-04 15:41:59.939261", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", @@ -189,5 +189,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 33d9a6ce41..438ec16096 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -5,35 +5,41 @@ "doctype": "DocType", "engine": "InnoDB", "field_order": [ + "defaults_tab", "item_defaults_section", "item_naming_by", "item_group", "stock_uom", - "default_warehouse", "column_break_4", - "valuation_method", + "default_warehouse", "sample_retention_warehouse", - "use_naming_series", - "naming_series_prefix", + "valuation_method", + "price_list_defaults_section", + "auto_insert_price_list_rate_if_missing", + "column_break_12", + "update_existing_price_list_rate", + "stock_validations_tab", "section_break_9", "over_delivery_receipt_allowance", - "role_allowed_to_over_deliver_receive", "mr_qty_allowance", - "column_break_12", - "auto_insert_price_list_rate_if_missing", - "update_existing_price_list_rate", + "column_break_121", + "role_allowed_to_over_deliver_receive", "allow_negative_stock", "show_barcode_field", "clean_description_html", "quality_inspection_settings_section", "action_if_quality_inspection_is_not_submitted", - "column_break_21", + "column_break_23", "action_if_quality_inspection_is_rejected", + "serial_and_batch_item_settings_tab", "section_break_7", "automatically_set_serial_nos_based_on_fifo", "set_qty_in_transactions_based_on_serial_no_input", "column_break_10", "disable_serial_no_and_batch_selector", + "use_naming_series", + "naming_series_prefix", + "stock_planning_tab", "auto_material_request", "auto_indent", "column_break_27", @@ -42,6 +48,7 @@ "allow_from_dn", "column_break_31", "allow_from_pr", + "stock_closing_tab", "control_historical_stock_transactions_section", "stock_frozen_upto", "stock_frozen_upto_days", @@ -122,7 +129,7 @@ { "fieldname": "section_break_7", "fieldtype": "Section Break", - "label": "Serialised and Batch Setting" + "label": "Serial & Batch Item Settings" }, { "default": "0", @@ -275,10 +282,6 @@ "fieldtype": "Section Break", "label": "Quality Inspection Settings" }, - { - "fieldname": "column_break_21", - "fieldtype": "Column Break" - }, { "default": "Stop", "fieldname": "action_if_quality_inspection_is_rejected", @@ -298,6 +301,44 @@ "fieldname": "update_existing_price_list_rate", "fieldtype": "Check", "label": "Update Existing Price List Rate" + }, + { + "fieldname": "defaults_tab", + "fieldtype": "Tab Break", + "label": "Defaults" + }, + { + "fieldname": "stock_validations_tab", + "fieldtype": "Tab Break", + "label": "Stock Validations" + }, + { + "fieldname": "stock_planning_tab", + "fieldtype": "Tab Break", + "label": "Stock Planning" + }, + { + "fieldname": "stock_closing_tab", + "fieldtype": "Tab Break", + "label": "Stock Closing" + }, + { + "fieldname": "serial_and_batch_item_settings_tab", + "fieldtype": "Tab Break", + "label": "Serial & Batch Item" + }, + { + "fieldname": "column_break_23", + "fieldtype": "Column Break" + }, + { + "fieldname": "price_list_defaults_section", + "fieldtype": "Section Break", + "label": "Price List Defaults" + }, + { + "fieldname": "column_break_121", + "fieldtype": "Column Break" } ], "icon": "icon-cog", @@ -305,7 +346,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-11-06 19:40:02.183592", + "modified": "2022-02-04 15:33:43.692736", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", @@ -324,5 +365,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "ASC", + "states": [], "track_changes": 1 } \ No newline at end of file From 5e6227e3d87cc54e842cb6f71f18083055c9a51a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 7 Feb 2022 13:19:08 +0530 Subject: [PATCH 15/31] fix(ux): make stock entry type the title field (#29674) --- .../doctype/stock_entry/stock_entry.json | 19 ++++++------------- .../stock/doctype/stock_entry/stock_entry.py | 9 --------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 2f37778896..c38dfaa1c8 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -8,7 +8,6 @@ "engine": "InnoDB", "field_order": [ "items_section", - "title", "naming_series", "stock_entry_type", "outgoing_stock_entry", @@ -83,14 +82,6 @@ "fieldtype": "Section Break", "oldfieldtype": "Section Break" }, - { - "fieldname": "title", - "fieldtype": "Data", - "hidden": 1, - "label": "Title", - "no_copy": 1, - "print_hide": 1 - }, { "fieldname": "naming_series", "fieldtype": "Select", @@ -353,9 +344,9 @@ }, { "fieldname": "scan_barcode", - "options": "Barcode", "fieldtype": "Data", - "label": "Scan Barcode" + "label": "Scan Barcode", + "options": "Barcode" }, { "allow_bulk_edit": 1, @@ -628,10 +619,11 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-08-20 19:19:31.514846", + "modified": "2022-02-07 12:55:14.614077", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -698,6 +690,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", - "title_field": "title", + "states": [], + "title_field": "stock_entry_type", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index c51c9bc5f4..a2ef7b42be 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -76,7 +76,6 @@ class StockEntry(StockController): self.validate_posting_time() self.validate_purpose() - self.set_title() self.validate_item() self.validate_customer_provided_item() self.validate_qty() @@ -1835,14 +1834,6 @@ class StockEntry(StockController): return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos))) - def set_title(self): - if frappe.flags.in_import and self.title: - # Allow updating title during data import/update - return - - self.title = self.purpose - - @frappe.whitelist() def move_sample_to_retention_warehouse(company, items): if isinstance(items, str): From 8ece2845f2dd6cc382c21c7794ee9d533cf4ea9b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 7 Feb 2022 13:50:31 +0530 Subject: [PATCH 16/31] fix: Add disbursement accounts to tests --- .../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 d367e92ac4..640709c095 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 94ec84ea5d..10be750b44 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 46aaaad9fd..e8c77506fc 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 4f097fa2c3..5f836db2f0 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -214,6 +214,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 597fd5a250..30b604b2c0 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -370,6 +370,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 ef69d1fd385bfe740ce1765f1a4998d27456ce79 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 7 Feb 2022 17:00:30 +0530 Subject: [PATCH 17/31] test: Add test case for repayment against partially disbursed loans --- .../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 1676c218c8..cb7337e8d6 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 204fce797d..acf3a655de 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -153,7 +153,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 72a812f18bfd27842156d7b1afb1f301fbead7ed Mon Sep 17 00:00:00 2001 From: Ritwik Puri Date: Mon, 7 Feb 2022 19:43:29 +0530 Subject: [PATCH 18/31] fix: use item_code instead of parent field in bom_stock_calculated report (#29684) --- .../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 090a3e74fc..2693352324 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 eab10a13e70994f782b2bddc0cc7c99be8837cf9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 7 Feb 2022 22:12:27 +0530 Subject: [PATCH 19/31] fix: Conflicts --- erpnext/patches.txt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index ffd85a6351..5a8f8ef6f8 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -324,11 +324,6 @@ erpnext.patches.v13_0.update_tax_category_for_rcm execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings') erpnext.patches.v14_0.set_payroll_cost_centers erpnext.patches.v13_0.agriculture_deprecation_warning -<<<<<<< HEAD -<<<<<<< HEAD -======= -erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit ->>>>>>> fedeb2a70f (chore: remove patch) erpnext.patches.v13_0.hospitality_deprecation_warning erpnext.patches.v13_0.update_exchange_rate_settings erpnext.patches.v13_0.update_asset_quantity_field @@ -347,6 +342,3 @@ erpnext.patches.v14_0.restore_einvoice_fields erpnext.patches.v13_0.update_sane_transfer_against erpnext.patches.v12_0.add_company_link_to_einvoice_settings erpnext.patches.v14_0.migrate_cost_center_allocations -======= -erpnext.patches.v13_0.set_billed_amount_in_returned_dn ->>>>>>> fc65a3d989 (feat: add patch) From c56d07dee3f0c35e211424d82fec628aea22d2e9 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 5 Feb 2022 17:28:21 +0530 Subject: [PATCH 20/31] fix: consider packed items too when reposting --- 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 8d17683953..92f8ff0d20 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 e6ab8df8f2c48932a7368c5ac69ebcac14cf015c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 6 Feb 2022 13:02:34 +0530 Subject: [PATCH 21/31] refactor: simplify get_items_and_warehouses Also remove dead code related to stock reconciliation_json. --- 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 92f8ff0d20..9be5c0d03f 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 1022db04745b3c6f16710b43720fef4f0fd0d295 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 6 Feb 2022 13:29:40 +0530 Subject: [PATCH 22/31] fix: merge stock ledger item warehouse with doc's --- .../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 01c5e3e4e2..977d470995 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 853e658dccf1a71fad03e23bf3b7d8f9d0784c37 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 6 Feb 2022 13:42:44 +0530 Subject: [PATCH 23/31] test: move bundle info to class variables --- .../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 5cbaa1ea66..fcb4727f6b 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 699519f7b69b0b5eaa53a0db2fb35857b080261c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 6 Feb 2022 14:19:58 +0530 Subject: [PATCH 24/31] test: product bundle reposting --- .../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 fcb4727f6b..2521ac9fe7 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 43f8ee1dd1525b6cf3e88154bec314aca7b23ca5 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 6 Feb 2022 14:28:55 +0530 Subject: [PATCH 25/31] chore: drop dead field from stock reconciliation --- .../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 3402972bb8..a882a61e5a 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 f82d7eb73fe6dd70a98fa6656e90ed9c6565be16 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 6 Feb 2022 15:18:00 +0530 Subject: [PATCH 26/31] test: commit item/warehouse creation to db --- .../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 428370cc75..86af0a0cf3 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 a3e69cf75d27198132d05c7c10475a0297b1e190 Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh <49878143+mohammedyusufshaikh@users.noreply.github.com> Date: Tue, 8 Feb 2022 01:00:37 +0530 Subject: [PATCH 27/31] feat: Bulk Transaction Processing (#28580) * feat: Bulk Transaction Processing * fix: add flags to ignore validations and exception handling correction * fix: remove duplicate code, added logger functionality and improved notifications * fix: linting and sider issues * test: added tests * fix: linter issues * fix: failing test case * fix: sider issues and test cases * refactor: mapping function calls to create order/invoice * fix: added more test cases to increase coverage * fix: test cases * fix: sider issue * fix: rename doctype, improve formatting and minor refactor * fix: update doctype name in hooks and sider issues * fix: entry log test case * fix: typos, translations and company name in tests * fix: linter issues and translations * fix: linter issue * fix: split into separate function for marking failed transaction * fix: typos, retry failed transaction logic and make log read only * fix: hide retry button when no failed transactions and remove test cases not rrelevant * fix: sider issues and indentation to tabs Co-authored-by: Ankush Menat --- .../test_bulk_transaction_processing.js | 44 ++++ .../purchase_invoice/purchase_invoice_list.js | 10 + .../sales_invoice/sales_invoice_list.js | 12 +- erpnext/bulk_transaction/__init__.py | 0 erpnext/bulk_transaction/doctype/__init__.py | 0 .../doctype/bulk_transaction_log/__init__.py | 0 .../bulk_transaction_log.js | 34 +++ .../bulk_transaction_log.json | 51 +++++ .../bulk_transaction_log.py | 66 ++++++ .../test_bulk_transaction_log.py | 81 +++++++ .../bulk_transaction_log_detail/__init__.py | 0 .../bulk_transaction_log_detail.json | 86 ++++++++ .../bulk_transaction_log_detail.py | 9 + .../purchase_order/purchase_order_list.js | 16 +- .../supplier_quotation/supplier_quotation.py | 20 ++ .../supplier_quotation_list.js | 10 + erpnext/hooks.py | 3 +- erpnext/modules.txt | 1 + erpnext/public/build.json | 3 +- .../public/js/bulk_transaction_processing.js | 30 +++ erpnext/public/js/erpnext.bundle.js | 1 + .../doctype/quotation/quotation_list.js | 8 + .../doctype/sales_order/sales_order_list.js | 14 +- .../doctype/delivery_note/delivery_note.py | 11 + .../delivery_note/delivery_note_list.js | 14 +- .../purchase_receipt/purchase_receipt_list.js | 8 + .../ui_test_bulk_transaction_processing.py | 21 ++ erpnext/utilities/bulk_transaction.py | 201 ++++++++++++++++++ 28 files changed, 747 insertions(+), 7 deletions(-) create mode 100644 cypress/integration/test_bulk_transaction_processing.js create mode 100644 erpnext/bulk_transaction/__init__.py create mode 100644 erpnext/bulk_transaction/doctype/__init__.py create mode 100644 erpnext/bulk_transaction/doctype/bulk_transaction_log/__init__.py create mode 100644 erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js create mode 100644 erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.json create mode 100644 erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py create mode 100644 erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py create mode 100644 erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/__init__.py create mode 100644 erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.json create mode 100644 erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.py create mode 100644 erpnext/public/js/bulk_transaction_processing.js create mode 100644 erpnext/tests/ui_test_bulk_transaction_processing.py create mode 100644 erpnext/utilities/bulk_transaction.py diff --git a/cypress/integration/test_bulk_transaction_processing.js b/cypress/integration/test_bulk_transaction_processing.js new file mode 100644 index 0000000000..428ec5100b --- /dev/null +++ b/cypress/integration/test_bulk_transaction_processing.js @@ -0,0 +1,44 @@ +describe("Bulk Transaction Processing", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + }); + + it("Creates To Sales Order", () => { + cy.visit("/app/sales-order"); + cy.url().should("include", "/sales-order"); + cy.window() + .its("frappe.csrf_token") + .then((csrf_token) => { + return cy + .request({ + url: "/api/method/erpnext.tests.ui_test_bulk_transaction_processing.create_records", + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "X-Frappe-CSRF-Token": csrf_token, + }, + timeout: 60000, + }) + .then((res) => { + expect(res.status).eq(200); + }); + }); + cy.wait(5000); + cy.get( + ".list-row-head > .list-header-subject > .list-row-col > .list-check-all" + ).check({ force: true }); + cy.wait(3000); + cy.get(".actions-btn-group > .btn-primary").click({ force: true }); + cy.wait(3000); + cy.get(".dropdown-menu-right > .user-action > .dropdown-item") + .contains("Sales Invoice") + .click({ force: true }); + cy.wait(3000); + cy.get(".modal-content > .modal-footer > .standard-actions") + .contains("Yes") + .click({ force: true }); + cy.contains("Creation of Sales Invoice successful"); + }); +}); diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js index f6ff83add8..82d00308db 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js @@ -56,4 +56,14 @@ frappe.listview_settings["Purchase Invoice"] = { ]; } }, + + onload: function(listview) { + listview.page.add_action_item(__("Purchase Receipt"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Purchase Receipt"); + }); + + listview.page.add_action_item(__("Payment"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment"); + }); + } }; diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js index 06e6f51183..1130284ecc 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js @@ -21,5 +21,15 @@ frappe.listview_settings['Sales Invoice'] = { }; return [__(doc.status), status_colors[doc.status], "status,=,"+doc.status]; }, - right_column: "grand_total" + right_column: "grand_total", + + onload: function(listview) { + listview.page.add_action_item(__("Delivery Note"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Delivery Note"); + }); + + listview.page.add_action_item(__("Payment"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment"); + }); + } }; diff --git a/erpnext/bulk_transaction/__init__.py b/erpnext/bulk_transaction/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/bulk_transaction/doctype/__init__.py b/erpnext/bulk_transaction/doctype/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/__init__.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js new file mode 100644 index 0000000000..a739cc3730 --- /dev/null +++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js @@ -0,0 +1,34 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Bulk Transaction Log', { + + before_load: function(frm) { + query(frm); + }, + + refresh: function(frm) { + frm.disable_save(); + frm.add_custom_button(__('Retry Failed Transactions'), ()=>{ + frappe.confirm(__("Retry Failing Transactions ?"), ()=>{ + query(frm); + } + ); + }); + } +}); + +function query(frm) { + frappe.call({ + method: "erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction", + args: { + log_date: frm.doc.log_date + } + }).then((r) => { + if (r.message) { + frm.remove_custom_button("Retry Failed Transactions"); + } else { + frappe.show_alert(__("Retrying Failed Transactions"), 5); + } + }); +} \ No newline at end of file diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.json b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.json new file mode 100644 index 0000000000..da42cf1bd4 --- /dev/null +++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.json @@ -0,0 +1,51 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2021-11-30 13:41:16.343827", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "log_date", + "logger_data" + ], + "fields": [ + { + "fieldname": "log_date", + "fieldtype": "Date", + "label": "Log Date", + "read_only": 1 + }, + { + "fieldname": "logger_data", + "fieldtype": "Table", + "label": "Logger Data", + "options": "Bulk Transaction Log Detail" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2022-02-03 17:23:02.935325", + "modified_by": "Administrator", + "module": "Bulk Transaction", + "name": "Bulk Transaction Log", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py new file mode 100644 index 0000000000..de7cde5a6d --- /dev/null +++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py @@ -0,0 +1,66 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from datetime import date + +import frappe +from frappe.model.document import Document + +from erpnext.utilities.bulk_transaction import task, update_logger + + +class BulkTransactionLog(Document): + pass + + +@frappe.whitelist() +def retry_failing_transaction(log_date=None): + btp = frappe.qb.DocType("Bulk Transaction Log Detail") + data = ( + frappe.qb.from_(btp) + .select(btp.transaction_name, btp.from_doctype, btp.to_doctype) + .distinct() + .where(btp.retried != 1) + .where(btp.transaction_status == "Failed") + .where(btp.date == log_date) + ).run(as_dict=True) + + if data: + if not log_date: + log_date = str(date.today()) + if len(data) > 10: + frappe.enqueue(job, queue="long", job_name="bulk_retry", data=data, log_date=log_date) + else: + job(data, log_date) + else: + return "No Failed Records" + +def job(data, log_date): + for d in data: + failed = [] + try: + frappe.db.savepoint("before_creation_of_record") + task(d.transaction_name, d.from_doctype, d.to_doctype) + except Exception as e: + frappe.db.rollback(save_point="before_creation_of_record") + failed.append(e) + update_logger( + d.transaction_name, + e, + d.from_doctype, + d.to_doctype, + status="Failed", + log_date=log_date, + restarted=1 + ) + + if not failed: + update_logger( + d.transaction_name, + None, + d.from_doctype, + d.to_doctype, + status="Success", + log_date=log_date, + restarted=1, + ) diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py new file mode 100644 index 0000000000..a78e697b6f --- /dev/null +++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py @@ -0,0 +1,81 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import unittest +from datetime import date + +import frappe + +from erpnext.utilities.bulk_transaction import transaction_processing + + +class TestBulkTransactionLog(unittest.TestCase): + + def setUp(self): + create_company() + create_customer() + create_item() + + def test_for_single_record(self): + so_name = create_so() + transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice") + data = frappe.db.get_list("Sales Invoice", filters = {"posting_date": date.today(), "customer": "Bulk Customer"}, fields=["*"]) + if not data: + self.fail("No Sales Invoice Created !") + + def test_entry_in_log(self): + so_name = create_so() + transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice") + doc = frappe.get_doc("Bulk Transaction Log", str(date.today())) + for d in doc.get("logger_data"): + if d.transaction_name == so_name: + self.assertEqual(d.transaction_name, so_name) + self.assertEqual(d.transaction_status, "Success") + self.assertEqual(d.from_doctype, "Sales Order") + self.assertEqual(d.to_doctype, "Sales Invoice") + self.assertEqual(d.retried, 0) + + + +def create_company(): + if not frappe.db.exists('Company', '_Test Company'): + frappe.get_doc({ + 'doctype': 'Company', + 'company_name': '_Test Company', + 'country': 'India', + 'default_currency': 'INR' + }).insert() + +def create_customer(): + if not frappe.db.exists('Customer', 'Bulk Customer'): + frappe.get_doc({ + 'doctype': 'Customer', + 'customer_name': 'Bulk Customer' + }).insert() + +def create_item(): + if not frappe.db.exists("Item", "MK"): + frappe.get_doc({ + "doctype": "Item", + "item_code": "MK", + "item_name": "Milk", + "description": "Milk", + "item_group": "Products" + }).insert() + +def create_so(intent=None): + so = frappe.new_doc("Sales Order") + so.customer = "Bulk Customer" + so.company = "_Test Company" + so.transaction_date = date.today() + + so.set_warehouse = "Finished Goods - _TC" + so.append("items", { + "item_code": "MK", + "delivery_date": date.today(), + "qty": 10, + "rate": 80, + }) + so.insert() + so.submit() + return so.name \ No newline at end of file diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/__init__.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.json b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.json new file mode 100644 index 0000000000..8262caa020 --- /dev/null +++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.json @@ -0,0 +1,86 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2021-11-30 13:38:30.926047", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "transaction_name", + "date", + "time", + "transaction_status", + "error_description", + "from_doctype", + "to_doctype", + "retried" + ], + "fields": [ + { + "fieldname": "transaction_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Name", + "options": "from_doctype" + }, + { + "fieldname": "transaction_status", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Status", + "read_only": 1 + }, + { + "fieldname": "error_description", + "fieldtype": "Long Text", + "label": "Error Description", + "read_only": 1 + }, + { + "fieldname": "from_doctype", + "fieldtype": "Link", + "label": "From Doctype", + "options": "DocType", + "read_only": 1 + }, + { + "fieldname": "to_doctype", + "fieldtype": "Link", + "label": "To Doctype", + "options": "DocType", + "read_only": 1 + }, + { + "fieldname": "date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Date ", + "read_only": 1 + }, + { + "fieldname": "time", + "fieldtype": "Time", + "label": "Time", + "read_only": 1 + }, + { + "fieldname": "retried", + "fieldtype": "Int", + "label": "Retried", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2022-02-03 19:57:31.650359", + "modified_by": "Administrator", + "module": "Bulk Transaction", + "name": "Bulk Transaction Log Detail", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.py new file mode 100644 index 0000000000..67795b9d49 --- /dev/null +++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class BulkTransactionLogDetail(Document): + pass diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_list.js b/erpnext/buying/doctype/purchase_order/purchase_order_list.js index 8413eb65c3..d7907e4274 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order_list.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order_list.js @@ -29,8 +29,22 @@ frappe.listview_settings['Purchase Order'] = { listview.call_for_selected_items(method, { "status": "Closed" }); }); - listview.page.add_menu_item(__("Re-open"), function () { + listview.page.add_menu_item(__("Reopen"), function () { listview.call_for_selected_items(method, { "status": "Submitted" }); }); + + + listview.page.add_action_item(__("Purchase Invoice"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Invoice"); + }); + + listview.page.add_action_item(__("Purchase Receipt"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Receipt"); + }); + + listview.page.add_action_item(__("Advance Payment"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Advance Payment"); + }); + } }; diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py index d65ab94a6d..171de7882d 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py @@ -142,6 +142,26 @@ def make_purchase_order(source_name, target_doc=None): return doclist +@frappe.whitelist() +def make_purchase_invoice(source_name, target_doc=None): + doc = get_mapped_doc("Supplier Quotation", source_name, { + "Supplier Quotation": { + "doctype": "Purchase Invoice", + "validation": { + "docstatus": ["=", 1], + } + }, + "Supplier Quotation Item": { + "doctype": "Purchase Invoice Item" + }, + "Purchase Taxes and Charges": { + "doctype": "Purchase Taxes and Charges" + } + }, target_doc) + + return doc + + @frappe.whitelist() def make_quotation(source_name, target_doc=None): doclist = get_mapped_doc("Supplier Quotation", source_name, { diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js index 5ab6c980d0..73685caa0b 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js @@ -8,5 +8,15 @@ frappe.listview_settings['Supplier Quotation'] = { } else if(doc.status==="Expired") { return [__("Expired"), "gray", "status,=,Expired"]; } + }, + + onload: function(listview) { + listview.page.add_action_item(__("Purchase Order"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Order"); + }); + + listview.page.add_action_item(__("Purchase Invoice"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Invoice"); + }); } }; diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 0e290384b4..d99f23ed64 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -341,7 +341,8 @@ scheduler_events = { "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts" ], "hourly_long": [ - "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries" + "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries", + "erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction" ], "daily": [ "erpnext.stock.reorder_item.reorder_item", diff --git a/erpnext/modules.txt b/erpnext/modules.txt index c5705c1763..8c79ee5c9a 100644 --- a/erpnext/modules.txt +++ b/erpnext/modules.txt @@ -21,4 +21,5 @@ Communication Loan Management Payroll Telephony +Bulk Transaction E-commerce diff --git a/erpnext/public/build.json b/erpnext/public/build.json index 569910dd9d..91a752c291 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -39,7 +39,8 @@ "public/js/utils/dimension_tree_filter.js", "public/js/telephony.js", "public/js/templates/call_link.html", - "public/js/templates/node_card.html" + "public/js/templates/node_card.html", + "public/js/bulk_transaction_processing.js" ], "js/item-dashboard.min.js": [ "stock/dashboard/item_dashboard.html", diff --git a/erpnext/public/js/bulk_transaction_processing.js b/erpnext/public/js/bulk_transaction_processing.js new file mode 100644 index 0000000000..101f50c64a --- /dev/null +++ b/erpnext/public/js/bulk_transaction_processing.js @@ -0,0 +1,30 @@ +frappe.provide("erpnext.bulk_transaction_processing"); + +$.extend(erpnext.bulk_transaction_processing, { + create: function(listview, from_doctype, to_doctype) { + let checked_items = listview.get_checked_items(); + const doc_name = []; + checked_items.forEach((Item)=> { + if (Item.docstatus == 0) { + doc_name.push(Item.name); + } + }); + + let count_of_rows = checked_items.length; + frappe.confirm(__("Create {0} {1} ?", [count_of_rows, to_doctype]), ()=>{ + if (doc_name.length == 0) { + frappe.call({ + method: "erpnext.utilities.bulk_transaction.transaction_processing", + args: {data: checked_items, from_doctype: from_doctype, to_doctype: to_doctype} + }).then(()=> { + + }); + if (count_of_rows > 10) { + frappe.show_alert("Starting a background job to create {0} {1}", [count_of_rows, to_doctype]); + } + } else { + frappe.msgprint(__("Selected document must be in submitted state")); + } + }); + } +}); \ No newline at end of file diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js index 5259bdcc76..b3a68b3862 100644 --- a/erpnext/public/js/erpnext.bundle.js +++ b/erpnext/public/js/erpnext.bundle.js @@ -22,5 +22,6 @@ import "./call_popup/call_popup"; import "./utils/dimension_tree_filter"; import "./telephony"; import "./templates/call_link.html"; +import "./bulk_transaction_processing"; // import { sum } from 'frappe/public/utils/util.js' diff --git a/erpnext/selling/doctype/quotation/quotation_list.js b/erpnext/selling/doctype/quotation/quotation_list.js index b631685bd1..4c8f9c4f84 100644 --- a/erpnext/selling/doctype/quotation/quotation_list.js +++ b/erpnext/selling/doctype/quotation/quotation_list.js @@ -12,6 +12,14 @@ frappe.listview_settings['Quotation'] = { }; }; } + + listview.page.add_action_item(__("Sales Order"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Order"); + }); + + listview.page.add_action_item(__("Sales Invoice"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Invoice"); + }); }, get_indicator: function(doc) { diff --git a/erpnext/selling/doctype/sales_order/sales_order_list.js b/erpnext/selling/doctype/sales_order/sales_order_list.js index 26d96d59f2..4691190d2a 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_list.js +++ b/erpnext/selling/doctype/sales_order/sales_order_list.js @@ -16,7 +16,7 @@ frappe.listview_settings['Sales Order'] = { return [__("Overdue"), "red", "per_delivered,<,100|delivery_date,<,Today|status,!=,Closed"]; } else if (flt(doc.grand_total) === 0) { - // not delivered (zero-amount order) + // not delivered (zeroount order) return [__("To Deliver"), "orange", "per_delivered,<,100|grand_total,=,0|status,!=,Closed"]; } else if (flt(doc.per_billed, 6) < 100) { @@ -48,5 +48,17 @@ frappe.listview_settings['Sales Order'] = { listview.call_for_selected_items(method, {"status": "Submitted"}); }); + listview.page.add_action_item(__("Sales Invoice"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Sales Invoice"); + }); + + listview.page.add_action_item(__("Delivery Note"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Delivery Note"); + }); + + listview.page.add_action_item(__("Advance Payment"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Advance Payment"); + }); + } }; diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index c3247fbe3e..2a4d63954a 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -608,7 +608,18 @@ def make_packing_slip(source_name, target_doc=None): "validation": { "docstatus": ["=", 0] } + }, + + "Delivery Note Item": { + "doctype": "Packing Slip Item", + "field_map": { + "item_code": "item_code", + "item_name": "item_name", + "description": "description", + "qty": "qty", + } } + }, target_doc) return doclist diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_list.js b/erpnext/stock/doctype/delivery_note/delivery_note_list.js index 0402898047..9e6f3bc932 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_list.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note_list.js @@ -14,7 +14,7 @@ frappe.listview_settings['Delivery Note'] = { return [__("Completed"), "green", "per_billed,=,100"]; } }, - onload: function (doclist) { + onload: function (listview) { const action = () => { const selected_docs = doclist.get_checked_items(); const docnames = doclist.get_checked_items(true); @@ -54,6 +54,16 @@ frappe.listview_settings['Delivery Note'] = { }; }; - doclist.page.add_actions_menu_item(__('Create Delivery Trip'), action, false); + // doclist.page.add_actions_menu_item(__('Create Delivery Trip'), action, false); + + listview.page.add_action_item(__('Create Delivery Trip'), action); + + listview.page.add_action_item(__("Sales Invoice"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Delivery Note", "Sales Invoice"); + }); + + listview.page.add_action_item(__("Packaging Slip From Delivery Note"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Delivery Note", "Packing Slip"); + }); } }; diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js index 77711de93f..4029f0c127 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js @@ -13,5 +13,13 @@ frappe.listview_settings['Purchase Receipt'] = { } else if (flt(doc.grand_total) === 0 || flt(doc.per_billed, 2) === 100) { return [__("Completed"), "green", "per_billed,=,100"]; } + }, + + onload: function(listview) { + + listview.page.add_action_item(__("Purchase Invoice"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Purchase Receipt", "Purchase Invoice"); + }); } + }; diff --git a/erpnext/tests/ui_test_bulk_transaction_processing.py b/erpnext/tests/ui_test_bulk_transaction_processing.py new file mode 100644 index 0000000000..d78689eb5b --- /dev/null +++ b/erpnext/tests/ui_test_bulk_transaction_processing.py @@ -0,0 +1,21 @@ +import frappe + +from erpnext.bulk_transaction.doctype.bulk_transaction_logger.test_bulk_transaction_logger import ( + create_company, + create_customer, + create_item, + create_so, +) + + +@frappe.whitelist() +def create_records(): + create_company() + create_customer() + create_item() + + gd = frappe.get_doc("Global Defaults") + gd.set("default_company", "Test Bulk") + gd.save() + frappe.clear_cache() + create_so() \ No newline at end of file diff --git a/erpnext/utilities/bulk_transaction.py b/erpnext/utilities/bulk_transaction.py new file mode 100644 index 0000000000..64e2ff4218 --- /dev/null +++ b/erpnext/utilities/bulk_transaction.py @@ -0,0 +1,201 @@ +import json +from datetime import date, datetime + +import frappe +from frappe import _ + + +@frappe.whitelist() +def transaction_processing(data, from_doctype, to_doctype): + if isinstance(data, str): + deserialized_data = json.loads(data) + + else: + deserialized_data = data + + length_of_data = len(deserialized_data) + + if length_of_data > 10: + frappe.msgprint( + _("Started a background job to create {1} {0}").format(to_doctype, length_of_data) + ) + frappe.enqueue( + job, + deserialized_data=deserialized_data, + from_doctype=from_doctype, + to_doctype=to_doctype, + ) + else: + job(deserialized_data, from_doctype, to_doctype) + + +def job(deserialized_data, from_doctype, to_doctype): + failed_history = [] + i = 0 + for d in deserialized_data: + failed = [] + + try: + i += 1 + doc_name = d.get("name") + frappe.db.savepoint("before_creation_state") + task(doc_name, from_doctype, to_doctype) + + except Exception as e: + frappe.db.rollback(save_point="before_creation_state") + failed_history.append(e) + failed.append(e) + update_logger(doc_name, e, from_doctype, to_doctype, status="Failed", log_date=str(date.today())) + if not failed: + update_logger(doc_name, None, from_doctype, to_doctype, status="Success", log_date=str(date.today())) + + show_job_status(failed_history, deserialized_data, to_doctype) + + +def task(doc_name, from_doctype, to_doctype): + from erpnext.accounts.doctype.payment_entry import payment_entry + from erpnext.accounts.doctype.purchase_invoice import purchase_invoice + from erpnext.accounts.doctype.sales_invoice import sales_invoice + from erpnext.buying.doctype.purchase_order import purchase_order + from erpnext.buying.doctype.supplier_quotation import supplier_quotation + from erpnext.selling.doctype.quotation import quotation + from erpnext.selling.doctype.sales_order import sales_order + from erpnext.stock.doctype.delivery_note import delivery_note + from erpnext.stock.doctype.purchase_receipt import purchase_receipt + + mapper = { + "Sales Order": { + "Sales Invoice": sales_order.make_sales_invoice, + "Delivery Note": sales_order.make_delivery_note, + "Advance Payment": payment_entry.get_payment_entry, + }, + "Sales Invoice": { + "Delivery Note": sales_invoice.make_delivery_note, + "Payment": payment_entry.get_payment_entry, + }, + "Delivery Note": { + "Sales Invoice": delivery_note.make_sales_invoice, + "Packing Slip": delivery_note.make_packing_slip, + }, + "Quotation": { + "Sales Order": quotation.make_sales_order, + "Sales Invoice": quotation.make_sales_invoice, + }, + "Supplier Quotation": { + "Purchase Order": supplier_quotation.make_purchase_order, + "Purchase Invoice": supplier_quotation.make_purchase_invoice, + "Advance Payment": payment_entry.get_payment_entry, + }, + "Purchase Order": { + "Purchase Invoice": purchase_order.make_purchase_invoice, + "Purchase Receipt": purchase_order.make_purchase_receipt, + }, + "Purhcase Invoice": { + "Purchase Receipt": purchase_invoice.make_purchase_receipt, + "Payment": payment_entry.get_payment_entry, + }, + "Purchase Receipt": {"Purchase Invoice": purchase_receipt.make_purchase_invoice}, + } + if to_doctype in ['Advance Payment', 'Payment']: + obj = mapper[from_doctype][to_doctype](from_doctype, doc_name) + else: + obj = mapper[from_doctype][to_doctype](doc_name) + + obj.flags.ignore_validate = True + obj.insert(ignore_mandatory=True) + + +def check_logger_doc_exists(log_date): + return frappe.db.exists("Bulk Transaction Log", log_date) + + +def get_logger_doc(log_date): + return frappe.get_doc("Bulk Transaction Log", log_date) + + +def create_logger_doc(): + log_doc = frappe.new_doc("Bulk Transaction Log") + log_doc.set_new_name(set_name=str(date.today())) + log_doc.log_date = date.today() + + return log_doc + + +def append_data_to_logger(log_doc, doc_name, error, from_doctype, to_doctype, status, restarted): + row = log_doc.append("logger_data", {}) + row.transaction_name = doc_name + row.date = date.today() + now = datetime.now() + row.time = now.strftime("%H:%M:%S") + row.transaction_status = status + row.error_description = str(error) + row.from_doctype = from_doctype + row.to_doctype = to_doctype + row.retried = restarted + + +def update_logger(doc_name, e, from_doctype, to_doctype, status, log_date=None, restarted=0): + if not check_logger_doc_exists(log_date): + log_doc = create_logger_doc() + append_data_to_logger(log_doc, doc_name, e, from_doctype, to_doctype, status, restarted) + log_doc.insert() + else: + log_doc = get_logger_doc(log_date) + if record_exists(log_doc, doc_name, status): + append_data_to_logger( + log_doc, doc_name, e, from_doctype, to_doctype, status, restarted + ) + log_doc.save() + + +def show_job_status(failed_history, deserialized_data, to_doctype): + if not failed_history: + frappe.msgprint( + _("Creation of {0} successful").format(to_doctype), + title="Successful", + indicator="green", + ) + + if len(failed_history) != 0 and len(failed_history) < len(deserialized_data): + frappe.msgprint( + _("""Creation of {0} partially successful. + Check Bulk Transaction Log""").format( + to_doctype + ), + title="Partially successful", + indicator="orange", + ) + + if len(failed_history) == len(deserialized_data): + frappe.msgprint( + _("""Creation of {0} failed. + Check Bulk Transaction Log""").format( + to_doctype + ), + title="Failed", + indicator="red", + ) + + +def record_exists(log_doc, doc_name, status): + + record = mark_retrired_transaction(log_doc, doc_name) + + if record and status == "Failed": + return False + elif record and status == "Success": + return True + else: + return True + + +def mark_retrired_transaction(log_doc, doc_name): + record = 0 + for d in log_doc.get("logger_data"): + if d.transaction_name == doc_name and d.transaction_status == "Failed": + d.retried = 1 + record = record + 1 + + log_doc.save() + + return record \ No newline at end of file From 0ca60afc3fb7190cbba58ef42b84c51bffb9d660 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 8 Feb 2022 10:24:19 +0530 Subject: [PATCH 28/31] fix: ignore cancelled SLEs (#29679) --- 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 9be5c0d03f..c8e5eddfea 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 e93bb8a3364c04c8a30b47f681e53747140e4fd9 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 8 Feb 2022 10:49:37 +0530 Subject: [PATCH 29/31] chore: show credit/debit-to account in error message --- .../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 279557adc7..09bfe35023 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -178,8 +178,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 bc443581e4..a161336bc2 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -572,7 +572,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 3969840ee8fef30742f9c834a91df79f9644afc5 Mon Sep 17 00:00:00 2001 From: Bhavesh Maheshwari <34086262+bhavesh95863@users.noreply.github.com> Date: Mon, 7 Feb 2022 20:11:39 +0530 Subject: [PATCH 30/31] fix: ignore rate validation for work order (cherry picked from commit f29aed7f7130b805810075db1e5e1003e997bef8) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index a2ef7b42be..782fcf04a5 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1115,7 +1115,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 bb105a33b28e5b86152dce0732424b2197e5dd57 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 8 Feb 2022 11:40:40 +0530 Subject: [PATCH 31/31] test: validate on save instead of on creation (cherry picked from commit 0efd5577bd9bdc0bb900d4d23d85aa86fd09929a) --- erpnext/manufacturing/doctype/work_order/test_work_order.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index a399edda70..76978017a6 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -703,7 +703,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 (