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