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'''