diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
index 1122e7fc5738..bc32b1d19714 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -181,12 +181,20 @@ def supplier_rfq_mail(self, data, update_password_link, rfq_link, preview=False)
doc_args = self.as_dict()
doc_args.update({"supplier": data.get("supplier"), "supplier_name": data.get("supplier_name")})
+ # Get Contact Full Name
+ supplier_name = None
+ if data.get("contact"):
+ contact_name = frappe.db.get_value(
+ "Contact", data.get("contact"), ["first_name", "middle_name", "last_name"]
+ )
+ supplier_name = (" ").join(x for x in contact_name if x) # remove any blank values
+
args = {
"update_password_link": update_password_link,
"message": frappe.render_template(self.message_for_supplier, doc_args),
"rfq_link": rfq_link,
"user_fullname": full_name,
- "supplier_name": data.get("supplier_name"),
+ "supplier_name": supplier_name or data.get("supplier_name"),
"supplier_salutation": self.salutation or "Dear Mx.",
}
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index f639dc7d3806..bdc43dd094a0 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -368,3 +368,4 @@ erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
erpnext.patches.v13_0.update_employee_advance_status
erpnext.patches.v13_0.job_card_status_on_hold
erpnext.patches.v13_0.add_cost_center_in_loans
+erpnext.patches.v13_0.show_india_localisation_deprecation_warning
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/show_india_localisation_deprecation_warning.py b/erpnext/patches/v13_0/show_india_localisation_deprecation_warning.py
new file mode 100644
index 000000000000..1d76b252192a
--- /dev/null
+++ b/erpnext/patches/v13_0/show_india_localisation_deprecation_warning.py
@@ -0,0 +1,15 @@
+import click
+import frappe
+
+
+def execute():
+ if not frappe.db.exists("Company", {"country": "India"}):
+ return
+
+ click.secho(
+ "India-specific regional features have been moved to a separate app"
+ " and will be removed from ERPNext in Version 14."
+ " Please install India Compliance after upgrading to Version 14:\n"
+ "https://github.com/resilient-tech/india-compliance",
+ fg="yellow",
+ )
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index e4c816c8fb45..f652125337b7 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -626,7 +626,7 @@ def add_structure_components(self, component_type):
for struct_row in self._salary_structure_doc.get(component_type):
amount = self.eval_condition_and_formula(struct_row, data)
if amount is not None and struct_row.statistical_component == 0:
- self.update_component_row(struct_row, amount, component_type)
+ self.update_component_row(struct_row, amount, component_type, data=data)
def get_data_for_eval(self):
"""Returns data for evaluating formula"""
@@ -780,7 +780,7 @@ def add_tax_components(self, payroll_period):
self.update_component_row(tax_row, tax_amount, "deductions")
def update_component_row(
- self, component_data, amount, component_type, additional_salary=None, is_recurring=0
+ self, component_data, amount, component_type, additional_salary=None, is_recurring=0, data=None
):
component_row = None
for d in self.get(component_type):
@@ -850,6 +850,8 @@ def update_component_row(
component_row.amount = amount
self.update_component_amount_based_on_payment_days(component_row)
+ if data:
+ data[component_row.abbr] = component_row.amount
def update_component_amount_based_on_payment_days(self, component_row):
joining_date, relieving_date = self.get_joining_and_relieving_dates()
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index 1f17138d0c87..425a03b3ba68 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -1119,6 +1119,7 @@ def make_earning_salary_component(
"formula": "BS*.5",
"type": "Earning",
"amount_based_on_formula": 1,
+ "depends_on_payment_days": 0,
},
{"salary_component": "Leave Encashment", "abbr": "LE", "type": "Earning"},
]
diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py
index ed2a863b3dc3..bebf0303798d 100644
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.py
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py
@@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
+import re
import frappe
from frappe import _
@@ -18,6 +19,7 @@ def validate(self):
self.strip_condition_and_formula_fields()
self.validate_max_benefits_with_flexi()
self.validate_component_based_on_tax_slab()
+ self.validate_payment_days_based_dependent_component()
def set_missing_values(self):
overwritten_fields = [
@@ -58,6 +60,35 @@ def validate_amount(self):
if flt(self.net_pay) < 0 and self.salary_slip_based_on_timesheet:
frappe.throw(_("Net pay cannot be negative"))
+ def validate_payment_days_based_dependent_component(self):
+ abbreviations = self.get_component_abbreviations()
+ for component_type in ("earnings", "deductions"):
+ for row in self.get(component_type):
+ if (
+ row.formula
+ and row.depends_on_payment_days
+ # check if the formula contains any of the payment days components
+ and any(re.search(r"\b" + abbr + r"\b", row.formula) for abbr in abbreviations)
+ ):
+ message = _("Row #{0}: The {1} Component has the options {2} and {3} enabled.").format(
+ row.idx,
+ frappe.bold(row.salary_component),
+ frappe.bold("Amount based on formula"),
+ frappe.bold("Depends On Payment Days"),
+ )
+ message += "
" + _(
+ "Disable {0} for the {1} component, to prevent the amount from being deducted twice, as its formula already uses a payment-days-based component."
+ ).format(
+ frappe.bold("Depends On Payment Days"), frappe.bold(row.salary_component)
+ )
+ frappe.throw(message, title=_("Payment Days Dependency"))
+
+ def get_component_abbreviations(self):
+ abbr = [d.abbr for d in self.earnings if d.depends_on_payment_days]
+ abbr += [d.abbr for d in self.deductions if d.depends_on_payment_days]
+
+ return abbr
+
def strip_condition_and_formula_fields(self):
# remove whitespaces from condition and formula fields
for row in self.earnings:
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index cca3f05270c3..f859791de62c 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -468,6 +468,7 @@ def make_item_gl_entries(self, gl_entries, warehouse_account=None):
and not d.is_fixed_asset
and flt(d.qty)
and provisional_accounting_for_non_stock_items
+ and d.get("provisional_expense_account")
):
self.add_provisional_gl_entry(
d, gl_entries, self.posting_date, d.get("provisional_expense_account")
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 8703aefdda7f..6156a6458b6d 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -5,7 +5,7 @@
import frappe
from frappe.permissions import add_user_permission, remove_user_permission
from frappe.tests.utils import FrappeTestCase, change_settings
-from frappe.utils import flt, nowdate, nowtime
+from frappe.utils import add_days, flt, nowdate, nowtime
from six import iteritems
from erpnext.accounts.doctype.account.test_account import get_inventory_account
@@ -1414,6 +1414,138 @@ def test_stock_entry_item_details(self):
self.assertEqual(se.items[0].item_name, item.item_name)
self.assertEqual(se.items[0].stock_uom, item.stock_uom)
+ def test_reposting_for_depedent_warehouse(self):
+ from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import repost_sl_entries
+ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+
+ # Inward at WH1 warehouse (Component)
+ # 1st Repack (Component (WH1) - Subcomponent (WH2))
+ # 2nd Repack (Subcomponent (WH2) - FG Item (WH3))
+ # Material Transfer of FG Item -> WH 3 -> WH2 -> Wh1 (Two transfer entries)
+ # Backdated transction which should update valuation rate in repack as well trasfer entries
+
+ for item_code in ["FG Item 1", "Sub Component 1", "Component 1"]:
+ create_item(item_code)
+
+ for warehouse in ["WH 1", "WH 2", "WH 3"]:
+ create_warehouse(warehouse)
+
+ make_stock_entry(
+ item_code="Component 1",
+ rate=100,
+ purpose="Material Receipt",
+ qty=10,
+ to_warehouse="WH 1 - _TC",
+ posting_date=add_days(nowdate(), -10),
+ )
+
+ repack1 = make_stock_entry(
+ item_code="Component 1",
+ purpose="Repack",
+ do_not_save=True,
+ qty=10,
+ from_warehouse="WH 1 - _TC",
+ posting_date=add_days(nowdate(), -9),
+ )
+
+ repack1.append(
+ "items",
+ {
+ "item_code": "Sub Component 1",
+ "qty": 10,
+ "t_warehouse": "WH 2 - _TC",
+ "transfer_qty": 10,
+ "uom": "Nos",
+ "stock_uom": "Nos",
+ "conversion_factor": 1.0,
+ },
+ )
+
+ repack1.save()
+ repack1.submit()
+
+ self.assertEqual(repack1.items[1].basic_rate, 100)
+ self.assertEqual(repack1.items[1].amount, 1000)
+
+ repack2 = make_stock_entry(
+ item_code="Sub Component 1",
+ purpose="Repack",
+ do_not_save=True,
+ qty=10,
+ from_warehouse="WH 2 - _TC",
+ posting_date=add_days(nowdate(), -8),
+ )
+
+ repack2.append(
+ "items",
+ {
+ "item_code": "FG Item 1",
+ "qty": 10,
+ "t_warehouse": "WH 3 - _TC",
+ "transfer_qty": 10,
+ "uom": "Nos",
+ "stock_uom": "Nos",
+ "conversion_factor": 1.0,
+ },
+ )
+
+ repack2.save()
+ repack2.submit()
+
+ self.assertEqual(repack2.items[1].basic_rate, 100)
+ self.assertEqual(repack2.items[1].amount, 1000)
+
+ transfer1 = make_stock_entry(
+ item_code="FG Item 1",
+ purpose="Material Transfer",
+ qty=10,
+ from_warehouse="WH 3 - _TC",
+ to_warehouse="WH 2 - _TC",
+ posting_date=add_days(nowdate(), -7),
+ )
+
+ self.assertEqual(transfer1.items[0].basic_rate, 100)
+ self.assertEqual(transfer1.items[0].amount, 1000)
+
+ transfer2 = make_stock_entry(
+ item_code="FG Item 1",
+ purpose="Material Transfer",
+ qty=10,
+ from_warehouse="WH 2 - _TC",
+ to_warehouse="WH 1 - _TC",
+ posting_date=add_days(nowdate(), -6),
+ )
+
+ self.assertEqual(transfer2.items[0].basic_rate, 100)
+ self.assertEqual(transfer2.items[0].amount, 1000)
+
+ # Backdated transaction
+ receipt2 = make_stock_entry(
+ item_code="Component 1",
+ rate=200,
+ purpose="Material Receipt",
+ qty=10,
+ to_warehouse="WH 1 - _TC",
+ posting_date=add_days(nowdate(), -15),
+ )
+
+ self.assertEqual(receipt2.items[0].basic_rate, 200)
+ self.assertEqual(receipt2.items[0].amount, 2000)
+
+ repost_name = frappe.db.get_value(
+ "Repost Item Valuation", {"voucher_no": receipt2.name, "docstatus": 1}, "name"
+ )
+
+ doc = frappe.get_doc("Repost Item Valuation", repost_name)
+ repost_sl_entries(doc)
+
+ for obj in [repack1, repack2, transfer1, transfer2]:
+ obj.load_from_db()
+
+ index = 1 if obj.purpose == "Repack" else 0
+ self.assertEqual(obj.items[index].basic_rate, 200)
+ self.assertEqual(obj.items[index].basic_amount, 2000)
+
def make_serialized_item(**args):
args = frappe._dict(args)
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index da57ba054dc7..068cc12ed91f 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -247,16 +247,11 @@ def repost_future_sle(
data.sle_changed = False
i += 1
- if doc and i % 2 == 0:
+ if doc:
update_args_in_repost_item_valuation(
doc, i, args, distinct_item_warehouses, affected_transactions
)
- if doc and args:
- update_args_in_repost_item_valuation(
- doc, i, args, distinct_item_warehouses, affected_transactions
- )
-
def update_args_in_repost_item_valuation(
doc, index, args, distinct_item_warehouses, affected_transactions
@@ -491,7 +486,8 @@ def get_dependent_entries_to_fix(self, entries_to_fix, sle):
elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
return entries_to_fix
else:
- return self.append_future_sle_for_dependant(dependant_sle, entries_to_fix)
+ self.append_future_sle_for_dependant(dependant_sle, entries_to_fix)
+ return entries_to_fix
def update_distinct_item_warehouses(self, dependant_sle):
key = (dependant_sle.item_code, dependant_sle.warehouse)
@@ -510,14 +506,11 @@ def update_distinct_item_warehouses(self, dependant_sle):
def append_future_sle_for_dependant(self, dependant_sle, entries_to_fix):
self.initialize_previous_data(dependant_sle)
-
- args = self.data[dependant_sle.warehouse].previous_sle or frappe._dict(
- {"item_code": self.item_code, "warehouse": dependant_sle.warehouse}
+ self.distinct_item_warehouses[(self.item_code, dependant_sle.warehouse)] = frappe._dict(
+ {"sle": dependant_sle}
)
- future_sle_for_dependant = list(self.get_sle_after_datetime(args))
- entries_to_fix.extend(future_sle_for_dependant)
- return sorted(entries_to_fix, key=lambda k: k["timestamp"])
+ self.new_items_found = True
def process_sle(self, sle):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos