From df71907be453a481a43d2c62a5c076602ab8af07 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 8 Feb 2022 17:36:28 +0530 Subject: [PATCH 001/121] fix: Fetch valuation rate --- .../doctype/asset_repair/asset_repair.js | 17 ++++++++++ .../doctype/asset_repair/asset_repair.py | 33 +++++++++++++++++++ .../asset_repair_consumed_item.json | 3 +- 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index d554d52a718..7bd3a7246d9 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -68,6 +68,23 @@ frappe.ui.form.on('Asset Repair', { }); frappe.ui.form.on('Asset Repair Consumed Item', { + item_code: function(frm, cdt, cdn) { + var row = locals[cdt][cdn]; + + frappe.call ({ + method: "erpnext.assets.doctype.asset_repair.asset_repair.get_valuation_rate", + args: { + "item_code": row.item_code, + "warehouse": frm.doc.warehouse + }, + callback: function(r) { + if(r.message) { + frappe.model.set_value(cdt, cdn, 'valuation_rate', r.message[0]); + } + } + }); + }, + consumed_quantity: function(frm, cdt, cdn) { var row = locals[cdt][cdn]; frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate); diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 36848e9f15c..86a63fd1bca 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -256,3 +256,36 @@ class AssetRepair(AccountsController): def get_downtime(failure_date, completion_date): downtime = time_diff_in_hours(completion_date, failure_date) return round(downtime, 2) + +@frappe.whitelist() +def get_valuation_rate(item_code, warehouse): + last_valuation_rate = frappe.get_all( + "Stock Ledger Entry", + filters = { + "item_code": item_code, + "warehouse": warehouse, + "valuation_rate": [">=", 0], + "docstatus": ["<", 2] + }, + pluck = "valuation_rate", + order_by = "posting_date desc, posting_time desc, name desc" + ) + + if last_valuation_rate: + return last_valuation_rate + else: + valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate") + + if not valuation_rate: + # try Item Standard rate + valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate") + + if not valuation_rate: + # try in price list + valuation_rate = frappe.db.get_value( + "Item Price", + dict(item_code=item_code, buying=1), + "price_list_rate" + ) + + return valuation_rate diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json index f63add12356..3c850c82653 100644 --- a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json +++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json @@ -13,7 +13,6 @@ ], "fields": [ { - "fetch_from": "item.valuation_rate", "fieldname": "valuation_rate", "fieldtype": "Currency", "in_list_view": 1, @@ -49,7 +48,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-11-11 18:23:00.492483", + "modified": "2022-02-08 17:37:20.028290", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair Consumed Item", From e1a9b61103e27099696ae4b186a6575b28c16b06 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 8 Feb 2022 17:59:06 +0530 Subject: [PATCH 002/121] fix: Pass value instead of array --- erpnext/assets/doctype/asset_repair/asset_repair.js | 2 +- erpnext/assets/doctype/asset_repair/asset_repair.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 7bd3a7246d9..18454fe3678 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -79,7 +79,7 @@ frappe.ui.form.on('Asset Repair Consumed Item', { }, callback: function(r) { if(r.message) { - frappe.model.set_value(cdt, cdn, 'valuation_rate', r.message[0]); + frappe.model.set_value(cdt, cdn, 'valuation_rate', r.message); } } }); diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 86a63fd1bca..d5e3d3c811a 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -272,7 +272,7 @@ def get_valuation_rate(item_code, warehouse): ) if last_valuation_rate: - return last_valuation_rate + return last_valuation_rate[0] else: valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate") From fd5ce9ca10e8da650455a4eb62a620b7f8729ef7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 15 Feb 2022 16:12:03 +0530 Subject: [PATCH 003/121] fix: dont attempt to set batch number if item doesn't have batch no (#29812) (#29813) This causes other triggers and unnecessary changes (e.g. price list) (cherry picked from commit f89a64db486b46ac756d5ba62faee87f28baf889) Co-authored-by: Ankush Menat --- erpnext/selling/sales_common.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index e2c752cecfa..8ef71ca86a1 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -227,11 +227,11 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ }, callback:function(r){ if (in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) { - if (doc.doctype === 'Sales Invoice' && (!doc.update_stock)) return; - - me.set_batch_number(cdt, cdn); - me.batch_no(doc, cdt, cdn); + if (has_batch_no) { + me.set_batch_number(cdt, cdn); + me.batch_no(doc, cdt, cdn); + } } } }); From 3b4e4955f72c4803f99fda70b6b38faca85f25b3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 16 Feb 2022 11:32:40 +0530 Subject: [PATCH 004/121] fix: task status loop (backport #26006) (#29821) Co-authored-by: Rucha Mahabal Co-authored-by: Jannat Patel <31363128+pateljannat@users.noreply.github.com> --- erpnext/projects/doctype/task/task.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 6d3f20f271a..8fa0538f360 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -76,9 +76,6 @@ class Task(NestedSet): if flt(self.progress or 0) > 100: frappe.throw(_("Progress % for a task cannot be more than 100.")) - if flt(self.progress) == 100: - self.status = 'Completed' - if self.status == 'Completed': self.progress = 100 From dccf2a3dec89af28c3bdb143f6761d1985ab4719 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 16 Feb 2022 11:52:03 +0530 Subject: [PATCH 005/121] test: set correct DocType (#29819) (#29820) (cherry picked from commit 08a391fa88d97ab003a00e58eb47fb263923adc1) Co-authored-by: Sagar Vora --- .../test_supplier_scorecard.py | 8 ++-- .../doctype/salary_slip/test_salary_slip.py | 38 +++++++++++-------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py index 49e33517e6f..7908c35cbbe 100644 --- a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py +++ b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py @@ -49,7 +49,7 @@ valid_scorecard = [ "min_grade":0.0,"name":"Very Poor", "prevent_rfqs":1, "notify_supplier":0, - "doctype":"Supplier Scorecard Standing", + "doctype":"Supplier Scorecard Scoring Standing", "max_grade":30.0, "prevent_pos":1, "warn_pos":0, @@ -65,7 +65,7 @@ valid_scorecard = [ "name":"Poor", "prevent_rfqs":1, "notify_supplier":0, - "doctype":"Supplier Scorecard Standing", + "doctype":"Supplier Scorecard Scoring Standing", "max_grade":50.0, "prevent_pos":0, "warn_pos":0, @@ -81,7 +81,7 @@ valid_scorecard = [ "name":"Average", "prevent_rfqs":0, "notify_supplier":0, - "doctype":"Supplier Scorecard Standing", + "doctype":"Supplier Scorecard Scoring Standing", "max_grade":80.0, "prevent_pos":0, "warn_pos":0, @@ -97,7 +97,7 @@ valid_scorecard = [ "name":"Excellent", "prevent_rfqs":0, "notify_supplier":0, - "doctype":"Supplier Scorecard Standing", + "doctype":"Supplier Scorecard Scoring Standing", "max_grade":100.0, "prevent_pos":0, "warn_pos":0, diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 60fc1f00dd5..4249fa76c71 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -6,6 +6,7 @@ import random import unittest import frappe +from frappe.model.document import Document from frappe.utils import ( add_days, add_months, @@ -692,20 +693,25 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): def make_salary_component(salary_components, test_tax, company_list=None): for salary_component in salary_components: - if not frappe.db.exists('Salary Component', salary_component["salary_component"]): - if test_tax: - if salary_component["type"] == "Earning": - salary_component["is_tax_applicable"] = 1 - elif salary_component["salary_component"] == "TDS": - salary_component["variable_based_on_taxable_salary"] = 1 - salary_component["amount_based_on_formula"] = 0 - salary_component["amount"] = 0 - salary_component["formula"] = "" - salary_component["condition"] = "" - salary_component["doctype"] = "Salary Component" - salary_component["salary_component_abbr"] = salary_component["abbr"] - frappe.get_doc(salary_component).insert() - get_salary_component_account(salary_component["salary_component"], company_list) + if frappe.db.exists('Salary Component', salary_component["salary_component"]): + continue + + if test_tax: + if salary_component["type"] == "Earning": + salary_component["is_tax_applicable"] = 1 + elif salary_component["salary_component"] == "TDS": + salary_component["variable_based_on_taxable_salary"] = 1 + salary_component["amount_based_on_formula"] = 0 + salary_component["amount"] = 0 + salary_component["formula"] = "" + salary_component["condition"] = "" + + salary_component["salary_component_abbr"] = salary_component["abbr"] + doc = frappe.new_doc("Salary Component") + doc.update(salary_component) + doc.insert() + + get_salary_component_account(doc, company_list) def get_salary_component_account(sal_comp, company_list=None): company = erpnext.get_default_company() @@ -713,7 +719,9 @@ def get_salary_component_account(sal_comp, company_list=None): if company_list and company not in company_list: company_list.append(company) - sal_comp = frappe.get_doc("Salary Component", sal_comp) + if not isinstance(sal_comp, Document): + sal_comp = frappe.get_doc("Salary Component", sal_comp) + if not sal_comp.get("accounts"): for d in company_list: company_abbr = frappe.get_cached_value('Company', d, 'abbr') From 765ade74e6c77a74e0cc8e670b7b06e518a12646 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 16 Feb 2022 13:04:20 +0530 Subject: [PATCH 006/121] fix: add supported currencies (#29805) (#29822) (cherry picked from commit a26183e205effa11d1fae7a3d6cb96c7db100e07) Co-authored-by: Kenneth Sequeira <33246109+kennethsequeira@users.noreply.github.com> --- .../doctype/gocardless_settings/gocardless_settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py index e242ace60f7..1499d258fe6 100644 --- a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py +++ b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py @@ -12,7 +12,7 @@ from six.moves.urllib.parse import urlencode class GoCardlessSettings(Document): - supported_currencies = ["EUR", "DKK", "GBP", "SEK"] + supported_currencies = ["EUR", "DKK", "GBP", "SEK", "AUD", "NZD", "CAD", "USD"] def validate(self): self.initialize_client() @@ -79,7 +79,7 @@ class GoCardlessSettings(Document): def validate_transaction_currency(self, currency): if currency not in self.supported_currencies: - frappe.throw(_("Please select another payment method. Stripe does not support transactions in currency '{0}'").format(currency)) + frappe.throw(_("Please select another payment method. Go Cardless does not support transactions in currency '{0}'").format(currency)) def get_payment_url(self, **kwargs): return get_url("./integrations/gocardless_checkout?{0}".format(urlencode(kwargs))) From 251656ab52f200517ee7a8c8157bba7ac86d3f7e Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 4 Feb 2022 17:34:56 +0530 Subject: [PATCH 007/121] perf: Weed out disabled variants via sql query instead of pythonic looping separately - If the number of variants are large (almost 2lakhs), the query to get variants and attribute data takes time - If the no.of disabled attributes is large as well, the list comprehension weeding out disabled variants takes forever - We dont need to loop over the variants data so many times - Avoid any `if a in list(b)` is best when the iterables have tremendous data (cherry picked from commit 26bd3053d190df07e8b75e0e86203050047b25cf) --- .../variant_selector/item_variants_cache.py | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/erpnext/e_commerce/variant_selector/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py index bb6b3ef37fe..9b22255d9aa 100644 --- a/erpnext/e_commerce/variant_selector/item_variants_cache.py +++ b/erpnext/e_commerce/variant_selector/item_variants_cache.py @@ -66,25 +66,39 @@ class ItemVariantsCacheManager: ) ] - # join with Website Item - item_variants_data = frappe.get_all( - 'Item Variant Attribute', - {'variant_of': parent_item_code}, - ['parent', 'attribute', 'attribute_value'], - order_by='name', - as_list=1 + # Get Variants and tehir Attributes that are not disabled + iva = frappe.qb.DocType("Item Variant Attribute") + item = frappe.qb.DocType("Item") + query = ( + frappe.qb.from_(iva) + .join(item).on(item.name == iva.parent) + .select( + iva.parent, iva.attribute, iva.attribute_value + ).where( + (iva.variant_of == parent_item_code) + & (item.disabled == 0) + ).orderby(iva.name) ) + item_variants_data = query.run() - disabled_items = set( - [i.name for i in frappe.db.get_all('Item', {'disabled': 1})] - ) + # item_variants_data = frappe.get_all( + # 'Item Variant Attribute', + # {'variant_of': parent_item_code}, + # ['parent', 'attribute', 'attribute_value'], + # order_by='name', + # as_list=1 + # ) + + # disabled_items = set( + # [i.name for i in frappe.db.get_all('Item', {'disabled': 1})] + # ) attribute_value_item_map = frappe._dict() item_attribute_value_map = frappe._dict() # dont consider variants that are disabled # pull all other variants - item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items] + # item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items] for row in item_variants_data: item_code, attribute, attribute_value = row From ca8986028d32f1653bda91b224b0aa3533f61284 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 8 Feb 2022 11:15:19 +0530 Subject: [PATCH 008/121] fix: Trim spaces from attributes (multi-variant creation) & explicit method for building cache - Multiple Item Variants creation fails due to extra spaces in attributes from popup. Clean them before passing to server side - Mention explicit method to build variants cache to avoid ambiguity between old method path (pre-refactor) (cherry picked from commit a64228741d065f7ac33b3208d3a704616250f925) --- erpnext/e_commerce/variant_selector/item_variants_cache.py | 5 ++++- erpnext/stock/doctype/item/item.js | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/e_commerce/variant_selector/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py index 9b22255d9aa..3aefc446c2c 100644 --- a/erpnext/e_commerce/variant_selector/item_variants_cache.py +++ b/erpnext/e_commerce/variant_selector/item_variants_cache.py @@ -138,4 +138,7 @@ def build_cache(item_code): def enqueue_build_cache(item_code): if frappe.cache().hget('item_cache_build_in_progress', item_code): return - frappe.enqueue(build_cache, item_code=item_code, queue='long') + frappe.enqueue( + "erpnext.e_commerce.variant_selector.item_variants_cache.build_cache", + item_code=item_code, queue='long' + ) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index da371d968c9..954e061caa6 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -545,7 +545,7 @@ $.extend(erpnext.item, { let selected_attributes = {}; me.multiple_variant_dialog.$wrapper.find('.form-column').each((i, col) => { if(i===0) return; - let attribute_name = $(col).find('label').html(); + let attribute_name = $(col).find('label').html().trim(); selected_attributes[attribute_name] = []; let checked_opts = $(col).find('.checkbox input'); checked_opts.each((i, opt) => { From b75e98228611d1afd1fa5f6aefd557211ef6a8be Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 8 Feb 2022 12:02:02 +0530 Subject: [PATCH 009/121] chore: Fix flaky test `test_exact_match_with_price` - Clear cart settings in cache to avoid stale values (cherry picked from commit 4f5a0b8941101f759f2d1af33d952a1bfdfd3cf4) --- erpnext/e_commerce/variant_selector/test_variant_selector.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/e_commerce/variant_selector/test_variant_selector.py b/erpnext/e_commerce/variant_selector/test_variant_selector.py index 0412abb4d9f..967be838e67 100644 --- a/erpnext/e_commerce/variant_selector/test_variant_selector.py +++ b/erpnext/e_commerce/variant_selector/test_variant_selector.py @@ -106,6 +106,8 @@ class TestVariantSelector(ERPNextTestCase): }) make_web_item_price(item_code="Test-Tshirt-Temp-S-R", price_list_rate=100) + + frappe.local.shopping_cart_settings = None # clear cached settings values next_values = get_next_attribute_and_values( "Test-Tshirt-Temp", selected_attributes={"Test Size": "Small", "Test Colour": "Red"} From de834a83627893fa3ba19572c577aa621bbf8816 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 16 Feb 2022 12:41:39 +0530 Subject: [PATCH 010/121] chore: Remove commented out code (cherry picked from commit 29c576e144489072c992e9b5bdfe4c9359639ef8) --- .../variant_selector/item_variants_cache.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/erpnext/e_commerce/variant_selector/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py index 3aefc446c2c..3107c019e62 100644 --- a/erpnext/e_commerce/variant_selector/item_variants_cache.py +++ b/erpnext/e_commerce/variant_selector/item_variants_cache.py @@ -81,25 +81,9 @@ class ItemVariantsCacheManager: ) item_variants_data = query.run() - # item_variants_data = frappe.get_all( - # 'Item Variant Attribute', - # {'variant_of': parent_item_code}, - # ['parent', 'attribute', 'attribute_value'], - # order_by='name', - # as_list=1 - # ) - - # disabled_items = set( - # [i.name for i in frappe.db.get_all('Item', {'disabled': 1})] - # ) - attribute_value_item_map = frappe._dict() item_attribute_value_map = frappe._dict() - # dont consider variants that are disabled - # pull all other variants - # item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items] - for row in item_variants_data: item_code, attribute, attribute_value = row # (attr, value) => [item1, item2] From bb119c8e52ea5e58bd1969e6c062706ce1fafa6f Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Feb 2022 12:40:39 +0530 Subject: [PATCH 011/121] fix: Update SO via Work Order made from MR (attached to SO) - Add SO Item reference in WO from MR (that was made from SO) (cherry picked from commit 18731622c43f3b8f7d792d4bb4139eb7cdda39d9) --- erpnext/stock/doctype/material_request/material_request.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index d85970665e1..bdbf5e47f03 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -534,6 +534,7 @@ def raise_work_orders(material_request): "stock_uom": d.stock_uom, "expected_delivery_date": d.schedule_date, "sales_order": d.sales_order, + "sales_order_item": d.get("sales_order_item"), "bom_no": get_item_details(d.item_code).bom_no, "material_request": mr.name, "material_request_item": d.name, From 2ce07eff7135af17bc773da08a48e1e37697fa0e Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Feb 2022 13:38:15 +0530 Subject: [PATCH 012/121] test: SO > MR > WO flow (cherry picked from commit f9d52e73469ea298e3a2d39d893f2da5e6baf9aa) --- .../doctype/sales_order/test_sales_order.py | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index c9d857d951f..41ba1126dea 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -6,7 +6,7 @@ import json import frappe import frappe.permissions from frappe.core.doctype.user_permission.test_user_permission import create_user -from frappe.utils import add_days, flt, getdate, nowdate +from frappe.utils import add_days, flt, getdate, nowdate, today from erpnext.controllers.accounts_controller import update_child_qty_rate from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order @@ -1295,6 +1295,48 @@ class TestSalesOrder(ERPNextTestCase): so.load_from_db() self.assertEqual(so.billing_status, 'Fully Billed') + def test_so_back_updated_from_wo_via_mr(self): + "SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO." + from erpnext.stock.doctype.material_request.material_request import raise_work_orders + from erpnext.manufacturing.doctype.work_order.work_order import ( + make_stock_entry as make_se_from_wo, + ) + + so = make_sales_order(item_list=[{"item_code": "_Test FG Item","qty": 2, "rate":100}]) + + mr = make_material_request(so.name) + mr.material_request_type = "Manufacture" + mr.schedule_date = today() + mr.submit() + + # WO from MR + wo_name = raise_work_orders(mr.name)[0] + wo = frappe.get_doc("Work Order", wo_name) + wo.wip_warehouse = "Work In Progress - _TC" + wo.skip_transfer = True + + self.assertEqual(wo.sales_order, so.name) + self.assertEqual(wo.sales_order_item, so.items[0].name) + + wo.submit() + make_stock_entry(item_code="_Test Item", # Stock RM + target="Work In Progress - _TC", + qty=4, basic_rate=100 + ) + make_stock_entry(item_code="_Test Item Home Desktop 100", # Stock RM + target="Work In Progress - _TC", + qty=4, basic_rate=100 + ) + + se = frappe.get_doc(make_se_from_wo(wo.name, "Manufacture", 2)) + se.submit() # Finish WO + + mr.reload() + wo.reload() + so.reload() + self.assertEqual(so.items[0].work_order_qty, wo.produced_qty) + self.assertEqual(mr.status, "Manufactured") + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") accounts_settings.automatically_fetch_payment_terms = enable From 98ce6a1c8de11cbde34ced2ac12df0824fa9a034 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Feb 2022 14:20:54 +0530 Subject: [PATCH 013/121] chore: Patch to update SO work_order_qty and Linter fix (cherry picked from commit 0ca58d762715fd10c751c4497f3037908f4dfb20) # Conflicts: # erpnext/patches.txt --- erpnext/patches.txt | 7 +++- .../v14_0/set_work_order_qty_in_so_from_mr.py | 36 +++++++++++++++++++ .../doctype/sales_order/test_sales_order.py | 2 +- 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index b3366f3cba7..013031cf913 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -349,4 +349,9 @@ erpnext.patches.v13_0.update_sane_transfer_against erpnext.patches.v13_0.enable_provisional_accounting erpnext.patches.v13_0.update_disbursement_account erpnext.patches.v13_0.update_reserved_qty_closed_wo -erpnext.patches.v13_0.amazon_mws_deprecation_warning \ No newline at end of file +<<<<<<< HEAD +erpnext.patches.v13_0.amazon_mws_deprecation_warning +======= +erpnext.patches.v14_0.delete_amazon_mws_doctype +erpnext.patches.v14_0.set_work_order_qty_in_so_from_mr +>>>>>>> 0ca58d7627 (chore: Patch to update SO work_order_qty and Linter fix) diff --git a/erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py b/erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py new file mode 100644 index 00000000000..f097ab9297f --- /dev/null +++ b/erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py @@ -0,0 +1,36 @@ +import frappe + + +def execute(): + """ + 1. Get submitted Work Orders with MR, MR Item and SO set + 2. Get SO Item detail from MR Item detail in WO, and set in WO + 3. Update work_order_qty in SO + """ + work_order = frappe.qb.DocType("Work Order") + query = ( + frappe.qb.from_(work_order) + .select( + work_order.name, work_order.produced_qty, + work_order.material_request, + work_order.material_request_item, + work_order.sales_order + ).where( + (work_order.material_request.isnotnull()) + & (work_order.material_request_item.isnotnull()) + & (work_order.sales_order.isnotnull()) + & (work_order.docstatus == 1) + & (work_order.produced_qty > 0) + ) + ) + results = query.run(as_dict=True) + + for row in results: + so_item = frappe.get_value( + "Material Request Item", row.material_request_item, "sales_order_item" + ) + frappe.db.set_value("Work Order", row.name, "sales_order_item", so_item) + + if so_item: + wo = frappe.get_doc("Work Order", row.name) + wo.update_work_order_qty_in_so() diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 41ba1126dea..788a8350caa 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1297,10 +1297,10 @@ class TestSalesOrder(ERPNextTestCase): def test_so_back_updated_from_wo_via_mr(self): "SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO." - from erpnext.stock.doctype.material_request.material_request import raise_work_orders from erpnext.manufacturing.doctype.work_order.work_order import ( make_stock_entry as make_se_from_wo, ) + from erpnext.stock.doctype.material_request.material_request import raise_work_orders so = make_sales_order(item_list=[{"item_code": "_Test FG Item","qty": 2, "rate":100}]) From bc2b4411ee2dea9afd527117f80f4d2a71bcdd47 Mon Sep 17 00:00:00 2001 From: ruthra Date: Tue, 4 Jan 2022 15:53:41 +0530 Subject: [PATCH 014/121] feat: Payment Terms Status report - calculate status at runtime for payment terms based on invoices - invoices are used in FIFO method (cherry picked from commit 1bac7930834d6f688950e836c45305a62e7ecb3f) --- .../__init__.py | 0 .../payment_terms_status_for_sales_order.js | 84 +++++++ .../payment_terms_status_for_sales_order.json | 38 ++++ .../payment_terms_status_for_sales_order.py | 211 ++++++++++++++++++ ...st_payment_terms_status_for_sales_order.py | 119 ++++++++++ 5 files changed, 452 insertions(+) create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/__init__.py create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py create mode 100644 erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/__init__.py b/erpnext/selling/report/payment_terms_status_for_sales_order/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js new file mode 100644 index 00000000000..0450631a3be --- /dev/null +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js @@ -0,0 +1,84 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +function get_filters() { + let filters = [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + }, + { + "fieldname":"period_start_date", + "label": __("Start Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1) + }, + { + "fieldname":"period_end_date", + "label": __("End Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.get_today() + }, + { + "fieldname":"sales_order", + "label": __("Sales Order"), + "fieldtype": "MultiSelectList", + "width": 100, + "options": "Sales Order", + "get_data": function(txt) { + return frappe.db.get_link_options("Sales Order", txt, this.filters()); + }, + "filters": () => { + return { + docstatus: 1, + payment_terms_template: ['not in', ['']], + company: frappe.query_report.get_filter_value("company"), + transaction_date: ['between', [frappe.query_report.get_filter_value("period_start_date"), frappe.query_report.get_filter_value("period_end_date")]] + } + }, + on_change: function(){ + frappe.query_report.refresh(); + } + } + ] + + return filters; +} + +frappe.query_reports["Payment Terms Status for Sales Order"] = { + "filters": get_filters(), + "formatter": function(value, row, column, data, default_formatter){ + if(column.fieldname == 'invoices' && value) { + invoices = value.split(','); + const invoice_formatter = (prev_value, curr_value) => { + if(prev_value != "") { + return prev_value + ", " + default_formatter(curr_value, row, column, data); + } + else { + return default_formatter(curr_value, row, column, data); + } + } + return invoices.reduce(invoice_formatter, "") + } + else if (column.fieldname == 'paid_amount' && value){ + formatted_value = default_formatter(value, row, column, data); + if(value > 0) { + formatted_value = "" + formatted_value + "" + } + return formatted_value; + } + else if (column.fieldname == 'status' && value == 'Completed'){ + return "" + default_formatter(value, row, column, data) + ""; + } + + return default_formatter(value, row, column, data); + }, + +}; diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json new file mode 100644 index 00000000000..850fa4dc47a --- /dev/null +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.json @@ -0,0 +1,38 @@ +{ + "add_total_row": 1, + "columns": [], + "creation": "2021-12-28 10:39:34.533964", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-12-30 10:42:06.058457", + "modified_by": "Administrator", + "module": "Selling", + "name": "Payment Terms Status for Sales Order", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Sales Order", + "report_name": "Payment Terms Status for Sales Order", + "report_type": "Script Report", + "roles": [ + { + "role": "Sales User" + }, + { + "role": "Sales Manager" + }, + { + "role": "Maintenance User" + }, + { + "role": "Accounts User" + }, + { + "role": "Stock User" + } + ] +} \ No newline at end of file diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py new file mode 100644 index 00000000000..aa2f757218e --- /dev/null +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -0,0 +1,211 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# License: MIT. See LICENSE + +import frappe +from frappe import _, qb, query_builder +from frappe.query_builder import functions + + +def get_columns(): + columns = [ + { + "label": _("Sales Order"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Sales Order", + "read_only": 1, + }, + { + "label": _("Submitted"), + "fieldname": "submitted", + "fieldtype": "Date", + "read_only": 1 + }, + { + "label": _("Payment Term"), + "fieldname": "payment_term", + "fieldtype": "Data", + "read_only": 1 + }, + { + "label": _("Description"), + "fieldname": "description", + "fieldtype": "Data", + "read_only": 1 + }, + { + "label": _("Due Date"), + "fieldname": "due_date", + "fieldtype": "Date", + "read_only": 1 + }, + { + "label": _("Invoice Portion"), + "fieldname": "invoice_portion", + "fieldtype": "Percent", + "read_only": 1, + }, + { + "label": _("Payment Amount"), + "fieldname": "payment_amount", + "fieldtype": "Currency", + "read_only": 1, + }, + { + "label": _("Paid Amount"), + "fieldname": "paid_amount", + "fieldtype": "Currency", + "read_only": 1 + }, + { + "label": _("Invoices"), + "fieldname": "invoices", + "fieldtype": "Link", + "options": "Sales Invoice", + "read_only": 1, + }, + { + "label": _("Status"), + "fieldname": "status", + "fieldtype": "Data", + "read_only": 1 + } + ] + return columns + + +def get_conditions(filters): + """ + Convert filter options to conditions used in query + """ + filters = frappe._dict(filters) if filters else frappe._dict({}) + conditions = frappe._dict({}) + + conditions.company = filters.company or frappe.defaults.get_user_default("company") + conditions.end_date = filters.period_end_date or frappe.utils.today() + conditions.start_date = filters.period_start_date or frappe.utils.add_months( + conditions.end_date, -1 + ) + conditions.sales_order = filters.sales_order or [] + + return conditions + + +def get_so_with_invoices(filters): + """ + Get Sales Order with payment terms template with their associated Invoices + """ + sorders = [] + + so = qb.DocType("Sales Order") + ps = qb.DocType("Payment Schedule") + datediff = query_builder.CustomFunction("DATEDIFF", ["cur_date", "due_date"]) + ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"]) + + conditions = get_conditions(filters) + query_so = ( + qb.from_(so) + .join(ps) + .on(ps.parent == so.name) + .select( + so.name, + so.transaction_date.as_("submitted"), + ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"), + ps.payment_term, + ps.description, + ps.due_date, + ps.invoice_portion, + ps.payment_amount, + ps.paid_amount, + ) + .where( + (so.docstatus == 1) + & (so.payment_terms_template != "NULL") + & (so.company == conditions.company) + & (so.transaction_date[conditions.start_date : conditions.end_date]) + ) + .orderby(so.name, so.transaction_date, ps.due_date) + ) + + if conditions.sales_order != []: + query_so = query_so.where(so.name.isin(conditions.sales_order)) + + sorders = query_so.run(as_dict=True) + + invoices = [] + if sorders != []: + soi = qb.DocType("Sales Order Item") + si = qb.DocType("Sales Invoice") + sii = qb.DocType("Sales Invoice Item") + query_inv = ( + qb.from_(sii) + .right_join(si) + .on(si.name == sii.parent) + .inner_join(soi) + .on(soi.name == sii.so_detail) + .select(sii.sales_order, sii.parent.as_("invoice"), si.base_net_total.as_("invoice_amount")) + .where((sii.sales_order.isin([x.name for x in sorders])) & (si.docstatus == 1)) + .groupby(sii.parent) + ) + invoices = query_inv.run(as_dict=True) + + return sorders, invoices + + +def set_payment_terms_statuses(sales_orders, invoices): + """ + compute status for payment terms with associated sales invoice using FIFO + """ + + for so in sales_orders: + for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]: + if so.payment_amount - so.paid_amount > 0: + amount = so.payment_amount - so.paid_amount + if inv.invoice_amount >= amount: + inv.invoice_amount -= amount + so.paid_amount += amount + if so.invoices: + so.invoices = so.invoices + "," + inv.invoice + else: + so.invoices = inv.invoice + so.status = "Completed" + break + else: + so.paid_amount += inv.invoice_amount + inv.invoice_amount = 0 + if so.invoices: + so.invoices = so.invoices + "," + inv.invoice + else: + so.invoices = inv.invoice + so.status = "Partly Paid" + + return sales_orders, invoices + + +def prepare_chart(s_orders): + if len(set([x.name for x in s_orders])) == 1: + chart = { + "data": { + "labels": [term.payment_term for term in s_orders], + "datasets": [ + {"name": "Payment Amount", "values": [x.payment_amount for x in s_orders],}, + {"name": "Paid Amount", "values": [x.paid_amount for x in s_orders],}, + ], + }, + "type": "bar", + } + return chart + + +def execute(filters=None): + columns = get_columns() + sales_orders, so_invoices = get_so_with_invoices(filters) + sales_orders, so_invoices = set_payment_terms_statuses(sales_orders, so_invoices) + + prepare_chart(sales_orders) + + data = sales_orders + message = [] + chart = prepare_chart(sales_orders) + + return columns, data, message, chart diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py new file mode 100644 index 00000000000..e9dba84f3aa --- /dev/null +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -0,0 +1,119 @@ +import datetime +import unittest + +import frappe +from frappe import qb +from frappe.utils import add_days + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order import ( + execute, +) +from erpnext.stock.doctype.item.test_item import create_item +from erpnext.tests.utils import ERPNextTestCase + +test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template"] + + +class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): + def test_payment_terms_status(self): + # disable Must be a whole number + nos = frappe.get_doc("UOM", "Nos") + nos.must_be_whole_number = 0 + nos.save() + + template = None + if frappe.db.exists("Payment Terms Template", "_Test 50-50"): + template = frappe.get_doc("Payment Terms Template", "_Test 50-50") + else: + template = frappe.get_doc( + { + "doctype": "Payment Terms Template", + "template_name": "_Test 50-50", + "terms": [ + { + "doctype": "Payment Terms Template Detail", + "due_date_based_on": "Day(s) after invoice date", + "payment_term_name": "_Test 50% on 15 Days", + "description": "_Test 50-50", + "invoice_portion": 50, + "credit_days": 15, + }, + { + "doctype": "Payment Terms Template Detail", + "due_date_based_on": "Day(s) after invoice date", + "payment_term_name": "_Test 50% on 30 Days", + "description": "_Test 50-50", + "invoice_portion": 50, + "credit_days": 30, + }, + ], + } + ) + template.insert() + + # item = create_item(item_code="_Test Excavator", is_stock_item=0, valuation_rate=1000000) + item = create_item(item_code="_Test Excavator", is_stock_item=0) + so = make_sales_order( + transaction_date="2021-06-15", + delivery_date=add_days("2021-06-15", -30), + item=item.item_code, + qty=1, + rate=1000000, + po_no=54321, + do_not_save=True, + ) + so.payment_terms_template = template.name + so.save() + so.submit() + + # make invoice with 60% of the total sales order value + sinv = make_sales_invoice(so.name) + # sinv.posting_date = "2021-06-29" + sinv.items[0].qty *= 0.60 + sinv.insert() + sinv.submit() + + columns, data, message, chart = execute( + { + "company": "_Test Company", + "period_start_date": "2021-06-01", + "period_end_date": "2021-06-30", + "sales_order": [so.name], + } + ) + + # revert changes to Nos + nos.must_be_whole_number = 1 + nos.save() + + expected_value = [ + { + "name": so.name, + "submitted": datetime.date(2021, 6, 15), + "status": "Completed", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 6, 30), + "invoice_portion": 50.0, + "payment_amount": 500000.0, + "paid_amount": 500000.0, + "invoices": sinv.name, + }, + { + "name": so.name, + "submitted": datetime.date(2021, 6, 15), + "status": "Partly Paid", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 7, 15), + "invoice_portion": 50.0, + "payment_amount": 500000.0, + "paid_amount": 100000.0, + "invoices": sinv.name, + }, + ] + + self.assertEqual(data, expected_value) From 408704e269e2070636d16a427eda4b83dafd0cac Mon Sep 17 00:00:00 2001 From: ruthra Date: Wed, 5 Jan 2022 10:11:19 +0530 Subject: [PATCH 015/121] test: fix failing test case payment terms status (cherry picked from commit 9f1e68801d527628551984402fd0c06e401084d8) --- .../test_payment_terms_status_for_sales_order.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index e9dba84f3aa..19c01f2d43b 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -21,8 +21,7 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): def test_payment_terms_status(self): # disable Must be a whole number nos = frappe.get_doc("UOM", "Nos") - nos.must_be_whole_number = 0 - nos.save() + nos.db_set("must_be_whole_number", 0, commit=True) template = None if frappe.db.exists("Payment Terms Template", "_Test 50-50"): @@ -62,9 +61,9 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): item=item.item_code, qty=1, rate=1000000, - po_no=54321, do_not_save=True, ) + so.po_no = "" so.payment_terms_template = template.name so.save() so.submit() @@ -86,8 +85,7 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): ) # revert changes to Nos - nos.must_be_whole_number = 1 - nos.save() + nos.db_set("must_be_whole_number", 1, commit=True) expected_value = [ { From c6fb52a31a01376140ec973f76f95e10573941ee Mon Sep 17 00:00:00 2001 From: ruthra Date: Wed, 5 Jan 2022 10:43:20 +0530 Subject: [PATCH 016/121] refactor: remove unused imports (cherry picked from commit edd980acdc9e51f74eb6b70a793ae17b2e827710) --- .../test_payment_terms_status_for_sales_order.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index 19c01f2d43b..4f27a5683de 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -1,11 +1,8 @@ import datetime -import unittest import frappe -from frappe import qb from frappe.utils import add_days -from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order import ( From b6910912a93cb9adaa27ef41beb7e99e7019334c Mon Sep 17 00:00:00 2001 From: ruthra Date: Wed, 5 Jan 2022 10:46:32 +0530 Subject: [PATCH 017/121] test: qty and rate changed to remove need for fractional Nos (cherry picked from commit 4535a7a301f76fa3b867902f19e806dcb01bdb75) --- .../test_payment_terms_status_for_sales_order.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index 4f27a5683de..5d6e91e8a50 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -16,9 +16,6 @@ test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Temp class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): def test_payment_terms_status(self): - # disable Must be a whole number - nos = frappe.get_doc("UOM", "Nos") - nos.db_set("must_be_whole_number", 0, commit=True) template = None if frappe.db.exists("Payment Terms Template", "_Test 50-50"): @@ -56,8 +53,8 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): transaction_date="2021-06-15", delivery_date=add_days("2021-06-15", -30), item=item.item_code, - qty=1, - rate=1000000, + qty=10, + rate=100000, do_not_save=True, ) so.po_no = "" @@ -67,8 +64,7 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): # make invoice with 60% of the total sales order value sinv = make_sales_invoice(so.name) - # sinv.posting_date = "2021-06-29" - sinv.items[0].qty *= 0.60 + sinv.items[0].qty = 6 sinv.insert() sinv.submit() @@ -81,9 +77,6 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): } ) - # revert changes to Nos - nos.db_set("must_be_whole_number", 1, commit=True) - expected_value = [ { "name": so.name, From b249478fcc0e22f58377596401cd974e948646c3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 7 Feb 2022 12:26:01 +0530 Subject: [PATCH 018/121] fix: Copyright info (cherry picked from commit 4284017e9de3b033156ca6947665bc99f0daefc3) --- .../payment_terms_status_for_sales_order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js index 0450631a3be..0e36b3fe3d2 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js @@ -1,4 +1,4 @@ -// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt /* eslint-disable */ From 2614de9ab316b66984a14457b2ad0a3a271c8aec Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 8 Feb 2022 18:53:08 +0530 Subject: [PATCH 019/121] refactor: currency field and code cleanup (cherry picked from commit bc244d074062d23be99922a370564bba13e15890) --- .../payment_terms_status_for_sales_order.py | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index aa2f757218e..4eafa9b2efc 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -13,62 +13,60 @@ def get_columns(): "fieldname": "name", "fieldtype": "Link", "options": "Sales Order", - "read_only": 1, }, { - "label": _("Submitted"), + "label": _("Posting Date"), "fieldname": "submitted", "fieldtype": "Date", - "read_only": 1 }, { "label": _("Payment Term"), "fieldname": "payment_term", "fieldtype": "Data", - "read_only": 1 }, { "label": _("Description"), "fieldname": "description", "fieldtype": "Data", - "read_only": 1 }, { "label": _("Due Date"), "fieldname": "due_date", "fieldtype": "Date", - "read_only": 1 }, { "label": _("Invoice Portion"), "fieldname": "invoice_portion", "fieldtype": "Percent", - "read_only": 1, }, { "label": _("Payment Amount"), "fieldname": "payment_amount", "fieldtype": "Currency", - "read_only": 1, + "options": "currency", }, { "label": _("Paid Amount"), "fieldname": "paid_amount", "fieldtype": "Currency", - "read_only": 1 + "options": "currency", }, { "label": _("Invoices"), "fieldname": "invoices", "fieldtype": "Link", "options": "Sales Invoice", - "read_only": 1, }, { "label": _("Status"), "fieldname": "status", "fieldtype": "Data", - "read_only": 1 + }, + { + "label": _("Currency"), + "fieldname": "currency", + "fieldtype": "Currency", + "hidden": 1 } ] return columns @@ -152,12 +150,13 @@ def get_so_with_invoices(filters): return sorders, invoices -def set_payment_terms_statuses(sales_orders, invoices): +def set_payment_terms_statuses(sales_orders, invoices, filters): """ compute status for payment terms with associated sales invoice using FIFO """ for so in sales_orders: + so.currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency') for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]: if so.payment_amount - so.paid_amount > 0: amount = so.payment_amount - so.paid_amount @@ -200,7 +199,7 @@ def prepare_chart(s_orders): def execute(filters=None): columns = get_columns() sales_orders, so_invoices = get_so_with_invoices(filters) - sales_orders, so_invoices = set_payment_terms_statuses(sales_orders, so_invoices) + sales_orders, so_invoices = set_payment_terms_statuses(sales_orders, so_invoices, filters) prepare_chart(sales_orders) From 63c73e6eaf2e7add08acfaa35c385b9dbeec6a0e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 15 Feb 2022 12:20:06 +0530 Subject: [PATCH 020/121] fix: default to company currency in report output (cherry picked from commit 85ed0fb8d6ef45197bfef4a71cb8f02355d61930) --- .../payment_terms_status_for_sales_order.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index 4eafa9b2efc..d0902e111a0 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -41,7 +41,7 @@ def get_columns(): }, { "label": _("Payment Amount"), - "fieldname": "payment_amount", + "fieldname": "base_payment_amount", "fieldtype": "Currency", "options": "currency", }, @@ -113,7 +113,7 @@ def get_so_with_invoices(filters): ps.description, ps.due_date, ps.invoice_portion, - ps.payment_amount, + ps.base_payment_amount, ps.paid_amount, ) .where( @@ -141,7 +141,7 @@ def get_so_with_invoices(filters): .on(si.name == sii.parent) .inner_join(soi) .on(soi.name == sii.so_detail) - .select(sii.sales_order, sii.parent.as_("invoice"), si.base_net_total.as_("invoice_amount")) + .select(sii.sales_order, sii.parent.as_("invoice"), si.base_grand_total.as_("invoice_amount")) .where((sii.sales_order.isin([x.name for x in sorders])) & (si.docstatus == 1)) .groupby(sii.parent) ) @@ -158,8 +158,8 @@ def set_payment_terms_statuses(sales_orders, invoices, filters): for so in sales_orders: so.currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency') for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]: - if so.payment_amount - so.paid_amount > 0: - amount = so.payment_amount - so.paid_amount + if so.base_payment_amount - so.paid_amount > 0: + amount = so.base_payment_amount - so.paid_amount if inv.invoice_amount >= amount: inv.invoice_amount -= amount so.paid_amount += amount @@ -187,7 +187,7 @@ def prepare_chart(s_orders): "data": { "labels": [term.payment_term for term in s_orders], "datasets": [ - {"name": "Payment Amount", "values": [x.payment_amount for x in s_orders],}, + {"name": "Payment Amount", "values": [x.base_payment_amount for x in s_orders],}, {"name": "Paid Amount", "values": [x.paid_amount for x in s_orders],}, ], }, From 07c094152e46761d8fb847fd14c5502860af2a29 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 15 Feb 2022 12:20:41 +0530 Subject: [PATCH 021/121] refactor: create invoices list without if else (cherry picked from commit a4b8d673232fd313396788ef745e67572c235dcc) --- .../payment_terms_status_for_sales_order.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index d0902e111a0..e6a56eea310 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -157,25 +157,20 @@ def set_payment_terms_statuses(sales_orders, invoices, filters): for so in sales_orders: so.currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency') + so.invoices = "" for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]: if so.base_payment_amount - so.paid_amount > 0: amount = so.base_payment_amount - so.paid_amount if inv.invoice_amount >= amount: inv.invoice_amount -= amount so.paid_amount += amount - if so.invoices: - so.invoices = so.invoices + "," + inv.invoice - else: - so.invoices = inv.invoice + so.invoices += "," + inv.invoice so.status = "Completed" break else: so.paid_amount += inv.invoice_amount inv.invoice_amount = 0 - if so.invoices: - so.invoices = so.invoices + "," + inv.invoice - else: - so.invoices = inv.invoice + so.invoices += "," + inv.invoice so.status = "Partly Paid" return sales_orders, invoices From fa03246a947c74ebec47e84e3cdcf325fa9819e6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 15 Feb 2022 17:20:29 +0530 Subject: [PATCH 022/121] test: refactor and fix failing test case (cherry picked from commit 49fdc6c52e9752362b754f1615ca77ac9e09b418) --- ...st_payment_terms_status_for_sales_order.py | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index 5d6e91e8a50..ee6cee3be8e 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -15,8 +15,8 @@ test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Temp class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): - def test_payment_terms_status(self): - + def create_payment_terms_template(self): + # create template for 50-50 payments template = None if frappe.db.exists("Payment Terms Template", "_Test 50-50"): template = frappe.get_doc("Payment Terms Template", "_Test 50-50") @@ -46,8 +46,10 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): } ) template.insert() + self.template = template - # item = create_item(item_code="_Test Excavator", is_stock_item=0, valuation_rate=1000000) + def test_payment_terms_status(self): + self.create_payment_terms_template() item = create_item(item_code="_Test Excavator", is_stock_item=0) so = make_sales_order( transaction_date="2021-06-15", @@ -58,16 +60,19 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): do_not_save=True, ) so.po_no = "" - so.payment_terms_template = template.name + so.taxes_and_charges = "" + so.taxes = "" + so.payment_terms_template = self.template.name so.save() so.submit() # make invoice with 60% of the total sales order value sinv = make_sales_invoice(so.name) + sinv.taxes_and_charges = "" + sinv.taxes = "" sinv.items[0].qty = 6 sinv.insert() sinv.submit() - columns, data, message, chart = execute( { "company": "_Test Company", @@ -86,9 +91,10 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): "description": "_Test 50-50", "due_date": datetime.date(2021, 6, 30), "invoice_portion": 50.0, - "payment_amount": 500000.0, + "currency": "INR", + "base_payment_amount": 500000.0, "paid_amount": 500000.0, - "invoices": sinv.name, + "invoices": ","+sinv.name, }, { "name": so.name, @@ -98,10 +104,13 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): "description": "_Test 50-50", "due_date": datetime.date(2021, 7, 15), "invoice_portion": 50.0, - "payment_amount": 500000.0, + "currency": "INR", + "base_payment_amount": 500000.0, "paid_amount": 100000.0, - "invoices": sinv.name, + "invoices": ","+sinv.name, }, ] + self.assertEqual(data, expected_value) + self.assertEqual(data, expected_value) From 2ca25fc9f14ec58d67daa195709e59aac3036ce5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 15 Feb 2022 17:21:08 +0530 Subject: [PATCH 023/121] test: added test for alternate currency - Sales Order and Invoice will be submitted in USD with exchange rate of 70 with the default company currency - Report will display in defauly company currency (cherry picked from commit 48f37c76594fad1cd64cd44b7126d6ef1ddd5bd1) --- ...st_payment_terms_status_for_sales_order.py | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index ee6cee3be8e..cad41e1dc03 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -112,5 +112,87 @@ class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): ] self.assertEqual(data, expected_value) + def create_exchange_rate(self, date): + # make an entry in Currency Exchange list. serves as a static exchange rate + if frappe.db.exists({'doctype': "Currency Exchange",'date': date,'from_currency': 'USD', 'to_currency':'INR'}): + return + else: + doc = frappe.get_doc({ + 'doctype': "Currency Exchange", + 'date': date, + 'from_currency': 'USD', + 'to_currency': frappe.get_cached_value("Company", '_Test Company','default_currency'), + 'exchange_rate': 70, + 'for_buying': True, + 'for_selling': True + }) + doc.insert() + def test_alternate_currency(self): + transaction_date = "2021-06-15" + self.create_payment_terms_template() + self.create_exchange_rate(transaction_date) + item = create_item(item_code="_Test Excavator", is_stock_item=0) + so = make_sales_order( + transaction_date=transaction_date, + currency="USD", + delivery_date=add_days(transaction_date, -30), + item=item.item_code, + qty=10, + rate=10000, + do_not_save=True, + ) + so.po_no = "" + so.taxes_and_charges = "" + so.taxes = "" + so.payment_terms_template = self.template.name + so.save() + so.submit() + + # make invoice with 60% of the total sales order value + sinv = make_sales_invoice(so.name) + sinv.currency = "USD" + sinv.taxes_and_charges = "" + sinv.taxes = "" + sinv.items[0].qty = 6 + sinv.insert() + sinv.submit() + columns, data, message, chart = execute( + { + "company": "_Test Company", + "period_start_date": "2021-06-01", + "period_end_date": "2021-06-30", + "sales_order": [so.name], + } + ) + + # report defaults to company currency. + expected_value = [ + { + "name": so.name, + "submitted": datetime.date(2021, 6, 15), + "status": "Completed", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 6, 30), + "invoice_portion": 50.0, + "currency": frappe.get_cached_value("Company", '_Test Company','default_currency'), + "base_payment_amount": 3500000.0, + "paid_amount": 3500000.0, + "invoices": ","+sinv.name, + }, + { + "name": so.name, + "submitted": datetime.date(2021, 6, 15), + "status": "Partly Paid", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 7, 15), + "invoice_portion": 50.0, + "currency": frappe.get_cached_value("Company", '_Test Company','default_currency'), + "base_payment_amount": 3500000.0, + "paid_amount": 700000.0, + "invoices": ","+sinv.name, + }, + ] self.assertEqual(data, expected_value) From 94a101662304bedd0914ba53d56889954a1db287 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 16 Feb 2022 17:40:16 +0530 Subject: [PATCH 024/121] chore: Move patch that updates SO from WO to v13 --- erpnext/patches.txt | 6 +----- .../{v14_0 => v13_0}/set_work_order_qty_in_so_from_mr.py | 0 2 files changed, 1 insertion(+), 5 deletions(-) rename erpnext/patches/{v14_0 => v13_0}/set_work_order_qty_in_so_from_mr.py (100%) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 013031cf913..6aaf9aa33aa 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -349,9 +349,5 @@ erpnext.patches.v13_0.update_sane_transfer_against erpnext.patches.v13_0.enable_provisional_accounting erpnext.patches.v13_0.update_disbursement_account erpnext.patches.v13_0.update_reserved_qty_closed_wo -<<<<<<< HEAD erpnext.patches.v13_0.amazon_mws_deprecation_warning -======= -erpnext.patches.v14_0.delete_amazon_mws_doctype -erpnext.patches.v14_0.set_work_order_qty_in_so_from_mr ->>>>>>> 0ca58d7627 (chore: Patch to update SO work_order_qty and Linter fix) +erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr diff --git a/erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py b/erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py similarity index 100% rename from erpnext/patches/v14_0/set_work_order_qty_in_so_from_mr.py rename to erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py From 8953794cda16b24c2271e3d929128599bb7e391f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 16 Feb 2022 17:53:00 +0530 Subject: [PATCH 025/121] fix: allow renaming and merging (backport #29830) (#29832) Co-authored-by: Wolfram Schmidt --- .../opportunity_lost_reason/opportunity_lost_reason.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json b/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json index 8a8d4252daa..0cfcf0e0ea4 100644 --- a/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json +++ b/erpnext/crm/doctype/opportunity_lost_reason/opportunity_lost_reason.json @@ -3,7 +3,7 @@ "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 0, - "allow_rename": 0, + "allow_rename": 1, "autoname": "field:lost_reason", "beta": 0, "creation": "2018-12-28 14:48:51.044975", @@ -57,7 +57,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-12-28 14:49:43.336437", + "modified": "2022-02-16 10:49:43.336437", "modified_by": "Administrator", "module": "CRM", "name": "Opportunity Lost Reason", @@ -150,4 +150,4 @@ "track_changes": 0, "track_seen": 0, "track_views": 0 -} \ No newline at end of file +} From 90f2cfd1029901e34adcf36eed7812143404a81f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 16 Feb 2022 20:04:45 +0530 Subject: [PATCH 026/121] fix: avoid updating items table if no change due to putaway (cherry picked from commit b000e93744c2730517172717ed63048bab50d62f) --- .../doctype/putaway_rule/putaway_rule.py | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 25330b71833..4e472a92dc1 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -9,8 +9,7 @@ from collections import defaultdict import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cint, floor, flt, nowdate -from six import string_types +from frappe.utils import cint, cstr, floor, flt, nowdate from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.utils import get_stock_balance @@ -75,7 +74,7 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): purpose: Purpose of Stock Entry sync (optional): Sync with client side only for client side calls """ - if isinstance(items, string_types): + if isinstance(items, str): items = json.loads(items) items_not_accomodated, updated_table = [], [] @@ -143,11 +142,44 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): if items_not_accomodated: show_unassigned_items_message(items_not_accomodated) - items[:] = updated_table if updated_table else items # modify items table + if updated_table and _items_changed(items, updated_table, doctype): + items[:] = updated_table + frappe.msgprint(_("Applied putaway rules."), alert=True) if sync and json.loads(sync): # sync with client side return items +def _items_changed(old, new, doctype: str) -> bool: + """ Check if any items changed by application of putaway rules. + + If not, changing item table can have side effects since `name` items also changes. + """ + if len(old) != len(new): + return True + + old = [frappe._dict(item) if isinstance(item, dict) else item for item in old] + + if doctype == "Stock Entry": + compare_keys = ("item_code", "t_warehouse", "transfer_qty", "serial_no") + sort_key = lambda item: (item.item_code, cstr(item.t_warehouse), # noqa + flt(item.transfer_qty), cstr(item.serial_no)) + else: + # purchase receipt / invoice + compare_keys = ("item_code", "warehouse", "stock_qty", "received_qty", "serial_no") + sort_key = lambda item: (item.item_code, cstr(item.warehouse), # noqa + flt(item.stock_qty), flt(item.received_qty), cstr(item.serial_no)) + + old_sorted = sorted(old, key=sort_key) + new_sorted = sorted(new, key=sort_key) + + # Once sorted by all relevant keys both tables should align if they are same. + for old_item, new_item in zip(old_sorted, new_sorted): + for key in compare_keys: + if old_item.get(key) != new_item.get(key): + return True + return False + + def get_ordered_putaway_rules(item_code, company, source_warehouse=None): """Returns an ordered list of putaway rules to apply on an item.""" filters = { From fb0167ffc6cc005aa47f1a1717f4f768049a10c8 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 17 Feb 2022 11:08:50 +0530 Subject: [PATCH 027/121] test: putaway rule re-application shouldn't do anything (cherry picked from commit d9fc3f3d902a98dc9b1c1ab6814c66b170e18a04) --- .../doctype/putaway_rule/test_putaway_rule.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py index bd4d811e76c..ff1c19a8275 100644 --- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py @@ -35,6 +35,18 @@ class TestPutawayRule(ERPNextTestCase): new_uom.uom_name = "Bag" new_uom.save() + def assertUnchangedItemsOnResave(self, doc): + """ Check if same items remain even after reapplication of rules. + + This is required since some business logic like subcontracting + depends on `name` of items to be same if item isn't changed. + """ + doc.reload() + old_items = {d.name for d in doc.items} + doc.save() + new_items = {d.name for d in doc.items} + self.assertSetEqual(old_items, new_items) + def test_putaway_rules_priority(self): """Test if rule is applied by priority, irrespective of free space.""" rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, @@ -50,6 +62,8 @@ class TestPutawayRule(ERPNextTestCase): self.assertEqual(pr.items[1].qty, 100) self.assertEqual(pr.items[1].warehouse, self.warehouse_2) + self.assertUnchangedItemsOnResave(pr) + pr.delete() rule_1.delete() rule_2.delete() @@ -162,6 +176,8 @@ class TestPutawayRule(ERPNextTestCase): # leftover space was for 500 kg (0.5 Bag) # Since Bag is a whole UOM, 1(out of 2) Bag will be unassigned + self.assertUnchangedItemsOnResave(pr) + pr.delete() rule_1.delete() rule_2.delete() @@ -196,6 +212,8 @@ class TestPutawayRule(ERPNextTestCase): self.assertEqual(pr.items[1].warehouse, self.warehouse_1) self.assertEqual(pr.items[1].putaway_rule, rule_1.name) + self.assertUnchangedItemsOnResave(pr) + pr.delete() rule_1.delete() @@ -239,6 +257,8 @@ class TestPutawayRule(ERPNextTestCase): self.assertEqual(stock_entry_item.qty, 100) # unassigned 100 out of 200 Kg self.assertEqual(stock_entry_item.putaway_rule, rule_2.name) + self.assertUnchangedItemsOnResave(stock_entry) + stock_entry.delete() rule_1.delete() rule_2.delete() @@ -294,6 +314,8 @@ class TestPutawayRule(ERPNextTestCase): self.assertEqual(stock_entry.items[2].qty, 200) self.assertEqual(stock_entry.items[2].putaway_rule, rule_2.name) + self.assertUnchangedItemsOnResave(stock_entry) + stock_entry.delete() rule_1.delete() rule_2.delete() @@ -344,6 +366,8 @@ class TestPutawayRule(ERPNextTestCase): self.assertEqual(stock_entry.items[1].serial_no, "\n".join(serial_nos[3:])) self.assertEqual(stock_entry.items[1].batch_no, "BOTTL-BATCH-1") + self.assertUnchangedItemsOnResave(stock_entry) + stock_entry.delete() pr.cancel() rule_1.delete() @@ -366,6 +390,8 @@ class TestPutawayRule(ERPNextTestCase): self.assertEqual(stock_entry_item.qty, 100) self.assertEqual(stock_entry_item.putaway_rule, rule_1.name) + self.assertUnchangedItemsOnResave(stock_entry) + stock_entry.delete() rule_1.delete() rule_2.delete() From 97505fdc82d87739829d7ceef96cbd3271626f4f Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 17 Feb 2022 14:14:47 +0530 Subject: [PATCH 028/121] fix: currency in bank reconciliation tool (cherry picked from commit 60674e52b8a08dc5785da73e9ce418fad00d836c) --- .../bank_reconciliation_tool/bank_reconciliation_tool.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js index 398e4576a68..cf52471912d 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js @@ -64,6 +64,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", { "account_currency", (r) => { frm.currency = r.account_currency; + frm.trigger("render_chart"); } ); } @@ -128,7 +129,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", { } }, - render_chart(frm) { + render_chart: frappe.utils.debounce((frm) => { frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager( { $reconciliation_tool_cards: frm.get_field( @@ -140,7 +141,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", { currency: frm.currency, } ); - }, + }, 500), render(frm) { if (frm.doc.bank_account) { From 20f7db98cbb7af50100c93e18867e1c328485694 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 17 Feb 2022 14:24:52 +0530 Subject: [PATCH 029/121] fix: production plan status should consider qty + WO status (cherry picked from commit db93f26f20fe315e46324bfb36de759637f918bc) --- .../production_plan/production_plan.py | 24 +++++++++++++------ .../production_plan/test_production_plan.py | 17 ++++++++++--- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 1ccd9dc7ad7..562c47fcf17 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -320,7 +320,7 @@ class ProductionPlan(Document): if self.total_produced_qty > 0: self.status = "In Process" - if self.check_have_work_orders_completed(): + if self.all_items_completed(): self.status = "Completed" if self.status != 'Completed': @@ -592,14 +592,24 @@ class ProductionPlan(Document): self.append("sub_assembly_items", data) - def check_have_work_orders_completed(self): - wo_status = frappe.db.get_list( + def all_items_completed(self): + all_items_produced = all(flt(d.planned_qty) - flt(d.produced_qty) < 0.000001 + for d in self.po_items) + if not all_items_produced: + return False + + wo_status = frappe.get_all( "Work Order", - filters={"production_plan": self.name}, + filters={ + "production_plan": self.name, + "status": ("not in", ["Closed", "Stopped"]), + "docstatus": ("<", 2), + }, fields="status", - pluck="status" + pluck="status", ) - return all(s == "Completed" for s in wo_status) + all_work_orders_completed = all(s == "Completed" for s in wo_status) + return all_work_orders_completed @frappe.whitelist() def download_raw_materials(doc, warehouses=None): @@ -1045,4 +1055,4 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0): def set_default_warehouses(row, default_warehouses): for field in ['wip_warehouse', 'fg_warehouse']: if not row.get(field): - row[field] = default_warehouses.get(field) \ No newline at end of file + row[field] = default_warehouses.get(field) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index afa1501efcd..d88e10a564c 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -409,9 +409,6 @@ class TestProductionPlan(ERPNextTestCase): boms = { "Assembly": { "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},}, - "SubAssembly2": {"ChildPart3": {}}, - "SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}}, - "ChildPart5": {}, "ChildPart6": {}, "SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}}, }, @@ -591,6 +588,20 @@ class TestProductionPlan(ERPNextTestCase): pln.reload() self.assertEqual(pln.po_items[0].pending_qty, 1) + def test_qty_based_status(self): + pp = frappe.new_doc("Production Plan") + pp.po_items = [ + frappe._dict(planned_qty=5, produce_qty=4) + ] + self.assertFalse(pp.all_items_completed()) + + pp.po_items = [ + frappe._dict(planned_qty=5, produce_qty=10), + frappe._dict(planned_qty=5, produce_qty=4) + ] + self.assertFalse(pp.all_items_completed()) + + def create_production_plan(**args): """ sales_order (obj): Sales Order Doc Object From 63079f3e0771c445c7c7bf21171ddb380e34df7a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 17 Feb 2022 15:59:12 +0530 Subject: [PATCH 030/121] fix: coupon code is applied even if ignore_pricing_rule is enabled (cherry picked from commit 274399978572b1f2e80fd2a1db2663efa544fcf7) --- erpnext/public/js/controllers/transaction.js | 20 +++++----------- .../selling/page/point_of_sale/pos_payment.js | 23 +++++++++++++++++++ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 76de31770a5..e742ae9a890 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2265,20 +2265,12 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ coupon_code: function() { var me = this; - if (this.frm.doc.coupon_code) { - frappe.run_serially([ - () => this.frm.doc.ignore_pricing_rule=1, - () => me.ignore_pricing_rule(), - () => this.frm.doc.ignore_pricing_rule=0, - () => me.apply_pricing_rule(), - () => this.frm.save() - ]); - } else { - frappe.run_serially([ - () => this.frm.doc.ignore_pricing_rule=1, - () => me.ignore_pricing_rule() - ]); - } + frappe.run_serially([ + () => this.frm.doc.ignore_pricing_rule=1, + () => me.ignore_pricing_rule(), + () => this.frm.doc.ignore_pricing_rule=0, + () => me.apply_pricing_rule() + ]); } }); diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index b9b65591dc7..9650bc88a42 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -169,6 +169,29 @@ erpnext.PointOfSale.Payment = class { } }); + frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => { + if (!frm.doc.ignore_pricing_rule) { + if (frm.doc.coupon_code) { + frappe.run_serially([ + () => frm.doc.ignore_pricing_rule=1, + () => frm.trigger('ignore_pricing_rule'), + () => frm.doc.ignore_pricing_rule=0, + () => frm.trigger('apply_pricing_rule'), + () => frm.save(), + () => this.update_totals_section(frm.doc) + ]); + } else { + frappe.run_serially([ + () => frm.doc.ignore_pricing_rule=1, + () => frm.trigger('ignore_pricing_rule'), + () => frm.doc.ignore_pricing_rule=0, + () => frm.save(), + () => this.update_totals_section(frm.doc) + ]); + } + } + }); + this.setup_listener_for_payments(); this.$payment_modes.on('click', '.shortcut', function() { From 6db0ea8be84f8a5479cbc23acf1cf77fa3603ac4 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 17 Feb 2022 12:00:19 +0530 Subject: [PATCH 031/121] fix(Timesheet): convert time logs to datetime while checking for overlap (cherry picked from commit e2e998fbd9baa6015bc9c376dd5b6db7ae6cae49) --- .../projects/doctype/timesheet/timesheet.py | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index dd0b5f90f4d..fa0411e0f8a 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -7,7 +7,7 @@ import json import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import add_to_date, flt, getdate, time_diff_in_hours +from frappe.utils import add_to_date, flt, get_datetime, getdate, time_diff_in_hours from erpnext.controllers.queries import get_match_cond from erpnext.hr.utils import validate_active_employee @@ -145,7 +145,7 @@ class Timesheet(Document): if not (data.from_time and data.hours): return - _to_time = add_to_date(data.from_time, hours=data.hours, as_datetime=True) + _to_time = get_datetime(add_to_date(data.from_time, hours=data.hours, as_datetime=True)) if data.to_time != _to_time: data.to_time = _to_time @@ -186,24 +186,37 @@ class Timesheet(Document): and ts.docstatus < 2""".format(cond), { "val": value, - "from_time": args.from_time, - "to_time": args.to_time, + "from_time": get_datetime(args.from_time), + "to_time": get_datetime(args.to_time), "name": args.name or "No Name", "parent": args.parent or "No Name" }, as_dict=True) - # check internal overlap - for time_log in self.time_logs: - if not (time_log.from_time and time_log.to_time - and args.from_time and args.to_time): continue - if (fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and \ - args.idx != time_log.idx and ((args.from_time > time_log.from_time and args.from_time < time_log.to_time) or - (args.to_time > time_log.from_time and args.to_time < time_log.to_time) or - (args.from_time <= time_log.from_time and args.to_time >= time_log.to_time)): - return self + if self.check_internal_overlap(fieldname, args): + return self return existing[0] if existing else None + def check_internal_overlap(self, fieldname, args): + for time_log in self.time_logs: + if not (time_log.from_time and time_log.to_time + and args.from_time and args.to_time): + continue + + from_time = get_datetime(time_log.from_time) + to_time = get_datetime(time_log.to_time) + args_from_time = get_datetime(args.from_time) + args_to_time = get_datetime(args.to_time) + + if (fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and \ + args.idx != time_log.idx and ( + (args_from_time > from_time and args_from_time < to_time) + or (args_to_time > from_time and args_to_time < to_time) + or (args_from_time <= from_time and args_to_time >= to_time) + ): + return True + return False + def update_cost(self): for data in self.time_logs: if data.activity_type or data.is_billable: From 91e6fea2cdb56d984bbafd3b11d9b4c1ec3d631e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 17 Feb 2022 14:39:17 +0530 Subject: [PATCH 032/121] fix: convert overlap raw query to frappe.qb (cherry picked from commit 3ec9acf8f7c8fd08e5709ac0f352728f6a9d6cfa) --- .../projects/doctype/timesheet/timesheet.py | 52 +++++++++++-------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index fa0411e0f8a..c43be8cbd8b 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -171,26 +171,35 @@ class Timesheet(Document): .format(args.idx, self.name, existing.name), OverlapError) def get_overlap_for(self, fieldname, args, value): - cond = "ts.`{0}`".format(fieldname) - if fieldname == 'workstation': - cond = "tsd.`{0}`".format(fieldname) + timesheet = frappe.qb.DocType("Timesheet") + timelog = frappe.qb.DocType("Timesheet Detail") - existing = frappe.db.sql("""select ts.name as name, tsd.from_time as from_time, tsd.to_time as to_time from - `tabTimesheet Detail` tsd, `tabTimesheet` ts where {0}=%(val)s and tsd.parent = ts.name and - ( - (%(from_time)s > tsd.from_time and %(from_time)s < tsd.to_time) or - (%(to_time)s > tsd.from_time and %(to_time)s < tsd.to_time) or - (%(from_time)s <= tsd.from_time and %(to_time)s >= tsd.to_time)) - and tsd.name!=%(name)s - and ts.name!=%(parent)s - and ts.docstatus < 2""".format(cond), - { - "val": value, - "from_time": get_datetime(args.from_time), - "to_time": get_datetime(args.to_time), - "name": args.name or "No Name", - "parent": args.parent or "No Name" - }, as_dict=True) + from_time = get_datetime(args.from_time) + to_time = get_datetime(args.to_time) + + query = ( + frappe.qb.from_(timesheet) + .join(timelog) + .on(timelog.parent == timesheet.name) + .select(timesheet.name.as_('name'), timelog.from_time.as_('from_time'), timelog.to_time.as_('to_time')) + .where( + (timelog.name != (args.name or "No Name")) + & (timesheet.name != (args.parent or "No Name")) + & (timesheet.docstatus < 2) + & ( + ((from_time > timelog.from_time) & (from_time < timelog.to_time)) + | ((to_time > timelog.from_time) & (to_time < timelog.to_time)) + | ((from_time <= timelog.from_time) & (to_time >= timelog.to_time)) + ) + ) + ) + + if fieldname == "workstation": + query = query.where(timelog[fieldname] == value) + else: + query = query.where(timesheet[fieldname] == value) + + existing = query.run(as_dict=True) if self.check_internal_overlap(fieldname, args): return self @@ -208,12 +217,13 @@ class Timesheet(Document): args_from_time = get_datetime(args.from_time) args_to_time = get_datetime(args.to_time) - if (fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and \ + if ((fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and args.idx != time_log.idx and ( (args_from_time > from_time and args_from_time < to_time) or (args_to_time > from_time and args_to_time < to_time) or (args_from_time <= from_time and args_to_time >= to_time) - ): + ) + ): return True return False From 98bafc589828268954f70ed75f636d390251c97f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 17 Feb 2022 14:39:26 +0530 Subject: [PATCH 033/121] test: timesheet not overlapping with continuous timelogs (cherry picked from commit 47ff968253ff7c4e7ca4e7769ccc29d93a8f71f2) --- .../doctype/timesheet/test_timesheet.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index 989bcd1670d..8b603570217 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -151,6 +151,35 @@ class TestTimesheet(unittest.TestCase): settings.ignore_employee_time_overlap = initial_setting settings.save() + def test_timesheet_not_overlapping_with_continuous_timelogs(self): + emp = make_employee("test_employee_6@salary.com") + + update_activity_type("_Test Activity Type") + timesheet = frappe.new_doc("Timesheet") + timesheet.employee = emp + timesheet.append( + 'time_logs', + { + "billable": 1, + "activity_type": "_Test Activity Type", + "from_time": now_datetime(), + "to_time": now_datetime() + datetime.timedelta(hours=3), + "company": "_Test Company" + } + ) + timesheet.append( + 'time_logs', + { + "billable": 1, + "activity_type": "_Test Activity Type", + "from_time": now_datetime() + datetime.timedelta(hours=3), + "to_time": now_datetime() + datetime.timedelta(hours=4), + "company": "_Test Company" + } + ) + + timesheet.save() # should not throw an error + def test_to_time(self): emp = make_employee("test_employee_6@salary.com") from_time = now_datetime() From ddfdb445fa509f523f7d904ff7eea8120a0f1434 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 17 Feb 2022 16:59:14 +0530 Subject: [PATCH 034/121] chore: remove unused code and fields related to workstation from Timesheet Detail (cherry picked from commit bef46e2b645f17eca8c1cd6ebe74e2845f6ea64f) --- .../projects/doctype/timesheet/timesheet.py | 22 +++------ .../timesheet_detail/timesheet_detail.json | 48 ++----------------- 2 files changed, 10 insertions(+), 60 deletions(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index c43be8cbd8b..b44d5017431 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -177,7 +177,7 @@ class Timesheet(Document): from_time = get_datetime(args.from_time) to_time = get_datetime(args.to_time) - query = ( + existing = ( frappe.qb.from_(timesheet) .join(timelog) .on(timelog.parent == timesheet.name) @@ -186,20 +186,14 @@ class Timesheet(Document): (timelog.name != (args.name or "No Name")) & (timesheet.name != (args.parent or "No Name")) & (timesheet.docstatus < 2) + & (timesheet[fieldname] == value) & ( ((from_time > timelog.from_time) & (from_time < timelog.to_time)) | ((to_time > timelog.from_time) & (to_time < timelog.to_time)) | ((from_time <= timelog.from_time) & (to_time >= timelog.to_time)) ) ) - ) - - if fieldname == "workstation": - query = query.where(timelog[fieldname] == value) - else: - query = query.where(timesheet[fieldname] == value) - - existing = query.run(as_dict=True) + ).run(as_dict=True) if self.check_internal_overlap(fieldname, args): return self @@ -217,12 +211,10 @@ class Timesheet(Document): args_from_time = get_datetime(args.from_time) args_to_time = get_datetime(args.to_time) - if ((fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and - args.idx != time_log.idx and ( - (args_from_time > from_time and args_from_time < to_time) - or (args_to_time > from_time and args_to_time < to_time) - or (args_from_time <= from_time and args_to_time >= to_time) - ) + if (args.get(fieldname) == time_log.get(fieldname)) and (args.idx != time_log.idx) and ( + (args_from_time > from_time and args_from_time < to_time) + or (args_to_time > from_time and args_to_time < to_time) + or (args_from_time <= from_time and args_to_time >= to_time) ): return True return False diff --git a/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json b/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json index ee04c612c9a..90fdb833315 100644 --- a/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json +++ b/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json @@ -14,12 +14,6 @@ "to_time", "hours", "completed", - "section_break_7", - "completed_qty", - "workstation", - "column_break_12", - "operation", - "operation_id", "project_details", "project", "project_name", @@ -83,43 +77,6 @@ "fieldtype": "Check", "label": "Completed" }, - { - "fieldname": "section_break_7", - "fieldtype": "Section Break" - }, - { - "depends_on": "eval:parent.work_order", - "fieldname": "completed_qty", - "fieldtype": "Float", - "label": "Completed Qty" - }, - { - "depends_on": "eval:parent.work_order", - "fieldname": "workstation", - "fieldtype": "Link", - "label": "Workstation", - "options": "Workstation", - "read_only": 1 - }, - { - "fieldname": "column_break_12", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval:parent.work_order", - "fieldname": "operation", - "fieldtype": "Link", - "label": "Operation", - "options": "Operation", - "read_only": 1 - }, - { - "depends_on": "eval:parent.work_order", - "fieldname": "operation_id", - "fieldtype": "Data", - "hidden": 1, - "label": "Operation Id" - }, { "fieldname": "project_details", "fieldtype": "Section Break" @@ -267,7 +224,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-05-18 12:19:33.205940", + "modified": "2022-02-17 16:53:34.878798", "modified_by": "Administrator", "module": "Projects", "name": "Timesheet Detail", @@ -275,5 +232,6 @@ "permissions": [], "quick_entry": 1, "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "states": [] } \ No newline at end of file From 90ebe6f266cbeced28daa7dbf2cb83e3ab74bcd3 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 17 Feb 2022 19:25:00 +0530 Subject: [PATCH 035/121] fix: avoid creating bins without item-wh Co-Authored-By: Shadrak Gurupnor <30501401+shadrak98@users.noreply.github.com> Co-Authored-By: Saurabh (cherry picked from commit c36bd7e1a6fe48c5fff4765e843571a0d6560dd1) --- erpnext/controllers/accounts_controller.py | 3 ++- erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py | 2 ++ erpnext/patches/v4_2/repost_reserved_qty.py | 8 +++++--- erpnext/patches/v4_2/update_requested_and_ordered_qty.py | 2 ++ 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index c5d354e445d..3513b0ad663 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1956,7 +1956,8 @@ def update_bin_on_delete(row, doctype): qty_dict["ordered_qty"] = get_ordered_qty(row.item_code, row.warehouse) - update_bin_qty(row.item_code, row.warehouse, qty_dict) + if row.warehouse: + update_bin_qty(row.item_code, row.warehouse, qty_dict) def validate_and_delete_children(parent, data): deleted_children = [] diff --git a/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py b/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py index c89e4bb9eae..50d97c4830d 100644 --- a/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py +++ b/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py @@ -10,6 +10,8 @@ def execute(): FROM `tabBin`""",as_dict=1) for entry in bin_details: + if not (entry.item_code and entry.warehouse): + continue update_bin_qty(entry.get("item_code"), entry.get("warehouse"), { "indented_qty": get_indented_qty(entry.get("item_code"), entry.get("warehouse")) }) diff --git a/erpnext/patches/v4_2/repost_reserved_qty.py b/erpnext/patches/v4_2/repost_reserved_qty.py index c2ca9be64aa..ed4b19d07d3 100644 --- a/erpnext/patches/v4_2/repost_reserved_qty.py +++ b/erpnext/patches/v4_2/repost_reserved_qty.py @@ -29,9 +29,11 @@ def execute(): """) for item_code, warehouse in repost_for: - update_bin_qty(item_code, warehouse, { - "reserved_qty": get_reserved_qty(item_code, warehouse) - }) + if not (item_code and warehouse): + continue + update_bin_qty(item_code, warehouse, { + "reserved_qty": get_reserved_qty(item_code, warehouse) + }) frappe.db.sql("""delete from tabBin where exists( diff --git a/erpnext/patches/v4_2/update_requested_and_ordered_qty.py b/erpnext/patches/v4_2/update_requested_and_ordered_qty.py index 42b0b04076f..dd79410ba58 100644 --- a/erpnext/patches/v4_2/update_requested_and_ordered_qty.py +++ b/erpnext/patches/v4_2/update_requested_and_ordered_qty.py @@ -14,6 +14,8 @@ def execute(): union select item_code, warehouse from `tabStock Ledger Entry`) a"""): try: + if not (item_code and warehouse): + continue count += 1 update_bin_qty(item_code, warehouse, { "indented_qty": get_indented_qty(item_code, warehouse), From 58aedaa0f7206cd9d505653a945f4d3fc46cac5b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 16 Feb 2022 18:15:57 +0530 Subject: [PATCH 036/121] fix: added item name in the excel sheet (cherry picked from commit 02e77029faed67ffff3e395c1de132cf15a14a03) --- .../doctype/production_plan/production_plan.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 562c47fcf17..60771592da5 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -616,7 +616,8 @@ def download_raw_materials(doc, warehouses=None): if isinstance(doc, str): doc = frappe._dict(json.loads(doc)) - item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM', + item_list = [['Item Code', 'Item Name', 'Description', + 'Stock UOM', 'Warehouse', 'Required Qty as per BOM', 'Projected Qty', 'Available Qty In Hand', 'Ordered Qty', 'Planned Qty', 'Reserved Qty for Production', 'Safety Stock', 'Required Qty']] @@ -625,7 +626,8 @@ def download_raw_materials(doc, warehouses=None): items = get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=True) for d in items: - item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('warehouse'), + item_list.append([d.get('item_code'), d.get('item_name'), + d.get('description'), d.get('stock_uom'), d.get('warehouse'), d.get('required_bom_qty'), d.get('projected_qty'), d.get('actual_qty'), d.get('ordered_qty'), d.get('planned_qty'), d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')]) From d86e63e22d530f4ac63044475bb4576e2fae0235 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Feb 2022 18:10:57 +0530 Subject: [PATCH 037/121] fix: Transfer Bucket logic for Repack Entry with split batch rows (cherry picked from commit 799671c7482fa8bca12a24636ea0000579ca9537) --- .../stock/report/stock_ageing/stock_ageing.py | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index a89a4038c20..9866e63fb5a 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -286,10 +286,11 @@ class FIFOSlots: def __compute_incoming_stock(self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List): "Update FIFO Queue on inward stock." - if self.transferred_item_details.get(transfer_key): - # inward/outward from same voucher, item & warehouse - slot = self.transferred_item_details[transfer_key].pop(0) - fifo_queue.append(slot) + transfer_data = self.transferred_item_details.get(transfer_key) + if transfer_data: + # [Repack] inward/outward from same voucher, item & warehouse + # consume transfer data and add stock to fifo queue + self.__adjust_incoming_transfer_qty(transfer_data, fifo_queue, row) else: if not serial_nos: if fifo_queue and flt(fifo_queue[0][0]) < 0: @@ -333,6 +334,27 @@ class FIFOSlots: self.transferred_item_details[transfer_key].append([qty_to_pop, slot[1]]) qty_to_pop = 0 + def __adjust_incoming_transfer_qty(self, transfer_data: Dict, fifo_queue: List, row: Dict): + "Add previously removed stock back to FIFO Queue." + transfer_qty_to_pop = flt(row.actual_qty) + first_bucket_qty = transfer_data[0][0] + first_bucket_date = transfer_data[0][1] + + while transfer_qty_to_pop: + if transfer_data and 0 > first_bucket_qty <= transfer_qty_to_pop: + # bucket qty is not enough, consume whole + transfer_qty_to_pop -= first_bucket_qty + slot = transfer_data.pop(0) + fifo_queue.append(slot) + elif not transfer_data: + # transfer bucket is empty, extra incoming qty + fifo_queue.append([transfer_qty_to_pop, row.posting_date]) + else: + # ample bucket qty to consume + first_bucket_qty -= transfer_qty_to_pop + fifo_queue.append([transfer_qty_to_pop, first_bucket_date]) + transfer_qty_to_pop = 0 + def __update_balances(self, row: Dict, key: Union[Tuple, str]): self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction From 690ae52a8e7a201da8ad7cb59e1f009c56c64f6a Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Feb 2022 18:41:42 +0530 Subject: [PATCH 038/121] test: Stock Ageing FIFO buckets for Repack entry with same item (cherry picked from commit ea3b7de867fdcc565567ec9ca1b7925116e16f2f) --- .../report/stock_ageing/test_stock_ageing.py | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 66d2f6b7539..3055332540f 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -236,6 +236,159 @@ class TestStockAgeing(ERPNextTestCase): item_wh_balances = [item_wh_wise_slots.get(i).get("qty_after_transaction") for i in item_wh_wise_slots] self.assertEqual(sum(item_wh_balances), item_result["qty_after_transaction"]) + def test_repack_entry_same_item_split_rows(self): + """ + Split consumption rows and have single repacked item row (same warehouse). + Ledger: + Item | Qty | Voucher + ------------------------ + Item 1 | 500 | 001 + Item 1 | -50 | 002 (repack) + Item 1 | -50 | 002 (repack) + Item 1 | 100 | 002 (repack) + + Case most likely for batch items. Test time bucket computation. + """ + sle = [ + frappe._dict( # stock up item + name="Flask Item", + actual_qty=500, qty_after_transaction=500, + warehouse="WH 1", + posting_date="2021-12-03", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-50), qty_after_transaction=450, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-50), qty_after_transaction=400, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=100, qty_after_transaction=500, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + ] + slots = FIFOSlots(self.filters, sle).generate() + item_result = slots["Flask Item"] + queue = item_result["fifo_queue"] + + self.assertEqual(item_result["total_qty"], 500.0) + self.assertEqual(queue[0][0], 400.0) + self.assertEqual(queue[1][0], 100.0) + # check if time buckets add up to balance qty + self.assertEqual(sum([i[0] for i in queue]), 500.0) + + def test_repack_entry_same_item_overconsume(self): + """ + Over consume item and have less repacked item qty (same warehouse). + Ledger: + Item | Qty | Voucher + ------------------------ + Item 1 | 500 | 001 + Item 1 | -100 | 002 (repack) + Item 1 | 50 | 002 (repack) + + Case most likely for batch items. Test time bucket computation. + """ + sle = [ + frappe._dict( # stock up item + name="Flask Item", + actual_qty=500, qty_after_transaction=500, + warehouse="WH 1", + posting_date="2021-12-03", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-100), qty_after_transaction=400, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=50, qty_after_transaction=450, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + ] + slots = FIFOSlots(self.filters, sle).generate() + item_result = slots["Flask Item"] + queue = item_result["fifo_queue"] + + self.assertEqual(item_result["total_qty"], 450.0) + self.assertEqual(queue[0][0], 400.0) + self.assertEqual(queue[1][0], 50.0) + # check if time buckets add up to balance qty + self.assertEqual(sum([i[0] for i in queue]), 450.0) + + def test_repack_entry_same_item_overproduce(self): + """ + Under consume item and have more repacked item qty (same warehouse). + Ledger: + Item | Qty | Voucher + ------------------------ + Item 1 | 500 | 001 + Item 1 | -50 | 002 (repack) + Item 1 | 100 | 002 (repack) + + Case most likely for batch items. Test time bucket computation. + """ + sle = [ + frappe._dict( # stock up item + name="Flask Item", + actual_qty=500, qty_after_transaction=500, + warehouse="WH 1", + posting_date="2021-12-03", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-50), qty_after_transaction=450, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=100, qty_after_transaction=550, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + ] + slots = FIFOSlots(self.filters, sle).generate() + item_result = slots["Flask Item"] + queue = item_result["fifo_queue"] + + self.assertEqual(item_result["total_qty"], 550.0) + self.assertEqual(queue[0][0], 450.0) + self.assertEqual(queue[1][0], 100.0) + # check if time buckets add up to balance qty + self.assertEqual(sum([i[0] for i in queue]), 550.0) + def generate_item_and_item_wh_wise_slots(filters, sle): "Return results with and without 'show_warehouse_wise_stock'" item_wise_slots = FIFOSlots(filters, sle).generate() From cab05dbefb79ce48ac00d7c942824ff9c7f4f5af Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Feb 2022 20:30:16 +0530 Subject: [PATCH 039/121] chore: Add transfer bucket working to .md file (cherry picked from commit f6233e77c6c2cbfeec6aeb82a73c1bbcbaa8f5da) --- .../stock_ageing/stock_ageing_fifo_logic.md | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md b/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md index 9e9bed48e3e..3d759dd9989 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md +++ b/erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md @@ -71,4 +71,39 @@ Date | Qty | Queue 2nd | -60 | [[-10, 1-12-2021]] 3rd | +5 | [[-5, 3-12-2021]] 4th | +10 | [[5, 4-12-2021]] -4th | +20 | [[5, 4-12-2021], [20, 4-12-2021]] \ No newline at end of file +4th | +20 | [[5, 4-12-2021], [20, 4-12-2021]] + +### Concept of Transfer Qty Bucket +In the case of **Repack**, Quantity that comes in, isn't really incoming. It is just new stock repurposed from old stock, due to incoming-outgoing of the same warehouse. + +Here, stock is consumed from the FIFO Queue. It is then re-added back to the queue. +While adding stock back to the queue we need to know how much to add. +For this we need to keep track of how much was previously consumed. +Hence we use **Transfer Qty Bucket**. + +While re-adding stock, we try to add buckets that were consumed earlier (date intact), to maintain correctness. + +#### Case 1: Same Item-Warehouse in Repack +Eg: +------------------------------------------------------------------------------------- +Date | Qty | Voucher | FIFO Queue | Transfer Qty Buckets +------------------------------------------------------------------------------------- +1st | +500 | PR | [[500, 1-12-2021]] | +2nd | -50 | Repack | [[450, 1-12-2021]] | [[50, 1-12-2021]] +2nd | +50 | Repack | [[450, 1-12-2021], [50, 1-12-2021]] | [] + +- The balance at the end is restored back to 500 +- However, the initial 500 qty bucket is now split into 450 and 50, with the same date +- The net effect is the same as that before the Repack + +#### Case 2: Same Item-Warehouse in Repack with Split Consumption rows +Eg: +------------------------------------------------------------------------------------- +Date | Qty | Voucher | FIFO Queue | Transfer Qty Buckets +------------------------------------------------------------------------------------- +1st | +500 | PR | [[500, 1-12-2021]] | +2nd | -50 | Repack | [[450, 1-12-2021]] | [[50, 1-12-2021]] +2nd | -50 | Repack | [[400, 1-12-2021]] | [[50, 1-12-2021], +- | | | |[50, 1-12-2021]] +2nd | +100 | Repack | [[400, 1-12-2021], [50, 1-12-2021], | [] +- | | | [50, 1-12-2021]] | From c66b5752e286a927e479a393d406940cb51824d5 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 17 Feb 2022 14:30:00 +0530 Subject: [PATCH 040/121] fix: Precision of available qty and negative stock in transfer bucket - Maintain only positive values in transfer bucket - Use it to neutralize/add stock to fifo queue (cherry picked from commit d3fbbcfed39570fbad52a77b2533c2b72da8679f) --- .../stock/report/stock_ageing/stock_ageing.py | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 9866e63fb5a..60f9e959c89 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -28,6 +28,7 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li "Returns ordered, formatted data with ranges." _func = itemgetter(1) data = [] + precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) for item, item_dict in item_details.items(): earliest_age, latest_age = 0, 0 @@ -48,10 +49,13 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li if filters.get("show_warehouse_wise_stock"): row.append(details.warehouse) - row.extend([item_dict.get("total_qty"), average_age, + row.extend([ + flt(item_dict.get("total_qty"), precision), + average_age, range1, range2, range3, above_range3, earliest_age, latest_age, - details.stock_uom]) + details.stock_uom + ]) data.append(row) @@ -288,13 +292,14 @@ class FIFOSlots: transfer_data = self.transferred_item_details.get(transfer_key) if transfer_data: - # [Repack] inward/outward from same voucher, item & warehouse + # inward/outward from same voucher, item & warehouse + # eg: Repack with same item, Stock reco for batch item # consume transfer data and add stock to fifo queue self.__adjust_incoming_transfer_qty(transfer_data, fifo_queue, row) else: if not serial_nos: - if fifo_queue and flt(fifo_queue[0][0]) < 0: - # neutralize negative stock by adding positive stock + if fifo_queue and flt(fifo_queue[0][0]) <= 0: + # neutralize 0/negative stock by adding positive stock fifo_queue[0][0] += flt(row.actual_qty) fifo_queue[0][1] = row.posting_date else: @@ -325,7 +330,7 @@ class FIFOSlots: elif not fifo_queue: # negative stock, no balance but qty yet to consume fifo_queue.append([-(qty_to_pop), row.posting_date]) - self.transferred_item_details[transfer_key].append([row.actual_qty, row.posting_date]) + self.transferred_item_details[transfer_key].append([qty_to_pop, row.posting_date]) qty_to_pop = 0 else: # qty to pop < slot qty, ample balance @@ -337,22 +342,28 @@ class FIFOSlots: def __adjust_incoming_transfer_qty(self, transfer_data: Dict, fifo_queue: List, row: Dict): "Add previously removed stock back to FIFO Queue." transfer_qty_to_pop = flt(row.actual_qty) - first_bucket_qty = transfer_data[0][0] - first_bucket_date = transfer_data[0][1] + + def add_to_fifo_queue(slot): + if fifo_queue and flt(fifo_queue[0][0]) <= 0: + # neutralize 0/negative stock by adding positive stock + fifo_queue[0][0] += flt(slot[0]) + fifo_queue[0][1] = slot[1] + else: + fifo_queue.append(slot) while transfer_qty_to_pop: - if transfer_data and 0 > first_bucket_qty <= transfer_qty_to_pop: + if transfer_data and 0 < transfer_data[0][0] <= transfer_qty_to_pop: # bucket qty is not enough, consume whole - transfer_qty_to_pop -= first_bucket_qty - slot = transfer_data.pop(0) - fifo_queue.append(slot) + transfer_qty_to_pop -= transfer_data[0][0] + add_to_fifo_queue(transfer_data.pop(0)) elif not transfer_data: # transfer bucket is empty, extra incoming qty - fifo_queue.append([transfer_qty_to_pop, row.posting_date]) + add_to_fifo_queue([transfer_qty_to_pop, row.posting_date]) + transfer_qty_to_pop = 0 else: # ample bucket qty to consume - first_bucket_qty -= transfer_qty_to_pop - fifo_queue.append([transfer_qty_to_pop, first_bucket_date]) + transfer_data[0][0] -= transfer_qty_to_pop + add_to_fifo_queue([transfer_qty_to_pop, transfer_data[0][1]]) transfer_qty_to_pop = 0 def __update_balances(self, row: Dict, key: Union[Tuple, str]): From fde5a9e6f78f1cb4cc30ed4fca48f28775023e4a Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 18 Feb 2022 18:52:42 +0530 Subject: [PATCH 041/121] fix: Range Qty precision (cherry picked from commit ed4a6c6cc63ca37a6033f9f87c35cd26aaa2cb43) --- erpnext/stock/report/stock_ageing/stock_ageing.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 60f9e959c89..97a740e1844 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -12,6 +12,7 @@ from frappe.utils import cint, date_diff, flt from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos Filters = frappe._dict +precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) def execute(filters: Filters = None) -> Tuple: to_date = filters["to_date"] @@ -28,7 +29,6 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li "Returns ordered, formatted data with ranges." _func = itemgetter(1) data = [] - precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) for item, item_dict in item_details.items(): earliest_age, latest_age = 0, 0 @@ -83,13 +83,13 @@ def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: D qty = flt(item[0]) if not item_dict["has_serial_no"] else 1.0 if age <= filters.range1: - range1 += qty + range1 = flt(range1 + qty, precision) elif age <= filters.range2: - range2 += qty + range2 = flt(range2 + qty, precision) elif age <= filters.range3: - range3 += qty + range3 = flt(range3 + qty, precision) else: - above_range3 += qty + above_range3 = flt(above_range3 + qty, precision) return range1, range2, range3, above_range3 From 88ca371887f9cf10069184e220a9410fe4a48842 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 18 Feb 2022 18:53:05 +0530 Subject: [PATCH 042/121] test: Negative Stock, over consumption & over production with split rows, balance precision (cherry picked from commit d5be536740642d0bef9ea23151a41ce2657b9cd2) --- .../report/stock_ageing/test_stock_ageing.py | 221 +++++++++++++++++- 1 file changed, 217 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 3055332540f..3fc357e8d4f 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -3,7 +3,7 @@ import frappe -from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots +from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, format_report_data from erpnext.tests.utils import ERPNextTestCase @@ -11,7 +11,8 @@ class TestStockAgeing(ERPNextTestCase): def setUp(self) -> None: self.filters = frappe._dict( company="_Test Company", - to_date="2021-12-10" + to_date="2021-12-10", + range1=30, range2=60, range3=90 ) def test_normal_inward_outward_queue(self): @@ -289,7 +290,8 @@ class TestStockAgeing(ERPNextTestCase): self.assertEqual(item_result["total_qty"], 500.0) self.assertEqual(queue[0][0], 400.0) - self.assertEqual(queue[1][0], 100.0) + self.assertEqual(queue[1][0], 50.0) + self.assertEqual(queue[2][0], 50.0) # check if time buckets add up to balance qty self.assertEqual(sum([i[0] for i in queue]), 500.0) @@ -341,6 +343,63 @@ class TestStockAgeing(ERPNextTestCase): # check if time buckets add up to balance qty self.assertEqual(sum([i[0] for i in queue]), 450.0) + def test_repack_entry_same_item_overconsume_with_split_rows(self): + """ + Over consume item and have less repacked item qty (same warehouse). + Ledger: + Item | Qty | Voucher + ------------------------ + Item 1 | 20 | 001 + Item 1 | -50 | 002 (repack) + Item 1 | -50 | 002 (repack) + Item 1 | 50 | 002 (repack) + """ + sle = [ + frappe._dict( # stock up item + name="Flask Item", + actual_qty=20, qty_after_transaction=20, + warehouse="WH 1", + posting_date="2021-12-03", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-50), qty_after_transaction=(-30), + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-50), qty_after_transaction=(-80), + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=50, qty_after_transaction=(-30), + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + ] + fifo_slots = FIFOSlots(self.filters, sle) + slots = fifo_slots.generate() + item_result = slots["Flask Item"] + queue = item_result["fifo_queue"] + + self.assertEqual(item_result["total_qty"], -30.0) + self.assertEqual(queue[0][0], -30.0) + + # check transfer bucket + transfer_bucket = fifo_slots.transferred_item_details[('002', 'Flask Item', 'WH 1')] + self.assertEqual(transfer_bucket[0][0], 50) + def test_repack_entry_same_item_overproduce(self): """ Under consume item and have more repacked item qty (same warehouse). @@ -385,10 +444,164 @@ class TestStockAgeing(ERPNextTestCase): self.assertEqual(item_result["total_qty"], 550.0) self.assertEqual(queue[0][0], 450.0) - self.assertEqual(queue[1][0], 100.0) + self.assertEqual(queue[1][0], 50.0) + self.assertEqual(queue[2][0], 50.0) # check if time buckets add up to balance qty self.assertEqual(sum([i[0] for i in queue]), 550.0) + def test_repack_entry_same_item_overproduce_with_split_rows(self): + """ + Over consume item and have less repacked item qty (same warehouse). + Ledger: + Item | Qty | Voucher + ------------------------ + Item 1 | 20 | 001 + Item 1 | -50 | 002 (repack) + Item 1 | 50 | 002 (repack) + Item 1 | 50 | 002 (repack) + """ + sle = [ + frappe._dict( # stock up item + name="Flask Item", + actual_qty=20, qty_after_transaction=20, + warehouse="WH 1", + posting_date="2021-12-03", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=(-50), qty_after_transaction=(-30), + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=50, qty_after_transaction=20, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + frappe._dict( + name="Flask Item", + actual_qty=50, qty_after_transaction=70, + warehouse="WH 1", + posting_date="2021-12-04", voucher_type="Stock Entry", + voucher_no="002", + has_serial_no=False, serial_no=None + ), + ] + fifo_slots = FIFOSlots(self.filters, sle) + slots = fifo_slots.generate() + item_result = slots["Flask Item"] + queue = item_result["fifo_queue"] + + self.assertEqual(item_result["total_qty"], 70.0) + self.assertEqual(queue[0][0], 20.0) + self.assertEqual(queue[1][0], 50.0) + + # check transfer bucket + transfer_bucket = fifo_slots.transferred_item_details[('002', 'Flask Item', 'WH 1')] + self.assertFalse(transfer_bucket) + + def test_negative_stock_same_voucher(self): + """ + Test negative stock scenario in transfer bucket via repack entry (same wh). + Ledger: + Item | Qty | Voucher + ------------------------ + Item 1 | -50 | 001 + Item 1 | -50 | 001 + Item 1 | 30 | 001 + Item 1 | 80 | 001 + """ + sle = [ + frappe._dict( # stock up item + name="Flask Item", + actual_qty=(-50), qty_after_transaction=(-50), + warehouse="WH 1", + posting_date="2021-12-01", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( # stock up item + name="Flask Item", + actual_qty=(-50), qty_after_transaction=(-100), + warehouse="WH 1", + posting_date="2021-12-01", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( # stock up item + name="Flask Item", + actual_qty=30, qty_after_transaction=(-70), + warehouse="WH 1", + posting_date="2021-12-01", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + ] + fifo_slots = FIFOSlots(self.filters, sle) + slots = fifo_slots.generate() + item_result = slots["Flask Item"] + + # check transfer bucket + transfer_bucket = fifo_slots.transferred_item_details[('001', 'Flask Item', 'WH 1')] + self.assertEqual(transfer_bucket[0][0], 20) + self.assertEqual(transfer_bucket[1][0], 50) + self.assertEqual(item_result["fifo_queue"][0][0], -70.0) + + sle.append(frappe._dict( + name="Flask Item", + actual_qty=80, qty_after_transaction=10, + warehouse="WH 1", + posting_date="2021-12-01", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + )) + + fifo_slots = FIFOSlots(self.filters, sle) + slots = fifo_slots.generate() + item_result = slots["Flask Item"] + + transfer_bucket = fifo_slots.transferred_item_details[('001', 'Flask Item', 'WH 1')] + self.assertFalse(transfer_bucket) + self.assertEqual(item_result["fifo_queue"][0][0], 10.0) + + def test_precision(self): + "Test if final balance qty is rounded off correctly." + sle = [ + frappe._dict( # stock up item + name="Flask Item", + actual_qty=0.3, qty_after_transaction=0.3, + warehouse="WH 1", + posting_date="2021-12-01", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + frappe._dict( # stock up item + name="Flask Item", + actual_qty=0.6, qty_after_transaction=0.9, + warehouse="WH 1", + posting_date="2021-12-01", voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, serial_no=None + ), + ] + + slots = FIFOSlots(self.filters, sle).generate() + report_data = format_report_data(self.filters, slots, self.filters["to_date"]) + row = report_data[0] # first row in report + bal_qty = row[5] + range_qty_sum = sum([i for i in row[7:11]]) # get sum of range balance + + # check if value of Available Qty column matches with range bucket post format + self.assertEqual(bal_qty, 0.9) + self.assertEqual(bal_qty, range_qty_sum) + def generate_item_and_item_wh_wise_slots(filters, sle): "Return results with and without 'show_warehouse_wise_stock'" item_wise_slots = FIFOSlots(filters, sle).generate() From 41b82145ea10e97b730685654f743218b5b8f47e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Feb 2022 22:01:00 +0530 Subject: [PATCH 043/121] fix: GSTIN filter for GSTR-1 report (cherry picked from commit 87b074ac0966ab26bf776c720fcb96b92a451d55) --- erpnext/regional/report/gstr_1/gstr_1.js | 23 ++++++++++++++++++++--- erpnext/regional/report/gstr_1/gstr_1.py | 23 ++++++++++++++++++++++- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js index 4b98978f130..1766fdb2ecd 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.js +++ b/erpnext/regional/report/gstr_1/gstr_1.js @@ -17,7 +17,7 @@ frappe.query_reports["GSTR-1"] = { "fieldtype": "Link", "options": "Address", "get_query": function () { - var company = frappe.query_report.get_filter_value('company'); + let company = frappe.query_report.get_filter_value('company'); if (company) { return { "query": 'frappe.contacts.doctype.address.address.address_query', @@ -26,6 +26,11 @@ frappe.query_reports["GSTR-1"] = { } } }, + { + "fieldname": "company_gstin", + "label": __("Company GSTIN"), + "fieldtype": "Select" + }, { "fieldname": "from_date", "label": __("From Date"), @@ -60,10 +65,22 @@ frappe.query_reports["GSTR-1"] = { } ], onload: function (report) { + let filters = report.get_values(); + + frappe.call({ + method: 'erpnext.regional.report.gstr_1.gstr_1.get_company_gstins', + args: { + company: filters.company + }, + callback: function(r) { + console.log(r.message); + frappe.query_report.page.fields_dict.company_gstin.df.options = r.message; + frappe.query_report.page.fields_dict.company_gstin.refresh(); + } + }); + report.page.add_inner_button(__("Download as JSON"), function () { - var filters = report.get_values(); - frappe.call({ method: 'erpnext.regional.report.gstr_1.gstr_1.get_json', args: { diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 8805678dc7d..1ba3d20bdbb 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -254,7 +254,8 @@ class Gstr1Report(object): for opts in (("company", " and company=%(company)s"), ("from_date", " and posting_date>=%(from_date)s"), ("to_date", " and posting_date<=%(to_date)s"), - ("company_address", " and company_address=%(company_address)s")): + ("company_address", " and company_address=%(company_address)s"), + ("company_gstin", " and company_gstin=%(company_gstin)s")): if self.filters.get(opts[0]): conditions += opts[1] @@ -1193,3 +1194,23 @@ def is_inter_state(invoice_detail): return True else: return False + + +@frappe.whitelist() +def get_company_gstins(company): + address = frappe.qb.DocType("Address") + links = frappe.qb.DocType("Dynamic Link") + + addresses = frappe.qb.from_(address).inner_join(links).on( + address.name == links.parent + ).select( + address.gstin + ).where( + links.link_doctype == 'Company' + ).where( + links.link_name == company + ).run(as_dict=1) + + address_list = [''] + [d.gstin for d in addresses] + + return address_list \ No newline at end of file From 3a98cc351c92374424ef5a8c75068b300a2e6f29 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Sat, 19 Feb 2022 19:35:57 +0530 Subject: [PATCH 044/121] Update gstr_1.js (cherry picked from commit a28ec89507fd42bf100b6a64c6bcdeef55f4b032) --- erpnext/regional/report/gstr_1/gstr_1.js | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js index 1766fdb2ecd..9999a6d167b 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.js +++ b/erpnext/regional/report/gstr_1/gstr_1.js @@ -73,7 +73,6 @@ frappe.query_reports["GSTR-1"] = { company: filters.company }, callback: function(r) { - console.log(r.message); frappe.query_report.page.fields_dict.company_gstin.df.options = r.message; frappe.query_report.page.fields_dict.company_gstin.refresh(); } From 02b7541fb9fc20631f3a3362ad061c48dc9e1d0b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 12 Feb 2022 21:54:22 +0530 Subject: [PATCH 046/121] fix: Error in consolidated financial statements (cherry picked from commit ae613008be59334e5ff72882ef9d70355f56805e) --- .../consolidated_financial_statement.py | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 758e3e93379..62bf156219a 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -367,7 +367,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies): accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0) def get_account_heads(root_type, companies, filters): - accounts = get_accounts(root_type, filters) + accounts = get_accounts(root_type, companies) if not accounts: return None, None, None @@ -396,7 +396,7 @@ def update_parent_account_names(accounts): for account in accounts: if account.parent_account: - account["parent_account_name"] = name_to_account_map[account.parent_account] + account["parent_account_name"] = name_to_account_map.get(account.parent_account) return accounts @@ -419,12 +419,21 @@ def get_subsidiary_companies(company): return frappe.db.sql_list("""select name from `tabCompany` where lft >= {0} and rgt <= {1} order by lft, rgt""".format(lft, rgt)) -def get_accounts(root_type, filters): - return frappe.db.sql(""" select name, is_group, company, - parent_account, lft, rgt, root_type, report_type, account_name, account_number - from - `tabAccount` where company = %s and root_type = %s - """ , (filters.get('company'), root_type), as_dict=1) +def get_accounts(root_type, companies): + accounts = [] + added_accounts = [] + + for company in companies: + for account in frappe.db.sql(""" select name, is_group, company, + parent_account, lft, rgt, root_type, report_type, account_name, account_number + from + `tabAccount` where company = %s and root_type = %s + """ , (company, root_type), as_dict=1): + if account.account_name not in added_accounts: + accounts.append(account) + added_accounts.append(account.account_name) + + return accounts def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters): data = [] From b99a95b280b328a75a0a64bda9e3900d257bd782 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 15 Feb 2022 12:05:51 +0530 Subject: [PATCH 047/121] fix: Remove commented out code (cherry picked from commit 42cdd6d2379d68efb592a5c8a8148979dce8cf1e) --- .../consolidated_financial_statement.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 62bf156219a..dad7384feaf 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -354,9 +354,6 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies): if d.parent_account: account = d.parent_account_name - # if not accounts_by_name.get(account): - # continue - for company in companies: accounts_by_name[account][company] = \ accounts_by_name[account].get(company, 0.0) + d.get(company, 0.0) From 232ba3580b9cd61fd609db55571d7c0ccff14248 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 15 Feb 2022 12:15:35 +0530 Subject: [PATCH 048/121] fix: Linting issues (cherry picked from commit fec40aac7a25c383e384f29471f9ea82382524b2) --- .../consolidated_financial_statement.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index dad7384feaf..1e20f7be3e4 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -421,11 +421,9 @@ def get_accounts(root_type, companies): added_accounts = [] for company in companies: - for account in frappe.db.sql(""" select name, is_group, company, - parent_account, lft, rgt, root_type, report_type, account_name, account_number - from - `tabAccount` where company = %s and root_type = %s - """ , (company, root_type), as_dict=1): + for account in frappe.get_all("Account", fields=["name", "is_group", "company", + "parent_account", "lft", "rgt", "root_type", "report_type", "account_name", "account_number"], + filters={"company": company, "root_type": root_type}): if account.account_name not in added_accounts: accounts.append(account) added_accounts.append(account.account_name) From 8d24cb44a814e25497390c302dd09ca08aa98ac9 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Feb 2022 11:38:16 +0530 Subject: [PATCH 049/121] fix(pos): removal of coupon code (cherry picked from commit fa38c291bd577b40f0d5007470108596d392f89b) --- erpnext/selling/page/point_of_sale/pos_payment.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 9650bc88a42..4d75e6ef1bf 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -180,14 +180,6 @@ erpnext.PointOfSale.Payment = class { () => frm.save(), () => this.update_totals_section(frm.doc) ]); - } else { - frappe.run_serially([ - () => frm.doc.ignore_pricing_rule=1, - () => frm.trigger('ignore_pricing_rule'), - () => frm.doc.ignore_pricing_rule=0, - () => frm.save(), - () => this.update_totals_section(frm.doc) - ]); } } }); From 57d5a027fb929803ffd62463da3e5b4611b17ff4 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Feb 2022 13:58:01 +0530 Subject: [PATCH 050/121] fix: remove redundant method --- .../doctype/asset_repair/asset_repair.js | 21 +++++++----- .../doctype/asset_repair/asset_repair.py | 33 ------------------- 2 files changed, 13 insertions(+), 41 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 18454fe3678..1e4c4e082d6 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -69,18 +69,23 @@ frappe.ui.form.on('Asset Repair', { frappe.ui.form.on('Asset Repair Consumed Item', { item_code: function(frm, cdt, cdn) { - var row = locals[cdt][cdn]; + var item = locals[cdt][cdn]; - frappe.call ({ - method: "erpnext.assets.doctype.asset_repair.asset_repair.get_valuation_rate", + let item_args = { + 'item_code': item.item_code, + 'warehouse': frm.doc.warehouse, + 'qty': item.consumed_quantity, + 'serial_no': item.serial_no, + 'company': frm.doc.company + } + + frappe.call({ + method: 'erpnext.stock.utils.get_incoming_rate', args: { - "item_code": row.item_code, - "warehouse": frm.doc.warehouse + args: item_args }, callback: function(r) { - if(r.message) { - frappe.model.set_value(cdt, cdn, 'valuation_rate', r.message); - } + frappe.model.set_value(cdt, cdn, 'valuation_rate', r.message); } }); }, diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index d5e3d3c811a..36848e9f15c 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -256,36 +256,3 @@ class AssetRepair(AccountsController): def get_downtime(failure_date, completion_date): downtime = time_diff_in_hours(completion_date, failure_date) return round(downtime, 2) - -@frappe.whitelist() -def get_valuation_rate(item_code, warehouse): - last_valuation_rate = frappe.get_all( - "Stock Ledger Entry", - filters = { - "item_code": item_code, - "warehouse": warehouse, - "valuation_rate": [">=", 0], - "docstatus": ["<", 2] - }, - pluck = "valuation_rate", - order_by = "posting_date desc, posting_time desc, name desc" - ) - - if last_valuation_rate: - return last_valuation_rate[0] - else: - valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate") - - if not valuation_rate: - # try Item Standard rate - valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate") - - if not valuation_rate: - # try in price list - valuation_rate = frappe.db.get_value( - "Item Price", - dict(item_code=item_code, buying=1), - "price_list_rate" - ) - - return valuation_rate From 2aba6c38b36e5fce373800eb129a93eb010c691f Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Feb 2022 14:01:12 +0530 Subject: [PATCH 051/121] fix: cannot edit valutaion_rate in asset repair --- .../asset_repair_consumed_item/asset_repair_consumed_item.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json index 3c850c82653..4685a09db63 100644 --- a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json +++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json @@ -16,8 +16,7 @@ "fieldname": "valuation_rate", "fieldtype": "Currency", "in_list_view": 1, - "label": "Valuation Rate", - "read_only": 1 + "label": "Valuation Rate" }, { "fieldname": "consumed_quantity", From 14c5be7c27c90d0de1247ff83ee5075a16f78b08 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 14:23:52 +0530 Subject: [PATCH 052/121] chore: Show 'Produced Qty' field in Sales Order Item (#29903) (#29904) (cherry picked from commit e952cce17d8931054575de2e430f6000ae80ef9f) Co-authored-by: Marica --- .../selling/doctype/sales_order_item/sales_order_item.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 95f6c4e96df..080d517d131 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -83,8 +83,8 @@ "planned_qty", "column_break_69", "work_order_qty", - "delivered_qty", "produced_qty", + "delivered_qty", "returned_qty", "shopping_cart_section", "additional_notes", @@ -701,10 +701,8 @@ "width": "50px" }, { - "description": "For Production", "fieldname": "produced_qty", "fieldtype": "Float", - "hidden": 1, "label": "Produced Quantity", "oldfieldname": "produced_qty", "oldfieldtype": "Currency", @@ -802,7 +800,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-10-05 12:27:25.014789", + "modified": "2022-02-21 13:55:08.883104", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", @@ -811,5 +809,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From 130f0395496c4019f3e52cee1aa2a761e3134414 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 19 Feb 2022 19:19:32 +0530 Subject: [PATCH 053/121] fix: Ledger entries on LIA for term loans (cherry picked from commit 1aa12fb3f1bee18a8a58d11954acd8112e96261d) --- .../loan_interest_accrual.py | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index 0de073f85da..1c800a06da0 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -74,39 +74,6 @@ class LoanInterestAccrual(AccountsController): }) ) - if self.payable_principal_amount: - gle_map.append( - self.get_gl_dict({ - "account": self.loan_account, - "party_type": self.applicant_type, - "party": self.applicant, - "against": self.interest_income_account, - "debit": self.payable_principal_amount, - "debit_in_account_currency": self.interest_amount, - "against_voucher_type": "Loan", - "against_voucher": self.loan, - "remarks": _("Interest accrued from {0} to {1} against loan: {2}").format( - self.last_accrual_date, self.posting_date, self.loan), - "cost_center": erpnext.get_default_cost_center(self.company), - "posting_date": self.posting_date - }) - ) - - gle_map.append( - self.get_gl_dict({ - "account": self.interest_income_account, - "against": self.loan_account, - "credit": self.payable_principal_amount, - "credit_in_account_currency": self.interest_amount, - "against_voucher_type": "Loan", - "against_voucher": self.loan, - "remarks": ("Interest accrued from {0} to {1} against loan: {2}").format( - self.last_accrual_date, self.posting_date, self.loan), - "cost_center": erpnext.get_default_cost_center(self.company), - "posting_date": self.posting_date - }) - ) - if gle_map: make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj) From bc28755978c17d7e85cdc5ffd4a45a629183e2c8 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Feb 2022 14:26:21 +0530 Subject: [PATCH 054/121] fix: sider --- erpnext/assets/doctype/asset_repair/asset_repair.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 1e4c4e082d6..3fe6b2d0d5d 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -77,7 +77,7 @@ frappe.ui.form.on('Asset Repair Consumed Item', { 'qty': item.consumed_quantity, 'serial_no': item.serial_no, 'company': frm.doc.company - } + }; frappe.call({ method: 'erpnext.stock.utils.get_incoming_rate', From a6fba24ab1582bc4fad99c901244e3662d91f417 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 21 Feb 2022 13:58:56 +0530 Subject: [PATCH 055/121] fix: Total Credit amount in TDS Payable monthly report (cherry picked from commit a82cf7214e301a3f70513e308d1625a726a1beea) --- .../accounts/report/tds_payable_monthly/tds_payable_monthly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index 57f79748f0a..e6cbff5d429 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -43,7 +43,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map): if entry.account in tds_accounts: tds_deducted += (entry.credit - entry.debit) - total_amount_credited += (entry.credit - entry.debit) + total_amount_credited += entry.credit if tds_deducted: row = { From 9adbaaea4a1c674941513a1fbfc69be9318134ca Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 21 Feb 2022 17:41:23 +0530 Subject: [PATCH 056/121] fix: round off increments in numeric item variant (cherry picked from commit 00e8565868e3bb8a1547abeedd2d158a9b7e5bf4) --- erpnext/stock/doctype/item/item.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 954e061caa6..1ce09f0152c 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -594,7 +594,7 @@ $.extend(erpnext.item, { const increment = r.message.increment; let values = []; - for(var i = from; i <= to; i += increment) { + for(var i = from; i <= to; i = flt(i + increment, 6)) { values.push(i); } attr_val_fields[d.attribute] = values; From 6b87d38a8090893fdb5639a6d784cf29dc74fb33 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 20:17:05 +0530 Subject: [PATCH 057/121] fix: JobCard TimeLog to_date (#29872) (#29919) (cherry picked from commit e4c4dc402e75d3ec501095fa3e914553fcd07a4d) Co-authored-by: Sagar Sharma --- erpnext/manufacturing/doctype/job_card/job_card.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index e6090ba02a2..3c406156ebd 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -62,7 +62,7 @@ class JobCard(Document): if self.get('time_logs'): for d in self.get('time_logs'): - if get_datetime(d.from_time) > get_datetime(d.to_time): + if d.to_time and get_datetime(d.from_time) > get_datetime(d.to_time): frappe.throw(_("Row {0}: From time must be less than to time").format(d.idx)) data = self.get_overlap_for(d) From 233a75a438d99774ab4ce30d7b67dae3acea9708 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 23:10:20 +0530 Subject: [PATCH 058/121] fix(LMS): program enrollment does not give any feedback (backport #29922) (#29924) Co-authored-by: Rucha Mahabal --- erpnext/www/lms/macros/hero.html | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/erpnext/www/lms/macros/hero.html b/erpnext/www/lms/macros/hero.html index e72bfc8175b..95ba8f7df28 100644 --- a/erpnext/www/lms/macros/hero.html +++ b/erpnext/www/lms/macros/hero.html @@ -11,7 +11,7 @@ {% if frappe.session.user == 'Guest' %} {{_('Sign Up')}} {% elif not has_access %} - + {% endif %}

@@ -20,34 +20,35 @@ From 0f101b52097d2681d475d2ccc0cc68dd3735ee31 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 22 Feb 2022 11:56:56 +0530 Subject: [PATCH 059/121] fix(Salary Slip): TypeError while clearing any amount field in components (backport #29931) (#29932) Co-authored-by: Rucha Mahabal --- erpnext/payroll/doctype/salary_slip/salary_slip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 8ea791f620f..e70c5116bed 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -1265,7 +1265,7 @@ class SalarySlip(TransactionBase): for i, earning in enumerate(self.earnings): if earning.salary_component == salary_component: self.earnings[i].amount = wages_amount - self.gross_pay += self.earnings[i].amount + self.gross_pay += flt(self.earnings[i].amount, earning.precision("amount")) self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) def compute_year_to_date(self): From 6539d32ca90e44a9abfd04b36fc998c4cb9c9ece Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 22 Feb 2022 13:01:31 +0530 Subject: [PATCH 060/121] fix: Fetch conversion factor even if it already existed in row, on item change (#29917) (#29935) * fix: Fetch conversion factor even if it already existed in row, on item change * fix: Retain manually changed conversion factor - If item code changes, reset conversion factor on client side - Keep API behavious consistent, if conversion factor is sent, same must come back - API should not ideally reset values in most cases (cherry picked from commit 235fc127b3ecf943176ed9c208425f9bda100798) Co-authored-by: Marica --- erpnext/public/js/controllers/transaction.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index e742ae9a890..4801b333df9 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -525,6 +525,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ item.weight_per_unit = 0; item.weight_uom = ''; + item.conversion_factor = 0; if(['Sales Invoice'].includes(this.frm.doc.doctype)) { update_stock = cint(me.frm.doc.update_stock); From 2e6a076f902846c1b06eb9876cb080a7dcf67524 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Feb 2022 12:41:08 +0530 Subject: [PATCH 061/121] fix(asset): no. of depreciation booked cannot be equal to total no. of depreciations (cherry picked from commit c5808543c83ea43f62784331fb7c513543e454f0) --- erpnext/assets/doctype/asset/asset.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index a4f5eb4d92d..fb48712c825 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -418,11 +418,12 @@ class Asset(AccountsController): def validate_asset_finance_books(self, row): if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount") - .format(row.idx)) + .format(row.idx), title=_("Invalid Schedule")) if not row.depreciation_start_date: if not self.available_for_use_date: - frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx)) + frappe.throw(_("Row {0}: Depreciation Start Date is required") + .format(row.idx), title=_("Invalid Schedule")) row.depreciation_start_date = get_last_day(self.available_for_use_date) if not self.is_existing_asset: @@ -440,8 +441,9 @@ class Asset(AccountsController): else: self.number_of_depreciations_booked = 0 - if cint(self.number_of_depreciations_booked) > cint(row.total_number_of_depreciations): - frappe.throw(_("Number of Depreciations Booked cannot be greater than Total Number of Depreciations")) + if flt(row.total_number_of_depreciations) <= cint(self.number_of_depreciations_booked): + frappe.throw(_("Row {0}: Total Number of Depreciations cannot be less than or equal to Number of Depreciations Booked") + .format(row.idx), title=_("Invalid Schedule")) if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date): frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date") From b6d42ff571b5be2f0fff44e3576753591d8acb80 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Feb 2022 12:45:52 +0530 Subject: [PATCH 062/121] test: number_of_depr_booked = total_number_of_depr (cherry picked from commit 780694f6e2d686ca7d037556a52e097802814266) --- erpnext/assets/doctype/asset/test_asset.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index a15a1b6f388..6afd6dab264 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -820,8 +820,9 @@ class TestDepreciationBasics(AssetSetup): self.assertRaises(frappe.ValidationError, asset.save) def test_number_of_depreciations(self): - """Tests if an error is raised when number_of_depreciations_booked > total_number_of_depreciations.""" + """Tests if an error is raised when number_of_depreciations_booked >= total_number_of_depreciations.""" + # number_of_depreciations_booked > total_number_of_depreciations asset = create_asset( item_code = "Macbook Pro", calculate_depreciation = 1, @@ -836,6 +837,21 @@ class TestDepreciationBasics(AssetSetup): self.assertRaises(frappe.ValidationError, asset.save) + # number_of_depreciations_booked = total_number_of_depreciations + asset_2 = create_asset( + item_code = "Macbook Pro", + calculate_depreciation = 1, + available_for_use_date = "2019-12-31", + total_number_of_depreciations = 5, + expected_value_after_useful_life = 10000, + depreciation_start_date = "2020-07-01", + opening_accumulated_depreciation = 10000, + number_of_depreciations_booked = 5, + do_not_save = 1 + ) + + self.assertRaises(frappe.ValidationError, asset_2.save) + def test_depreciation_start_date_is_before_purchase_date(self): asset = create_asset( item_code = "Macbook Pro", From 807997c6ecb183172a513b17062426c0fad39092 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 22 Feb 2022 18:20:59 +0530 Subject: [PATCH 063/121] fix: remove customer field value when MR is not customer provided (#29938) (#29941) (cherry picked from commit 7f55226a5807645db4f93c8038f1cc03a6fc0ce6) Co-authored-by: Ankush Menat --- .../stock/doctype/material_request/material_request.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index bdbf5e47f03..50d43171f80 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -57,14 +57,13 @@ class MaterialRequest(BuyingController): if actual_so_qty and (flt(so_items[so_no][item]) + already_indented > actual_so_qty): frappe.throw(_("Material Request of maximum {0} can be made for Item {1} against Sales Order {2}").format(actual_so_qty - already_indented, item, so_no)) - # Validate - # --------------------- def validate(self): super(MaterialRequest, self).validate() self.validate_schedule_date() self.check_for_on_hold_or_closed_status('Sales Order', 'sales_order') self.validate_uom_is_integer("uom", "qty") + self.validate_material_request_type() if not self.status: self.status = "Draft" @@ -84,6 +83,12 @@ class MaterialRequest(BuyingController): self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") + def validate_material_request_type(self): + """ Validate fields in accordance with selected type """ + + if self.material_request_type != "Customer Provided": + self.customer = None + def set_title(self): '''Set title as comma separated list of items''' if not self.title: From 46ed9a059c9384b65500cc28c19eef5f2c50a70a Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Tue, 22 Feb 2022 17:20:48 +0530 Subject: [PATCH 064/121] fix: Taxjar minor fixes (cherry picked from commit 1682a26fe69b9b3fa64293e692e79a553b842ca2) --- .../taxjar_integration.py | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/erpnext/erpnext_integrations/taxjar_integration.py b/erpnext/erpnext_integrations/taxjar_integration.py index a4e21579e32..14c86d56328 100644 --- a/erpnext/erpnext_integrations/taxjar_integration.py +++ b/erpnext/erpnext_integrations/taxjar_integration.py @@ -8,10 +8,6 @@ from frappe.utils import cint, flt from erpnext import get_default_company, get_region -TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head") -SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head") -TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions") -TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax") SUPPORTED_COUNTRY_CODES = ["AT", "AU", "BE", "BG", "CA", "CY", "CZ", "DE", "DK", "EE", "ES", "FI", "FR", "GB", "GR", "HR", "HU", "IE", "IT", "LT", "LU", "LV", "MT", "NL", "PL", "PT", "RO", "SE", "SI", "SK", "US"] @@ -35,12 +31,14 @@ def get_client(): if api_key and api_url: client = taxjar.Client(api_key=api_key, api_url=api_url) client.set_api_config('headers', { - 'x-api-version': '2020-08-07' + 'x-api-version': '2022-01-24' }) return client def create_transaction(doc, method): + TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions") + """Create an order transaction in TaxJar""" if not TAXJAR_CREATE_TRANSACTIONS: @@ -51,6 +49,7 @@ def create_transaction(doc, method): if not client: return + TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head") sales_tax = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == TAX_ACCOUNT_HEAD]) if not sales_tax: @@ -79,6 +78,7 @@ def create_transaction(doc, method): def delete_transaction(doc, method): """Delete an existing TaxJar order transaction""" + TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions") if not TAXJAR_CREATE_TRANSACTIONS: return @@ -92,6 +92,8 @@ def delete_transaction(doc, method): def get_tax_data(doc): + SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head") + from_address = get_company_address_details(doc) from_shipping_state = from_address.get("state") from_country_code = frappe.db.get_value("Country", from_address.country, "code") @@ -113,20 +115,20 @@ def get_tax_data(doc): to_shipping_state = get_state_code(to_address, 'Shipping') tax_dict = { - 'from_country': from_country_code, - 'from_zip': from_address.pincode, - 'from_state': from_shipping_state, - 'from_city': from_address.city, - 'from_street': from_address.address_line1, - 'to_country': to_country_code, - 'to_zip': to_address.pincode, - 'to_city': to_address.city, - 'to_street': to_address.address_line1, - 'to_state': to_shipping_state, - 'shipping': shipping, - 'amount': doc.net_total, - 'plugin': 'erpnext', - 'line_items': line_items + "from_country": from_country_code, + "from_zip": from_address.pincode, + "from_state": from_shipping_state, + "from_city": from_address.city, + "from_street": from_address.address_line1, + "to_country": to_country_code, + "to_zip": to_address.pincode, + "to_city": to_address.city, + "to_street": to_address.address_line1, + "to_state": to_shipping_state, + "shipping": shipping, + "amount": doc.net_total, + "plugin": "erpnext", + "line_items": line_items } return tax_dict @@ -156,6 +158,9 @@ def get_line_item_dict(item, docstatus): return tax_dict def set_sales_tax(doc, method): + TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head") + TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax") + if not TAXJAR_CALCULATE_TAX: return @@ -206,6 +211,7 @@ def set_sales_tax(doc, method): doc.run_method("calculate_taxes_and_totals") def check_for_nexus(doc, tax_dict): + TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head") if not frappe.db.get_value('TaxJar Nexus', {'region_code': tax_dict["to_state"]}): for item in doc.get("items"): item.tax_collectable = flt(0) @@ -218,6 +224,8 @@ def check_for_nexus(doc, tax_dict): def check_sales_tax_exemption(doc): # if the party is exempt from sales tax, then set all tax account heads to zero + TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head") + sales_tax_exempted = hasattr(doc, "exempt_from_sales_tax") and doc.exempt_from_sales_tax \ or frappe.db.has_column("Customer", "exempt_from_sales_tax") \ and frappe.db.get_value("Customer", doc.customer, "exempt_from_sales_tax") From cebe8888af49f4540da5633334ecd87baf542344 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 23 Feb 2022 10:54:44 +0530 Subject: [PATCH 065/121] fix: Email translations (cherry picked from commit aaa84a21ba8c1749735c6510c5f311c3db505aef) --- erpnext/translations/de.csv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index d46ffb56096..6e70bc579e7 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -3726,7 +3726,7 @@ Earliest Age,Frühestes Alter, Edit Details,Details bearbeiten, Edit Profile,Profil bearbeiten, Either GST Transporter ID or Vehicle No is required if Mode of Transport is Road,Bei Straßentransport ist entweder die GST-Transporter-ID oder die Fahrzeug-Nr. Erforderlich, -Email,Email, +Email,E-Mail, Email Campaigns,E-Mail-Kampagnen, Employee ID is linked with another instructor,Die Mitarbeiter-ID ist mit einem anderen Ausbilder verknüpft, Employee Tax and Benefits,Mitarbeitersteuern und -leistungen, @@ -6481,7 +6481,7 @@ Select Users,Wählen Sie Benutzer aus, Send Emails At,Die E-Mails senden um, Reminder,Erinnerung, Daily Work Summary Group User,Tägliche Arbeit Zusammenfassung Gruppenbenutzer, -email,Email, +email,E-Mail, Parent Department,Elternabteilung, Leave Block List,Urlaubssperrenliste, Days for which Holidays are blocked for this department.,"Tage, an denen eine Urlaubssperre für diese Abteilung gilt.", From e82f55ce9eb419842068e8fb2ab2b2cef245c8f6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 18 Feb 2022 20:05:49 +0530 Subject: [PATCH 066/121] fix: Validate party account with company (cherry picked from commit 5a2b571aa9e0f448d2030e1901dfb9ec3e547d46) --- .../accounts/doctype/payment_entry/payment_entry.py | 2 +- .../sales_taxes_and_charges_template.py | 2 +- erpnext/accounts/party.py | 5 ++++- erpnext/controllers/accounts_controller.py | 12 +++++------- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 80f61d4f02e..a9134022b53 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1078,7 +1078,7 @@ def get_outstanding_reference_documents(args): if d.voucher_type in ("Purchase Invoice"): d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no") - # Get all SO / PO which are not fully billed or aginst which full advance not paid + # Get all SO / PO which are not fully billed or against which full advance not paid orders_to_be_billed = [] if (args.get("party_type") != "Student"): orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"), diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py index b5909447dc8..1d30934df92 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py @@ -46,7 +46,7 @@ def valdiate_taxes_and_charges_template(doc): for tax in doc.get("taxes"): validate_taxes_and_charges(tax) - validate_account_head(tax, doc) + validate_account_head(tax.idx, tax.account_head, doc.company) validate_cost_center(tax, doc) validate_inclusive_tax(tax, doc) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 4f4f180e542..907964720ff 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -308,7 +308,7 @@ def validate_party_gle_currency(party_type, party, company, party_account_curren .format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency) def validate_party_accounts(doc): - + from erpnext.controllers.accounts_controller import validate_account_head companies = [] for account in doc.get("accounts"): @@ -331,6 +331,9 @@ def validate_party_accounts(doc): if doc.default_currency != party_account_currency and doc.default_currency != company_default_currency: frappe.throw(_("Billing currency must be equal to either default company's currency or party account currency")) + # validate if account is mapped for same company + validate_account_head(account.idx, account.account, account.company) + @frappe.whitelist() def get_due_date(posting_date, party_type, party, company=None, bill_date=None): diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 3513b0ad663..810ac8218a9 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1567,13 +1567,12 @@ def validate_taxes_and_charges(tax): tax.rate = None -def validate_account_head(tax, doc): - company = frappe.get_cached_value('Account', - tax.account_head, 'company') +def validate_account_head(idx, account, company): + account_company = frappe.get_cached_value('Account', account, 'company') - if company != doc.company: + if account_company != company: frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}') - .format(tax.idx, frappe.bold(tax.account_head), frappe.bold(doc.company)), title=_('Invalid Account')) + .format(idx, frappe.bold(account), frappe.bold(company)), title=_('Invalid Account')) def validate_cost_center(tax, doc): @@ -1956,8 +1955,7 @@ def update_bin_on_delete(row, doctype): qty_dict["ordered_qty"] = get_ordered_qty(row.item_code, row.warehouse) - if row.warehouse: - update_bin_qty(row.item_code, row.warehouse, qty_dict) + update_bin_qty(row.item_code, row.warehouse, qty_dict) def validate_and_delete_children(parent, data): deleted_children = [] From da1713ad0093d99e84ace753edaafc6aea809561 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 22 Feb 2022 20:58:10 +0530 Subject: [PATCH 067/121] fix: Remove unintended changes (cherry picked from commit a61790c00fa2b3c53ba49d930c7d08b3f0213b65) --- erpnext/controllers/accounts_controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 810ac8218a9..f146071c0bd 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1955,7 +1955,8 @@ def update_bin_on_delete(row, doctype): qty_dict["ordered_qty"] = get_ordered_qty(row.item_code, row.warehouse) - update_bin_qty(row.item_code, row.warehouse, qty_dict) + if row.warehouse: + update_bin_qty(row.item_code, row.warehouse, qty_dict) def validate_and_delete_children(parent, data): deleted_children = [] From 982476d6842be30e036fc7b7014b4392030f56df Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Feb 2022 21:21:39 +0530 Subject: [PATCH 068/121] chore: batch flag and consumption rate in invariant report --- .../stock_ledger_invariant_check.py | 7 +++++-- erpnext/stock/stock_ledger.py | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py index 48753b0edd4..8a6210cc2be 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py @@ -60,6 +60,9 @@ def add_invariant_check_fields(sles): fifo_qty += qty fifo_value += qty * rate + if sle.actual_qty < 0: + sle.consumption_rate = sle.stock_value_difference / sle.actual_qty + balance_qty += sle.actual_qty balance_stock_value += sle.stock_value_difference if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no: @@ -145,9 +148,9 @@ def get_columns(): "label": "Incoming Rate", }, { - "fieldname": "outgoing_rate", + "fieldname": "consumption_rate", "fieldtype": "Float", - "label": "Outgoing Rate", + "label": "Consumption Rate", }, { "fieldname": "qty_after_transaction", diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index b47097553ba..971b6bcb126 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -23,7 +23,6 @@ class NegativeStockError(frappe.ValidationError): pass class SerialNoExistsInFutureTransaction(frappe.ValidationError): pass -_exceptions = frappe.local('stockledger_exceptions') def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): from erpnext.controllers.stock_controller import future_sle_exists From 6d354edb6b860299b3a03d26081b24d311f376ea Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 23 Feb 2022 11:31:33 +0530 Subject: [PATCH 069/121] refactor: code duplication for fallback rates --- erpnext/stock/stock_ledger.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 971b6bcb126..1af75047064 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -621,9 +621,7 @@ class update_entries_after(object): if not self.wh_data.valuation_rate and sle.voucher_detail_no: allow_zero_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no) if not allow_zero_rate: - self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, - sle.voucher_type, sle.voucher_no, self.allow_zero_rate, - currency=erpnext.get_company_currency(sle.company), company=sle.company) + self.wh_data.valuation_rate = self.get_fallback_rate(sle) def get_incoming_value_for_serial_nos(self, sle, serial_nos): # get rate from serial nos within same company @@ -689,9 +687,7 @@ class update_entries_after(object): if not self.wh_data.valuation_rate and sle.voucher_detail_no: allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no) if not allow_zero_valuation_rate: - self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, - sle.voucher_type, sle.voucher_no, self.allow_zero_rate, - currency=erpnext.get_company_currency(sle.company), company=sle.company) + self.wh_data.valuation_rate = self.get_fallback_rate(sle) def get_fifo_values(self, sle): incoming_rate = flt(sle.incoming_rate) @@ -722,9 +718,7 @@ class update_entries_after(object): # Get valuation rate from last sle if exists or from valuation rate field in item master allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no) if not allow_zero_valuation_rate: - _rate = get_valuation_rate(sle.item_code, sle.warehouse, - sle.voucher_type, sle.voucher_no, self.allow_zero_rate, - currency=erpnext.get_company_currency(sle.company), company=sle.company) + _rate = self.get_fallback_rate(sle) else: _rate = 0 @@ -787,6 +781,13 @@ class update_entries_after(object): else: return 0 + def get_fallback_rate(self, sle) -> float: + """When exact incoming rate isn't available use any of other "average" rates as fallback. + This should only get used for negative stock.""" + return get_valuation_rate(sle.item_code, sle.warehouse, + sle.voucher_type, sle.voucher_no, self.allow_zero_rate, + currency=erpnext.get_company_currency(sle.company), company=sle.company) + def get_sle_before_datetime(self, args): """get previous stock ledger entry before current time-bucket""" sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False) From 37ba3666aca72479a3afd0eb8aa8f37123bd3c22 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 23 Feb 2022 11:34:01 +0530 Subject: [PATCH 070/121] fix: reset stock value if no qty --- erpnext/stock/stock_ledger.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 1af75047064..c2cb2ca6746 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -458,6 +458,8 @@ class update_entries_after(object): # rounding as per precision self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision) + if not self.wh_data.qty_after_transaction: + self.wh_data.stock_value = 0.0 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value self.wh_data.prev_stock_value = self.wh_data.stock_value From cdcd60a1ea40d997b783f40c275a152dcf202840 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 23 Feb 2022 16:09:37 +0530 Subject: [PATCH 071/121] chore: Rollback after each test, due to premature commit via `remove_user_permission` (backport #29964) (#29967) * chore: Rollback after each test, due to premature commit via `remove_user_permission` - `remove_user_permission` in `test_warehouse_user` calls delete_doc that enqueues dynamic link deletion - Execution of background job eventually commits - While in the test suite it runs sequentially in the same thread and commits whatever was done until then - Which is why the rollback in `tearDownClass` is quite useless here - This premature commit causes many illegal transactions caught by `assertRaises` to be committed in the db - This creates faulty/dirty ledgers and breaks reports, as outiside the test suite this shouldn't/wouldn't happen - Rollback after each test, and for `test_warehouse_user` in particular, manually cancel transaction (cherry picked from commit bf8743713dcb318958c01e693c6d1071fd5fc218) # Conflicts: # erpnext/stock/doctype/stock_entry/test_stock_entry.py * test: Make Variant if absent in `test_variant_work_order`, keep test atomic (cherry picked from commit 5ff3705872960190031066dbe34158ad3c659820) * fix: Merge conflicts Co-authored-by: marination --- erpnext/stock/doctype/stock_entry/test_stock_entry.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 3d936c50f7a..3c34d4795cb 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -45,6 +45,7 @@ def get_sle(**args): class TestStockEntry(ERPNextTestCase): def tearDown(self): + frappe.db.rollback() frappe.set_user("Administrator") frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0") @@ -566,6 +567,7 @@ class TestStockEntry(ERPNextTestCase): st1.set_stock_entry_type() st1.insert() st1.submit() + st1.cancel() frappe.set_user("Administrator") remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com") @@ -690,6 +692,8 @@ class TestStockEntry(ERPNextTestCase): bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item", "is_default": 1, "docstatus": 1}) + make_item_variant() # make variant of _Test Variant Item if absent + work_order = frappe.new_doc("Work Order") work_order.update({ "company": "_Test Company", @@ -1101,13 +1105,10 @@ class TestStockEntry(ERPNextTestCase): # Check if FG cost is calculated based on RM total cost # RM total cost = 200, FG rate = 200/4(FG qty) = 50 - self.assertEqual(se.items[1].basic_rate, 50) + self.assertEqual(se.items[1].basic_rate, flt(se.items[0].basic_rate/4)) self.assertEqual(se.value_difference, 0.0) self.assertEqual(se.total_incoming_value, se.total_outgoing_value) - # teardown - se.delete() - def make_serialized_item(**args): args = frappe._dict(args) se = frappe.copy_doc(test_records[0]) From 5d1b7ec61d984ae5e03e3d8330fbcc69c37f2b0a Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 21 Feb 2022 16:14:40 +0530 Subject: [PATCH 072/121] fix: Block merging items if both have product bundles (cherry picked from commit 28cc2dbb72fc3d716ffcb19b039dccd67c13eb33) --- erpnext/stock/doctype/item/item.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 557e07bf0a2..cf261e5ba7b 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -399,6 +399,7 @@ class Item(Document): if merge: self.validate_properties_before_merge(new_name) + self.validate_duplicate_product_bundles_before_merge(old_name, new_name) self.validate_duplicate_website_item_before_merge(old_name, new_name) def after_rename(self, old_name, new_name, merge): @@ -463,6 +464,18 @@ class Item(Document): msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list]) frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError) + def validate_duplicate_product_bundles_before_merge(self, old_name, new_name): + "Block merge if both old and new items have product bundles." + bundle = frappe.get_value("Product Bundle",filters={"new_item_code": old_name}) + if bundle: + bundle_link = get_link_to_form("Product Bundle", bundle) + old_name, new_name = frappe.bold(old_name), frappe.bold(new_name) + + msg = _("Please delete Product Bundle {0}, before merging {1} into {2}").format( + bundle_link, old_name, new_name + ) + frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError) + def validate_duplicate_website_item_before_merge(self, old_name, new_name): """ Block merge if both old and new items have website items against them. @@ -480,8 +493,9 @@ class Item(Document): old_web_item = [d.get("name") for d in web_items if d.get("item_code") == old_name][0] web_item_link = get_link_to_form("Website Item", old_web_item) + old_name, new_name = frappe.bold(old_name), frappe.bold(new_name) - msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} and {new_name}" + msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} into {new_name}" frappe.throw(_(msg), title=_("Cannot Merge"), exc=DataValidationError) def set_last_purchase_rate(self, new_name): From e0ed91433ba74af7fd9525e4d67f190a7d20890c Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 21 Feb 2022 16:48:04 +0530 Subject: [PATCH 073/121] test: Item Merging with Product Bundles (cherry picked from commit 530f9f70291758d51babd7ec4f52eefe1a899ef1) --- erpnext/stock/doctype/item/test_item.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 150cead465b..e149086ae77 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -14,6 +14,7 @@ from erpnext.controllers.item_variant import ( get_variant, ) from erpnext.stock.doctype.item.item import ( + DataValidationError, InvalidBarcode, StockExistsForTemplate, get_item_attribute, @@ -387,6 +388,25 @@ class TestItem(ERPNextTestCase): self.assertTrue(frappe.db.get_value("Bin", {"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"})) + def test_item_merging_with_product_bundle(self): + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + + create_item("Test Item Bundle Item 1", is_stock_item=False) + create_item("Test Item Bundle Item 2", is_stock_item=False) + create_item("Test Item inside Bundle") + bundle_items = ["Test Item inside Bundle"] + + bundle1 = make_product_bundle("Test Item Bundle Item 1", bundle_items, qty=2) + make_product_bundle("Test Item Bundle Item 2", bundle_items, qty=2) + + with self.assertRaises(DataValidationError): + frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True) + + bundle1.delete() + frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True) + + self.assertFalse(frappe.db.exists("Item", "Test Item Bundle Item 1")) + def test_uom_conversion_factor(self): if frappe.db.exists('Item', 'Test Item UOM'): frappe.delete_doc('Item', 'Test Item UOM') From 34e0b85d6a37d8b60aaac12d9a6d326b884d1bfb Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 23 Feb 2022 16:26:20 +0530 Subject: [PATCH 074/121] fix: Check if both old and new items have bundles before merging - If only one has bundle against it, they can be merged (cherry picked from commit a33f04ea41087305c95111aa86bf96d3df2e2b36) --- erpnext/stock/doctype/item/item.py | 8 +++++--- erpnext/stock/doctype/item/test_item.py | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index cf261e5ba7b..7bc875ac12f 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -466,9 +466,11 @@ class Item(Document): def validate_duplicate_product_bundles_before_merge(self, old_name, new_name): "Block merge if both old and new items have product bundles." - bundle = frappe.get_value("Product Bundle",filters={"new_item_code": old_name}) - if bundle: - bundle_link = get_link_to_form("Product Bundle", bundle) + old_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": old_name}) + new_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": new_name}) + + if old_bundle and new_bundle: + bundle_link = get_link_to_form("Product Bundle", old_bundle) old_name, new_name = frappe.bold(old_name), frappe.bold(new_name) msg = _("Please delete Product Bundle {0}, before merging {1} into {2}").format( diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index e149086ae77..c912101a4ac 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -396,6 +396,7 @@ class TestItem(ERPNextTestCase): create_item("Test Item inside Bundle") bundle_items = ["Test Item inside Bundle"] + # make bundles for both items bundle1 = make_product_bundle("Test Item Bundle Item 1", bundle_items, qty=2) make_product_bundle("Test Item Bundle Item 2", bundle_items, qty=2) From 95bf7acff87fb3faba91bea5bdc0d86a7c803fbc Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 23 Feb 2022 17:07:13 +0530 Subject: [PATCH 075/121] test: fix flaky stateful tests (#29749) (#29969) Co-authored-by: Ankush Menat Co-Authored-By: Marica --- .../opening_invoice_creation_tool.py | 2 +- .../test_opening_invoice_creation_tool.py | 39 ++++++++----------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index 5ba0131f6c0..a33892e044d 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -166,7 +166,7 @@ class OpeningInvoiceCreationTool(Document): frappe.scrub(row.party_type): row.party, "is_pos": 0, "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice", - "update_stock": 0, + "update_stock": 0, # important: https://github.com/frappe/erpnext/pull/23559 "invoice_number": row.invoice_number, "disable_rounded_total": 1 }) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py index 6700e9b975d..3eaf6a28f37 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py @@ -1,11 +1,7 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest - import frappe -from frappe.cache_manager import clear_doctype_cache -from frappe.custom.doctype.property_setter.property_setter import make_property_setter from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( create_dimension, @@ -14,14 +10,17 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import ( get_temporary_opening_account, ) +from erpnext.tests.utils import ERPNextTestCase test_dependencies = ["Customer", "Supplier", "Accounting Dimension"] -class TestOpeningInvoiceCreationTool(unittest.TestCase): - def setUp(self): +class TestOpeningInvoiceCreationTool(ERPNextTestCase): + @classmethod + def setUpClass(self): if not frappe.db.exists("Company", "_Test Opening Invoice Company"): make_company() create_dimension() + return super().setUpClass() def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None, department=None): doc = frappe.get_single("Opening Invoice Creation Tool") @@ -31,26 +30,20 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase): return doc.make_invoices() def test_opening_sales_invoice_creation(self): - property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check") - try: - invoices = self.make_invoices(company="_Test Opening Invoice Company") + invoices = self.make_invoices(company="_Test Opening Invoice Company") - self.assertEqual(len(invoices), 2) - expected_value = { - "keys": ["customer", "outstanding_amount", "status"], - 0: ["_Test Customer", 300, "Overdue"], - 1: ["_Test Customer 1", 250, "Overdue"], - } - self.check_expected_values(invoices, expected_value) + self.assertEqual(len(invoices), 2) + expected_value = { + "keys": ["customer", "outstanding_amount", "status"], + 0: ["_Test Customer", 300, "Overdue"], + 1: ["_Test Customer 1", 250, "Overdue"], + } + self.check_expected_values(invoices, expected_value) - si = frappe.get_doc("Sales Invoice", invoices[0]) + si = frappe.get_doc("Sales Invoice", invoices[0]) - # Check if update stock is not enabled - self.assertEqual(si.update_stock, 0) - - finally: - property_setter.delete() - clear_doctype_cache("Sales Invoice") + # Check if update stock is not enabled + self.assertEqual(si.update_stock, 0) def check_expected_values(self, invoices, expected_value, invoice_type="Sales"): doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice" From dcef0de906fa3268dc22f1bc6477fdbbbb45c542 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 25 Feb 2022 14:36:29 +0530 Subject: [PATCH 076/121] fix(pos): mode of payment disappears after save (cherry picked from commit 69c34cd7ae128dde56cde10c53b479331c33d56f) --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 2d6bef5bca5..9d585411582 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -439,7 +439,6 @@ class POSInvoice(SalesInvoice): self.paid_amount = 0 def set_account_for_mode_of_payment(self): - self.payments = [d for d in self.payments if d.amount or d.base_amount or d.default] for pay in self.payments: if not pay.account: pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account") From f2264fa58dedd14b7240b91233b560a80a895672 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 25 Feb 2022 15:18:06 +0530 Subject: [PATCH 077/121] fix(pos): coupon code is applied even if ignore pricing rule is check (cherry picked from commit 81514516f3c7106a5b211796bb74ddad0a6add20) # Conflicts: # erpnext/public/js/controllers/transaction.js --- erpnext/public/js/controllers/transaction.js | 15 +++++++++++ .../selling/page/point_of_sale/pos_payment.js | 25 +++++++++++-------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 4801b333df9..a6c223385a3 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2264,6 +2264,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }, +<<<<<<< HEAD coupon_code: function() { var me = this; frappe.run_serially([ @@ -2272,6 +2273,20 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ () => this.frm.doc.ignore_pricing_rule=0, () => me.apply_pricing_rule() ]); +======= + coupon_code() { + if (this.frm.doc.coupon_code || this.frm._last_coupon_code) { + // reset pricing rules if coupon code is set or is unset + const _ignore_pricing_rule = this.frm.doc.ignore_pricing_rule; + return frappe.run_serially([ + () => this.frm.doc.ignore_pricing_rule=1, + () => this.frm.trigger('ignore_pricing_rule'), + () => this.frm.doc.ignore_pricing_rule=_ignore_pricing_rule, + () => this.frm.trigger('apply_pricing_rule'), + () => this.frm._last_coupon_code = this.frm.doc.coupon_code + ]); + } +>>>>>>> 81514516f3 (fix(pos): coupon code is applied even if ignore pricing rule is check) } }); diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 4d75e6ef1bf..1e9f6d7d920 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -170,17 +170,20 @@ erpnext.PointOfSale.Payment = class { }); frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => { - if (!frm.doc.ignore_pricing_rule) { - if (frm.doc.coupon_code) { - frappe.run_serially([ - () => frm.doc.ignore_pricing_rule=1, - () => frm.trigger('ignore_pricing_rule'), - () => frm.doc.ignore_pricing_rule=0, - () => frm.trigger('apply_pricing_rule'), - () => frm.save(), - () => this.update_totals_section(frm.doc) - ]); - } + if (!frm.doc.ignore_pricing_rule && frm.doc.coupon_code) { + frappe.run_serially([ + () => frm.doc.ignore_pricing_rule=1, + () => frm.trigger('ignore_pricing_rule'), + () => frm.doc.ignore_pricing_rule=0, + () => frm.trigger('apply_pricing_rule'), + () => frm.save(), + () => this.update_totals_section(frm.doc) + ]); + } else if (frm.doc.ignore_pricing_rule && frm.doc.coupon_code) { + frappe.show_alert({ + message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."), + indicator: "orange" + }); } }); From 6bb1dccddd3ed903177aaff186a2655d89c23eb6 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 25 Feb 2022 15:45:52 +0530 Subject: [PATCH 078/121] fix: merge conflicts --- erpnext/public/js/controllers/transaction.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index a6c223385a3..a89776250f2 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2264,17 +2264,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }, -<<<<<<< HEAD coupon_code: function() { - var me = this; - frappe.run_serially([ - () => this.frm.doc.ignore_pricing_rule=1, - () => me.ignore_pricing_rule(), - () => this.frm.doc.ignore_pricing_rule=0, - () => me.apply_pricing_rule() - ]); -======= - coupon_code() { if (this.frm.doc.coupon_code || this.frm._last_coupon_code) { // reset pricing rules if coupon code is set or is unset const _ignore_pricing_rule = this.frm.doc.ignore_pricing_rule; @@ -2286,7 +2276,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ () => this.frm._last_coupon_code = this.frm.doc.coupon_code ]); } ->>>>>>> 81514516f3 (fix(pos): coupon code is applied even if ignore pricing rule is check) } }); From a067aabb3c2197f1f53918424c678b8e70e434af Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Wed, 9 Feb 2022 10:52:38 -0500 Subject: [PATCH 079/121] test: many users linked to customer shopping cart --- .../shopping_cart/test_shopping_cart.py | 25 +++++++++++-------- erpnext/tests/utils.py | 14 +++++++++++ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py index 578f3893490..ba3a36685df 100644 --- a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py +++ b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py @@ -56,13 +56,19 @@ class TestShoppingCart(unittest.TestCase): return quotation def test_get_cart_customer(self): - self.login_as_customer() + def validate_quotation(): + # test if quotation with customer is fetched + quotation = _get_cart_quotation() + self.assertEqual(quotation.quotation_to, "Customer") + self.assertEqual(quotation.party_name, "_Test Customer") + self.assertEqual(quotation.contact_email, frappe.session.user) + return quotation - # test if quotation with customer is fetched - quotation = _get_cart_quotation() - self.assertEqual(quotation.quotation_to, "Customer") - self.assertEqual(quotation.party_name, "_Test Customer") - self.assertEqual(quotation.contact_email, frappe.session.user) + self.login_as_customer("test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer") + validate_quotation() + + self.login_as_customer() + quotation = validate_quotation() return quotation @@ -253,10 +259,9 @@ class TestShoppingCart(unittest.TestCase): self.create_user_if_not_exists("test_cart_user@example.com") frappe.set_user("test_cart_user@example.com") - def login_as_customer(self): - self.create_user_if_not_exists("test_contact_customer@example.com", - "_Test Contact For _Test Customer") - frappe.set_user("test_contact_customer@example.com") + def login_as_customer(self, email="test_contact_customer@example.com", name="_Test Contact For _Test Customer"): + self.create_user_if_not_exists(email, name) + frappe.set_user(email) def clear_existing_quotations(self): quotations = frappe.get_all("Quotation", filters={ diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index 2378a54311f..1568b142022 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -66,6 +66,20 @@ def create_test_contact_and_address(): contact.add_phone("+91 0000000000", is_primary_phone=True) contact.insert() + contact_two = frappe.get_doc({ + "doctype": 'Contact', + "first_name": "_Test Contact 2 for _Test Customer", + "links": [ + { + "link_doctype": "Customer", + "link_name": "_Test Customer" + } + ] + }) + contact_two.add_email("test_contact_two_customer@example.com", is_primary=True) + contact_two.add_phone("+92 0000000000", is_primary_phone=True) + contact_two.insert() + @contextmanager def change_settings(doctype, settings_dict): From 78c2a369c44e2671f2c2912ea01bce695f49e44c Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Wed, 9 Feb 2022 10:53:24 -0500 Subject: [PATCH 080/121] fix: get cart for logged in user. --- erpnext/e_commerce/shopping_cart/cart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py index 75366e9f33a..fff9f079744 100644 --- a/erpnext/e_commerce/shopping_cart/cart.py +++ b/erpnext/e_commerce/shopping_cart/cart.py @@ -311,7 +311,7 @@ def _get_cart_quotation(party=None): party = get_party() quotation = frappe.get_all("Quotation", fields=["name"], filters= - {"party_name": party.name, "order_type": "Shopping Cart", "docstatus": 0}, + {"party_name": party.name, "contact_email": frappe.session.user, "order_type": "Shopping Cart", "docstatus": 0}, order_by="modified desc", limit_page_length=1) if quotation: From 03945aae37214d5096bd2fdd06408a4317219bb6 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Wed, 9 Feb 2022 12:03:05 -0500 Subject: [PATCH 081/121] fix: get cart items for logged in user. --- erpnext/e_commerce/product_data_engine/query.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/e_commerce/product_data_engine/query.py b/erpnext/e_commerce/product_data_engine/query.py index b927b0146e1..1a2ddeb0251 100644 --- a/erpnext/e_commerce/product_data_engine/query.py +++ b/erpnext/e_commerce/product_data_engine/query.py @@ -265,7 +265,7 @@ class ProductQuery: customer = get_customer(silent=True) if customer: quotation = frappe.get_all("Quotation", fields=["name"], filters= - {"party_name": customer, "order_type": "Shopping Cart", "docstatus": 0}, + {"party_name": customer, "contact_email": frappe.session.user, "order_type": "Shopping Cart", "docstatus": 0}, order_by="modified desc", limit_page_length=1) if quotation: items = frappe.get_all( @@ -299,4 +299,4 @@ class ProductQuery: # slice results manually result[:self.page_length] - return result \ No newline at end of file + return result From afade046fb60c77c1099dc3caaf2367a6ed4d214 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 25 Feb 2022 23:07:04 +0530 Subject: [PATCH 082/121] fix: org chart connectors not rendered when Employee Naming is set to Full Name (backport #29997) (#29998) Co-authored-by: Rucha Mahabal --- .../js/hierarchy_chart/hierarchy_chart_desktop.js | 14 ++++++++------ .../js/hierarchy_chart/hierarchy_chart_mobile.js | 9 +++++---- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js index 831626aa915..a585aa614fb 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js @@ -304,12 +304,13 @@ erpnext.HierarchyChart = class { } get_child_nodes(node_id) { + let me = this; return new Promise(resolve => { frappe.call({ - method: this.method, + method: me.method, args: { parent: node_id, - company: this.company + company: me.company } }).then(r => resolve(r.message)); }); @@ -350,12 +351,13 @@ erpnext.HierarchyChart = class { } get_all_nodes() { + let me = this; return new Promise(resolve => { frappe.call({ method: 'erpnext.utilities.hierarchy_chart.get_all_nodes', args: { - method: this.method, - company: this.company + method: me.method, + company: me.company }, callback: (r) => { resolve(r.message); @@ -427,8 +429,8 @@ erpnext.HierarchyChart = class { add_connector(parent_id, child_id) { // using pure javascript for better performance - const parent_node = document.querySelector(`#${parent_id}`); - const child_node = document.querySelector(`#${child_id}`); + const parent_node = document.getElementById(`${parent_id}`); + const child_node = document.getElementById(`${child_id}`); let path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js index 0a8ba78f643..52236e7df96 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js @@ -235,7 +235,7 @@ erpnext.HierarchyChartMobile = class { let me = this; return new Promise(resolve => { frappe.call({ - method: this.method, + method: me.method, args: { parent: node_id, company: me.company, @@ -286,8 +286,8 @@ erpnext.HierarchyChartMobile = class { } add_connector(parent_id, child_id) { - const parent_node = document.querySelector(`#${parent_id}`); - const child_node = document.querySelector(`#${child_id}`); + const parent_node = document.getElementById(`${parent_id}`); + const child_node = document.getElementById(`${child_id}`); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); @@ -518,7 +518,8 @@ erpnext.HierarchyChartMobile = class { level.nextAll('li').remove(); let node_object = this.nodes[node.id]; - let current_node = level.find(`#${node.id}`).detach(); + let current_node = level.find(`[id="${node.id}"]`).detach(); + current_node.removeClass('active-child active-path'); node_object.expanded = 0; From 067ede76ea0851a29e270e150f482368e0afa194 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Sat, 26 Feb 2022 11:13:00 +0530 Subject: [PATCH 083/121] fix: validate Work Order qty against Production Plan (#29721) * fix: validate Work Order qty against Production Plan * chore: err msg when max_qty is 0 * test: add test for overproduction * fix: CI --- .../production_plan/test_production_plan.py | 20 +++++++++++-------- .../doctype/work_order/work_order.py | 15 ++++++++++++++ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index d88e10a564c..2359815813d 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -9,6 +9,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import ( get_sales_orders, get_warehouse_list, ) +from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry @@ -466,26 +467,29 @@ class TestProductionPlan(ERPNextTestCase): bom = make_bom(item=item, raw_materials=raw_materials) # Create Production Plan - pln = create_production_plan(item_code=bom.item, planned_qty=10) + pln = create_production_plan(item_code=bom.item, planned_qty=5) # All the created Work Orders wo_list = [] - # Create and Submit 1st Work Order for 5 qty - create_work_order(item, pln, 5) + # Create and Submit 1st Work Order for 3 qty + create_work_order(item, pln, 3) + pln.reload() + self.assertEqual(pln.po_items[0].ordered_qty, 3) + + # Create and Submit 2nd Work Order for 2 qty + create_work_order(item, pln, 2) pln.reload() self.assertEqual(pln.po_items[0].ordered_qty, 5) - # Create and Submit 2nd Work Order for 3 qty - create_work_order(item, pln, 3) - pln.reload() - self.assertEqual(pln.po_items[0].ordered_qty, 8) + # Overproduction + self.assertRaises(OverProductionError, create_work_order, item=item, pln=pln, qty=2) # Cancel 1st Work Order wo1 = frappe.get_doc("Work Order", wo_list[0]) wo1.cancel() pln.reload() - self.assertEqual(pln.po_items[0].ordered_qty, 3) + self.assertEqual(pln.po_items[0].ordered_qty, 2) # Cancel 2nd Work Order wo2 = frappe.get_doc("Work Order", wo_list[1]) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index f50c82c66b6..204a6df6289 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -632,6 +632,21 @@ class WorkOrder(Document): if not self.qty > 0: frappe.throw(_("Quantity to Manufacture must be greater than 0.")) + if self.production_plan and self.production_plan_item: + qty_dict = frappe.db.get_value("Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1) + + allowance_qty =flt(frappe.db.get_single_value("Manufacturing Settings", + "overproduction_percentage_for_work_order"))/100 * qty_dict.get("planned_qty", 0) + + max_qty = qty_dict.get("planned_qty", 0) + allowance_qty - qty_dict.get("ordered_qty", 0) + + if max_qty < 1: + frappe.throw(_("Cannot produce more item for {0}") + .format(self.production_item), OverProductionError) + elif self.qty > max_qty: + frappe.throw(_("Cannot produce more than {0} items for {1}") + .format(max_qty, self.production_item), OverProductionError) + def validate_transfer_against(self): if not self.docstatus == 1: # let user configure operations until they're ready to submit From 5d69a182f604e0f4b2ed75891a7b21e64ff25ea5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 26 Feb 2022 12:12:10 +0530 Subject: [PATCH 084/121] fix(ux): make "allow zero valuation rate" readonly if "s_warehouse" is set (#29681) (#30002) * chore: make allow zero valuation rate readonly if s_warehouse is set * fix: setting the checkbox to zero whenever the source warehouse is set * fix: remove allow_on_submit and refresh trigger Co-authored-by: Ankush Menat (cherry picked from commit 77ffcd3aed9b6fb99bc6f508897d3ebe6880de15) Co-authored-by: Sagar Sharma --- erpnext/stock/doctype/stock_entry/stock_entry.js | 6 ++++++ .../stock_entry_detail/stock_entry_detail.json | 12 +++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index fc9d1ed98f7..61466cff032 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -627,6 +627,12 @@ frappe.ui.form.on('Stock Entry Detail', { frm.events.set_serial_no(frm, cdt, cdn, () => { frm.events.get_warehouse_details(frm, cdt, cdn); }); + + // set allow_zero_valuation_rate to 0 if s_warehouse is selected. + let item = frappe.get_doc(cdt, cdn); + if (item.s_warehouse) { + item.allow_zero_valuation_rate = 0; + } }, t_warehouse: function(frm, cdt, cdn) { diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 2282b6aa167..a6b21891715 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -1,7 +1,7 @@ { "actions": [], "autoname": "hash", - "creation": "2013-03-29 18:22:12", + "creation": "2022-02-05 00:17:49.860824", "doctype": "DocType", "document_type": "Other", "editable_grid": 1, @@ -340,13 +340,13 @@ "label": "More Information" }, { - "allow_on_submit": 1, "default": "0", "fieldname": "allow_zero_valuation_rate", "fieldtype": "Check", "label": "Allow Zero Valuation Rate", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "read_only_depends_on": "eval:doc.s_warehouse" }, { "allow_on_submit": 1, @@ -556,12 +556,14 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-06-22 16:47:11.268975", + "modified": "2022-02-26 00:51:24.963653", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "states": [] } \ No newline at end of file From e509ce54388eccb6edf87b26beac7e725cf9016f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 22 Feb 2022 09:16:08 +0530 Subject: [PATCH 085/121] fix: Account filter in PSOA (cherry picked from commit 70b960e650bbc1c418eecd14ac42d64a3103a43c) --- .../process_statement_of_accounts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 09aa72352e4..1b34d6d1f2f 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -73,7 +73,7 @@ def get_report_pdf(doc, consolidated=True): 'to_date': doc.to_date, 'company': doc.company, 'finance_book': doc.finance_book if doc.finance_book else None, - 'account': doc.account if doc.account else None, + 'account': [doc.account] if doc.account else None, 'party_type': 'Customer', 'party': [entry.customer], 'presentation_currency': presentation_currency, From bce4ca939082910259d6ea79686b702aad1435ab Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Feb 2022 14:42:56 +0530 Subject: [PATCH 086/121] fix: Commission not applied while making Sales Order from Quotation (cherry picked from commit 03739147049ed78bd2bb43f5f47cfc70d6be43ba) # Conflicts: # erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json --- .../doctype/sales_invoice_item/sales_invoice_item.json | 9 ++++++++- .../doctype/sales_order_item/sales_order_item.json | 7 +++++-- .../doctype/delivery_note_item/delivery_note_item.json | 9 ++++++--- 3 files changed, 19 insertions(+), 6 deletions(-) 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 a9412d86396..8e19bc928b6 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -832,6 +832,7 @@ }, { "default": "0", + "fetch_from": "item_code.grant_commission", "fieldname": "grant_commission", "fieldtype": "Check", "label": "Grant Commission", @@ -841,7 +842,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-10-05 12:24:54.968907", + "modified": "2022-02-24 14:41:36.392560", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", @@ -849,5 +850,11 @@ "owner": "Administrator", "permissions": [], "sort_field": "modified", +<<<<<<< HEAD "sort_order": "DESC" } +======= + "sort_order": "DESC", + "states": [] +} +>>>>>>> 0373914704 (fix: Commission not applied while making Sales Order from Quotation) diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 080d517d131..7e55499533b 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -83,8 +83,8 @@ "planned_qty", "column_break_69", "work_order_qty", - "produced_qty", "delivered_qty", + "produced_qty", "returned_qty", "shopping_cart_section", "additional_notes", @@ -701,8 +701,10 @@ "width": "50px" }, { + "description": "For Production", "fieldname": "produced_qty", "fieldtype": "Float", + "hidden": 1, "label": "Produced Quantity", "oldfieldname": "produced_qty", "oldfieldtype": "Currency", @@ -791,6 +793,7 @@ }, { "default": "0", + "fetch_from": "item_code.grant_commission", "fieldname": "grant_commission", "fieldtype": "Check", "label": "Grant Commission", @@ -800,7 +803,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2022-02-21 13:55:08.883104", + "modified": "2022-02-24 14:41:57.325799", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 51c88bed61d..f1f5d96e628 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -757,6 +757,7 @@ }, { "default": "0", + "fetch_from": "item_code.grant_commission", "fieldname": "grant_commission", "fieldtype": "Check", "label": "Grant Commission", @@ -767,12 +768,14 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-10-06 12:12:44.018872", + "modified": "2022-02-24 14:42:20.211085", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", - "sort_order": "DESC" -} + "sort_order": "DESC", + "states": [] +} \ No newline at end of file From ebf70a4b5d368cf8ce6d595cd4c945ba382e67e4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 28 Feb 2022 12:41:59 +0530 Subject: [PATCH 087/121] test: add correct test case (cherry picked from commit 4e9a9f35a605f4f886cc9cc920ec4ab4262b9709) --- .../doctype/sales_invoice/test_sales_invoice.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 0cda5a05b78..5fd4d0bf22d 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2354,14 +2354,22 @@ class TestSalesInvoice(unittest.TestCase): def test_sales_commission(self): - si = frappe.copy_doc(test_records[0]) + si = frappe.copy_doc(test_records[2]) + + frappe.db.set_value('Item', si.get('items')[0].item_code, 'grant_commission', 1) + frappe.db.set_value('Item', si.get('items')[1].item_code, 'grant_commission', 0) + item = copy.deepcopy(si.get('items')[0]) item.update({ "qty": 1, "rate": 500, - "grant_commission": 1 }) - si.append("items", item) + + item = copy.deepcopy(si.get('items')[1]) + item.update({ + "qty": 1, + "rate": 500, + }) # Test valid values for commission_rate, total_commission in ((0, 0), (10, 50), (100, 500)): From aa684e6f3deb66a41f83ac084a7e39baff124acc Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 28 Feb 2022 12:44:39 +0530 Subject: [PATCH 088/121] fix: dont fetch draft/cancelled BOMs (cherry picked from commit 1d1203d5eca07e40d8ce32248a0d81b0d7130627) --- .../manufacturing/doctype/production_plan/production_plan.js | 2 +- erpnext/manufacturing/doctype/work_order/work_order.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index dba85a9fb6e..f3ded994814 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -49,7 +49,7 @@ frappe.ui.form.on('Production Plan', { if (d.item_code) { return { query: "erpnext.controllers.queries.bom", - filters:{'item': cstr(d.item_code)} + filters:{'item': cstr(d.item_code), 'docstatus': 1} } } else frappe.msgprint(__("Please enter Item first")); } diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 204a6df6289..47fe3296cf1 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -850,7 +850,7 @@ def get_item_details(item, project = None, skip_bom_info=False): res = res[0] if skip_bom_info: return res - filters = {"item": item, "is_default": 1} + filters = {"item": item, "is_default": 1, "docstatus": 1} if project: filters = {"item": item, "project": project} From b98d3da8e32f16105f3f9f9c2a6c8e20164419ca Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 28 Feb 2022 14:19:48 +0530 Subject: [PATCH 089/121] fix: removed validation to check zero qty (cherry picked from commit 39d3f20d9b45939e1b7a5564a1c79ad99a304953) --- erpnext/controllers/subcontracting.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py index 3addb91aaa0..c52c688b73e 100644 --- a/erpnext/controllers/subcontracting.py +++ b/erpnext/controllers/subcontracting.py @@ -363,8 +363,6 @@ class Subcontracting(): return for row in self.get(self.raw_material_table): - self.__validate_consumed_qty(row) - key = (row.rm_item_code, row.main_item_code, row.purchase_order) if not self.__transferred_items or not self.__transferred_items.get(key): return @@ -372,12 +370,6 @@ class Subcontracting(): self.__validate_batch_no(row, key) self.__validate_serial_no(row, key) - def __validate_consumed_qty(self, row): - if self.backflush_based_on != 'BOM' and flt(row.consumed_qty) == 0.0: - msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}' - - frappe.throw(_(msg),title=_('Consumed Items Qty Check')) - def __validate_batch_no(self, row, key): if row.get('batch_no') and row.get('batch_no') not in self.__transferred_items.get(key).get('batch_no'): link = get_link_to_form('Purchase Order', row.purchase_order) From ca6f196badd0828d1b5d7411f49fccc603f6478d Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 28 Feb 2022 18:15:29 +0530 Subject: [PATCH 090/121] fix: Remove conflicts --- .../doctype/sales_invoice_item/sales_invoice_item.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) 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 8e19bc928b6..5a095fbb7ea 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -850,11 +850,6 @@ "owner": "Administrator", "permissions": [], "sort_field": "modified", -<<<<<<< HEAD "sort_order": "DESC" } -======= - "sort_order": "DESC", - "states": [] -} ->>>>>>> 0373914704 (fix: Commission not applied while making Sales Order from Quotation) + From 6ed01fedd4646e33a9b70877dbac5946eb7de0ea Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Feb 2022 13:11:17 +0530 Subject: [PATCH 091/121] fix: Total taxes and charges in payment entry for multicurrency payments (cherry picked from commit b1a46c80d5571d23a749656c98056ba1411b433f) --- .../doctype/payment_entry/payment_entry.js | 13 ++++++++++--- .../doctype/payment_entry/payment_entry.json | 14 +++++++++++++- .../doctype/payment_entry/payment_entry.py | 8 ++++++-- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 727ef55b3c7..abe8d097d83 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -196,8 +196,14 @@ frappe.ui.form.on('Payment Entry', { frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency)); frm.toggle_display("base_paid_amount", frm.doc.paid_from_account_currency != company_currency); - frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges && - (frm.doc.paid_from_account_currency != company_currency)); + + if (frm.doc.payment_type == "Pay") { + frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges && + (frm.doc.paid_to_account_currency != company_currency)); + } else { + frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges && + (frm.doc.paid_from_account_currency != company_currency)); + } frm.toggle_display("base_received_amount", ( frm.doc.paid_to_account_currency != company_currency @@ -232,7 +238,8 @@ frappe.ui.form.on('Payment Entry', { var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company).default_currency: ""; frm.set_currency_labels(["base_paid_amount", "base_received_amount", "base_total_allocated_amount", - "difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax"], company_currency); + "difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax", + "base_total_taxes_and_charges"], company_currency); frm.set_currency_labels(["paid_amount"], frm.doc.paid_from_account_currency); frm.set_currency_labels(["received_amount"], frm.doc.paid_to_account_currency); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index c8d1db91f54..3fc1adff2d3 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -66,7 +66,9 @@ "tax_withholding_category", "section_break_56", "taxes", + "section_break_60", "base_total_taxes_and_charges", + "column_break_61", "total_taxes_and_charges", "deductions_or_loss_section", "deductions", @@ -715,12 +717,21 @@ "fieldtype": "Data", "hidden": 1, "label": "Paid To Account Type" + }, + { + "fieldname": "column_break_61", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_60", + "fieldtype": "Section Break", + "hide_border": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-11-24 18:58:24.919764", + "modified": "2022-02-23 20:08:39.559814", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", @@ -763,6 +774,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "title", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index a9134022b53..341ad07ce6e 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -946,8 +946,12 @@ class PaymentEntry(AccountsController): tax.base_total = tax.total * self.source_exchange_rate - self.total_taxes_and_charges += current_tax_amount - self.base_total_taxes_and_charges += current_tax_amount * self.source_exchange_rate + if self.payment_type == 'Pay': + self.base_total_taxes_and_charges += current_tax_amount * self.source_exchange_rate + self.total_taxes_and_charges += current_tax_amount * self.target_exchange_rate + else: + self.base_total_taxes_and_charges += current_tax_amount * self.target_exchange_rate + self.total_taxes_and_charges += current_tax_amount * self.source_exchange_rate if self.get('taxes'): self.paid_amount_after_tax = self.get('taxes')[-1].base_total From de978ca3d76ff1f7ac7a9513c6233f0777618aa9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 28 Feb 2022 18:05:58 +0530 Subject: [PATCH 092/121] test: Add test case for payment entry taxes (cherry picked from commit 19fb7ead9f249d28403f618297009d498675d224) --- .../doctype/payment_entry/payment_entry.py | 8 ++-- .../payment_entry/test_payment_entry.py | 39 +++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 341ad07ce6e..7c3574266ea 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -947,11 +947,11 @@ class PaymentEntry(AccountsController): tax.base_total = tax.total * self.source_exchange_rate if self.payment_type == 'Pay': - self.base_total_taxes_and_charges += current_tax_amount * self.source_exchange_rate - self.total_taxes_and_charges += current_tax_amount * self.target_exchange_rate + self.base_total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate) + self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate) else: - self.base_total_taxes_and_charges += current_tax_amount * self.target_exchange_rate - self.total_taxes_and_charges += current_tax_amount * self.source_exchange_rate + self.base_total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate) + self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate) if self.get('taxes'): self.paid_amount_after_tax = self.get('taxes')[-1].base_total diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index cc3528e9aaa..349b8bb5b1b 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -633,6 +633,45 @@ class TestPaymentEntry(unittest.TestCase): self.assertEqual(flt(expected_party_balance), party_balance) self.assertEqual(flt(expected_party_account_balance), party_account_balance) + def test_multi_currency_payment_entry_with_taxes(self): + payment_entry = create_payment_entry(party='_Test Supplier USD', paid_to = '_Test Payable USD - _TC', + save=True) + payment_entry.append('taxes', { + 'account_head': '_Test Account Service Tax - _TC', + 'charge_type': 'Actual', + 'tax_amount': 10, + 'add_deduct_tax': 'Add', + 'description': 'Test' + }) + + payment_entry.save() + self.assertEqual(payment_entry.base_total_taxes_and_charges, 10) + self.assertEqual(flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2)) + +def create_payment_entry(**args): + payment_entry = frappe.new_doc('Payment Entry') + payment_entry.company = args.get('company') or '_Test Company' + payment_entry.payment_type = args.get('payment_type') or 'Pay' + payment_entry.party_type = args.get('party_type') or 'Supplier' + payment_entry.party = args.get('party') or '_Test Supplier' + payment_entry.paid_from = args.get('paid_from') or '_Test Bank - _TC' + payment_entry.paid_to = args.get('paid_to') or 'Creditors - _TC' + payment_entry.paid_amount = args.get('paid_amount') or 1000 + + payment_entry.setup_party_account_field() + payment_entry.set_missing_values() + payment_entry.set_exchange_rate() + payment_entry.received_amount = payment_entry.paid_amount / payment_entry.target_exchange_rate + payment_entry.reference_no = 'Test001' + payment_entry.reference_date = nowdate() + + if args.get('save'): + payment_entry.save() + if args.get('submit'): + payment_entry.submit() + + return payment_entry + def create_payment_terms_template(): create_payment_term('Basic Amount Receivable') From e89d60147ea3d18321a2635fed6a1fff6f8c7347 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 28 Feb 2022 17:12:51 +0530 Subject: [PATCH 093/121] test: remove transaction commits from buying module (cherry picked from commit 829c453cb616107471ed5fd6b0ef7b75bb0420e7) --- erpnext/buying/doctype/purchase_order/test_purchase_order.py | 4 ++-- .../request_for_quotation/test_request_for_quotation.py | 4 ++-- erpnext/buying/doctype/supplier/test_supplier.py | 4 ++-- .../doctype/supplier_quotation/test_supplier_quotation.py | 4 ++-- .../doctype/supplier_scorecard/test_supplier_scorecard.py | 4 ++-- .../test_supplier_scorecard_criteria.py | 4 ++-- .../test_supplier_scorecard_variable.py | 4 ++-- .../report/procurement_tracker/test_procurement_tracker.py | 4 ++-- .../test_subcontracted_item_to_be_received.py | 4 ++-- .../test_subcontracted_raw_materials_to_be_transferred.py | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 645e97ee7c8..efa2ab12685 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -3,9 +3,9 @@ import json -import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, flt, getdate, nowdate from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry @@ -27,7 +27,7 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry -class TestPurchaseOrder(unittest.TestCase): +class TestPurchaseOrder(FrappeTestCase): def test_make_purchase_receipt(self): po = create_purchase_order(do_not_submit=True) self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name) diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py index 51901991b5a..5b2112424c9 100644 --- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py @@ -1,9 +1,9 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import nowdate from erpnext.buying.doctype.request_for_quotation.request_for_quotation import ( @@ -16,7 +16,7 @@ from erpnext.stock.doctype.item.test_item import make_item from erpnext.templates.pages.rfq import check_supplier_has_docname_access -class TestRequestforQuotation(unittest.TestCase): +class TestRequestforQuotation(FrappeTestCase): def test_quote_status(self): rfq = make_request_for_quotation() diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index 13fe9df13ee..662a758751c 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -1,10 +1,10 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -import unittest import frappe from frappe.test_runner import make_test_records +from frappe.tests.utils import FrappeTestCase from erpnext.accounts.party import get_due_date from erpnext.exceptions import PartyDisabled @@ -13,7 +13,7 @@ test_dependencies = ['Payment Term', 'Payment Terms Template'] test_records = frappe.get_test_records('Supplier') -class TestSupplier(unittest.TestCase): +class TestSupplier(FrappeTestCase): def test_get_supplier_group_details(self): doc = frappe.new_doc("Supplier Group") doc.supplier_group_name = "_Testing Supplier Group" diff --git a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py index d48ac7eb3b4..a4d45975c30 100644 --- a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py @@ -3,12 +3,12 @@ -import unittest import frappe +from frappe.tests.utils import FrappeTestCase -class TestPurchaseOrder(unittest.TestCase): +class TestPurchaseOrder(FrappeTestCase): def test_make_purchase_order(self): from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order diff --git a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py index 7908c35cbbe..8ecc2cd4667 100644 --- a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py +++ b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py @@ -1,12 +1,12 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest import frappe +from frappe.tests.utils import FrappeTestCase -class TestSupplierScorecard(unittest.TestCase): +class TestSupplierScorecard(FrappeTestCase): def test_create_scorecard(self): doc = make_supplier_scorecard().insert() diff --git a/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py b/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py index dacc982420e..7ff84c15e52 100644 --- a/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py +++ b/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py @@ -1,12 +1,12 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest import frappe +from frappe.tests.utils import FrappeTestCase -class TestSupplierScorecardCriteria(unittest.TestCase): +class TestSupplierScorecardCriteria(FrappeTestCase): def test_variables_exist(self): delete_test_scorecards() for d in test_good_criteria: diff --git a/erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py b/erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py index 4d75981125f..32005a37dc7 100644 --- a/erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py +++ b/erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py @@ -1,16 +1,16 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.buying.doctype.supplier_scorecard_variable.supplier_scorecard_variable import ( VariablePathNotFound, ) -class TestSupplierScorecardVariable(unittest.TestCase): +class TestSupplierScorecardVariable(FrappeTestCase): def test_variable_exist(self): for d in test_existing_variables: my_doc = frappe.get_doc("Supplier Scorecard Variable", d.get("name")) diff --git a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py index 84de8c67438..44524527e3a 100644 --- a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py @@ -2,10 +2,10 @@ # For license information, please see license.txt -import unittest from datetime import datetime import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt from erpnext.buying.report.procurement_tracker.procurement_tracker import execute @@ -14,7 +14,7 @@ from erpnext.stock.doctype.material_request.test_material_request import make_ma from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse -class TestProcurementTracker(unittest.TestCase): +class TestProcurementTracker(FrappeTestCase): def test_result_for_procurement_tracker(self): filters = { 'company': '_Test Procurement Company', diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py index 144523ad522..c2b38d38e18 100644 --- a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py +++ b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py @@ -3,9 +3,9 @@ # Compiled at: 2019-05-06 09:51:46 # Decompiled by https://python-decompiler.com -import unittest import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order @@ -15,7 +15,7 @@ from erpnext.buying.report.subcontracted_item_to_be_received.subcontracted_item_ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry -class TestSubcontractedItemToBeReceived(unittest.TestCase): +class TestSubcontractedItemToBeReceived(FrappeTestCase): def test_pending_and_received_qty(self): po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes') diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py index 3c203ac23fa..fc9acabc81d 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py @@ -4,9 +4,9 @@ # Decompiled by https://python-decompiler.com import json -import unittest import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.buying.doctype.purchase_order.purchase_order import make_rm_stock_entry from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order @@ -16,7 +16,7 @@ from erpnext.buying.report.subcontracted_raw_materials_to_be_transferred.subcont from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry -class TestSubcontractedItemToBeTransferred(unittest.TestCase): +class TestSubcontractedItemToBeTransferred(FrappeTestCase): def test_pending_and_transferred_qty(self): po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes', supplier_warehouse="_Test Warehouse 1 - _TC") From 86e6bdf2c9ffc0a656c14663b6f790071a7f3afd Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 28 Feb 2022 21:07:08 +0530 Subject: [PATCH 094/121] fix: Exchange rate not getting set in payment entry --- .../doctype/payment_entry/payment_entry.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 727ef55b3c7..730bb68260a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -341,6 +341,8 @@ frappe.ui.form.on('Payment Entry', { } frm.set_party_account_based_on_party = true; + let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; + return frappe.call({ method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_details", args: { @@ -374,7 +376,11 @@ frappe.ui.form.on('Payment Entry', { if (r.message.bank_account) { frm.set_value("bank_account", r.message.bank_account); } - } + }, + () => frm.events.set_current_exchange_rate(frm, "source_exchange_rate", + frm.doc.paid_from_account_currency, company_currency), + () => frm.events.set_current_exchange_rate(frm, "target_exchange_rate", + frm.doc.paid_to_account_currency, company_currency) ]); } } @@ -478,14 +484,14 @@ frappe.ui.form.on('Payment Entry', { }, paid_from_account_currency: function(frm) { - if(!frm.doc.paid_from_account_currency) return; - var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; + if(!frm.doc.paid_from_account_currency || !frm.doc.company) return; + let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; if (frm.doc.paid_from_account_currency == company_currency) { frm.set_value("source_exchange_rate", 1); } else if (frm.doc.paid_from){ if (in_list(["Internal Transfer", "Pay"], frm.doc.payment_type)) { - var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; + let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; frappe.call({ method: "erpnext.setup.utils.get_exchange_rate", args: { @@ -505,8 +511,8 @@ frappe.ui.form.on('Payment Entry', { }, paid_to_account_currency: function(frm) { - if(!frm.doc.paid_to_account_currency) return; - var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; + if(!frm.doc.paid_to_account_currency || !frm.doc.company) return; + let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; frm.events.set_current_exchange_rate(frm, "target_exchange_rate", frm.doc.paid_to_account_currency, company_currency); From 5f9b2cc1bfa88c5653a62458c52d24c1302bfed6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 1 Mar 2022 11:56:20 +0530 Subject: [PATCH 095/121] fix: Deferred revenue booking (cherry picked from commit 366120ffeed36c68b9ceb1f677cc4497673b4289) --- erpnext/accounts/deferred_revenue.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 4b1535e1000..46b7dc6a2a6 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -121,6 +121,7 @@ def get_booking_dates(doc, item, posting_date=None): prev_gl_entry = frappe.db.sql(''' select name, posting_date from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s + and is_cancelled = 0 order by posting_date desc limit 1 ''', (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True) @@ -228,6 +229,7 @@ def get_already_booked_amount(doc, item): gl_entries_details = frappe.db.sql(''' select sum({0}) as total_credit, sum({1}) as total_credit_in_account_currency, voucher_detail_no from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s + and is_cancelled = 0 group by voucher_detail_no '''.format(total_credit_debit, total_credit_debit_currency), (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True) @@ -283,7 +285,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): return # check if books nor frozen till endate: - if getdate(end_date) >= getdate(accounts_frozen_upto): + if accounts_frozen_upto and (end_date) <= getdate(accounts_frozen_upto): end_date = get_last_day(add_days(accounts_frozen_upto, 1)) if via_journal_entry: From 5f4965022fb9a71486545b2d16d088fb98c43739 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 1 Mar 2022 13:06:40 +0530 Subject: [PATCH 096/121] fix: track changes on warehouse (cherry picked from commit d22a1d440c96d5a476ce466074a264362a4c814a) --- erpnext/stock/doctype/warehouse/warehouse.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json index 05076b51a3e..c695d541bf9 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.json +++ b/erpnext/stock/doctype/warehouse/warehouse.json @@ -244,7 +244,7 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2021-12-03 04:40:06.414630", + "modified": "2022-03-01 02:37:48.034944", "modified_by": "Administrator", "module": "Stock", "name": "Warehouse", @@ -301,5 +301,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", - "title_field": "warehouse_name" + "states": [], + "title_field": "warehouse_name", + "track_changes": 1 } \ No newline at end of file From d93a41e0d0ce647e695a0a0fff8d59361f365a85 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 1 Mar 2022 13:32:34 +0530 Subject: [PATCH 097/121] fix: debit credit difference case with rounding adjustment (cherry picked from commit ad2c64f3ff3cafa0abcee35639c9e5d4952bccdf) --- .../sales_invoice/test_sales_invoice.py | 50 +++++++++++++++++++ erpnext/accounts/general_ledger.py | 2 +- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 0cda5a05b78..791a115fd2a 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1603,6 +1603,56 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account][1], gle.debit) self.assertEqual(expected_values[gle.account][2], gle.credit) + def test_rounding_adjustment_3(self): + si = create_sales_invoice(do_not_save=True) + si.items = [] + for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]: + si.append("items", { + "item_code": "_Test Item", + "gst_hsn_code": "999800", + "warehouse": "_Test Warehouse - _TC", + "qty": d[1], + "rate": d[0], + "income_account": "Sales - _TC", + "cost_center": "_Test Cost Center - _TC" + }) + for tax_account in ["_Test Account VAT - _TC", "_Test Account Service Tax - _TC"]: + si.append("taxes", { + "charge_type": "On Net Total", + "account_head": tax_account, + "description": tax_account, + "rate": 6, + "cost_center": "_Test Cost Center - _TC", + "included_in_print_rate": 1 + }) + si.save() + si.submit() + self.assertEqual(si.net_total, 4007.16) + self.assertEqual(si.grand_total, 4488.02) + self.assertEqual(si.total_taxes_and_charges, 480.86) + self.assertEqual(si.rounding_adjustment, -0.02) + + expected_values = dict((d[0], d) for d in [ + [si.debit_to, 4488.0, 0.0], + ["_Test Account Service Tax - _TC", 0.0, 240.43], + ["_Test Account VAT - _TC", 0.0, 240.43], + ["Sales - _TC", 0.0, 4007.15], + ["Round Off - _TC", 0.01, 0] + ]) + + gl_entries = frappe.db.sql("""select account, debit, credit + from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s + order by account asc""", si.name, as_dict=1) + + debit_credit_diff = 0 + for gle in gl_entries: + self.assertEqual(expected_values[gle.account][0], gle.account) + self.assertEqual(expected_values[gle.account][1], gle.debit) + self.assertEqual(expected_values[gle.account][2], gle.credit) + debit_credit_diff += (gle.debit - gle.credit) + + self.assertEqual(debit_credit_diff, 0) + def test_sales_invoice_with_shipping_rule(self): from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 1836db6477f..fd5173fd659 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -221,7 +221,7 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision): debit_credit_diff += flt(d.credit) round_off_account_exists = True - if round_off_account_exists and abs(debit_credit_diff) <= (1.0 / (10**precision)): + if round_off_account_exists and abs(debit_credit_diff) < (1.0 / (10**precision)): gl_map.remove(round_off_gle) return From c7539ae416758235671e140166c370bb857a84c7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 17:51:40 +0530 Subject: [PATCH 098/121] fix(test): flaky test_point_of_sale (#30035) (cherry picked from commit 47fe87a72f40d1651951cecc97e9ae2ae0115fa4) Co-authored-by: Saqib Ansari --- erpnext/tests/test_point_of_sale.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/tests/test_point_of_sale.py b/erpnext/tests/test_point_of_sale.py index 3299c8885f2..38f2c16d939 100644 --- a/erpnext/tests/test_point_of_sale.py +++ b/erpnext/tests/test_point_of_sale.py @@ -25,7 +25,7 @@ class TestPointOfSale(unittest.TestCase): Test Stock and Service Item Search. """ - pos_profile = make_pos_profile() + pos_profile = make_pos_profile(name="Test POS Profile for Search") item1 = make_item("Test Search Stock Item", {"is_stock_item": 1}) make_stock_entry( item_code="Test Search Stock Item", From 28a7c79f3cf9c6eacefdece479139fe096f4c6f6 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 17:57:10 +0530 Subject: [PATCH 099/121] fix: dont validate empty category (#30038) (cherry picked from commit 65bb72703029a38fedc02f2ad8f03ebfadf0b223) Co-authored-by: Ankush Menat --- .../sales_taxes_and_charges_template.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py index 1d30934df92..8043a1b66f2 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py @@ -55,5 +55,8 @@ def validate_disabled(doc): frappe.throw(_("Disabled template must not be default template")) def validate_for_tax_category(doc): + if not doc.tax_category: + return + if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0, "name": ["!=", doc.name]}): frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category))) From cdafed109230e2ba7df67066cf18702b6ac84c5f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 2 Mar 2022 12:29:03 +0530 Subject: [PATCH 100/121] test: fix flaky bin value test (cherry picked from commit 5d85b35f41d792879e47d63ec2bf45696ee6553c) --- .../doctype/purchase_receipt/test_purchase_receipt.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 89f11ca78d4..b13d6d3d05a 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -162,6 +162,15 @@ class TestPurchaseReceipt(ERPNextTestCase): qty=abs(existing_bin_qty) ) + existing_bin_qty, existing_bin_stock_value = frappe.db.get_value( + "Bin", + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC" + }, + ["actual_qty", "stock_value"] + ) + pr = make_purchase_receipt() stock_value_difference = frappe.db.get_value( From 5ebfb65b427764e737edac9e3f63e23e9d7de6d5 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 2 Mar 2022 11:19:12 +0530 Subject: [PATCH 101/121] fix: ignore serial no during landed cost voucher (cherry picked from commit efc4b943f8f250de20c806a6dc923710b0f21885) --- .../test_landed_cost_voucher.py | 48 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index df8cadd7f86..1ea0596d333 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -10,6 +10,7 @@ from erpnext.accounts.doctype.account.test_account import create_account, get_in from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.utils import update_gl_entries_after from erpnext.assets.doctype.asset.test_asset import create_asset_category, create_fixed_asset_item +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import ( get_gl_entries, make_purchase_receipt, @@ -177,6 +178,53 @@ class TestLandedCostVoucher(ERPNextTestCase): self.assertEqual(serial_no.purchase_rate - serial_no_rate, 5.0) self.assertEqual(serial_no.warehouse, "Stores - TCP1") + def test_serialized_lcv_delivered(self): + """In some cases you'd want to deliver before you can know all the + landed costs, this should be allowed for serial nos too. + + Case: + - receipt a serial no @ X rate + - delivery the serial no @ X rate + - add LCV to receipt X + Y + - LCV should be successful + - delivery should reflect X+Y valuation. + """ + serial_no = "LCV_TEST_SR_NO" + item_code = "_Test Serialized Item" + warehouse = "Stores - TCP1" + + pr = make_purchase_receipt(company="_Test Company with perpetual inventory", + warehouse=warehouse, qty=1, rate=200, + item_code=item_code, serial_no=serial_no) + + serial_no_rate = frappe.db.get_value("Serial No", serial_no, "purchase_rate") + + # deliver it before creating LCV + dn = create_delivery_note(item_code=item_code, + company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', + serial_no=serial_no, qty=1, rate=500, + cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1") + + charges = 10 + create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges) + + new_purchase_rate = serial_no_rate + charges + + serial_no = frappe.db.get_value("Serial No", serial_no, + ["warehouse", "purchase_rate"], as_dict=1) + + self.assertEqual(serial_no.purchase_rate, new_purchase_rate) + + stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + filters={ + "voucher_no": dn.name, + "voucher_type": dn.doctype, + "is_cancelled": 0 # LCV cancels with same name. + }, + fieldname="stock_value_difference") + + # reposting should update the purchase rate in future delivery + self.assertEqual(stock_value_difference, -new_purchase_rate) def test_landed_cost_voucher_for_odd_numbers (self): pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", do_not_save=True) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index c2cb2ca6746..2a9164d71a7 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -36,7 +36,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc future_sle_exists(args, sl_entries) for sle in sl_entries: - if sle.serial_no: + if sle.serial_no and not via_landed_cost_voucher: validate_serial_no(sle) if cancel: From 5be64c2b7fb6f41ce4db563e046d021e666236fa Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 2 Mar 2022 12:01:51 +0530 Subject: [PATCH 102/121] docs: explain make_sl_entries arguments (cherry picked from commit eb8495a401ceb9f1118bdb50f80a836a07994b9b) --- erpnext/stock/stock_ledger.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 2a9164d71a7..47a97c47fe5 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -25,6 +25,16 @@ class SerialNoExistsInFutureTransaction(frappe.ValidationError): def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): + """ Create SL entries from SL entry dicts + + args: + - allow_negative_stock: disable negative stock valiations if true + - via_landed_cost_voucher: landed cost voucher cancels and reposts + entries of purchase document. This flag is used to identify if + cancellation and repost is happening via landed cost voucher, in + such cases certain validations need to be ignored (like negative + stock) + """ from erpnext.controllers.stock_controller import future_sle_exists if sl_entries: cancel = sl_entries[0].get("is_cancelled") From a84e7c633e5c93c082534a90be94d79a0fcf58d6 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 1 Mar 2022 14:33:46 +0530 Subject: [PATCH 103/121] test: FIFO transfer for multi-batch transaction (cherry picked from commit b3b7cdfb49cdc67e2dc3688b472deb5c2294addc) # Conflicts: # erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py --- .../test_stock_ledger_entry.py | 353 ++++++++++++++++++ 1 file changed, 353 insertions(+) diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index a1030d54964..d2489bf54fa 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -349,6 +349,359 @@ class TestStockLedgerEntry(ERPNextTestCase): frappe.set_user("Administrator") user.remove_roles("Stock Manager") +<<<<<<< HEAD +======= + def test_batchwise_item_valuation_moving_average(self): + item, warehouses, batches = setup_item_valuation_test(valuation_method="Moving Average") + + # Incoming Entries for Stock Value check + pr_entry_list = [ + (item, warehouses[0], batches[0], 1, 100), + (item, warehouses[0], batches[1], 1, 50), + (item, warehouses[0], batches[0], 1, 150), + (item, warehouses[0], batches[1], 1, 100), + ] + prs = create_purchase_receipt_entries_for_batchwise_item_valuation_test(pr_entry_list) + sle_details = fetch_sle_details_for_doc_list(prs, ['stock_value']) + sv_list = [d['stock_value'] for d in sle_details] + expected_sv = [100, 150, 300, 400] + self.assertEqual(expected_sv, sv_list, "Incorrect 'Stock Value' values") + + # Outgoing Entries for Stock Value Difference check + dn_entry_list = [ + (item, warehouses[0], batches[1], 1, 200), + (item, warehouses[0], batches[0], 1, 200), + (item, warehouses[0], batches[1], 1, 200), + (item, warehouses[0], batches[0], 1, 200) + ] + dns = create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list) + sle_details = fetch_sle_details_for_doc_list(dns, ['stock_value_difference']) + svd_list = [-1 * d['stock_value_difference'] for d in sle_details] + expected_incoming_rates = expected_abs_svd = [75, 125, 75, 125] + + self.assertEqual(expected_abs_svd, svd_list, "Incorrect 'Stock Value Difference' values") + for dn, incoming_rate in zip(dns, expected_incoming_rates): + self.assertEqual( + dn.items[0].incoming_rate, incoming_rate, + "Incorrect 'Incoming Rate' values fetched for DN items" + ) + + + def assertSLEs(self, doc, expected_sles, sle_filters=None): + """ Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line""" + + filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled":0} + if sle_filters: + filters.update(sle_filters) + sles = frappe.get_all("Stock Ledger Entry", fields=["*"], filters=filters, + order_by="timestamp(posting_date, posting_time), creation") + + for exp_sle, act_sle in zip(expected_sles, sles): + for k, v in exp_sle.items(): + act_value = act_sle[k] + if k == "stock_queue": + act_value = json.loads(act_value) + if act_value and act_value[0][0] == 0: + # ignore empty fifo bins + continue + + self.assertEqual(v, act_value, msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}") + + + def test_batchwise_item_valuation_stock_reco(self): + item, warehouses, batches = setup_item_valuation_test() + state = { + "stock_value" : 0.0, + "qty": 0.0 + } + def update_invariants(exp_sles): + for sle in exp_sles: + state["stock_value"] += sle["stock_value_difference"] + state["qty"] += sle["actual_qty"] + sle["stock_value"] = state["stock_value"] + sle["qty_after_transaction"] = state["qty"] + + osr1 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=10, rate=100, batch_no=batches[1]) + expected_sles = [ + {"actual_qty": 10, "stock_value_difference": 1000}, + ] + update_invariants(expected_sles) + self.assertSLEs(osr1, expected_sles) + + osr2 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=13, rate=200, batch_no=batches[0]) + expected_sles = [ + {"actual_qty": 13, "stock_value_difference": 200*13}, + ] + update_invariants(expected_sles) + self.assertSLEs(osr2, expected_sles) + + sr1 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=5, rate=50, batch_no=batches[1]) + + expected_sles = [ + {"actual_qty": -10, "stock_value_difference": -10 * 100}, + {"actual_qty": 5, "stock_value_difference": 250} + ] + update_invariants(expected_sles) + self.assertSLEs(sr1, expected_sles) + + sr2 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=20, rate=75, batch_no=batches[0]) + expected_sles = [ + {"actual_qty": -13, "stock_value_difference": -13 * 200}, + {"actual_qty": 20, "stock_value_difference": 20 * 75} + ] + update_invariants(expected_sles) + self.assertSLEs(sr2, expected_sles) + + def test_batch_wise_valuation_across_warehouse(self): + item_code, warehouses, batches = setup_item_valuation_test() + source = warehouses[0] + target = warehouses[1] + + unrelated_batch = make_stock_entry(item_code=item_code, target=source, batch_no=batches[1], + qty=5, rate=10) + self.assertSLEs(unrelated_batch, [ + {"actual_qty": 5, "stock_value_difference": 10 * 5}, + ]) + + reciept = make_stock_entry(item_code=item_code, target=source, batch_no=batches[0], qty=5, rate=10) + self.assertSLEs(reciept, [ + {"actual_qty": 5, "stock_value_difference": 10 * 5}, + ]) + + transfer = make_stock_entry(item_code=item_code, source=source, target=target, batch_no=batches[0], qty=5) + self.assertSLEs(transfer, [ + {"actual_qty": -5, "stock_value_difference": -10 * 5, "warehouse": source}, + {"actual_qty": 5, "stock_value_difference": 10 * 5, "warehouse": target} + ]) + + backdated_receipt = make_stock_entry(item_code=item_code, target=source, batch_no=batches[0], + qty=5, rate=20, posting_date=add_days(today(), -1)) + self.assertSLEs(backdated_receipt, [ + {"actual_qty": 5, "stock_value_difference": 20 * 5}, + ]) + + # check reposted average rate in *future* transfer + self.assertSLEs(transfer, [ + {"actual_qty": -5, "stock_value_difference": -15 * 5, "warehouse": source, "stock_value": 15 * 5 + 10 * 5}, + {"actual_qty": 5, "stock_value_difference": 15 * 5, "warehouse": target, "stock_value": 15 * 5} + ]) + + transfer_unrelated = make_stock_entry(item_code=item_code, source=source, + target=target, batch_no=batches[1], qty=5) + self.assertSLEs(transfer_unrelated, [ + {"actual_qty": -5, "stock_value_difference": -10 * 5, "warehouse": source, "stock_value": 15 * 5}, + {"actual_qty": 5, "stock_value_difference": 10 * 5, "warehouse": target, "stock_value": 15 * 5 + 10 * 5} + ]) + + def test_intermediate_average_batch_wise_valuation(self): + """ A batch has moving average up until posting time, + check if same is respected when backdated entry is inserted in middle""" + item_code, warehouses, batches = setup_item_valuation_test() + warehouse = warehouses[0] + + batch = batches[0] + + yesterday = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batch, + qty=1, rate=10, posting_date=add_days(today(), -1)) + self.assertSLEs(yesterday, [ + {"actual_qty": 1, "stock_value_difference": 10}, + ]) + + tomorrow = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], + qty=1, rate=30, posting_date=add_days(today(), 1)) + self.assertSLEs(tomorrow, [ + {"actual_qty": 1, "stock_value_difference": 30}, + ]) + + create_today = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], + qty=1, rate=20) + self.assertSLEs(create_today, [ + {"actual_qty": 1, "stock_value_difference": 20}, + ]) + + consume_today = make_stock_entry(item_code=item_code, source=warehouse, batch_no=batches[0], + qty=1) + self.assertSLEs(consume_today, [ + {"actual_qty": -1, "stock_value_difference": -15}, + ]) + + consume_tomorrow = make_stock_entry(item_code=item_code, source=warehouse, batch_no=batches[0], + qty=2, posting_date=add_days(today(), 2)) + self.assertSLEs(consume_tomorrow, [ + {"stock_value_difference": -(30 + 15), "stock_value": 0, "qty_after_transaction": 0}, + ]) + + def test_legacy_item_valuation_stock_entry(self): + columns = [ + 'stock_value_difference', + 'stock_value', + 'actual_qty', + 'qty_after_transaction', + 'stock_queue', + ] + item, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0) + + def check_sle_details_against_expected(sle_details, expected_sle_details, detail, columns): + for i, (sle_vals, ex_sle_vals) in enumerate(zip(sle_details, expected_sle_details)): + for col, sle_val, ex_sle_val in zip(columns, sle_vals, ex_sle_vals): + if col == 'stock_queue': + sle_val = get_stock_value_from_q(sle_val) + ex_sle_val = get_stock_value_from_q(ex_sle_val) + self.assertEqual( + sle_val, ex_sle_val, + f"Incorrect {col} value on transaction #: {i} in {detail}" + ) + + # List used to defer assertions to prevent commits cause of error skipped rollback + details_list = [] + + + # Test Material Receipt Entries + se_entry_list_mr = [ + (item, None, warehouses[0], batches[0], 1, 50, "2021-01-21"), + (item, None, warehouses[0], batches[1], 1, 100, "2021-01-23"), + ] + ses = create_stock_entry_entries_for_batchwise_item_valuation_test( + se_entry_list_mr, "Material Receipt" + ) + sle_details = fetch_sle_details_for_doc_list(ses, columns=columns, as_dict=0) + expected_sle_details = [ + (50.0, 50.0, 1.0, 1.0, '[[1.0, 50.0]]'), + (100.0, 150.0, 1.0, 2.0, '[[1.0, 50.0], [1.0, 100.0]]'), + ] + details_list.append(( + sle_details, expected_sle_details, + "Material Receipt Entries", columns + )) + + + # Test Material Issue Entries + se_entry_list_mi = [ + (item, warehouses[0], None, batches[1], 1, None, "2021-01-29"), + ] + ses = create_stock_entry_entries_for_batchwise_item_valuation_test( + se_entry_list_mi, "Material Issue" + ) + sle_details = fetch_sle_details_for_doc_list(ses, columns=columns, as_dict=0) + expected_sle_details = [ + (-50.0, 100.0, -1.0, 1.0, '[[1, 100.0]]') + ] + details_list.append(( + sle_details, expected_sle_details, + "Material Issue Entries", columns + )) + + + # Run assertions + for details in details_list: + check_sle_details_against_expected(*details) + + def test_mixed_valuation_batches_fifo(self): + item_code, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0) + warehouse = warehouses[0] + + state = { + "qty": 0.0, + "stock_value": 0.0 + } + def update_invariants(exp_sles): + for sle in exp_sles: + state["stock_value"] += sle["stock_value_difference"] + state["qty"] += sle["actual_qty"] + sle["stock_value"] = state["stock_value"] + sle["qty_after_transaction"] = state["qty"] + return exp_sles + + old1 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], + qty=10, rate=10) + self.assertSLEs(old1, update_invariants([ + {"actual_qty": 10, "stock_value_difference": 10*10, "stock_queue": [[10, 10]]}, + ])) + old2 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[1], + qty=10, rate=20) + self.assertSLEs(old2, update_invariants([ + {"actual_qty": 10, "stock_value_difference": 10*20, "stock_queue": [[10, 10], [10, 20]]}, + ])) + old3 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], + qty=5, rate=15) + + self.assertSLEs(old3, update_invariants([ + {"actual_qty": 5, "stock_value_difference": 5*15, "stock_queue": [[10, 10], [10, 20], [5, 15]]}, + ])) + + new1 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=40) + batches.append(new1.items[0].batch_no) + # assert old queue remains + self.assertSLEs(new1, update_invariants([ + {"actual_qty": 10, "stock_value_difference": 10*40, "stock_queue": [[10, 10], [10, 20], [5, 15]]}, + ])) + + new2 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=42) + batches.append(new2.items[0].batch_no) + self.assertSLEs(new2, update_invariants([ + {"actual_qty": 10, "stock_value_difference": 10*42, "stock_queue": [[10, 10], [10, 20], [5, 15]]}, + ])) + + # consume old batch as per FIFO + consume_old1 = make_stock_entry(item_code=item_code, source=warehouse, qty=15, batch_no=batches[0]) + self.assertSLEs(consume_old1, update_invariants([ + {"actual_qty": -15, "stock_value_difference": -10*10 - 5*20, "stock_queue": [[5, 20], [5, 15]]}, + ])) + + # consume new batch as per batch + consume_new2 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-1]) + self.assertSLEs(consume_new2, update_invariants([ + {"actual_qty": -10, "stock_value_difference": -10*42, "stock_queue": [[5, 20], [5, 15]]}, + ])) + + # finish all old batches + consume_old2 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[1]) + self.assertSLEs(consume_old2, update_invariants([ + {"actual_qty": -10, "stock_value_difference": -5*20 - 5*15, "stock_queue": []}, + ])) + + # finish all new batches + consume_new1 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-2]) + self.assertSLEs(consume_new1, update_invariants([ + {"actual_qty": -10, "stock_value_difference": -10*40, "stock_queue": []}, + ])) +>>>>>>> b3b7cdfb49 (test: FIFO transfer for multi-batch transaction) + + def test_fifo_dependent_consumption(self): + item = make_item("_TestFifoTransferRates") + source = "_Test Warehouse - _TC" + target = "Stores - _TC" + + rates = [10 * i for i in range(1, 20)] + + receipt = make_stock_entry(item_code=item.name, target=source, qty=10, do_not_save=True, rate=10) + for rate in rates[1:]: + row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False) + row.basic_rate = rate + receipt.append("items", row) + + receipt.save() + receipt.submit() + + expected_queues = [] + for idx, rate in enumerate(rates, start=1): + expected_queues.append( + {"stock_queue": [[10, 10 * i] for i in range(1, idx + 1)]} + ) + self.assertSLEs(receipt, expected_queues) + + transfer = make_stock_entry(item_code=item.name, source=source, target=target, qty=10, do_not_save=True, rate=10) + for rate in rates[1:]: + row = frappe.copy_doc(transfer.items[0], ignore_no_copy=False) + row.basic_rate = rate + transfer.append("items", row) + + transfer.save() + transfer.submit() + + # same exact queue should be transferred + self.assertSLEs(transfer, expected_queues, sle_filters={"warehouse": target}) + def create_repack_entry(**args): args = frappe._dict(args) From bb58c49aef3b8f07fb0e71edbad5528780a2481c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 1 Mar 2022 14:04:02 +0530 Subject: [PATCH 104/121] fix: FIFO valuation in case of multi-item entries (cherry picked from commit ccd2ce56b1d6e24005e5a24b11c72e78adb0a4e4) --- erpnext/stock/stock_ledger.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 47a97c47fe5..b2366d99c45 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -438,8 +438,8 @@ class update_entries_after(object): return # Get dynamic incoming/outgoing rate - if not self.args.get("sle_id"): - self.get_dynamic_incoming_outgoing_rate(sle) + # XXX: performance regression + self.get_dynamic_incoming_outgoing_rate(sle) if get_serial_nos(sle.serial_no): self.get_serialized_values(sle) @@ -482,8 +482,8 @@ class update_entries_after(object): sle.doctype="Stock Ledger Entry" frappe.get_doc(sle).db_update() - if not self.args.get("sle_id"): - self.update_outgoing_rate_on_transaction(sle) + # XXX: performance regression + self.update_outgoing_rate_on_transaction(sle) def validate_negative_stock(self, sle): """ @@ -565,9 +565,8 @@ class update_entries_after(object): def update_rate_on_stock_entry(self, sle, outgoing_rate): frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate) - # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount - if not sle.dependant_sle_voucher_detail_no: - self.recalculate_amounts_in_stock_entry(sle.voucher_no) + # XXX: performance regression + self.recalculate_amounts_in_stock_entry(sle.voucher_no) def recalculate_amounts_in_stock_entry(self, voucher_no): stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True) From 9fc0da16887523d1775bb1db1725f28f25f03b99 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 1 Mar 2022 17:04:10 +0530 Subject: [PATCH 105/121] test: repack FIFO rates (cherry picked from commit 2f71c5bccaad5924a3047912761240681698d0ee) --- .../test_stock_ledger_entry.py | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index d2489bf54fa..2a03cf4f19e 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -390,7 +390,7 @@ class TestStockLedgerEntry(ERPNextTestCase): def assertSLEs(self, doc, expected_sles, sle_filters=None): """ Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line""" - filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled":0} + filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0} if sle_filters: filters.update(sle_filters) sles = frappe.get_all("Stock Ledger Entry", fields=["*"], filters=filters, @@ -693,7 +693,6 @@ class TestStockLedgerEntry(ERPNextTestCase): transfer = make_stock_entry(item_code=item.name, source=source, target=target, qty=10, do_not_save=True, rate=10) for rate in rates[1:]: row = frappe.copy_doc(transfer.items[0], ignore_no_copy=False) - row.basic_rate = rate transfer.append("items", row) transfer.save() @@ -702,6 +701,43 @@ class TestStockLedgerEntry(ERPNextTestCase): # same exact queue should be transferred self.assertSLEs(transfer, expected_queues, sle_filters={"warehouse": target}) + def test_fifo_multi_item_repack_consumption(self): + rm = make_item("_TestFifoRepackRM") + packed = make_item("_TestFifoRepackFinished") + warehouse = "_Test Warehouse - _TC" + + rates = [10 * i for i in range(1, 5)] + + receipt = make_stock_entry(item_code=rm.name, target=warehouse, qty=10, do_not_save=True, rate=10) + for rate in rates[1:]: + row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False) + row.basic_rate = rate + receipt.append("items", row) + + receipt.save() + receipt.submit() + + repack = make_stock_entry(item_code=rm.name, source=warehouse, qty=10, + do_not_save=True, rate=10, purpose="Repack") + for rate in rates[1:]: + row = frappe.copy_doc(repack.items[0], ignore_no_copy=False) + repack.append("items", row) + + repack.append("items", { + "item_code": packed.name, + "t_warehouse": warehouse, + "qty": 1, + "transfer_qty": 1, + }) + + repack.save() + repack.submit() + + # same exact queue should be transferred + self.assertSLEs(repack, [ + {"incoming_rate": sum(rates) * 10} + ], sle_filters={"item_code": packed.name}) + def create_repack_entry(**args): args = frappe._dict(args) From a1c3e9aef989f0d95b8a0cf2b7224d70228380db Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 1 Mar 2022 18:08:29 +0530 Subject: [PATCH 106/121] revert "fix: FIFO valuation in case of multi-item entries" This reverts commit b8ee193d1a124668691b3d8181ce4e3bf612afe0. This is huge performance regression for large docs. (cherry picked from commit 701878f60b4a9b60035295f23ae65973680b03e5) --- erpnext/stock/stock_ledger.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index b2366d99c45..47a97c47fe5 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -438,8 +438,8 @@ class update_entries_after(object): return # Get dynamic incoming/outgoing rate - # XXX: performance regression - self.get_dynamic_incoming_outgoing_rate(sle) + if not self.args.get("sle_id"): + self.get_dynamic_incoming_outgoing_rate(sle) if get_serial_nos(sle.serial_no): self.get_serialized_values(sle) @@ -482,8 +482,8 @@ class update_entries_after(object): sle.doctype="Stock Ledger Entry" frappe.get_doc(sle).db_update() - # XXX: performance regression - self.update_outgoing_rate_on_transaction(sle) + if not self.args.get("sle_id"): + self.update_outgoing_rate_on_transaction(sle) def validate_negative_stock(self, sle): """ @@ -565,8 +565,9 @@ class update_entries_after(object): def update_rate_on_stock_entry(self, sle, outgoing_rate): frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate) - # XXX: performance regression - self.recalculate_amounts_in_stock_entry(sle.voucher_no) + # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount + if not sle.dependant_sle_voucher_detail_no: + self.recalculate_amounts_in_stock_entry(sle.voucher_no) def recalculate_amounts_in_stock_entry(self, voucher_no): stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True) From 870799c4e22a5f71f99b5004caf72e45e2157fd6 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 1 Mar 2022 18:17:14 +0530 Subject: [PATCH 107/121] fix: repost items with repeating item-warehouses (cherry picked from commit 3638fbf06bfc492515096e4b4a065a52a495420a) --- erpnext/controllers/stock_controller.py | 30 ++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index c8e5eddfeac..8972c328796 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -507,13 +507,41 @@ class StockController(AccountsController): "voucher_no": self.name, "company": self.company }) - if future_sle_exists(args): + + if future_sle_exists(args) or repost_required_for_queue(self): item_based_reposting = cint(frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting")) if item_based_reposting: create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name) else: create_repost_item_valuation_entry(args) +def repost_required_for_queue(doc: StockController) -> bool: + """check if stock document contains repeated item-warehouse with queue based valuation. + + if queue exists for repeated items then SLEs need to reprocessed in background again. + """ + + consuming_sles = frappe.db.get_all("Stock Ledger Entry", + filters={ + "voucher_type": doc.doctype, + "voucher_no": doc.name, + "actual_qty": ("<", 0), + "is_cancelled": 0 + }, + fields=["item_code", "warehouse", "stock_queue"] + ) + item_warehouses = [(sle.item_code, sle.warehouse) for sle in consuming_sles] + + unique_item_warehouses = set(item_warehouses) + + if len(unique_item_warehouses) == len(item_warehouses): + return False + + for sle in consuming_sles: + if sle.stock_queue != "[]": # using FIFO/LIFO valuation + return True + return False + @frappe.whitelist() def make_quality_inspections(doctype, docname, items): From 95f30ada7bbf0f87af8e349fddc80abc913596b2 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 2 Mar 2022 13:14:18 +0530 Subject: [PATCH 108/121] fix: resolve conflicts --- .../test_stock_ledger_entry.py | 341 ++---------------- 1 file changed, 23 insertions(+), 318 deletions(-) diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index 2a03cf4f19e..6d113ba4eb6 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -1,6 +1,8 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt +import json + import frappe from frappe.core.page.permission_manager.permission_manager import reset from frappe.utils import add_days, today @@ -32,6 +34,27 @@ class TestStockLedgerEntry(ERPNextTestCase): frappe.db.sql("delete from `tabStock Ledger Entry` where item_code in (%s)" % (', '.join(['%s']*len(items))), items) frappe.db.sql("delete from `tabBin` where item_code in (%s)" % (', '.join(['%s']*len(items))), items) + + def assertSLEs(self, doc, expected_sles, sle_filters=None): + """ Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line""" + + filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0} + if sle_filters: + filters.update(sle_filters) + sles = frappe.get_all("Stock Ledger Entry", fields=["*"], filters=filters, + order_by="timestamp(posting_date, posting_time), creation") + + for exp_sle, act_sle in zip(expected_sles, sles): + for k, v in exp_sle.items(): + act_value = act_sle[k] + if k == "stock_queue": + act_value = json.loads(act_value) + if act_value and act_value[0][0] == 0: + # ignore empty fifo bins + continue + + self.assertEqual(v, act_value, msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}") + def test_item_cost_reposting(self): company = "_Test Company" @@ -349,324 +372,6 @@ class TestStockLedgerEntry(ERPNextTestCase): frappe.set_user("Administrator") user.remove_roles("Stock Manager") -<<<<<<< HEAD -======= - def test_batchwise_item_valuation_moving_average(self): - item, warehouses, batches = setup_item_valuation_test(valuation_method="Moving Average") - - # Incoming Entries for Stock Value check - pr_entry_list = [ - (item, warehouses[0], batches[0], 1, 100), - (item, warehouses[0], batches[1], 1, 50), - (item, warehouses[0], batches[0], 1, 150), - (item, warehouses[0], batches[1], 1, 100), - ] - prs = create_purchase_receipt_entries_for_batchwise_item_valuation_test(pr_entry_list) - sle_details = fetch_sle_details_for_doc_list(prs, ['stock_value']) - sv_list = [d['stock_value'] for d in sle_details] - expected_sv = [100, 150, 300, 400] - self.assertEqual(expected_sv, sv_list, "Incorrect 'Stock Value' values") - - # Outgoing Entries for Stock Value Difference check - dn_entry_list = [ - (item, warehouses[0], batches[1], 1, 200), - (item, warehouses[0], batches[0], 1, 200), - (item, warehouses[0], batches[1], 1, 200), - (item, warehouses[0], batches[0], 1, 200) - ] - dns = create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list) - sle_details = fetch_sle_details_for_doc_list(dns, ['stock_value_difference']) - svd_list = [-1 * d['stock_value_difference'] for d in sle_details] - expected_incoming_rates = expected_abs_svd = [75, 125, 75, 125] - - self.assertEqual(expected_abs_svd, svd_list, "Incorrect 'Stock Value Difference' values") - for dn, incoming_rate in zip(dns, expected_incoming_rates): - self.assertEqual( - dn.items[0].incoming_rate, incoming_rate, - "Incorrect 'Incoming Rate' values fetched for DN items" - ) - - - def assertSLEs(self, doc, expected_sles, sle_filters=None): - """ Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line""" - - filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0} - if sle_filters: - filters.update(sle_filters) - sles = frappe.get_all("Stock Ledger Entry", fields=["*"], filters=filters, - order_by="timestamp(posting_date, posting_time), creation") - - for exp_sle, act_sle in zip(expected_sles, sles): - for k, v in exp_sle.items(): - act_value = act_sle[k] - if k == "stock_queue": - act_value = json.loads(act_value) - if act_value and act_value[0][0] == 0: - # ignore empty fifo bins - continue - - self.assertEqual(v, act_value, msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}") - - - def test_batchwise_item_valuation_stock_reco(self): - item, warehouses, batches = setup_item_valuation_test() - state = { - "stock_value" : 0.0, - "qty": 0.0 - } - def update_invariants(exp_sles): - for sle in exp_sles: - state["stock_value"] += sle["stock_value_difference"] - state["qty"] += sle["actual_qty"] - sle["stock_value"] = state["stock_value"] - sle["qty_after_transaction"] = state["qty"] - - osr1 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=10, rate=100, batch_no=batches[1]) - expected_sles = [ - {"actual_qty": 10, "stock_value_difference": 1000}, - ] - update_invariants(expected_sles) - self.assertSLEs(osr1, expected_sles) - - osr2 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=13, rate=200, batch_no=batches[0]) - expected_sles = [ - {"actual_qty": 13, "stock_value_difference": 200*13}, - ] - update_invariants(expected_sles) - self.assertSLEs(osr2, expected_sles) - - sr1 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=5, rate=50, batch_no=batches[1]) - - expected_sles = [ - {"actual_qty": -10, "stock_value_difference": -10 * 100}, - {"actual_qty": 5, "stock_value_difference": 250} - ] - update_invariants(expected_sles) - self.assertSLEs(sr1, expected_sles) - - sr2 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=20, rate=75, batch_no=batches[0]) - expected_sles = [ - {"actual_qty": -13, "stock_value_difference": -13 * 200}, - {"actual_qty": 20, "stock_value_difference": 20 * 75} - ] - update_invariants(expected_sles) - self.assertSLEs(sr2, expected_sles) - - def test_batch_wise_valuation_across_warehouse(self): - item_code, warehouses, batches = setup_item_valuation_test() - source = warehouses[0] - target = warehouses[1] - - unrelated_batch = make_stock_entry(item_code=item_code, target=source, batch_no=batches[1], - qty=5, rate=10) - self.assertSLEs(unrelated_batch, [ - {"actual_qty": 5, "stock_value_difference": 10 * 5}, - ]) - - reciept = make_stock_entry(item_code=item_code, target=source, batch_no=batches[0], qty=5, rate=10) - self.assertSLEs(reciept, [ - {"actual_qty": 5, "stock_value_difference": 10 * 5}, - ]) - - transfer = make_stock_entry(item_code=item_code, source=source, target=target, batch_no=batches[0], qty=5) - self.assertSLEs(transfer, [ - {"actual_qty": -5, "stock_value_difference": -10 * 5, "warehouse": source}, - {"actual_qty": 5, "stock_value_difference": 10 * 5, "warehouse": target} - ]) - - backdated_receipt = make_stock_entry(item_code=item_code, target=source, batch_no=batches[0], - qty=5, rate=20, posting_date=add_days(today(), -1)) - self.assertSLEs(backdated_receipt, [ - {"actual_qty": 5, "stock_value_difference": 20 * 5}, - ]) - - # check reposted average rate in *future* transfer - self.assertSLEs(transfer, [ - {"actual_qty": -5, "stock_value_difference": -15 * 5, "warehouse": source, "stock_value": 15 * 5 + 10 * 5}, - {"actual_qty": 5, "stock_value_difference": 15 * 5, "warehouse": target, "stock_value": 15 * 5} - ]) - - transfer_unrelated = make_stock_entry(item_code=item_code, source=source, - target=target, batch_no=batches[1], qty=5) - self.assertSLEs(transfer_unrelated, [ - {"actual_qty": -5, "stock_value_difference": -10 * 5, "warehouse": source, "stock_value": 15 * 5}, - {"actual_qty": 5, "stock_value_difference": 10 * 5, "warehouse": target, "stock_value": 15 * 5 + 10 * 5} - ]) - - def test_intermediate_average_batch_wise_valuation(self): - """ A batch has moving average up until posting time, - check if same is respected when backdated entry is inserted in middle""" - item_code, warehouses, batches = setup_item_valuation_test() - warehouse = warehouses[0] - - batch = batches[0] - - yesterday = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batch, - qty=1, rate=10, posting_date=add_days(today(), -1)) - self.assertSLEs(yesterday, [ - {"actual_qty": 1, "stock_value_difference": 10}, - ]) - - tomorrow = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], - qty=1, rate=30, posting_date=add_days(today(), 1)) - self.assertSLEs(tomorrow, [ - {"actual_qty": 1, "stock_value_difference": 30}, - ]) - - create_today = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], - qty=1, rate=20) - self.assertSLEs(create_today, [ - {"actual_qty": 1, "stock_value_difference": 20}, - ]) - - consume_today = make_stock_entry(item_code=item_code, source=warehouse, batch_no=batches[0], - qty=1) - self.assertSLEs(consume_today, [ - {"actual_qty": -1, "stock_value_difference": -15}, - ]) - - consume_tomorrow = make_stock_entry(item_code=item_code, source=warehouse, batch_no=batches[0], - qty=2, posting_date=add_days(today(), 2)) - self.assertSLEs(consume_tomorrow, [ - {"stock_value_difference": -(30 + 15), "stock_value": 0, "qty_after_transaction": 0}, - ]) - - def test_legacy_item_valuation_stock_entry(self): - columns = [ - 'stock_value_difference', - 'stock_value', - 'actual_qty', - 'qty_after_transaction', - 'stock_queue', - ] - item, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0) - - def check_sle_details_against_expected(sle_details, expected_sle_details, detail, columns): - for i, (sle_vals, ex_sle_vals) in enumerate(zip(sle_details, expected_sle_details)): - for col, sle_val, ex_sle_val in zip(columns, sle_vals, ex_sle_vals): - if col == 'stock_queue': - sle_val = get_stock_value_from_q(sle_val) - ex_sle_val = get_stock_value_from_q(ex_sle_val) - self.assertEqual( - sle_val, ex_sle_val, - f"Incorrect {col} value on transaction #: {i} in {detail}" - ) - - # List used to defer assertions to prevent commits cause of error skipped rollback - details_list = [] - - - # Test Material Receipt Entries - se_entry_list_mr = [ - (item, None, warehouses[0], batches[0], 1, 50, "2021-01-21"), - (item, None, warehouses[0], batches[1], 1, 100, "2021-01-23"), - ] - ses = create_stock_entry_entries_for_batchwise_item_valuation_test( - se_entry_list_mr, "Material Receipt" - ) - sle_details = fetch_sle_details_for_doc_list(ses, columns=columns, as_dict=0) - expected_sle_details = [ - (50.0, 50.0, 1.0, 1.0, '[[1.0, 50.0]]'), - (100.0, 150.0, 1.0, 2.0, '[[1.0, 50.0], [1.0, 100.0]]'), - ] - details_list.append(( - sle_details, expected_sle_details, - "Material Receipt Entries", columns - )) - - - # Test Material Issue Entries - se_entry_list_mi = [ - (item, warehouses[0], None, batches[1], 1, None, "2021-01-29"), - ] - ses = create_stock_entry_entries_for_batchwise_item_valuation_test( - se_entry_list_mi, "Material Issue" - ) - sle_details = fetch_sle_details_for_doc_list(ses, columns=columns, as_dict=0) - expected_sle_details = [ - (-50.0, 100.0, -1.0, 1.0, '[[1, 100.0]]') - ] - details_list.append(( - sle_details, expected_sle_details, - "Material Issue Entries", columns - )) - - - # Run assertions - for details in details_list: - check_sle_details_against_expected(*details) - - def test_mixed_valuation_batches_fifo(self): - item_code, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0) - warehouse = warehouses[0] - - state = { - "qty": 0.0, - "stock_value": 0.0 - } - def update_invariants(exp_sles): - for sle in exp_sles: - state["stock_value"] += sle["stock_value_difference"] - state["qty"] += sle["actual_qty"] - sle["stock_value"] = state["stock_value"] - sle["qty_after_transaction"] = state["qty"] - return exp_sles - - old1 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], - qty=10, rate=10) - self.assertSLEs(old1, update_invariants([ - {"actual_qty": 10, "stock_value_difference": 10*10, "stock_queue": [[10, 10]]}, - ])) - old2 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[1], - qty=10, rate=20) - self.assertSLEs(old2, update_invariants([ - {"actual_qty": 10, "stock_value_difference": 10*20, "stock_queue": [[10, 10], [10, 20]]}, - ])) - old3 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0], - qty=5, rate=15) - - self.assertSLEs(old3, update_invariants([ - {"actual_qty": 5, "stock_value_difference": 5*15, "stock_queue": [[10, 10], [10, 20], [5, 15]]}, - ])) - - new1 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=40) - batches.append(new1.items[0].batch_no) - # assert old queue remains - self.assertSLEs(new1, update_invariants([ - {"actual_qty": 10, "stock_value_difference": 10*40, "stock_queue": [[10, 10], [10, 20], [5, 15]]}, - ])) - - new2 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=42) - batches.append(new2.items[0].batch_no) - self.assertSLEs(new2, update_invariants([ - {"actual_qty": 10, "stock_value_difference": 10*42, "stock_queue": [[10, 10], [10, 20], [5, 15]]}, - ])) - - # consume old batch as per FIFO - consume_old1 = make_stock_entry(item_code=item_code, source=warehouse, qty=15, batch_no=batches[0]) - self.assertSLEs(consume_old1, update_invariants([ - {"actual_qty": -15, "stock_value_difference": -10*10 - 5*20, "stock_queue": [[5, 20], [5, 15]]}, - ])) - - # consume new batch as per batch - consume_new2 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-1]) - self.assertSLEs(consume_new2, update_invariants([ - {"actual_qty": -10, "stock_value_difference": -10*42, "stock_queue": [[5, 20], [5, 15]]}, - ])) - - # finish all old batches - consume_old2 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[1]) - self.assertSLEs(consume_old2, update_invariants([ - {"actual_qty": -10, "stock_value_difference": -5*20 - 5*15, "stock_queue": []}, - ])) - - # finish all new batches - consume_new1 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-2]) - self.assertSLEs(consume_new1, update_invariants([ - {"actual_qty": -10, "stock_value_difference": -10*40, "stock_queue": []}, - ])) ->>>>>>> b3b7cdfb49 (test: FIFO transfer for multi-batch transaction) - def test_fifo_dependent_consumption(self): item = make_item("_TestFifoTransferRates") source = "_Test Warehouse - _TC" From d1cc291c80d1859a068e968d82631089f7551f3f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 2 Mar 2022 14:19:11 +0530 Subject: [PATCH 109/121] fix(Timesheet): fetch exchange rate only if currency is set (backport #30057) (#30058) Co-authored-by: Rucha Mahabal --- erpnext/projects/doctype/timesheet/timesheet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index f615f051f0c..453d46c7c4e 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -116,7 +116,7 @@ frappe.ui.form.on("Timesheet", { currency: function(frm) { let base_currency = frappe.defaults.get_global_default('currency'); - if (base_currency != frm.doc.currency) { + if (frm.doc.currency && (base_currency != frm.doc.currency)) { frappe.call({ method: "erpnext.setup.utils.get_exchange_rate", args: { From 8ef1f679f72587dec4dc1e1b095d7da464057b30 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 2 Mar 2022 14:50:54 +0530 Subject: [PATCH 110/121] chore: get stock reco qty from SR instead of SLE (#30059) (#30060) [skip ci] (cherry picked from commit 55a966ec4137a5d60ef4351ea401e0fcbc5dff91) Co-authored-by: Ankush Menat --- .../stock_ledger_invariant_check.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py index 8a6210cc2be..1b61863ce6a 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py @@ -21,6 +21,7 @@ SLE_FIELDS = ( "stock_value", "stock_value_difference", "valuation_rate", + "voucher_detail_no", ) @@ -66,7 +67,9 @@ def add_invariant_check_fields(sles): balance_qty += sle.actual_qty balance_stock_value += sle.stock_value_difference if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no: - balance_qty = sle.qty_after_transaction + balance_qty = frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "qty") + if balance_qty is None: + balance_qty = sle.qty_after_transaction sle.fifo_queue_qty = fifo_qty sle.fifo_stock_value = fifo_value From 478e1eb8dbc750827442b8f1c8d450e3e330072e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 2 Mar 2022 16:25:18 +0530 Subject: [PATCH 111/121] fix: remove dead dashboard links (cherry picked from commit f8ac4c082a8512349187cecf058c8945231e7d52) --- .../manufacturing/doctype/operation/operation_dashboard.py | 2 +- .../doctype/workstation/workstation_dashboard.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/operation/operation_dashboard.py b/erpnext/manufacturing/doctype/operation/operation_dashboard.py index 076f6663bea..4a548a64709 100644 --- a/erpnext/manufacturing/doctype/operation/operation_dashboard.py +++ b/erpnext/manufacturing/doctype/operation/operation_dashboard.py @@ -8,7 +8,7 @@ def get_data(): 'transactions': [ { 'label': _('Manufacture'), - 'items': ['BOM', 'Work Order', 'Job Card', 'Timesheet'] + 'items': ['BOM', 'Work Order', 'Job Card'] } ] } diff --git a/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py b/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py index c779fbf9c3b..9c0f6b8b789 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py +++ b/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py @@ -12,9 +12,9 @@ def get_data(): }, { 'label': _('Transaction'), - 'items': ['Work Order', 'Job Card', 'Timesheet'] + 'items': ['Work Order', 'Job Card',] } ], 'disable_create_buttons': ['BOM', 'Routing', 'Operation', - 'Work Order', 'Job Card', 'Timesheet'] + 'Work Order', 'Job Card',] } From 5bad8d0e1016a9f6134918642c01e00e0b44fd6e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 2 Mar 2022 16:32:48 +0530 Subject: [PATCH 112/121] fix: dont hardcode precision in routing Co-Authored-By: Suraj Shetty (cherry picked from commit 9ef35ef7735683041d412f25eed9d7b59e2e8dd7) --- erpnext/manufacturing/doctype/routing/routing.js | 2 +- erpnext/manufacturing/doctype/routing/routing.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/routing/routing.js b/erpnext/manufacturing/doctype/routing/routing.js index 032c9cd9a21..ebed6fcde64 100644 --- a/erpnext/manufacturing/doctype/routing/routing.js +++ b/erpnext/manufacturing/doctype/routing/routing.js @@ -17,7 +17,7 @@ frappe.ui.form.on('Routing', { }, calculate_operating_cost: function(frm, child) { - const operating_cost = flt(flt(child.hour_rate) * flt(child.time_in_mins) / 60, 2); + const operating_cost = flt(flt(child.hour_rate) * flt(child.time_in_mins) / 60, precision("operating_cost", child)); frappe.model.set_value(child.doctype, child.name, "operating_cost", operating_cost); } }); diff --git a/erpnext/manufacturing/doctype/routing/routing.py b/erpnext/manufacturing/doctype/routing/routing.py index 1c76634646d..b207906c5e3 100644 --- a/erpnext/manufacturing/doctype/routing/routing.py +++ b/erpnext/manufacturing/doctype/routing/routing.py @@ -20,7 +20,8 @@ class Routing(Document): for operation in self.operations: if not operation.hour_rate: operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, 'hour_rate') - operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60, 2) + operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60, + operation.precision("operating_cost")) def set_routing_id(self): sequence_id = 0 From 131158a24c326f202d4e9967eb28cf2fff8fa12f Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 22 Feb 2022 15:06:19 +0530 Subject: [PATCH 113/121] feat: update ordered qty for packed items (cherry picked from commit 8005fee6569abed607c57d31b26531925fd7e15b) # Conflicts: # erpnext/selling/doctype/sales_order/sales_order.js --- .../doctype/purchase_order/purchase_order.py | 10 +++++++ .../purchase_order_item.json | 13 +++++++++- .../doctype/sales_order/sales_order.js | 26 ++++++++++++++++++- .../doctype/sales_order/sales_order.py | 5 ++++ .../doctype/packed_item/packed_item.json | 11 +++++++- 5 files changed, 62 insertions(+), 3 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 1b5f35efbb4..2e7d3063ccb 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -316,6 +316,16 @@ class PurchaseOrder(BuyingController): 'target_ref_field': 'stock_qty', 'source_field': 'stock_qty' }) + self.status_updater.append({ + 'source_dt': 'Purchase Order Item', + 'target_dt': 'Packed Item', + 'target_field': 'ordered_qty', + 'target_parent_dt': 'Sales Order', + 'target_parent_field': '', + 'join_field': 'sales_order_packed_item', + 'target_ref_field': 'qty', + 'source_field': 'stock_qty' + }) def update_delivered_qty_in_sales_order(self): """Update delivered qty in Sales Order for drop ship""" diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 87cd57517e2..c26d592e3ee 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -63,6 +63,7 @@ "material_request_item", "sales_order", "sales_order_item", + "sales_order_packed_item", "supplier_quotation", "supplier_quotation_item", "col_break5", @@ -837,21 +838,31 @@ "label": "Product Bundle", "options": "Product Bundle", "read_only": 1 + }, + { + "fieldname": "sales_order_packed_item", + "fieldtype": "Data", + "label": "Sales Order Packed Item", + "no_copy": 1, + "print_hide": 1, + "search_index": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-08-30 20:06:26.712097", + "modified": "2022-02-02 13:10:18.398976", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "quick_entry": 1, "search_fields": "item_name", "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 2d5bb2013f2..f7e6c19cfa2 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -562,6 +562,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( var me = this; var dialog = new frappe.ui.Dialog({ title: __("Select Items"), + size: "large", fields: [ { "fieldtype": "Check", @@ -663,7 +664,8 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( } else { let po_items = []; me.frm.doc.items.forEach(d => { - let pending_qty = (flt(d.stock_qty) - flt(d.ordered_qty)) / flt(d.conversion_factor); + let ordered_qty = me.get_ordered_qty(d, me.frm.doc); + let pending_qty = (flt(d.stock_qty) - ordered_qty) / flt(d.conversion_factor); if (pending_qty > 0) { po_items.push({ "doctype": "Sales Order Item", @@ -689,7 +691,29 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( dialog.show(); }, +<<<<<<< HEAD hold_sales_order: function(){ +======= + get_ordered_qty(item, so) { + let ordered_qty = item.ordered_qty; + if (so.packed_items) { + // calculate ordered qty based on packed items in case of product bundle + let packed_items = so.packed_items.filter( + (pi) => pi.parent_detail_docname == item.name + ); + if (packed_items) { + ordered_qty = packed_items.reduce( + (sum, pi) => sum + flt(pi.ordered_qty), + 0 + ); + ordered_qty = ordered_qty / packed_items.length; + } + } + return ordered_qty; + } + + hold_sales_order(){ +>>>>>>> 8005fee656 (feat: update ordered qty for packed items) var me = this; var d = new frappe.ui.Dialog({ title: __('Reason for Hold'), diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 8336a143617..57c67424f7d 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -923,6 +923,9 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty)) target.project = source_parent.project + def update_item_for_packed_item(source, target, source_parent): + target.qty = flt(source.qty) - flt(source.ordered_qty) + # po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")}) doc = get_mapped_doc("Sales Order", source_name, { "Sales Order": { @@ -966,6 +969,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): "Packed Item": { "doctype": "Purchase Order Item", "field_map": [ + ["name", "sales_order_packed_item"], ["parent", "sales_order"], ["uom", "uom"], ["conversion_factor", "conversion_factor"], @@ -980,6 +984,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): "supplier", "pricing_rules" ], + "postprocess": update_item_for_packed_item, "condition": lambda doc: doc.parent_item in items_to_map } }, target_doc, set_missing_values) diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index d2d47897658..d6e2e9ce2d7 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -26,6 +26,7 @@ "section_break_13", "actual_qty", "projected_qty", + "ordered_qty", "column_break_16", "incoming_rate", "page_break", @@ -224,13 +225,21 @@ "label": "Rate", "print_hide": 1, "read_only": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "ordered_qty", + "fieldtype": "Float", + "label": "Ordered Qty", + "no_copy": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-01-28 16:03:30.780111", + "modified": "2022-02-22 12:57:45.325488", "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", From 14f31ac2fad08b715c7223ccb73c16d8bca3dd11 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 22 Feb 2022 15:34:26 +0530 Subject: [PATCH 114/121] test: po updates packed item's ordered_qty (cherry picked from commit 8e3f1e306d705109a51271ba262b46fe4798a793) # Conflicts: # erpnext/selling/doctype/sales_order/test_sales_order.py --- .../doctype/sales_order/test_sales_order.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 788a8350caa..e79d8e898b9 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -921,6 +921,77 @@ class TestSalesOrder(ERPNextTestCase): self.assertEqual(purchase_orders[0].supplier, '_Test Supplier') self.assertEqual(purchase_orders[1].supplier, '_Test Supplier 1') +<<<<<<< HEAD +======= + def test_product_bundles_in_so_are_replaced_with_bundle_items_in_po(self): + """ + Tests if the the Product Bundles in the Items table of Sales Orders are replaced with + their child items(from the Packed Items table) on creating a Purchase Order from it. + """ + from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order + + product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0}) + make_item("_Test Bundle Item 1", {"is_stock_item": 1}) + make_item("_Test Bundle Item 2", {"is_stock_item": 1}) + + make_product_bundle("_Test Product Bundle", + ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + + so_items = [ + { + "item_code": product_bundle.item_code, + "warehouse": "", + "qty": 2, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' + } + ] + + so = make_sales_order(item_list=so_items) + + purchase_order = make_purchase_order(so.name, selected_items=so_items) + + self.assertEqual(purchase_order.items[0].item_code, "_Test Bundle Item 1") + self.assertEqual(purchase_order.items[1].item_code, "_Test Bundle Item 2") + + def test_purchase_order_updates_packed_item_ordered_qty(self): + """ + Tests if the packed item's `ordered_qty` is updated with the quantity of the Purchase Order + """ + from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order + + product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0}) + make_item("_Test Bundle Item 1", {"is_stock_item": 1}) + make_item("_Test Bundle Item 2", {"is_stock_item": 1}) + + make_product_bundle("_Test Product Bundle", + ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + + so_items = [ + { + "item_code": product_bundle.item_code, + "warehouse": "", + "qty": 2, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' + } + ] + + so = make_sales_order(item_list=so_items) + + purchase_order = make_purchase_order(so.name, selected_items=so_items) + purchase_order.supplier = "_Test Supplier" + purchase_order.set_warehouse = "_Test Warehouse - _TC" + purchase_order.save() + purchase_order.submit() + + so.reload() + self.assertEqual(so.packed_items[0].ordered_qty, 2) + self.assertEqual(so.packed_items[1].ordered_qty, 2) + +>>>>>>> 8e3f1e306d (test: po updates packed item's ordered_qty) def test_reserved_qty_for_closing_so(self): bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, fields=["reserved_qty"]) From 1c0d0e1ac76a11ee3dfbf38c923f51e60bd07687 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 24 Feb 2022 15:12:12 +0530 Subject: [PATCH 115/121] chore: remove unintentional search index (cherry picked from commit 1e139cf9a115228b993c83be29415b1f6971b33e) --- .../buying/doctype/purchase_order_item/purchase_order_item.json | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index c26d592e3ee..2c9fc36794e 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -845,7 +845,6 @@ "label": "Sales Order Packed Item", "no_copy": 1, "print_hide": 1, - "search_index": 1 } ], "idx": 1, From 447692d770dd1bc8f9e21c8be32f984979125a28 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 24 Feb 2022 15:12:35 +0530 Subject: [PATCH 116/121] chore: remove unintentional search index (cherry picked from commit 2f1709dfef55ed17db3e2ae885aef5588dd7a31a) --- .../buying/doctype/purchase_order_item/purchase_order_item.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 2c9fc36794e..a18c527644e 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -844,7 +844,7 @@ "fieldtype": "Data", "label": "Sales Order Packed Item", "no_copy": 1, - "print_hide": 1, + "print_hide": 1 } ], "idx": 1, From 1e418f9ecc8bed10be8e32615dfd1f6d27c2e716 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sun, 6 Mar 2022 13:13:50 +0530 Subject: [PATCH 117/121] fix: merge conflicts --- erpnext/selling/doctype/sales_order/sales_order.js | 10 +++------- .../selling/doctype/sales_order/test_sales_order.py | 3 --- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index f7e6c19cfa2..69c85a32533 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -691,10 +691,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( dialog.show(); }, -<<<<<<< HEAD - hold_sales_order: function(){ -======= - get_ordered_qty(item, so) { + get_ordered_qty: function(item, so) { let ordered_qty = item.ordered_qty; if (so.packed_items) { // calculate ordered qty based on packed items in case of product bundle @@ -710,10 +707,9 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( } } return ordered_qty; - } + }, - hold_sales_order(){ ->>>>>>> 8005fee656 (feat: update ordered qty for packed items) + hold_sales_order: function(){ var me = this; var d = new frappe.ui.Dialog({ title: __('Reason for Hold'), diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index e79d8e898b9..1102fe96fc4 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -921,8 +921,6 @@ class TestSalesOrder(ERPNextTestCase): self.assertEqual(purchase_orders[0].supplier, '_Test Supplier') self.assertEqual(purchase_orders[1].supplier, '_Test Supplier 1') -<<<<<<< HEAD -======= def test_product_bundles_in_so_are_replaced_with_bundle_items_in_po(self): """ Tests if the the Product Bundles in the Items table of Sales Orders are replaced with @@ -991,7 +989,6 @@ class TestSalesOrder(ERPNextTestCase): self.assertEqual(so.packed_items[0].ordered_qty, 2) self.assertEqual(so.packed_items[1].ordered_qty, 2) ->>>>>>> 8e3f1e306d (test: po updates packed item's ordered_qty) def test_reserved_qty_for_closing_so(self): bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, fields=["reserved_qty"]) From cf2580083c0054c40486cb2d84ec650587d63733 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 7 Mar 2022 11:40:01 +0530 Subject: [PATCH 118/121] fix(e-invoicing): remove batch no from e-invoices (cherry picked from commit 031a0dd7035aced02c0a943ce3bff7dba49d3a7f) --- erpnext/regional/india/e_invoice/einv_item_template.json | 6 +----- erpnext/regional/india/e_invoice/utils.py | 2 -- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einv_item_template.json b/erpnext/regional/india/e_invoice/einv_item_template.json index 78e56518dff..2c04c6dcf4d 100644 --- a/erpnext/regional/india/e_invoice/einv_item_template.json +++ b/erpnext/regional/india/e_invoice/einv_item_template.json @@ -23,9 +23,5 @@ "StateCesAmt": "{item.state_cess_amount}", "StateCesNonAdvlAmt": "{item.state_cess_nadv_amount}", "OthChrg": "{item.other_charges}", - "TotItemVal": "{item.total_value}", - "BchDtls": {{ - "Nm": "{item.batch_no}", - "ExpDt": "{item.batch_expiry_date}" - }} + "TotItemVal": "{item.total_value}" }} \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index afb0f592435..cfad29beeb6 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -215,8 +215,6 @@ def get_item_list(invoice): item.taxable_value = abs(item.taxable_value) item.discount_amount = 0 - item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None - item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None item.is_service_item = 'Y' if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else 'N' item.serial_no = "" From 150d1e6e60b351d4bca8ac36c176cb03bb9c3a47 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 7 Mar 2022 18:01:07 +0530 Subject: [PATCH 119/121] fix(pos): multiple pos round off cases (cherry picked from commit 17445c7e04ff88cc5db727cb9f769647bcbebfdf) --- .../pos_invoice_merge_log.py | 9 -- .../test_pos_invoice_merge_log.py | 98 +++++++++++++++++++ .../doctype/sales_invoice/sales_invoice.py | 3 + erpnext/controllers/taxes_and_totals.py | 29 +++--- 4 files changed, 117 insertions(+), 22 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 40ab0c50deb..41dfa226a56 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -85,20 +85,12 @@ class POSInvoiceMergeLog(Document): sales_invoice.set_posting_time = 1 sales_invoice.posting_date = getdate(self.posting_date) sales_invoice.save() - self.write_off_fractional_amount(sales_invoice, data) sales_invoice.submit() self.consolidated_invoice = sales_invoice.name return sales_invoice.name - def write_off_fractional_amount(self, invoice, data): - pos_invoice_grand_total = sum(d.grand_total for d in data) - - if abs(pos_invoice_grand_total - invoice.grand_total) < 1: - invoice.write_off_amount += -1 * (pos_invoice_grand_total - invoice.grand_total) - invoice.save() - def process_merging_into_credit_note(self, data): credit_note = self.get_new_sales_invoice() credit_note.is_return = 1 @@ -111,7 +103,6 @@ class POSInvoiceMergeLog(Document): # TODO: return could be against multiple sales invoice which could also have been consolidated? # credit_note.return_against = self.consolidated_invoice credit_note.save() - self.write_off_fractional_amount(credit_note, data) credit_note.submit() self.consolidated_credit_note = credit_note.name diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index 5930aa097f7..89f7f18b42c 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py @@ -5,6 +5,7 @@ import json import unittest import frappe +from frappe.tests.utils import change_settings from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return @@ -280,3 +281,100 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") + + @change_settings("System Settings", {"number_format": "#,###.###", "currency_precision": 3, "float_precision": 3}) + def test_consolidation_round_off_error_3(self): + frappe.db.sql("delete from `tabPOS Invoice`") + + try: + make_stock_entry( + to_warehouse="_Test Warehouse - _TC", + item_code="_Test Item", + rate=8000, + qty=10, + ) + init_user_and_profile() + + item_rates = [69, 59, 29] + for i in [1, 2]: + inv = create_pos_invoice(is_return=1, do_not_save=1) + inv.items = [] + for rate in item_rates: + inv.append("items", { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "qty": -1, + "rate": rate, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }) + inv.append("taxes", { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 15, + "included_in_print_rate": 1 + }) + inv.payments = [] + inv.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -157 + }) + inv.paid_amount = -157 + inv.save() + inv.submit() + + consolidate_pos_invoices() + + inv.load_from_db() + consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice) + self.assertEqual(consolidated_invoice.status, 'Return') + self.assertEqual(consolidated_invoice.rounding_adjustment, -0.001) + + finally: + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") + + def test_consolidation_rounding_adjustment(self): + ''' + Test if the rounding adjustment is calculated correctly + ''' + frappe.db.sql("delete from `tabPOS Invoice`") + + try: + make_stock_entry( + to_warehouse="_Test Warehouse - _TC", + item_code="_Test Item", + rate=8000, + qty=10, + ) + + init_user_and_profile() + + inv = create_pos_invoice(qty=1, rate=69.5, do_not_save=True) + inv.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 70 + }) + inv.insert() + inv.submit() + + inv2 = create_pos_invoice(qty=1, rate=59.5, do_not_save=True) + inv2.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60 + }) + inv2.insert() + inv2.submit() + + consolidate_pos_invoices() + + inv.load_from_db() + consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice) + self.assertEqual(consolidated_invoice.rounding_adjustment, 1) + + finally: + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 42da6b7708f..409677f3c26 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -272,6 +272,9 @@ class SalesInvoice(SellingController): self.process_common_party_accounting() def validate_pos_return(self): + if self.is_consolidated: + # pos return is already validated in pos invoice + return if self.is_pos and self.is_return: total_amount_in_payments = 0 diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 08d1dcea7dc..bcaf7a145a2 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -270,7 +270,8 @@ class calculate_taxes_and_totals(object): shipping_rule.apply(self.doc) def calculate_taxes(self): - if not self.doc.get('is_consolidated'): + rounding_adjustment_computed = self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment') + if not rounding_adjustment_computed: self.doc.rounding_adjustment = 0 # maintain actual tax rate based on idx @@ -326,7 +327,7 @@ class calculate_taxes_and_totals(object): if i == (len(self.doc.get("taxes")) - 1) and self.discount_amount_applied \ and self.doc.discount_amount \ and self.doc.apply_discount_on == "Grand Total" \ - and not self.doc.get('is_consolidated'): + and not rounding_adjustment_computed: self.doc.rounding_adjustment = flt(self.doc.grand_total - flt(self.doc.discount_amount) - tax.total, self.doc.precision("rounding_adjustment")) @@ -465,20 +466,22 @@ class calculate_taxes_and_totals(object): self.doc.total_net_weight += d.total_weight def set_rounded_total(self): - if not self.doc.get('is_consolidated'): - if self.doc.meta.get_field("rounded_total"): - if self.doc.is_rounded_total_disabled(): - self.doc.rounded_total = self.doc.base_rounded_total = 0 - return + if self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment'): + return - self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total, - self.doc.currency, self.doc.precision("rounded_total")) + if self.doc.meta.get_field("rounded_total"): + if self.doc.is_rounded_total_disabled(): + self.doc.rounded_total = self.doc.base_rounded_total = 0 + return - #if print_in_rate is set, we would have already calculated rounding adjustment - self.doc.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total, - self.doc.precision("rounding_adjustment")) + self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total, + self.doc.currency, self.doc.precision("rounded_total")) - self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"]) + #if print_in_rate is set, we would have already calculated rounding adjustment + self.doc.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total, + self.doc.precision("rounding_adjustment")) + + self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"]) def _cleanup(self): if not self.doc.get('is_consolidated'): From 9538c94b69c90dae487bc62d9516c95e358b557c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 8 Mar 2022 12:48:27 +0530 Subject: [PATCH 120/121] chore: change log for v13.22.0 --- erpnext/change_log/v13/v13_22_0.md | 42 ++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 erpnext/change_log/v13/v13_22_0.md diff --git a/erpnext/change_log/v13/v13_22_0.md b/erpnext/change_log/v13/v13_22_0.md new file mode 100644 index 00000000000..24e3539d833 --- /dev/null +++ b/erpnext/change_log/v13/v13_22_0.md @@ -0,0 +1,42 @@ +## Version 13.22.0 Release Notes + +### Features & Enhancements + +- feat: Payment Terms Status report (backport #29137) ([#29137](https://github.com/frappe/erpnext/pull/29137)) + +### Fixes + +- fix(LMS): program enrollment does not give any feedback (backport #29922) ([#29922](https://github.com/frappe/erpnext/pull/29922)) +- fix: Update SO via Work Order made from MR (attached to SO) (backport #29803) ([#29803](https://github.com/frappe/erpnext/pull/29803)) +- fix: org chart connectors not rendered when Employee Naming is set to Full Name ([#29997](https://github.com/frappe/erpnext/pull/29997)) +- perf: Weed out disabled variants via sql query instead of pythonic looping separately (backport #29639) ([#29639](https://github.com/frappe/erpnext/pull/29639)) +- fix: task status loop ([#26006](https://github.com/frappe/erpnext/pull/26006)) +- fix: Commission not applied while making Sales Order from Quotation (backport #29978) ([#29978](https://github.com/frappe/erpnext/pull/29978)) +- fix: Validate party account with company (backport #29879) ([#29879](https://github.com/frappe/erpnext/pull/29879)) +- fix: add supported currencies for GoCardless (backport #29805) ([#29805](https://github.com/frappe/erpnext/pull/29805)) +- fix(asset): no. of depr booked cannot be equal to total no. of depr (backport #29900) ([#29900](https://github.com/frappe/erpnext/pull/29900)) +- fix: Fetch conversion factor even if it already existed in row, on item change ([#29917](https://github.com/frappe/erpnext/pull/29917)) +- fix(ux): make "allow zero valuation rate" readonly if "s_warehouse" is set ([#29681](https://github.com/frappe/erpnext/pull/29681)) +- fix: Block merging items if both have product bundles (backport #29913) ([#29913](https://github.com/frappe/erpnext/pull/29913)) +- fix: JobCard TimeLog to_date (backport #29872) ([#29872](https://github.com/frappe/erpnext/pull/29872)) +- fix: Stock Ageing Transfer Bucket logic for Repack Entry with split batch rows (backport #29816) ([#29816](https://github.com/frappe/erpnext/pull/29816)) +- fix(Salary Slip): TypeError while clearing any amount field in components (backport #29931) ([#29931](https://github.com/frappe/erpnext/pull/29931)) +- fix: allow renaming and merging ([#29830](https://github.com/frappe/erpnext/pull/29830)) +- fix(pos): minor fixes (backport #29991) ([#29991](https://github.com/frappe/erpnext/pull/29991)) +- fix(e-commerce): Unique Shopping Cart Per Logged In User ([#29994](https://github.com/frappe/erpnext/pull/29994)) +- fix: currency in bank reconciliation tool (backport #29848) ([#29848](https://github.com/frappe/erpnext/pull/29848)) +- fix: Account filter in PSOA (backport #29928) ([#29928](https://github.com/frappe/erpnext/pull/29928)) +- fix: Taxjar minor fixes (backport #29942) ([#29942](https://github.com/frappe/erpnext/pull/29942)) +- fix: Total taxes and charges in payment entry for multi-currency payments (backport #29977) ([#29977](https://github.com/frappe/erpnext/pull/29977)) +- fix(e-invoicing): remove batch no from e-invoices (backport #30084) ([#30084](https://github.com/frappe/erpnext/pull/30084)) +- fix: Total Credit amount in TDS Payable monthly report (backport #29907) ([#29907](https://github.com/frappe/erpnext/pull/29907)) +- fix: GSTIN filter for GSTR-1 report (backport #29869) ([#29869](https://github.com/frappe/erpnext/pull/29869)) +- fix: coupon code is applied even if ignore_pricing_rule is enabled (backport #29859) ([#29859](https://github.com/frappe/erpnext/pull/29859)) +- feat: update ordered qty for packed items (backport #29939) ([#29939](https://github.com/frappe/erpnext/pull/29939)) +- fix: validate Work Order qty against Production Plan ([#29721](https://github.com/frappe/erpnext/pull/29721)) +- fix: Fetch valuation rate for stock items consumed during asset repair ([#29714](https://github.com/frappe/erpnext/pull/29714)) +- fix: Email translations (backport #29956) ([#29956](https://github.com/frappe/erpnext/pull/29956)) +- fix(Timesheet): fetch exchange rate only if currency is set (backport #30057) ([#30057](https://github.com/frappe/erpnext/pull/30057)) +- refactor: removed validation to check zero qty (backport #30015) ([#30015](https://github.com/frappe/erpnext/pull/30015)) +- fix(pos): removal of coupon code (backport #29896) ([#29896](https://github.com/frappe/erpnext/pull/29896)) +- fix: Error in consolidated financial statements (backport #29771) ([#29771](https://github.com/frappe/erpnext/pull/29771)) \ No newline at end of file From a7776f85ef72294add41d008761c6a58cbb51536 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 8 Mar 2022 15:17:18 +0550 Subject: [PATCH 121/121] bumped to version 13.22.0 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 7a5dff041c7..59ac78cd09a 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides -__version__ = '13.21.1' +__version__ = '13.22.0' def get_default_company(user=None): '''Get default company for user'''