From 76c6b501f91a0550a554be94ec66e6ebd89ef40f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 27 Dec 2023 15:31:58 +0530 Subject: [PATCH 01/28] fix: use `Stock Qty` while getting `POS Reserved Qty` (backport #38962) (#38965) fix: use `Stock Qty` while getting `POS Reserved Qty` (cherry picked from commit 722310641700ce767cc2dd132e5ee41610d2e525) Co-authored-by: s-aga-r --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index bfb51da58b3..e542d3cc630 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -765,7 +765,7 @@ def get_pos_reserved_qty(item_code, warehouse): reserved_qty = ( frappe.qb.from_(p_inv) .from_(p_item) - .select(Sum(p_item.qty).as_("qty")) + .select(Sum(p_item.stock_qty).as_("stock_qty")) .where( (p_inv.name == p_item.parent) & (IfNull(p_inv.consolidated_invoice, "") == "") @@ -775,7 +775,7 @@ def get_pos_reserved_qty(item_code, warehouse): ) ).run(as_dict=True) - return reserved_qty[0].qty or 0 if reserved_qty else 0 + return flt(reserved_qty[0].stock_qty) if reserved_qty else 0 @frappe.whitelist() From 3caabf23a5efbba6081ea23d2597397b2fc46f1e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 27 Dec 2023 20:40:03 +0530 Subject: [PATCH 02/28] fix : correct logic for overlap error (backport #38967) (#38968) fix : correct logic for overlap error (#38967) fixing overlap error logic with taking care of sequential time job cards in overlap job card list (cherry picked from commit fe77b9d633dcd168bef6f7c106ec1bd6d1a6591c) Co-authored-by: VihangT <151990347+VihangT@users.noreply.github.com> --- .../doctype/job_card/job_card.py | 58 ++++++++++--------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index d696cc4082f..23650b68736 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -273,35 +273,39 @@ class JobCard(Document): def has_overlap(self, production_capacity, time_logs): overlap = False - if production_capacity == 1 and len(time_logs) > 0: + if production_capacity == 1 and len(time_logs) >= 1: return True + if not len(time_logs): + return False - # Check overlap exists or not between the overlapping time logs with the current Job Card - for row in time_logs: - count = 1 - for next_row in time_logs: - if row.name == next_row.name: - continue - - if ( - ( - get_datetime(next_row.from_time) >= get_datetime(row.from_time) - and get_datetime(next_row.from_time) <= get_datetime(row.to_time) - ) - or ( - get_datetime(next_row.to_time) >= get_datetime(row.from_time) - and get_datetime(next_row.to_time) <= get_datetime(row.to_time) - ) - or ( - get_datetime(next_row.from_time) <= get_datetime(row.from_time) - and get_datetime(next_row.to_time) >= get_datetime(row.to_time) - ) - ): - count += 1 - - if count > production_capacity: - return True - + # sorting overlapping job cards as per from_time + time_logs = sorted(time_logs, key=lambda x: x.get("from_time")) + # alloted_capacity has key number starting from 1. Key number will increment by 1 if non sequential job card found + # if key number reaches/crosses to production_capacity means capacity is full and overlap error generated + # this will store last to_time of sequential job cards + alloted_capacity = {1: time_logs[0]["to_time"]} + # flag for sequential Job card found + sequential_job_card_found = False + for i in range(1, len(time_logs)): + # scanning for all Existing keys + for key in alloted_capacity.keys(): + # if current Job Card from time is greater than last to_time in that key means these job card are sequential + if alloted_capacity[key] <= time_logs[i]["from_time"]: + # So update key's value with last to_time + alloted_capacity[key] = time_logs[i]["to_time"] + # flag is true as we get sequential Job Card for that key + sequential_job_card_found = True + # Immediately break so that job card to time is not added with any other key except this + break + # if sequential job card not found above means it is overlapping so increment key number to alloted_capacity + if not sequential_job_card_found: + # increment key number + key = key + 1 + # for that key last to time is assigned. + alloted_capacity[key] = time_logs[i]["to_time"] + if len(alloted_capacity) >= production_capacity: + # if number of keys greater or equal to production caoacity means full capacity is utilized and we should throw overlap error + return True return overlap def get_time_logs(self, args, doctype, check_next_available_slot=False): From 4e2304818b432f3ab5bf22268267cebf2266b117 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 28 Dec 2023 08:28:02 +0530 Subject: [PATCH 03/28] feat: Merge taxes from mapped docs (#38346) feat: Merge taxes from mapped docs (#38346) * feat: Merge taxes from mapped docs * chore: ci failures (cherry picked from commit 9b1c22250f1da37a563ba0885fbbd732b176816c) Co-authored-by: Deepesh Garg --- erpnext/public/js/utils.js | 12 +++--- .../purchase_receipt/purchase_receipt.py | 43 ++++++++++++++++++- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index b0ea56833b7..866e94f95aa 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -843,7 +843,7 @@ erpnext.utils.map_current_doc = function(opts) { freeze_message: __("Mapping {0} ...", [opts.source_doctype]), callback: function(r) { if(!r.exc) { - var doc = frappe.model.sync(r.message); + frappe.model.sync(r.message); cur_frm.dirty(); cur_frm.refresh(); } @@ -870,6 +870,11 @@ erpnext.utils.map_current_doc = function(opts) { target: opts.target, date_field: opts.date_field || undefined, setters: opts.setters, + data_fields: [{ + fieldname: 'merge_taxes', + fieldtype: 'Check', + label: __('Merge taxes from multiple documents'), + }], get_query: opts.get_query, add_filters_group: 1, allow_child_item_selection: opts.allow_child_item_selection, @@ -883,10 +888,7 @@ erpnext.utils.map_current_doc = function(opts) { return; } opts.source_name = values; - if (opts.allow_child_item_selection) { - // args contains filtered child docnames - opts.args = args; - } + opts.args = args; d.dialog.hide(); _map(); }, diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 23956ce0b7d..b7a64bb8b1a 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1121,8 +1121,39 @@ def get_item_wise_returned_qty(pr_doc): ) +def merge_taxes(source_taxes, target_doc): + from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import ( + update_item_wise_tax_detail, + ) + + existing_taxes = target_doc.get("taxes") or [] + idx = 1 + for tax in source_taxes: + found = False + for t in existing_taxes: + if t.account_head == tax.account_head and t.cost_center == tax.cost_center: + t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount) + t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount) + update_item_wise_tax_detail(t, tax) + found = True + + if not found: + tax.charge_type = "Actual" + tax.idx = idx + idx += 1 + tax.included_in_print_rate = 0 + tax.dont_recompute_tax = 1 + tax.row_id = "" + tax.tax_amount = tax.tax_amount_after_discount_amount + tax.base_tax_amount = tax.base_tax_amount_after_discount_amount + tax.item_wise_tax_detail = tax.item_wise_tax_detail + existing_taxes.append(tax) + + target_doc.set("taxes", existing_taxes) + + @frappe.whitelist() -def make_purchase_invoice(source_name, target_doc=None): +def make_purchase_invoice(source_name, target_doc=None, args=None): from erpnext.accounts.party import get_payment_terms_template doc = frappe.get_doc("Purchase Receipt", source_name) @@ -1139,6 +1170,10 @@ def make_purchase_invoice(source_name, target_doc=None): ) doc.run_method("onload") doc.run_method("set_missing_values") + + if args and args.get("merge_taxes"): + merge_taxes(source.get("taxes") or [], doc) + doc.run_method("calculate_taxes_and_totals") doc.set_payment_schedule() @@ -1202,7 +1237,11 @@ def make_purchase_invoice(source_name, target_doc=None): if not doc.get("is_return") else get_pending_qty(d)[0] > 0, }, - "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "add_if_empty": True}, + "Purchase Taxes and Charges": { + "doctype": "Purchase Taxes and Charges", + "add_if_empty": True, + "ignore": args.get("merge_taxes") if args else 0, + }, }, target_doc, set_missing_values, From f8625f3eb7e60b3985de900f7443864c7dec20e3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 28 Dec 2023 11:21:51 +0530 Subject: [PATCH 04/28] fix: not able to cancel sales invoice (backport #38979) (#38980) fix: not able to cancel sales invoice (#38979) (cherry picked from commit a117ef3cb88223f0a9f94f863e232b2771b277c8) Co-authored-by: rohitwaghchaure --- .../accounts/doctype/purchase_invoice/purchase_invoice.js | 4 +++- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 5 ++++- erpnext/public/js/controllers/transaction.js | 5 +++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 215d8ec2153..44d4d816434 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -35,7 +35,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. super.onload(); // Ignore linked advances - this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"]; + this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Serial and Batch Bundle"]; if(!this.frm.doc.__islocal) { // show credit_to in print format @@ -408,6 +408,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. } on_submit() { + super.on_submit(); + $.each(this.frm.doc["items"] || [], function(i, row) { if(row.purchase_receipt) frappe.model.clear_doc("Purchase Receipt", row.purchase_receipt) }) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 6763e446a5d..c8d92d0d705 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -37,7 +37,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e super.onload(); this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', - 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"]; + 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", + 'Serial and Batch Bundle' + ]; if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { // show debit_to in print format @@ -197,6 +199,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e on_submit(doc, dt, dn) { var me = this; + super.on_submit(); if (frappe.get_route()[0] != 'Form') { return } diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 908eec4d7cd..07b1e8f8477 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -715,6 +715,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } on_submit() { + if (in_list(["Purchase Invoice", "Sales Invoice"], this.frm.doc.doctype) + && !this.frm.doc.update_stock) { + return; + } + this.refresh_serial_batch_bundle_field(); } From 46035ed9cab2b150e88a31be79b839f910aca24e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 28 Dec 2023 14:22:18 +0530 Subject: [PATCH 05/28] fix: remove bad defaults (backport #38986) (#38988) fix: remove bad defaults (#38986) Child tables can't have a default. (cherry picked from commit b71b0d599775cd445acb555f7df6f849056a7a9b) Co-authored-by: Ankush Menat --- .../cashier_closing/cashier_closing.json | 371 ++---------------- .../selling/doctype/customer/customer.json | 3 +- erpnext/stock/tests/test_valuation.py | 1 - 3 files changed, 34 insertions(+), 341 deletions(-) diff --git a/erpnext/accounts/doctype/cashier_closing/cashier_closing.json b/erpnext/accounts/doctype/cashier_closing/cashier_closing.json index 1b38f0d36d7..051b44b5868 100644 --- a/erpnext/accounts/doctype/cashier_closing/cashier_closing.json +++ b/erpnext/accounts/doctype/cashier_closing/cashier_closing.json @@ -1,457 +1,152 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, + "actions": [], "autoname": "naming_series:", - "beta": 0, "creation": "2018-06-18 16:51:49.994750", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "naming_series", + "user", + "date", + "from_time", + "time", + "expense", + "custody", + "returns", + "outstanding_amount", + "payments", + "net_amount", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "POS-CLO-", "fieldname": "naming_series", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, "in_global_search": 1, - "in_list_view": 0, "in_standard_filter": 1, "label": "Series", - "length": 0, - "no_copy": 0, "options": "POS-CLO-", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "user", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, - "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 1, "label": "User", - "length": 0, - "no_copy": 0, "options": "User", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Today", "fieldname": "date", "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, - "in_global_search": 0, - "in_list_view": 0, "in_standard_filter": 1, "label": "Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "from_time", "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, - "in_global_search": 0, - "in_list_view": 0, "in_standard_filter": 1, "label": "From Time", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", "fieldname": "time", "fieldtype": "Time", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, - "in_global_search": 0, - "in_list_view": 0, "in_standard_filter": 1, "label": "To Time", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "0.00", "fieldname": "expense", "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Expense", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Expense" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "0.00", "fieldname": "custody", "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Custody", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Custody" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "0.00", "fieldname": "returns", "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Returns", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "precision": "2" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "0.00", "fieldname": "outstanding_amount", "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Outstanding Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0.0", "fieldname": "payments", "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Payments", - "length": 0, - "no_copy": 0, - "options": "Cashier Closing Payments", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Cashier Closing Payments" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "net_amount", "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, - "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 1, "label": "Net Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "amended_from", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Amended From", - "length": 0, "no_copy": 1, "options": "Cashier Closing", - "permlevel": 0, "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-02-19 08:35:24.157327", + "links": [], + "modified": "2023-12-28 13:15:46.858427", "modified_by": "Administrator", "module": "Accounts", "name": "Cashier Closing", - "name_case": "", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, "submit": 1, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 31bbbcf51b8..db712d96b50 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -448,7 +448,6 @@ "report_hide": 1 }, { - "default": "0", "fieldname": "credit_limits", "fieldtype": "Table", "label": "Credit Limit", @@ -584,7 +583,7 @@ "link_fieldname": "party" } ], - "modified": "2023-10-19 16:56:27.327035", + "modified": "2023-12-28 13:15:36.298369", "modified_by": "Administrator", "module": "Selling", "name": "Customer", diff --git a/erpnext/stock/tests/test_valuation.py b/erpnext/stock/tests/test_valuation.py index 05f153b4a0c..4d8990ae40b 100644 --- a/erpnext/stock/tests/test_valuation.py +++ b/erpnext/stock/tests/test_valuation.py @@ -195,7 +195,6 @@ class TestFIFOValuation(unittest.TestCase): total_value -= sum(q * r for q, r in consumed) self.assertTotalQty(total_qty) self.assertTotalValue(total_value) - self.assertGreaterEqual(total_value, 0) class TestLIFOValuation(unittest.TestCase): From 7c7c3c932fb8b8942d87b785f37e991c6c83818c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 27 Dec 2023 16:32:57 +0530 Subject: [PATCH 06/28] fix: incorrect total when Accumulating values (cherry picked from commit d54f8318fba2b60eaad4a93a811a5b569c3344dc) --- .../accounts/report/financial_statements.py | 20 ++++++++++++++---- .../profit_and_loss_statement.py | 21 ++++++++++++++----- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 7355c4b8a16..004a9299ea2 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -211,7 +211,13 @@ def get_data( ignore_accumulated_values_for_fy, ) accumulate_values_into_parents(accounts, accounts_by_name, period_list) - out = prepare_data(accounts, balance_must_be, period_list, company_currency) + out = prepare_data( + accounts, + balance_must_be, + period_list, + company_currency, + accumulated_values=filters.accumulated_values, + ) out = filter_out_zero_value_rows(out, parent_children_map) if out and total: @@ -270,7 +276,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, period_list): ) + d.get("opening_balance", 0.0) -def prepare_data(accounts, balance_must_be, period_list, company_currency): +def prepare_data(accounts, balance_must_be, period_list, company_currency, accumulated_values): data = [] year_start_date = period_list[0]["year_start_date"].strftime("%Y-%m-%d") year_end_date = period_list[-1]["year_end_date"].strftime("%Y-%m-%d") @@ -310,8 +316,14 @@ def prepare_data(accounts, balance_must_be, period_list, company_currency): has_value = True total += flt(row[period.key]) - row["has_value"] = has_value - row["total"] = total + if accumulated_values: + # when 'accumulated_values' is enabled, periods have running balance. + # so, last period will have the net amount. + row["has_value"] = has_value + row["total"] = flt(d.get(period_list[-1].key, 0.0), 3) + else: + row["has_value"] = has_value + row["total"] = total data.append(row) return data diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py index 66353358a06..0b7ce518918 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py @@ -82,14 +82,25 @@ def get_report_summary( if filters.get("accumulated_in_group_company"): period_list = get_filtered_list_for_consolidated_report(filters, period_list) - for period in period_list: - key = period if consolidated else period.key + if filters.accumulated_values: + # when 'accumulated_values' is enabled, periods have running balance. + # so, last period will have the net amount. + key = period_list[-1].key if income: - net_income += income[-2].get(key) + net_income = income[-2].get(key) if expense: - net_expense += expense[-2].get(key) + net_expense = expense[-2].get(key) if net_profit_loss: - net_profit += net_profit_loss.get(key) + net_profit = net_profit_loss.get(key) + else: + for period in period_list: + key = period if consolidated else period.key + if income: + net_income += income[-2].get(key) + if expense: + net_expense += expense[-2].get(key) + if net_profit_loss: + net_profit += net_profit_loss.get(key) if len(period_list) == 1 and periodicity == "Yearly": profit_label = _("Profit This Year") From a3cecb892af373a846722ebd10fd61a451e4d6de Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 28 Dec 2023 11:39:55 +0530 Subject: [PATCH 07/28] test: profit and loss report output (cherry picked from commit 855561729544770d77106bc1e4e4a964884377ea) --- .../test_profit_and_loss_statement.py | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py diff --git a/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py new file mode 100644 index 00000000000..b4423abc7ff --- /dev/null +++ b/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py @@ -0,0 +1,94 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, getdate, today + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.financial_statements import get_period_list +from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import execute +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin + + +class TestProfitAndLossStatement(AccountsTestMixin, FrappeTestCase): + def setUp(self): + self.create_company() + self.create_customer() + self.create_item() + + def tearDown(self): + frappe.db.rollback() + + def create_sales_invoice(self, qty=1, rate=150, no_payment_schedule=False, do_not_submit=False): + frappe.set_user("Administrator") + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=rate, + price_list_rate=rate, + qty=qty, + do_not_save=1, + ) + si = si.save() + if not do_not_submit: + si = si.submit() + return si + + def get_fiscal_year(self): + active_fy = frappe.db.get_all( + "Fiscal Year", + filters={"disabled": 0, "year_start_date": ("<=", today()), "year_end_date": (">=", today())}, + )[0] + return frappe.get_doc("Fiscal Year", active_fy.name) + + def get_report_filters(self): + fy = self.get_fiscal_year() + return frappe._dict( + company=self.company, + from_fiscal_year=fy.name, + to_fiscal_year=fy.name, + period_start_date=fy.year_start_date, + period_end_date=fy.year_end_date, + filter_based_on="Fiscal Year", + periodicity="Monthly", + accumulated_vallues=True, + ) + + def test_profit_and_loss_output_and_summary(self): + si = self.create_sales_invoice(qty=1, rate=150) + + filters = self.get_report_filters() + period_list = get_period_list( + filters.from_fiscal_year, + filters.to_fiscal_year, + filters.period_start_date, + filters.period_end_date, + filters.filter_based_on, + filters.periodicity, + company=filters.company, + ) + + result = execute(filters)[1] + current_period = [x for x in period_list if x.from_date <= getdate() and x.to_date >= getdate()][ + 0 + ] + current_period_key = current_period.key + without_current_period = [x for x in period_list if x.key != current_period.key] + # all period except current period(whence invoice was posted), should be '0' + for acc in result: + if acc: + with self.subTest(acc=acc): + for period in without_current_period: + self.assertEqual(acc[period.key], 0) + + for acc in result: + if acc: + with self.subTest(current_period_key=current_period_key): + self.assertEqual(acc[current_period_key], 150) + self.assertEqual(acc["total"], 150) From 49f93f9fa1ce17838c66873771f9b943a4575102 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 28 Dec 2023 17:36:00 +0530 Subject: [PATCH 08/28] fix: undefined error in consolidated financial report --- .../consolidated_financial_statement.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js index 0e0c42dad95..6210fd17f10 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js @@ -128,7 +128,7 @@ frappe.query_reports["Consolidated Financial Statement"] = { } value = default_formatter(value, row, column, data); - if (!data.parent_account) { + if (data && !data.parent_account) { value = $(`${value}`); var $value = $(value).css("font-weight", "bold"); From e0755f9a9a7d57ce4dd4c4c4c5954c56bc3fe13a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 28 Dec 2023 17:53:44 +0530 Subject: [PATCH 09/28] fix: Opening balance in bank reconciliation tool (#38977) fix: Opening balance in bank reconciliation tool (#38977) (cherry picked from commit bbee9b56377f24109217515360789558566f6aa1) Co-authored-by: Deepesh Garg --- .../bank_reconciliation_tool/bank_reconciliation_tool.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js index d961ead642d..1f2d6ce59a8 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js @@ -137,7 +137,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", { "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance", args: { bank_account: frm.doc.bank_account, - till_date: frm.doc.bank_statement_from_date, + till_date: frappe.datetime.add_days(frm.doc.bank_statement_from_date, -1) }, callback: (response) => { frm.set_value("account_opening_balance", response.message); From d2580be4fd0a08aeddbdbf9b87d99b71a45d7820 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 28 Dec 2023 18:22:14 +0530 Subject: [PATCH 10/28] fix: serial and batch bundle company mandatory error (backport #38994) (#38999) fix: serial and batch bundle company mandatory error (#38994) (cherry picked from commit 10074e9980c8f2a1473827c61c888d970da4b73a) Co-authored-by: rohitwaghchaure --- .../serial_and_batch_bundle.py | 12 +++--- .../test_serial_and_batch_bundle.py | 40 +++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index dd38e1127f8..a4fb5324ee6 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -1145,15 +1145,16 @@ def add_serial_batch_ledgers(entries, child_row, doc, warehouse) -> object: if isinstance(entries, str): entries = parse_json(entries) - if doc and isinstance(doc, str): - parent_doc = parse_json(doc) + parent_doc = doc + if parent_doc and isinstance(parent_doc, str): + parent_doc = parse_json(parent_doc) if frappe.db.exists("Serial and Batch Bundle", child_row.serial_and_batch_bundle): - doc = update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse) + sb_doc = update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse) else: - doc = create_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse) + sb_doc = create_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse) - return doc + return sb_doc def create_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=None) -> object: @@ -1177,6 +1178,7 @@ def create_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=Non "type_of_transaction": type_of_transaction, "posting_date": parent_doc.get("posting_date"), "posting_time": parent_doc.get("posting_time"), + "company": parent_doc.get("company"), } ) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index d74d657f385..478cfa4d1c7 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -8,6 +8,9 @@ from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, add_to_date, flt, nowdate, nowtime, today from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + add_serial_batch_ledgers, +) from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -420,6 +423,43 @@ class TestSerialandBatchBundle(FrappeTestCase): ste.delete() self.assertFalse(frappe.db.exists("Serial and Batch Bundle", bundle_doc.name)) + def test_serial_and_batch_bundle_company(self): + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + + item = make_item( + properties={ + "has_serial_no": 1, + "serial_no_series": "TT-SER-VAL-.#####", + } + ) + + pr = make_purchase_receipt( + item_code=item, + warehouse="_Test Warehouse - _TC", + qty=3, + rate=500, + do_not_submit=True, + ) + + entries = [] + for serial_no in ["TT-SER-VAL-00001", "TT-SER-VAL-00002", "TT-SER-VAL-00003"]: + entries.append(frappe._dict({"serial_no": serial_no, "qty": 1})) + + if not frappe.db.exists("Serial No", serial_no): + frappe.get_doc( + { + "doctype": "Serial No", + "serial_no": serial_no, + "item_code": item, + } + ).insert(ignore_permissions=True) + + item_row = pr.items[0] + item_row.type_of_transaction = "Inward" + item_row.is_rejected = 0 + sn_doc = add_serial_batch_ledgers(entries, item_row, pr, "_Test Warehouse - _TC") + self.assertEqual(sn_doc.company, "_Test Company") + def get_batch_from_bundle(bundle): from erpnext.stock.serial_batch_bundle import get_batch_nos From 2e919344df8cd9018b2fff6d70b49c8db6ba0d8c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 28 Dec 2023 18:54:38 +0530 Subject: [PATCH 11/28] fix: auto fetch not working if bundle exists (backport #39002) (#39004) fix: auto fetch not working if bundle exists (#39002) (cherry picked from commit b5340c5ec0d418dcf764efa961f743f88b660a24) Co-authored-by: rohitwaghchaure --- erpnext/public/js/utils/serial_no_batch_selector.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 4cd1243413b..bf362e338e4 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -337,16 +337,18 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { } get_auto_data() { + let { qty, based_on } = this.dialog.get_values(); + if (this.item.serial_and_batch_bundle || this.item.rejected_serial_and_batch_bundle) { - return; + if (qty === this.qty) { + return; + } } if (this.item.serial_no || this.item.batch_no) { return; } - let { qty, based_on } = this.dialog.get_values(); - if (!based_on) { based_on = 'FIFO'; } From 5158884dc9660cba1353b285e1eeb930ed9a204e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 29 Dec 2023 12:52:41 +0530 Subject: [PATCH 12/28] fix: Multiple subscription fixes (#39005) fix: Multiple subscription fixes (#39005) (cherry picked from commit 3b4b2275de7a49ba48b09f7229bee45f1e890ac9) Co-authored-by: Deepesh Garg --- .../doctype/subscription/subscription.json | 6 +++--- .../doctype/subscription/subscription.py | 19 ++++++++++++------- erpnext/controllers/accounts_controller.py | 5 +++++ 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/subscription/subscription.json b/erpnext/accounts/doctype/subscription/subscription.json index 187b7abce18..97fd4d040f1 100644 --- a/erpnext/accounts/doctype/subscription/subscription.json +++ b/erpnext/accounts/doctype/subscription/subscription.json @@ -148,13 +148,13 @@ { "fieldname": "additional_discount_percentage", "fieldtype": "Percent", - "label": "Additional DIscount Percentage" + "label": "Additional Discount Percentage" }, { "collapsible": 1, "fieldname": "additional_discount_amount", "fieldtype": "Currency", - "label": "Additional DIscount Amount" + "label": "Additional Discount Amount" }, { "collapsible": 1, @@ -267,7 +267,7 @@ "link_fieldname": "subscription" } ], - "modified": "2023-09-18 17:48:21.900252", + "modified": "2023-12-28 17:20:42.687789", "modified_by": "Administrator", "module": "Accounts", "name": "Subscription", diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 6cc2d1e9ee9..94f5e29f068 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -356,18 +356,20 @@ class Subscription(Document): self, from_date: Optional[Union[str, datetime.date]] = None, to_date: Optional[Union[str, datetime.date]] = None, + posting_date: Optional[Union[str, datetime.date]] = None, ) -> Document: """ Creates a `Invoice` for the `Subscription`, updates `self.invoices` and saves the `Subscription`. Backwards compatibility """ - return self.create_invoice(from_date=from_date, to_date=to_date) + return self.create_invoice(from_date=from_date, to_date=to_date, posting_date=posting_date) def create_invoice( self, from_date: Optional[Union[str, datetime.date]] = None, to_date: Optional[Union[str, datetime.date]] = None, + posting_date: Optional[Union[str, datetime.date]] = None, ) -> Document: """ Creates a `Invoice`, submits it and returns it @@ -385,11 +387,13 @@ class Subscription(Document): invoice = frappe.new_doc(self.invoice_document_type) invoice.company = company invoice.set_posting_time = 1 - invoice.posting_date = ( - self.current_invoice_start - if self.generate_invoice_at == "Beginning of the current subscription period" - else self.current_invoice_end - ) + + if self.generate_invoice_at == "Beginning of the current subscription period": + invoice.posting_date = self.current_invoice_start + elif self.generate_invoice_at == "Days before the current subscription period": + invoice.posting_date = posting_date or self.current_invoice_start + else: + invoice.posting_date = self.current_invoice_end invoice.cost_center = self.cost_center @@ -413,6 +417,7 @@ class Subscription(Document): # Subscription is better suited for service items. I won't update `update_stock` # for that reason items_list = self.get_items_from_plans(self.plans, is_prorate()) + for item in items_list: item["cost_center"] = self.cost_center invoice.append("items", item) @@ -556,7 +561,7 @@ class Subscription(Document): if not self.is_current_invoice_generated( self.current_invoice_start, self.current_invoice_end ) and self.can_generate_new_invoice(posting_date): - self.generate_invoice() + self.generate_invoice(posting_date=posting_date) self.update_subscription_period(add_days(self.current_invoice_end, 1)) if self.cancel_at_period_end and ( diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 4447b076c71..9e4abff6b47 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -225,6 +225,11 @@ class AccountsController(TransactionBase): apply_pricing_rule_on_transaction(self) self.set_total_in_words() + self.set_default_letter_head() + + def set_default_letter_head(self): + if hasattr(self, "letter_head") and not self.letter_head: + self.letter_head = frappe.db.get_value("Company", self.company, "default_letter_head") def init_internal_values(self): # init all the internal values as 0 on sa From 2a390ac2de0905b479fa237a852a27121f6924ba Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 29 Dec 2023 13:07:38 +0530 Subject: [PATCH 13/28] fix: purchase return without item code not working (backport #39014) (#39015) fix: purchase return without item code not working (#39014) (cherry picked from commit f983e09f92c6fb179e8e8a972b1cb969b40c8fd5) Co-authored-by: rohitwaghchaure --- .../purchase_invoice/test_purchase_invoice.py | 21 +++++++++++++++++++ .../controllers/sales_and_purchase_return.py | 17 ++++++++------- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index e43ea6ecbe0..e41cec7eee3 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1985,6 +1985,26 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): self.assertEqual(pi.items[0].cost_center, "_Test Cost Center Buying - _TC") + def test_debit_note_without_item(self): + pi = make_purchase_invoice(item_name="_Test Item", qty=10, do_not_submit=True) + pi.items[0].item_code = "" + pi.save() + + self.assertFalse(pi.items[0].item_code) + pi.submit() + + return_pi = make_purchase_invoice( + item_name="_Test Item", + is_return=1, + return_against=pi.name, + qty=-10, + do_not_save=True, + ) + return_pi.items[0].item_code = "" + return_pi.save() + return_pi.submit() + self.assertEqual(return_pi.docstatus, 1) + def set_advance_flag(company, flag, default_account): frappe.db.set_value( @@ -2121,6 +2141,7 @@ def make_purchase_invoice(**args): "items", { "item_code": args.item or args.item_code or "_Test Item", + "item_name": args.item_name, "warehouse": args.warehouse or "_Test Warehouse - _TC", "qty": args.qty or 5, "received_qty": args.received_qty or 0, diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 81080f02665..e7bd2a7265c 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -562,16 +562,17 @@ def make_return_doc( if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return - item_details = frappe.get_cached_value( - "Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1 - ) + if source_doc.item_code: + item_details = frappe.get_cached_value( + "Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1 + ) - if not item_details.has_batch_no and not item_details.has_serial_no: - return + if not item_details.has_batch_no and not item_details.has_serial_no: + return - for qty_field in ["stock_qty", "rejected_qty"]: - if target_doc.get(qty_field): - update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field) + for qty_field in ["stock_qty", "rejected_qty"]: + if target_doc.get(qty_field): + update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field) def update_terms(source_doc, target_doc, source_parent): target_doc.payment_amount = -source_doc.payment_amount From f3254c201000a822dab2dde2b6340f04051cbd4a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 29 Dec 2023 16:16:02 +0530 Subject: [PATCH 14/28] fix: Validate account in Sales/Purchase Taxes and Charges Template (#39013) fix: Validate account in Sales/Purchase Taxes and Charges Template (#39013) (cherry picked from commit cd37fd790b9d7a28b9df8697c9b40c5477a580ec) Co-authored-by: Deepesh Garg --- erpnext/controllers/accounts_controller.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 9e4abff6b47..4aa8e86f317 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2398,6 +2398,7 @@ def validate_taxes_and_charges(tax): def validate_account_head(idx, account, company, context=""): account_company = frappe.get_cached_value("Account", account, "company") + is_group = frappe.get_cached_value("Account", account, "is_group") if account_company != company: frappe.throw( @@ -2407,6 +2408,12 @@ def validate_account_head(idx, account, company, context=""): title=_("Invalid Account"), ) + if is_group: + frappe.throw( + _("Row {0}: Account {1} is a Group Account").format(idx, frappe.bold(account)), + title=_("Invalid Account"), + ) + def validate_cost_center(tax, doc): if not tax.cost_center: From 3a7506ecbcc369bcf1cb10db49d4e93f4e3cf7a4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 29 Dec 2023 20:54:26 +0530 Subject: [PATCH 15/28] fix: work order with multi level, fetch operting cost from sub-assembly (backport #38992) (#39028) fix: work order with multi level, fetch operting cost from sub-assembly (#38992) (cherry picked from commit 70abedc57ad2147719c23537ab9aaf348d6e9182) Co-authored-by: rohitwaghchaure --- erpnext/manufacturing/doctype/bom/bom.py | 44 ++++++++++ .../manufacturing_settings.json | 11 ++- .../manufacturing_settings.py | 1 + .../production_plan/test_production_plan.py | 4 + .../doctype/work_order/test_work_order.py | 88 +++++++++++++++++++ .../stock/doctype/stock_entry/stock_entry.py | 37 ++++++-- 6 files changed, 178 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8cb024209c2..682c4fb82a7 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1486,3 +1486,47 @@ def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None): ) return doc + + +def get_op_cost_from_sub_assemblies(bom_no, op_cost=0): + # Get operating cost from sub-assemblies + + bom_items = frappe.get_all( + "BOM Item", filters={"parent": bom_no, "docstatus": 1}, fields=["bom_no"], order_by="idx asc" + ) + + for row in bom_items: + if not row.bom_no: + continue + + if cost := frappe.get_cached_value("BOM", row.bom_no, "operating_cost_per_bom_quantity"): + op_cost += flt(cost) + get_op_cost_from_sub_assemblies(row.bom_no, op_cost) + + return op_cost + + +def get_scrap_items_from_sub_assemblies(bom_no, company, qty, scrap_items=None): + if not scrap_items: + scrap_items = {} + + bom_items = frappe.get_all( + "BOM Item", + filters={"parent": bom_no, "docstatus": 1}, + fields=["bom_no", "qty"], + order_by="idx asc", + ) + + for row in bom_items: + if not row.bom_no: + continue + + qty = flt(row.qty) * flt(qty) + items = get_bom_items_as_dict( + row.bom_no, company, qty=qty, fetch_exploded=0, fetch_scrap_items=1 + ) + scrap_items.update(items) + + get_scrap_items_from_sub_assemblies(row.bom_no, company, qty, scrap_items) + + return scrap_items diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json index 01647d56c91..d3ad51f7236 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -31,6 +31,7 @@ "job_card_excess_transfer", "other_settings_section", "update_bom_costs_automatically", + "set_op_cost_and_scrape_from_sub_assemblies", "column_break_23", "make_serial_no_batch_from_work_order" ], @@ -194,13 +195,20 @@ "fieldname": "job_card_excess_transfer", "fieldtype": "Check", "label": "Allow Excess Material Transfer" + }, + { + "default": "0", + "description": "In the case of 'Use Multi-Level BOM' in a work order, if the user wishes to add sub-assembly costs to Finished Goods items without using a job card as well the scrap items, then this option needs to be enable.", + "fieldname": "set_op_cost_and_scrape_from_sub_assemblies", + "fieldtype": "Check", + "label": "Set Operating Cost / Scrape Items From Sub-assemblies" } ], "icon": "icon-wrench", "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-09-13 22:09:09.401559", + "modified": "2023-12-28 16:37:44.874096", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing Settings", @@ -216,5 +224,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py index bfc8f4e9150..463ba9fe4bf 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py @@ -32,6 +32,7 @@ class ManufacturingSettings(Document): mins_between_operations: DF.Int overproduction_percentage_for_sales_order: DF.Percent overproduction_percentage_for_work_order: DF.Percent + set_op_cost_and_scrape_from_sub_assemblies: DF.Check update_bom_costs_automatically: DF.Check # end: auto-generated types diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index cb99b8845a3..f6dfaa50586 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1602,6 +1602,10 @@ def make_bom(**args): } ) + if args.operating_cost_per_bom_quantity: + bom.fg_based_operating_cost = 1 + bom.operating_cost_per_bom_quantity = args.operating_cost_per_bom_quantity + for item in args.raw_materials: item_doc = frappe.get_doc("Item", item) bom.append( diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 802c23d660a..91d9430712f 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -1731,6 +1731,93 @@ class TestWorkOrder(FrappeTestCase): job_card2.time_logs = [] job_card2.save() + def test_op_cost_and_scrap_based_on_sub_assemblies(self): + # Make Sub Assembly BOM 1 + + frappe.db.set_single_value( + "Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies", 1 + ) + + items = { + "Test Final FG Item": 0, + "Test Final SF Item 1": 0, + "Test Final SF Item 2": 0, + "Test Final RM Item 1": 100, + "Test Final RM Item 2": 200, + "Test Final Scrap Item 1": 50, + "Test Final Scrap Item 2": 60, + } + + for item in items: + if not frappe.db.exists("Item", item): + item_properties = {"is_stock_item": 1, "valuation_rate": items[item]} + + make_item(item_code=item, properties=item_properties), + + prepare_boms_for_sub_assembly_test() + + wo_order = make_wo_order_test_record( + production_item="Test Final FG Item", + qty=10, + use_multi_level_bom=1, + skip_transfer=1, + from_wip_warehouse=1, + ) + + se_doc = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10)) + se_doc.save() + + self.assertTrue(se_doc.additional_costs) + scrap_items = [] + for item in se_doc.items: + if item.is_scrap_item: + scrap_items.append(item.item_code) + + self.assertEqual( + sorted(scrap_items), sorted(["Test Final Scrap Item 1", "Test Final Scrap Item 2"]) + ) + for row in se_doc.additional_costs: + self.assertEqual(row.amount, 3000) + + frappe.db.set_single_value( + "Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies", 0 + ) + + +def prepare_boms_for_sub_assembly_test(): + if not frappe.db.exists("BOM", {"item": "Test Final SF Item 1"}): + bom = make_bom( + item="Test Final SF Item 1", + source_warehouse="Stores - _TC", + raw_materials=["Test Final RM Item 1"], + operating_cost_per_bom_quantity=100, + do_not_submit=True, + ) + + bom.append("scrap_items", {"item_code": "Test Final Scrap Item 1", "qty": 1}) + + bom.submit() + + if not frappe.db.exists("BOM", {"item": "Test Final SF Item 2"}): + bom = make_bom( + item="Test Final SF Item 2", + source_warehouse="Stores - _TC", + raw_materials=["Test Final RM Item 2"], + operating_cost_per_bom_quantity=200, + do_not_submit=True, + ) + + bom.append("scrap_items", {"item_code": "Test Final Scrap Item 2", "qty": 1}) + + bom.submit() + + if not frappe.db.exists("BOM", {"item": "Test Final FG Item"}): + bom = make_bom( + item="Test Final FG Item", + source_warehouse="Stores - _TC", + raw_materials=["Test Final SF Item 1", "Test Final SF Item 2"], + ) + def prepare_data_for_workstation_type_check(): from erpnext.manufacturing.doctype.operation.test_operation import make_operation @@ -1977,6 +2064,7 @@ def make_wo_order_test_record(**args): wo_order.sales_order = args.sales_order or None wo_order.planned_start_date = args.planned_start_date or now() wo_order.transfer_material_against = args.transfer_material_against or "Work Order" + wo_order.from_wip_warehouse = args.from_wip_warehouse or 0 if args.source_warehouse: for item in wo_order.get("required_items"): diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 2f1520f0d52..d35288a91cb 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -25,7 +25,12 @@ from frappe.utils import ( import erpnext from erpnext.accounts.general_ledger import process_gl_map from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals -from erpnext.manufacturing.doctype.bom.bom import add_additional_cost, validate_bom_no +from erpnext.manufacturing.doctype.bom.bom import ( + add_additional_cost, + get_op_cost_from_sub_assemblies, + get_scrap_items_from_sub_assemblies, + validate_bom_no, +) from erpnext.setup.doctype.brand.brand import get_brand_defaults from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.stock.doctype.batch.batch import get_batch_qty @@ -1898,11 +1903,22 @@ class StockEntry(StockController): def get_bom_scrap_material(self, qty): from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict - # item dict = { item_code: {qty, description, stock_uom} } - item_dict = ( - get_bom_items_as_dict(self.bom_no, self.company, qty=qty, fetch_exploded=0, fetch_scrap_items=1) - or {} - ) + if ( + frappe.db.get_single_value( + "Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies" + ) + and self.work_order + and frappe.get_cached_value("Work Order", self.work_order, "use_multi_level_bom") + ): + item_dict = get_scrap_items_from_sub_assemblies(self.bom_no, self.company, qty) + else: + # item dict = { item_code: {qty, description, stock_uom} } + item_dict = ( + get_bom_items_as_dict( + self.bom_no, self.company, qty=qty, fetch_exploded=0, fetch_scrap_items=1 + ) + or {} + ) for item in item_dict.values(): item.from_warehouse = "" @@ -2653,6 +2669,15 @@ def get_work_order_details(work_order, company): def get_operating_cost_per_unit(work_order=None, bom_no=None): operating_cost_per_unit = 0 if work_order: + if ( + bom_no + and frappe.db.get_single_value( + "Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies" + ) + and frappe.get_cached_value("Work Order", work_order, "use_multi_level_bom") + ): + return get_op_cost_from_sub_assemblies(bom_no) + if not bom_no: bom_no = work_order.bom_no From 89d106947279b07073aa87a9678d9c655e562077 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 29 Dec 2023 20:54:49 +0530 Subject: [PATCH 16/28] fix: non stock uom validation for serial and batch (backport #39018) (#39026) fix: non stock uom validation for serial and batch (#39018) * fix: non stock uom validation for serial and batch * test: delivery note for batch with non stock uom (cherry picked from commit 0c6de4ecb299daf5359fcd51041942a26f94693d) Co-authored-by: rohitwaghchaure --- .../doctype/sales_invoice/sales_invoice.py | 2 + .../sales_invoice/test_sales_invoice.py | 5 ++- .../sales_invoice_item.json | 9 ++++- .../doctype/delivery_note/delivery_note.py | 2 + .../delivery_note/test_delivery_note.py | 40 +++++++++++++++++++ .../serial_and_batch_bundle.py | 6 ++- 6 files changed, 60 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 6aba1faa84b..5924586e73f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -586,6 +586,8 @@ class SalesInvoice(SellingController): "Serial and Batch Bundle", ) + self.delete_auto_created_batches() + def update_status_updater_args(self): if cint(self.update_stock): self.status_updater.append( diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 450c8effea7..98d4ed46ccc 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1414,10 +1414,11 @@ class TestSalesInvoice(FrappeTestCase): def test_serialized_cancel(self): si = self.test_serialized() - si.cancel() - + si.reload() serial_nos = get_serial_nos_from_bundle(si.get("items")[0].serial_and_batch_bundle) + si.cancel() + self.assertEqual( frappe.db.get_value("Serial No", serial_nos[0], "warehouse"), "_Test Warehouse - _TC" ) diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index a403b14c54c..ec9e792d7d4 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -81,6 +81,7 @@ "warehouse", "target_warehouse", "quality_inspection", + "pick_serial_and_batch", "serial_and_batch_bundle", "batch_no", "incoming_rate", @@ -897,12 +898,18 @@ "options": "Serial and Batch Bundle", "print_hide": 1, "search_index": 1 + }, + { + "depends_on": "eval:parent.update_stock === 1", + "fieldname": "pick_serial_and_batch", + "fieldtype": "Button", + "label": "Pick Serial / Batch No" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:34:10.479329", + "modified": "2023-12-29 13:03:14.121298", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 132f8f2e29f..7d7b0cd4769 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -431,6 +431,8 @@ class DeliveryNote(SellingController): "Serial and Batch Bundle", ) + self.delete_auto_created_batches() + def update_stock_reservation_entries(self) -> None: """Updates Delivered Qty in Stock Reservation Entries.""" diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 933be53b078..3abd1d9e5ed 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1478,6 +1478,46 @@ class TestDeliveryNote(FrappeTestCase): returned_dn.reload() self.assertAlmostEqual(returned_dn.items[0].incoming_rate, 200.0) + def test_batch_with_non_stock_uom(self): + frappe.db.set_single_value( + "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 1 + ) + + item = make_item( + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TESTBATCH.#####", + "stock_uom": "Nos", + } + ) + if not frappe.db.exists("UOM Conversion Detail", {"parent": item.name, "uom": "Kg"}): + item.append("uoms", {"uom": "Kg", "conversion_factor": 5.0}) + item.save() + + item_code = item.name + + make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=5, basic_rate=100.0) + dn = create_delivery_note( + item_code=item_code, qty=1, rate=500, warehouse="_Test Warehouse - _TC", do_not_save=True + ) + dn.items[0].uom = "Kg" + dn.items[0].conversion_factor = 5.0 + + dn.save() + dn.submit() + + self.assertEqual(dn.items[0].stock_qty, 5.0) + voucher_detail_no = dn.items[0].name + delivered_batch_qty = frappe.db.get_value( + "Serial and Batch Bundle", {"voucher_detail_no": voucher_detail_no}, "total_qty" + ) + self.assertEqual(abs(delivered_batch_qty), 5.0) + + frappe.db.set_single_value( + "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 0 + ) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index a4fb5324ee6..774e5c657da 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -483,7 +483,11 @@ class SerialandBatchBundle(Document): if row.get("doctype") in ["Subcontracting Receipt Supplied Item"]: qty_field = "consumed_qty" - if abs(abs(flt(self.total_qty, precision)) - abs(flt(row.get(qty_field), precision))) > 0.01: + qty = row.get(qty_field) + if qty_field == "qty" and row.get("stock_qty"): + qty = row.get("stock_qty") + + if abs(abs(flt(self.total_qty, precision)) - abs(flt(qty, precision))) > 0.01: self.throw_error_message( f"Total quantity {abs(flt(self.total_qty))} in the Serial and Batch Bundle {bold(self.name)} does not match with the quantity {abs(flt(row.get(qty_field)))} for the Item {bold(self.item_code)} in the {self.voucher_type} # {self.voucher_no}" ) From d8cf994e94bec89897cde8ebf463637f850293f8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 31 Dec 2023 18:25:15 +0530 Subject: [PATCH 17/28] fix: enqueue demo data setup on setup complete (backport #39043) (#39044) fix: enqueue demo data setup on setup complete (#39043) (cherry picked from commit 877cc7255dfc80cbdd0dab971bcde81f5ebf57ba) Co-authored-by: Smit Vora --- erpnext/hooks.py | 1 + erpnext/setup/setup_wizard/setup_wizard.py | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index f6b6802d581..78bb2d2c270 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -37,6 +37,7 @@ welcome_email = "erpnext.setup.utils.welcome_email" # setup wizard setup_wizard_requires = "assets/erpnext/js/setup_wizard.js" setup_wizard_stages = "erpnext.setup.setup_wizard.setup_wizard.get_setup_stages" +setup_wizard_complete = "erpnext.setup.setup_wizard.setup_wizard.setup_demo" setup_wizard_test = "erpnext.setup.setup_wizard.test_setup_wizard.run_setup_wizard_test" before_install = [ diff --git a/erpnext/setup/setup_wizard/setup_wizard.py b/erpnext/setup/setup_wizard/setup_wizard.py index 2da107e4e94..9a49af2b10e 100644 --- a/erpnext/setup/setup_wizard/setup_wizard.py +++ b/erpnext/setup/setup_wizard/setup_wizard.py @@ -37,11 +37,6 @@ def get_setup_stages(args=None): {"fn": setup_defaults, "args": args, "fail_msg": _("Failed to setup defaults")}, ], }, - { - "status": _("Setting up demo data"), - "fail_msg": _("Failed to setup demo data"), - "tasks": [{"fn": setup_demo, "args": args, "fail_msg": _("Failed to setup demo data")}], - }, { "status": _("Wrapping up"), "fail_msg": _("Failed to login"), From 2e932754e079d29b88335fdc0d6d3d192d2c149c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 11:27:55 +0530 Subject: [PATCH 18/28] fix: take quantity into account when setting asset's gross purchase amt (backport #39056) (#39058) fix: take quantity into account when setting asset's gross purchase amt (#39056) fix: take quantity into account when setting asset's gross purchase amount (cherry picked from commit 0346f47c1d25e1d60de21dd0dc04e8c7c0b23938) Co-authored-by: Anand Baburajan --- .../doctype/purchase_invoice/purchase_invoice.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index db33271bccf..657723796cf 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1135,11 +1135,17 @@ class PurchaseInvoice(BuyingController): ) assets = frappe.db.get_all( - "Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code} + "Asset", + filters={"purchase_invoice": self.name, "item_code": item.item_code}, + fields=["name", "asset_quantity"], ) for asset in assets: - frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate)) - frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)) + frappe.db.set_value( + "Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate) * asset.asset_quantity + ) + frappe.db.set_value( + "Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate) * asset.asset_quantity + ) def make_stock_adjustment_entry( self, gl_entries, item, voucher_wise_stock_value, account_currency From 8ad7fafe2ac6ca4a4cc9da39ca6ddf06dea42e99 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 13:12:07 +0530 Subject: [PATCH 19/28] fix(DX): capture tracebacks with context (backport #39060) (#39063) fix(DX): capture tracebacks with context (#39060) (cherry picked from commit 510fdf7bf651788e549184c23d89c2387c1c2f10) Co-authored-by: Ankush Menat --- .../process_payment_reconciliation.py | 2 +- .../doctype/repost_payment_ledger/repost_payment_ledger.py | 2 +- erpnext/manufacturing/doctype/bom_creator/bom_creator.py | 2 +- .../doctype/closing_stock_balance/closing_stock_balance.py | 4 +--- .../doctype/repost_item_valuation/repost_item_valuation.py | 2 +- erpnext/stock/reorder_item.py | 2 +- erpnext/utilities/bulk_transaction.py | 4 ++-- 7 files changed, 8 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py index 67a7f90042b..bf1cb418fe4 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py @@ -440,7 +440,7 @@ def reconcile(doc: None | str = None) -> None: # Update the parent doc about the exception frappe.db.rollback() - traceback = frappe.get_traceback() + traceback = frappe.get_traceback(with_context=True) if traceback: message = "Traceback:
" + traceback frappe.db.set_value("Process Payment Reconciliation Log", log, "error_log", message) diff --git a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py index 38bc1a6fb82..2d10f7c7624 100644 --- a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py +++ b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py @@ -43,7 +43,7 @@ def start_payment_ledger_repost(docname=None): except Exception as e: frappe.db.rollback() - traceback = frappe.get_traceback() + traceback = frappe.get_traceback(with_context=True) if traceback: message = "Traceback:
" + traceback frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_error_log", message) diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py index bd010d9f9a6..1709a1f71af 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -251,7 +251,7 @@ class BOMCreator(Document): frappe.msgprint(_("BOMs created successfully")) except Exception: - traceback = frappe.get_traceback() + traceback = frappe.get_traceback(with_context=True) self.db_set( { "status": "Failed", diff --git a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py index 7db8522f639..f71d21dd0b8 100644 --- a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py +++ b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py @@ -149,6 +149,4 @@ def prepare_closing_stock_balance(name): doc.db_set("status", "Completed") except Exception as e: doc.db_set("status", "Failed") - traceback = frappe.get_traceback() - - frappe.log_error("Closing Stock Balance Failed", traceback, doc.doctype, doc.name) + doc.log_error(title="Closing Stock Balance Failed") 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 97ada06e1dd..79b8ee30cfe 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -294,7 +294,7 @@ def repost(doc): raise frappe.db.rollback() - traceback = frappe.get_traceback() + traceback = frappe.get_traceback(with_context=True) doc.log_error("Unable to repost item valuation") message = frappe.message_log.pop() if frappe.message_log else "" diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py index 907560826b3..a6f52f3731d 100644 --- a/erpnext/stock/reorder_item.py +++ b/erpnext/stock/reorder_item.py @@ -141,7 +141,7 @@ def create_material_request(material_requests): exceptions_list.extend(frappe.local.message_log) frappe.local.message_log = [] else: - exceptions_list.append(frappe.get_traceback()) + exceptions_list.append(frappe.get_traceback(with_context=True)) mr.log_error("Unable to create material request") diff --git a/erpnext/utilities/bulk_transaction.py b/erpnext/utilities/bulk_transaction.py index df21b61139a..679d5bd348e 100644 --- a/erpnext/utilities/bulk_transaction.py +++ b/erpnext/utilities/bulk_transaction.py @@ -62,7 +62,7 @@ def retry_failed_transactions(failed_docs: list | None): task(log.transaction_name, log.from_doctype, log.to_doctype) except Exception as e: frappe.db.rollback(save_point="before_creation_state") - update_log(log.name, "Failed", 1, str(frappe.get_traceback())) + update_log(log.name, "Failed", 1, str(frappe.get_traceback(with_context=True))) else: update_log(log.name, "Success", 1) @@ -86,7 +86,7 @@ def job(deserialized_data, from_doctype, to_doctype): fail_count += 1 create_log( doc_name, - str(frappe.get_traceback()), + str(frappe.get_traceback(with_context=True)), from_doctype, to_doctype, status="Failed", From e5d73c780bc5e272dd7c3db7db65665010ef6623 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 13:30:29 +0530 Subject: [PATCH 20/28] feat: voucher subtype for general ledger (#38822) * feat: voucher subtype for general ledger (#38822) * feat: add voucher subtype column to gle * feat: add logic to set voucher subtypes * feat: fetch voucher subtype in ledger report * fix: order of conditions (cherry picked from commit 47f7b65058e860e91c58dd12da6e48bd70ae60f6) # Conflicts: # erpnext/accounts/doctype/gl_entry/gl_entry.json * chore: resolve conflicts --------- Co-authored-by: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Co-authored-by: Deepesh Garg --- .../accounts/doctype/gl_entry/gl_entry.json | 8 +++++++- erpnext/accounts/doctype/gl_entry/gl_entry.py | 3 +++ .../report/general_ledger/general_ledger.py | 8 +++++++- erpnext/controllers/accounts_controller.py | 20 +++++++++++++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.json b/erpnext/accounts/doctype/gl_entry/gl_entry.json index de2a9db2c8f..0800971269b 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.json +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.json @@ -21,6 +21,7 @@ "against_voucher_type", "against_voucher", "voucher_type", + "voucher_subtype", "voucher_no", "voucher_detail_no", "project", @@ -278,13 +279,18 @@ "fieldtype": "Currency", "label": "Credit Amount in Transaction Currency", "options": "transaction_currency" + }, + { + "fieldname": "voucher_subtype", + "fieldtype": "Small Text", + "label": "Voucher Subtype" } ], "icon": "fa fa-list", "idx": 1, "in_create": 1, "links": [], - "modified": "2023-08-16 21:38:44.072267", + "modified": "2023-12-18 15:38:14.006208", "modified_by": "Administrator", "module": "Accounts", "name": "GL Entry", diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index f7dd29ab1c1..139f52696bc 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -39,6 +39,8 @@ class GLEntry(Document): account: DF.Link | None account_currency: DF.Link | None against: DF.Text | None + against_link: DF.DynamicLink | None + against_type: DF.Link | None against_voucher: DF.DynamicLink | None against_voucher_type: DF.Link | None company: DF.Link | None @@ -66,6 +68,7 @@ class GLEntry(Document): transaction_exchange_rate: DF.Float voucher_detail_no: DF.Data | None voucher_no: DF.DynamicLink | None + voucher_subtype: DF.SmallText | None voucher_type: DF.Link | None # end: auto-generated types diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 896c4c98002..707a76da901 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -200,7 +200,7 @@ def get_gl_entries(filters, accounting_dimensions): """ select name as gl_entry, posting_date, account, party_type, party, - voucher_type, voucher_no, {dimension_fields} + voucher_type, voucher_subtype, voucher_no, {dimension_fields} cost_center, project, {transaction_currency_fields} against_voucher_type, against_voucher, account_currency, against, is_opening, creation {select_fields} @@ -608,6 +608,12 @@ def get_columns(filters): columns += [ {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 120}, + { + "label": _("Voucher Subtype"), + "fieldname": "voucher_subtype", + "fieldtype": "Data", + "width": 180, + }, { "label": _("Voucher No"), "fieldname": "voucher_no", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 4aa8e86f317..aba10782220 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -879,6 +879,7 @@ class AccountsController(TransactionBase): "project": self.get("project"), "post_net_value": args.get("post_net_value"), "voucher_detail_no": args.get("voucher_detail_no"), + "voucher_subtype": self.get_voucher_subtype(), } ) @@ -934,6 +935,25 @@ class AccountsController(TransactionBase): return gl_dict + def get_voucher_subtype(self): + voucher_subtypes = { + "Journal Entry": "voucher_type", + "Payment Entry": "payment_type", + "Stock Entry": "stock_entry_type", + "Asset Capitalization": "entry_type", + } + if self.doctype in voucher_subtypes: + return self.get(voucher_subtypes[self.doctype]) + elif self.doctype == "Purchase Receipt" and self.is_return: + return "Purchase Return" + elif self.doctype == "Delivery Note" and self.is_return: + return "Sales Return" + elif (self.doctype == "Sales Invoice" and self.is_return) or self.doctype == "Purchase Invoice": + return "Credit Note" + elif (self.doctype == "Purchase Invoice" and self.is_return) or self.doctype == "Sales Invoice": + return "Debit Note" + return self.doctype + def get_value_in_transaction_currency(self, account_currency, args, field): if account_currency == self.get("currency"): return args.get(field + "_in_account_currency") From d85dad719806146032c10a1c4b032cfb22ebbe1d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 15:05:04 +0530 Subject: [PATCH 21/28] feat: group by Asset in Asset Depreciations and Balances report (backport #38923) (#39066) feat: group by Asset in Asset Depreciations and Balances report (#38923) feat: group by asset in asset depreciations and balances report (cherry picked from commit a9576f0cf6c7c2eb0711c3c8ffb0e1095c6da69e) Co-authored-by: Anand Baburajan --- .../asset_depreciations_and_balances.js | 19 +- .../asset_depreciations_and_balances.py | 210 +++++++++++++++++- 2 files changed, 215 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js index 5f78b779342..06fa9f3175d 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js @@ -25,11 +25,26 @@ frappe.query_reports["Asset Depreciations and Balances"] = { "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], "reqd": 1 }, + { + "fieldname":"group_by", + "label": __("Group By"), + "fieldtype": "Select", + "options": ["Asset Category", "Asset"], + "default": "Asset Category", + }, { "fieldname":"asset_category", "label": __("Asset Category"), "fieldtype": "Link", - "options": "Asset Category" - } + "options": "Asset Category", + "depends_on": "eval: doc.group_by == 'Asset Category'", + }, + { + "fieldname":"asset", + "label": __("Asset"), + "fieldtype": "Link", + "options": "Asset", + "depends_on": "eval: doc.group_by == 'Asset'", + }, ] } diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index bdc8d8504f8..48da17ab625 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -14,10 +14,17 @@ def execute(filters=None): def get_data(filters): + if filters.get("group_by") == "Asset Category": + return get_group_by_asset_category_data(filters) + elif filters.get("group_by") == "Asset": + return get_group_by_asset_data(filters) + + +def get_group_by_asset_category_data(filters): data = [] - asset_categories = get_asset_categories(filters) - assets = get_assets(filters) + asset_categories = get_asset_categories_for_grouped_by_category(filters) + assets = get_assets_for_grouped_by_category(filters) for asset_category in asset_categories: row = frappe._dict() @@ -38,6 +45,7 @@ def get_data(filters): if asset["asset_category"] == asset_category.get("asset_category", "") ) ) + row.accumulated_depreciation_as_on_to_date = ( flt(row.accumulated_depreciation_as_on_from_date) + flt(row.depreciation_amount_during_the_period) @@ -57,7 +65,7 @@ def get_data(filters): return data -def get_asset_categories(filters): +def get_asset_categories_for_grouped_by_category(filters): condition = "" if filters.get("asset_category"): condition += " and asset_category = %(asset_category)s" @@ -116,7 +124,105 @@ def get_asset_categories(filters): ) -def get_assets(filters): +def get_asset_details_for_grouped_by_category(filters): + condition = "" + if filters.get("asset"): + condition += " and name = %(asset)s" + return frappe.db.sql( + """ + SELECT name, + ifnull(sum(case when purchase_date < %(from_date)s then + case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then + gross_purchase_amount + else + 0 + end + else + 0 + end), 0) as cost_as_on_from_date, + ifnull(sum(case when purchase_date >= %(from_date)s then + gross_purchase_amount + else + 0 + end), 0) as cost_of_new_purchase, + ifnull(sum(case when ifnull(disposal_date, 0) != 0 + and disposal_date >= %(from_date)s + and disposal_date <= %(to_date)s then + case when status = "Sold" then + gross_purchase_amount + else + 0 + end + else + 0 + end), 0) as cost_of_sold_asset, + ifnull(sum(case when ifnull(disposal_date, 0) != 0 + and disposal_date >= %(from_date)s + and disposal_date <= %(to_date)s then + case when status = "Scrapped" then + gross_purchase_amount + else + 0 + end + else + 0 + end), 0) as cost_of_scrapped_asset + from `tabAsset` + where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {} + group by name + """.format( + condition + ), + { + "to_date": filters.to_date, + "from_date": filters.from_date, + "company": filters.company, + "asset": filters.get("asset"), + }, + as_dict=1, + ) + + +def get_group_by_asset_data(filters): + data = [] + + asset_details = get_asset_details_for_grouped_by_category(filters) + assets = get_assets_for_grouped_by_asset(filters) + + for asset_detail in asset_details: + row = frappe._dict() + # row.asset_category = asset_category + row.update(asset_detail) + + row.cost_as_on_to_date = ( + flt(row.cost_as_on_from_date) + + flt(row.cost_of_new_purchase) + - flt(row.cost_of_sold_asset) + - flt(row.cost_of_scrapped_asset) + ) + + row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", ""))) + + row.accumulated_depreciation_as_on_to_date = ( + flt(row.accumulated_depreciation_as_on_from_date) + + flt(row.depreciation_amount_during_the_period) + - flt(row.depreciation_eliminated_during_the_period) + ) + + row.net_asset_value_as_on_from_date = flt(row.cost_as_on_from_date) - flt( + row.accumulated_depreciation_as_on_from_date + ) + + row.net_asset_value_as_on_to_date = flt(row.cost_as_on_to_date) - flt( + row.accumulated_depreciation_as_on_to_date + ) + + data.append(row) + + return data + + +def get_assets_for_grouped_by_category(filters): condition = "" if filters.get("asset_category"): condition = " and a.asset_category = '{}'".format(filters.get("asset_category")) @@ -178,15 +284,93 @@ def get_assets(filters): ) +def get_assets_for_grouped_by_asset(filters): + condition = "" + if filters.get("asset"): + condition = " and a.name = '{}'".format(filters.get("asset")) + return frappe.db.sql( + """ + SELECT results.name as asset, + sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date, + sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period, + sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period + from (SELECT a.name as name, + ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then + gle.debit + else + 0 + end), 0) as accumulated_depreciation_as_on_from_date, + ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s + and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then + gle.debit + else + 0 + end), 0) as depreciation_eliminated_during_the_period, + ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s + and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then + gle.debit + else + 0 + end), 0) as depreciation_amount_during_the_period + from `tabGL Entry` gle + join `tabAsset` a on + gle.against_voucher = a.name + join `tabAsset Category Account` aca on + aca.parent = a.asset_category and aca.company_name = %(company)s + join `tabCompany` company on + company.name = %(company)s + where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0} + group by a.name + union + SELECT a.name as name, + ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then + 0 + else + a.opening_accumulated_depreciation + end), 0) as accumulated_depreciation_as_on_from_date, + ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then + a.opening_accumulated_depreciation + else + 0 + end), 0) as depreciation_eliminated_during_the_period, + 0 as depreciation_amount_during_the_period + from `tabAsset` a + where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0} + group by a.name) as results + group by results.name + """.format( + condition + ), + {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, + as_dict=1, + ) + + def get_columns(filters): - return [ - { - "label": _("Asset Category"), - "fieldname": "asset_category", - "fieldtype": "Link", - "options": "Asset Category", - "width": 120, - }, + columns = [] + + if filters.get("group_by") == "Asset Category": + columns.append( + { + "label": _("Asset Category"), + "fieldname": "asset_category", + "fieldtype": "Link", + "options": "Asset Category", + "width": 120, + } + ) + elif filters.get("group_by") == "Asset": + columns.append( + { + "label": _("Asset"), + "fieldname": "asset", + "fieldtype": "Link", + "options": "Asset", + "width": 120, + } + ) + + columns += [ { "label": _("Cost as on") + " " + formatdate(filters.day_before_from_date), "fieldname": "cost_as_on_from_date", @@ -254,3 +438,5 @@ def get_columns(filters): "width": 200, }, ] + + return columns From cbaa617d20211cee8c9f9c39c63aab402de8381b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 29 Dec 2023 13:35:18 +0530 Subject: [PATCH 22/28] refactor(perf): replace account subquery with 'in' condition (cherry picked from commit a517125d64a48b61a9b2d943b3a01bfe8ae87f41) --- .../customer_ledger_summary.py | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index 4765e3b318a..0464f99d200 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -3,7 +3,7 @@ import frappe -from frappe import _, scrub +from frappe import _, qb, scrub from frappe.utils import getdate, nowdate @@ -38,7 +38,6 @@ class PartyLedgerSummaryReport(object): """ Additional Columns for 'User Permission' based access control """ - from frappe import qb if self.filters.party_type == "Customer": self.territories = frappe._dict({}) @@ -365,13 +364,29 @@ class PartyLedgerSummaryReport(object): def get_party_adjustment_amounts(self): conditions = self.prepare_conditions() - income_or_expense = ( - "Expense Account" if self.filters.party_type == "Customer" else "Income Account" + account_type = "Expense Account" if self.filters.party_type == "Customer" else "Income Account" + income_or_expense_accounts = frappe.db.get_all( + "Account", filters={"account_type": account_type, "company": self.filters.company}, pluck="name" ) invoice_dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit" reverse_dr_or_cr = "credit" if self.filters.party_type == "Customer" else "debit" round_off_account = frappe.get_cached_value("Company", self.filters.company, "round_off_account") + gl = qb.DocType("GL Entry") + if not income_or_expense_accounts: + # prevent empty 'in' condition + income_or_expense_accounts.append("") + + accounts_query = ( + qb.from_(gl) + .select(gl.voucher_type, gl.voucher_no) + .where( + (gl.account.isin(income_or_expense_accounts)) + & (gl.posting_date.gte(self.filters.from_date)) + & (gl.posting_date.lte(self.filters.to_date)) + ) + ) + gl_entries = frappe.db.sql( """ select @@ -381,16 +396,15 @@ class PartyLedgerSummaryReport(object): where docstatus < 2 and is_cancelled = 0 and (voucher_type, voucher_no) in ( - select voucher_type, voucher_no from `tabGL Entry` gle, `tabAccount` acc - where acc.name = gle.account and acc.account_type = '{income_or_expense}' - and gle.posting_date between %(from_date)s and %(to_date)s and gle.docstatus < 2 + {accounts_query} ) and (voucher_type, voucher_no) in ( select voucher_type, voucher_no from `tabGL Entry` gle where gle.party_type=%(party_type)s and ifnull(party, '') != '' and gle.posting_date between %(from_date)s and %(to_date)s and gle.docstatus < 2 {conditions} ) - """.format( - conditions=conditions, income_or_expense=income_or_expense + """.format( + accounts_query=accounts_query, + conditions=conditions, ), self.filters, as_dict=True, @@ -414,7 +428,7 @@ class PartyLedgerSummaryReport(object): elif gle.party: parties.setdefault(gle.party, 0) parties[gle.party] += gle.get(reverse_dr_or_cr) - gle.get(invoice_dr_or_cr) - elif frappe.get_cached_value("Account", gle.account, "account_type") == income_or_expense: + elif frappe.get_cached_value("Account", gle.account, "account_type") == account_type: accounts.setdefault(gle.account, 0) accounts[gle.account] += gle.get(invoice_dr_or_cr) - gle.get(reverse_dr_or_cr) else: From 9fdf5c8091933de39431ffe2315ad6e24a6faf90 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 1 Jan 2024 14:46:15 +0530 Subject: [PATCH 23/28] fix: undefined error in Budget Variance and Profitability report 'Budget' and 'Budget Account' doesn't have support for dynamic dimension. It only supports hard-coded ones - Project and Cost Center (cherry picked from commit 92bc962f60adb3967a2ef378511822d53ff2f4a8) --- .../report/budget_variance_report/budget_variance_report.js | 4 ---- .../report/profitability_analysis/profitability_analysis.js | 5 ----- 2 files changed, 9 deletions(-) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js index 15aa265b56a..15088335e5c 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js @@ -88,7 +88,3 @@ frappe.query_reports["Budget Variance Report"] = { return value; } } - -erpnext.dimension_filters.forEach((dimension) => { - frappe.query_reports["Budget Variance Report"].filters[4].options.push(dimension["document_type"]); -}); diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js index b6bbd979ed9..5dd3617a926 100644 --- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js @@ -117,8 +117,3 @@ frappe.query_reports["Profitability Analysis"] = { "parent_field": "parent_account", "initial_depth": 3 } - -erpnext.dimension_filters.forEach((dimension) => { - frappe.query_reports["Profitability Analysis"].filters[1].options.push(dimension["document_type"]); -}); - From 21f90011bcbe00ba545cd9460a5cab8965c6f886 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 1 Jan 2024 17:32:33 +0530 Subject: [PATCH 24/28] fix: select options should dynamically load dimensions (cherry picked from commit 1a9e091d12f9149638f3774b9def0fa80370b59b) --- .../budget_variance_report.js | 63 ++++++++++++++----- 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js index 15088335e5c..9c356bf28ea 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js @@ -2,7 +2,44 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["Budget Variance Report"] = { - "filters": [ + "filters": get_filters(), + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname.includes(__("variance"))) { + + if (data[column.fieldname] < 0) { + value = "" + value + ""; + } + else if (data[column.fieldname] > 0) { + value = "" + value + ""; + } + } + + return value; + } +} +function get_filters() { + function get_dimensions() { + let result = []; + frappe.call({ + method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions", + args: { + 'with_cost_center_and_project': true + }, + async: false, + callback: function(r) { + if(!r.exc) { + result = r.message[0].map(elem => elem.document_type); + } + } + }); + return result; + } + + let budget_against_options = get_dimensions(); + + let filters = [ { fieldname: "from_fiscal_year", label: __("From Fiscal Year"), @@ -44,9 +81,13 @@ frappe.query_reports["Budget Variance Report"] = { fieldname: "budget_against", label: __("Budget Against"), fieldtype: "Select", - options: ["Cost Center", "Project"], + options: budget_against_options, default: "Cost Center", reqd: 1, + get_data: function() { + console.log(this.options); + return ["Emacs", "Rocks"]; + }, on_change: function() { frappe.query_report.set_filter_value("budget_against_filter", []); frappe.query_report.refresh(); @@ -71,20 +112,8 @@ frappe.query_reports["Budget Variance Report"] = { fieldtype: "Check", default: 0, }, - ], - "formatter": function (value, row, column, data, default_formatter) { - value = default_formatter(value, row, column, data); + ] - if (column.fieldname.includes(__("variance"))) { - - if (data[column.fieldname] < 0) { - value = "" + value + ""; - } - else if (data[column.fieldname] > 0) { - value = "" + value + ""; - } - } - - return value; - } + return filters; } + From 73c0f3703cd57100a86b750bbfd1a37a2878d3b9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 19:28:29 +0530 Subject: [PATCH 25/28] fix: Unable to save Sales Invoice (#39059) fix: Unable to save Sales Invoice (#39059) (cherry picked from commit bacf2b74312a4dfeb95ccc697047df2bc4d5f92e) Co-authored-by: Deepesh Garg --- erpnext/regional/united_arab_emirates/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index efeaeed324c..634a152147d 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -25,7 +25,7 @@ def update_itemised_tax_data(doc): # dont even bother checking in item tax template as it contains both input and output accounts - double the tax rate item_code = row.item_code or row.item_name if itemised_tax.get(item_code): - for tax in itemised_tax.get(row.item_code).values(): + for tax in itemised_tax.get(item_code).values(): _tax_rate = flt(tax.get("tax_rate", 0), row.precision("tax_rate")) tax_amount += flt((row.net_amount * _tax_rate) / 100, row.precision("tax_amount")) tax_rate += _tax_rate From 2e0abbd2748eec7020708a06b9b30486f177e75a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 2 Jan 2024 13:28:43 +0530 Subject: [PATCH 26/28] fix: on cancellation of document cancel the serial and batch bundle (backport #39076) (#39082) fix: on cancellation of document cancel the serial and batch bundle (#39076) (cherry picked from commit 6e5484ea031027de4e6a897b02bd0f2e7b8adda9) Co-authored-by: rohitwaghchaure --- .../doctype/work_order/test_work_order.py | 2 -- .../serial_and_batch_bundle.py | 16 +++++++++++- .../test_serial_and_batch_bundle.py | 25 +++++++++++++++++-- erpnext/stock/serial_batch_bundle.py | 6 +++++ 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 91d9430712f..19d8b9a1b2d 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -920,11 +920,9 @@ class TestWorkOrder(FrappeTestCase): "Test RM Item 2 for Scrap Item Test", ] - from_time = add_days(now(), -1) job_cards = frappe.get_all( "Job Card Time Log", fields=["distinct parent as name", "docstatus"], - filters={"from_time": (">", from_time)}, order_by="creation asc", ) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 774e5c657da..218406f56fd 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -85,6 +85,7 @@ class SerialandBatchBundle(Document): # end: auto-generated types def validate(self): + self.reset_serial_batch_bundle() self.set_batch_no() self.validate_serial_and_batch_no() self.validate_duplicate_serial_and_batch_no() @@ -100,6 +101,15 @@ class SerialandBatchBundle(Document): self.set_incoming_rate() self.calculate_qty_and_amount() + def reset_serial_batch_bundle(self): + if self.is_new() and self.amended_from: + for field in ["is_cancelled", "is_rejected"]: + if self.get(field): + self.set(field, 0) + + if self.voucher_detail_no: + self.voucher_detail_no = None + def set_batch_no(self): if self.has_serial_no and self.has_batch_no: serial_nos = [d.serial_no for d in self.entries if d.serial_no] @@ -914,7 +924,11 @@ def upload_csv_file(item_code, file_path): def get_serial_batch_from_csv(item_code, file_path): - file_path = frappe.get_site_path() + file_path + if "private" in file_path: + file_path = frappe.get_site_path() + file_path + else: + file_path = frappe.get_site_path() + "/public" + file_path + serial_nos = [] batch_nos = [] diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index 478cfa4d1c7..19757479a5a 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -427,11 +427,12 @@ class TestSerialandBatchBundle(FrappeTestCase): from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt item = make_item( + "Test Serial and Batch Bundle Company Item", properties={ "has_serial_no": 1, "serial_no_series": "TT-SER-VAL-.#####", - } - ) + }, + ).name pr = make_purchase_receipt( item_code=item, @@ -460,6 +461,26 @@ class TestSerialandBatchBundle(FrappeTestCase): sn_doc = add_serial_batch_ledgers(entries, item_row, pr, "_Test Warehouse - _TC") self.assertEqual(sn_doc.company, "_Test Company") + def test_auto_cancel_serial_and_batch(self): + item_code = make_item( + properties={"has_serial_no": 1, "serial_no_series": "ATC-TT-SER-VAL-.#####"} + ).name + + se = make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=5, + rate=500, + ) + + bundle = se.items[0].serial_and_batch_bundle + docstatus = frappe.db.get_value("Serial and Batch Bundle", bundle, "docstatus") + self.assertEqual(docstatus, 1) + + se.cancel() + docstatus = frappe.db.get_value("Serial and Batch Bundle", bundle, "docstatus") + self.assertEqual(docstatus, 2) + def get_batch_from_bundle(bundle): from erpnext.stock.serial_batch_bundle import get_batch_nos diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index a1874b84dc7..39df2279cd2 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -242,6 +242,12 @@ class SerialBatchBundle: if self.item_details.has_batch_no == 1: self.update_batch_qty() + if self.sle.is_cancelled and self.sle.serial_and_batch_bundle: + self.cancel_serial_and_batch_bundle() + + def cancel_serial_and_batch_bundle(self): + frappe.get_cached_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle).cancel() + def submit_serial_and_batch_bundle(self): doc = frappe.get_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle) self.validate_actual_qty(doc) From 57bcf9f56890f3e3f2cb30ec5c54279fc00c8dca Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 2 Jan 2024 14:43:10 +0530 Subject: [PATCH 27/28] perf: index item_code in bom explosion item (backport #39085) (#39088) perf: index item_code in bom explosion item (#39085) (cherry picked from commit 739434b72779fe12aed86a0a61d352948c81b680) Co-authored-by: Ankush Menat --- .../doctype/bom_explosion_item/bom_explosion_item.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json index c75ac32cd12..27ecd57b873 100644 --- a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json +++ b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json @@ -37,7 +37,8 @@ "oldfieldname": "item_code", "oldfieldtype": "Link", "options": "Item", - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "item_name", @@ -170,7 +171,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:35:40.856895", + "modified": "2024-01-02 13:49:36.211586", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Explosion Item", From 458064f8a16507fcfb5684c783ebb9dc85c4ee01 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 3 Jan 2024 10:51:34 +0530 Subject: [PATCH 28/28] feat: Against voucher filter in General Ledger (#39102) --- erpnext/accounts/report/general_ledger/general_ledger.js | 5 +++++ erpnext/accounts/report/general_ledger/general_ledger.py | 3 +++ erpnext/controllers/accounts_controller.py | 6 ++++++ 3 files changed, 14 insertions(+) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 4cb443cf920..79b5e4d9ec8 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -52,6 +52,11 @@ frappe.query_reports["General Ledger"] = { frappe.query_report.set_filter_value('group_by', "Group by Voucher (Consolidated)"); } }, + { + "fieldname":"against_voucher_no", + "label": __("Against Voucher No"), + "fieldtype": "Data", + }, { "fieldtype": "Break", }, diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 707a76da901..ff6cd9f4b25 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -238,6 +238,9 @@ def get_conditions(filters): if filters.get("voucher_no"): conditions.append("voucher_no=%(voucher_no)s") + if filters.get("against_voucher_no"): + conditions.append("against_voucher=%(against_voucher_no)s") + if filters.get("voucher_no_not_in"): conditions.append("voucher_no not in %(voucher_no_not_in)s") diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index aba10782220..9c3135d6b10 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -933,6 +933,12 @@ class AccountsController(TransactionBase): } ) + if not args.get("against_voucher_type") and self.get("against_voucher_type"): + gl_dict.update({"against_voucher_type": self.get("against_voucher_type")}) + + if not args.get("against_voucher") and self.get("against_voucher"): + gl_dict.update({"against_voucher": self.get("against_voucher")}) + return gl_dict def get_voucher_subtype(self):