diff --git a/.releaserc b/.releaserc index 8a758ed30a6..1f5bb53d248 100644 --- a/.releaserc +++ b/.releaserc @@ -10,7 +10,7 @@ "@semantic-release/release-notes-generator", [ "@semantic-release/exec", { - "prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" erpnext/__init__.py' + "prepareCmd": 'sed -ir -E "s/\"[0-9]+\.[0-9]+\.[0-9]+\"/\"${nextRelease.version}\"/" erpnext/__init__.py' } ], [ diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 96975e9d116..59c7c843edf 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -222,9 +222,6 @@ class POSInvoice(SalesInvoice): allow_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock") for d in self.get("items"): - is_service_item = not (frappe.db.get_value("Item", d.get("item_code"), "is_stock_item")) - if is_service_item: - return if d.serial_no: self.validate_pos_reserved_serial_nos(d) self.validate_delivered_serial_nos(d) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index b8f1e314e82..29f3970d0dc 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2173,13 +2173,13 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): target_detail_field = "sales_invoice_item" if doctype == "Sales Invoice" else "sales_order_item" source_document_warehouse_field = "target_warehouse" target_document_warehouse_field = "from_warehouse" + received_items = get_received_items(source_name, target_doctype, target_detail_field) else: source_doc = frappe.get_doc(doctype, source_name) target_doctype = "Sales Invoice" if doctype == "Purchase Invoice" else "Sales Order" source_document_warehouse_field = "from_warehouse" target_document_warehouse_field = "target_warehouse" - - received_items = get_received_items(source_name, target_doctype, target_detail_field) + received_items = {} validate_inter_company_transaction(source_doc, doctype) details = get_inter_company_details(source_doc, doctype) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js index 158ff4d3437..4000d3baf29 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.js +++ b/erpnext/accounts/report/gross_profit/gross_profit.js @@ -44,14 +44,14 @@ frappe.query_reports["Gross Profit"] = { "parent_field": "parent_invoice", "initial_depth": 3, "formatter": function(value, row, column, data, default_formatter) { - if (column.fieldname == "sales_invoice" && column.options == "Item" && data.indent == 0) { + if (column.fieldname == "sales_invoice" && column.options == "Item" && data && data.indent == 0) { column._options = "Sales Invoice"; } else { column._options = "Item"; } value = default_formatter(value, row, column, data); - if (data && (data.indent == 0.0 || row[1].content == "Total")) { + if (data && (data.indent == 0.0 || (row[1] && row[1].content == "Total"))) { value = $(`${value}`); var $value = $(value).css("font-weight", "bold"); value = $value.wrap("
").parent().html(); diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py index 08d20086823..db3d5d44a0c 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py @@ -14,9 +14,9 @@ def execute(filters=None): filters.naming_series = frappe.db.get_single_value("Buying Settings", "supp_master_name") columns = get_columns(filters) - tds_docs, tds_accounts, tax_category_map = get_tds_docs(filters) + tds_docs, tds_accounts, tax_category_map, journal_entry_party_map = get_tds_docs(filters) - res = get_result(filters, tds_docs, tds_accounts, tax_category_map) + res = get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map) final_result = group_by_supplier_and_category(res) return columns, final_result 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 16e0ac1de60..f2809a99c55 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -26,7 +26,6 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_ supplier_map = get_supplier_pan_map() tax_rate_map = get_tax_rate_map(filters) gle_map = get_gle_map(tds_docs) - print(journal_entry_party_map) out = [] for name, details in gle_map.items(): 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 bc32b1d1971..24555961666 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -113,7 +113,7 @@ class RequestforQuotation(BuyingController): def get_link(self): # RFQ link for supplier portal - return get_url("/rfq/" + self.name) + return get_url("/app/request-for-quotation/" + self.name) def update_supplier_part_no(self, supplier): self.vendor = supplier diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 64e70b1aef5..9c31ebfbb33 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -301,7 +301,8 @@ class BuyingController(StockController, Subcontracting): rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate")) else: - rate = frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), "rate") + field = "incoming_rate" if self.get("is_internal_supplier") else "rate" + rate = frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field) if self.is_internal_transfer(): if rate != d.rate: diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 6519028c934..53a17422004 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -18,8 +18,9 @@ from erpnext.stock.get_item_details import _get_item_tax_template @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def employee_query(doctype, txt, searchfield, start, page_len, filters): + doctype = "Employee" conditions = [] - fields = get_fields("Employee", ["name", "employee_name"]) + fields = get_fields(doctype, ["name", "employee_name"]) return frappe.db.sql( """select {fields} from `tabEmployee` @@ -49,7 +50,8 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def lead_query(doctype, txt, searchfield, start, page_len, filters): - fields = get_fields("Lead", ["name", "lead_name", "company_name"]) + doctype = "Lead" + fields = get_fields(doctype, ["name", "lead_name", "company_name"]) return frappe.db.sql( """select {fields} from `tabLead` @@ -77,6 +79,7 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def customer_query(doctype, txt, searchfield, start, page_len, filters): + doctype = "Customer" conditions = [] cust_master_name = frappe.defaults.get_user_default("cust_master_name") @@ -85,9 +88,9 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): else: fields = ["name", "customer_name", "customer_group", "territory"] - fields = get_fields("Customer", fields) + fields = get_fields(doctype, fields) - searchfields = frappe.get_meta("Customer").get_search_fields() + searchfields = frappe.get_meta(doctype).get_search_fields() searchfields = " or ".join(field + " like %(txt)s" for field in searchfields) return frappe.db.sql( @@ -116,6 +119,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def supplier_query(doctype, txt, searchfield, start, page_len, filters): + doctype = "Supplier" supp_master_name = frappe.defaults.get_user_default("supp_master_name") if supp_master_name == "Supplier Name": @@ -123,7 +127,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters): else: fields = ["name", "supplier_name", "supplier_group"] - fields = get_fields("Supplier", fields) + fields = get_fields(doctype, fields) return frappe.db.sql( """select {field} from `tabSupplier` @@ -147,6 +151,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def tax_account_query(doctype, txt, searchfield, start, page_len, filters): + doctype = "Account" company_currency = erpnext.get_company_currency(filters.get("company")) def get_accounts(with_account_type_filter): @@ -197,13 +202,14 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): + doctype = "Item" conditions = [] if isinstance(filters, str): filters = json.loads(filters) # Get searchfields from meta and use in Item Link field query - meta = frappe.get_meta("Item", cached=True) + meta = frappe.get_meta(doctype, cached=True) searchfields = meta.get_search_fields() # these are handled separately @@ -257,7 +263,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals filters.pop("supplier", None) description_cond = "" - if frappe.db.count("Item", cache=True) < 50000: + if frappe.db.count(doctype, cache=True) < 50000: # scan description only if items are less than 50000 description_cond = "or tabItem.description LIKE %(txt)s" return frappe.db.sql( @@ -300,8 +306,9 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def bom(doctype, txt, searchfield, start, page_len, filters): + doctype = "BOM" conditions = [] - fields = get_fields("BOM", ["name", "item"]) + fields = get_fields(doctype, ["name", "item"]) return frappe.db.sql( """select {fields} @@ -331,6 +338,7 @@ def bom(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_project_name(doctype, txt, searchfield, start, page_len, filters): + doctype = "Project" cond = "" if filters and filters.get("customer"): cond = """(`tabProject`.customer = %s or @@ -338,9 +346,9 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): frappe.db.escape(filters.get("customer")) ) - fields = get_fields("Project", ["name", "project_name"]) - searchfields = frappe.get_meta("Project").get_search_fields() - searchfields = " or ".join([field + " like %(txt)s" for field in searchfields]) + fields = get_fields(doctype, ["name", "project_name"]) + searchfields = frappe.get_meta(doctype).get_search_fields() + searchfields = " or ".join(["`tabProject`." + field + " like %(txt)s" for field in searchfields]) return frappe.db.sql( """select {fields} from `tabProject` @@ -366,7 +374,8 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, filters, as_dict): - fields = get_fields("Delivery Note", ["name", "customer", "posting_date"]) + doctype = "Delivery Note" + fields = get_fields(doctype, ["name", "customer", "posting_date"]) return frappe.db.sql( """ @@ -402,6 +411,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_batch_no(doctype, txt, searchfield, start, page_len, filters): + doctype = "Batch" cond = "" if filters.get("posting_date"): cond = "and (batch.expiry_date is null or batch.expiry_date >= %(posting_date)s)" @@ -420,7 +430,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): if filters.get("is_return"): having_clause = "" - meta = frappe.get_meta("Batch", cached=True) + meta = frappe.get_meta(doctype, cached=True) searchfields = meta.get_search_fields() search_columns = "" @@ -496,6 +506,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_account_list(doctype, txt, searchfield, start, page_len, filters): + doctype = "Account" filter_list = [] if isinstance(filters, dict): @@ -514,7 +525,7 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters): filter_list.append([doctype, searchfield, "like", "%%%s%%" % txt]) return frappe.desk.reportview.execute( - "Account", + doctype, filters=filter_list, fields=["name", "parent_account"], limit_start=start, @@ -553,6 +564,7 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters): if not filters: filters = {} + doctype = "Account" condition = "" if filters.get("company"): condition += "and tabAccount.company = %(company)s" @@ -628,6 +640,7 @@ def get_expense_account(doctype, txt, searchfield, start, page_len, filters): if not filters: filters = {} + doctype = "Account" condition = "" if filters.get("company"): condition += "and tabAccount.company = %(company)s" @@ -650,6 +663,7 @@ def get_expense_account(doctype, txt, searchfield, start, page_len, filters): @frappe.validate_and_sanitize_search_inputs def warehouse_query(doctype, txt, searchfield, start, page_len, filters): # Should be used when item code is passed in filters. + doctype = "Warehouse" conditions, bin_conditions = [], [] filter_dict = get_doctype_wise_filters(filters) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index cf1714a25e1..0556d40a4b8 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -589,6 +589,7 @@ accounting_dimension_doctypes = [ "Shipping Rule", "Landed Cost Item", "Asset Value Adjustment", + "Asset Repair", "Loyalty Program", "Fee Schedule", "Fee Structure", diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index 8aaae5956b0..9c55af067f0 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -10,7 +10,7 @@ from frappe.permissions import ( remove_user_permission, set_user_permission_if_allowed, ) -from frappe.utils import add_years, cstr, getdate, today, validate_email_address +from frappe.utils import add_days, add_years, cstr, getdate, today, validate_email_address from frappe.utils.nestedset import NestedSet from erpnext.utilities.transaction_base import delete_events @@ -64,6 +64,8 @@ class Employee(NestedSet): if existing_user_id: remove_user_permission("Employee", self.name, existing_user_id) + self.update_to_date_in_work_history() + def after_rename(self, old, new, merge): self.db_set("employee", new) @@ -166,6 +168,18 @@ class Employee(NestedSet): user.flags.ignore_permissions = True user.add_roles("Expense Approver") + def update_to_date_in_work_history(self): + if not self.internal_work_history: + return + + for idx, row in enumerate(self.internal_work_history): + if not row.from_date or idx == 0: + continue + + self.internal_work_history[idx - 1].to_date = add_days(row.from_date, -1) + + self.internal_work_history[-1].to_date = None + def validate_date(self): if self.date_of_birth and getdate(self.date_of_birth) > getdate(today()): throw(_("Date of Birth cannot be greater than today.")) diff --git a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py index 37a190a1627..c6bd7d23a76 100644 --- a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py +++ b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py @@ -80,12 +80,14 @@ class TestEmployeeTransfer(unittest.TestCase): department = ["Accounts - TC", "Management - TC"] designation = ["Accountant", "Manager"] dt = [getdate("01-10-2021"), getdate()] + to_date = [add_days(dt[1], -1), None] employee = frappe.get_doc("Employee", employee) for data in employee.internal_work_history: self.assertEqual(data.department, department[count]) self.assertEqual(data.designation, designation[count]) self.assertEqual(data.from_date, dt[count]) + self.assertEqual(data.to_date, to_date[count]) count = count + 1 transfer.cancel() @@ -95,6 +97,7 @@ class TestEmployeeTransfer(unittest.TestCase): self.assertEqual(data.designation, designation[0]) self.assertEqual(data.department, department[0]) self.assertEqual(data.from_date, dt[0]) + self.assertEqual(data.to_date, None) def create_company(): diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index af80b63845e..f44611368ad 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -254,9 +254,11 @@ frappe.ui.form.on("Expense Claim", { }, __("View")); } - if (frm.doc.docstatus===1 && !cint(frm.doc.is_paid) && cint(frm.doc.grand_total) > 0 - && (cint(frm.doc.total_amount_reimbursed) < cint(frm.doc.total_sanctioned_amount)) - && frappe.model.can_create("Payment Entry")) { + if ( + frm.doc.docstatus === 1 + && frm.doc.status !== "Paid" + && frappe.model.can_create("Payment Entry") + ) { frm.add_custom_button(__('Payment'), function() { frm.events.make_payment_entry(frm); }, __('Create')); } diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index 0645bd1f937..43bbede6804 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -305,8 +305,9 @@ class ExpenseClaim(AccountsController): if self.total_advance_amount: precision = self.precision("total_advance_amount") - amount_with_taxes = flt(self.total_sanctioned_amount, precision) + flt( - self.total_taxes_and_charges, precision + amount_with_taxes = flt( + (flt(self.total_sanctioned_amount, precision) + flt(self.total_taxes_and_charges, precision)), + precision, ) if flt(self.total_advance_amount, precision) > amount_with_taxes: diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 9707b81089b..b80226b224c 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -224,6 +224,7 @@ def delete_employee_work_history(details, employee, date): filters["from_date"] = date if filters: frappe.db.delete("Employee Internal Work History", filters) + employee.reload() @frappe.whitelist() diff --git a/erpnext/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.py b/erpnext/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.py index 0a576d69692..514a5fcfafb 100644 --- a/erpnext/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.py +++ b/erpnext/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.py @@ -99,7 +99,7 @@ class LoanBalanceAdjustment(AccountsController): loan_account = frappe.db.get_value("Loan", self.loan, "loan_account") remarks = "{} against loan {}".format(self.adjustment_type.capitalize(), self.loan) if self.reference_number: - remarks += "with reference no. {}".format(self.reference_number) + remarks += " with reference no. {}".format(self.reference_number) loan_entry = { "account": loan_account, diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json index 50926d77268..c7b5c033756 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json @@ -163,11 +163,11 @@ }, { "fetch_from": "against_loan.disbursement_account", + "fetch_if_empty": 1, "fieldname": "disbursement_account", "fieldtype": "Link", "label": "Disbursement Account", - "options": "Account", - "read_only": 1 + "options": "Account" }, { "fieldname": "column_break_16", @@ -185,7 +185,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-02-17 18:23:44.157598", + "modified": "2022-08-04 17:16:04.922444", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Disbursement", diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index 10174e531a1..8a493a5fc77 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -209,6 +209,9 @@ def get_disbursal_amount(loan, on_current_security_price=0): "loan_amount", "disbursed_amount", "total_payment", + "debit_adjustment_amount", + "credit_adjustment_amount", + "refund_amount", "total_principal_paid", "total_interest_payable", "status", 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 3a4c6513e45..ef53303f5a1 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 @@ -147,6 +147,9 @@ def make_accrual_interest_entry_for_demand_loans( "name", "total_payment", "total_amount_paid", + "debit_adjustment_amount", + "credit_adjustment_amount", + "refund_amount", "loan_account", "interest_income_account", "loan_amount", diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json index 480e010b49a..71f93e26bdb 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json @@ -281,11 +281,11 @@ }, { "fetch_from": "against_loan.payment_account", + "fetch_if_empty": 1, "fieldname": "payment_account", "fieldtype": "Link", "label": "Repayment Account", - "options": "Account", - "read_only": 1 + "options": "Account" }, { "fieldname": "column_break_36", @@ -311,7 +311,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-02-18 19:10:07.742298", + "modified": "2022-08-04 17:13:51.964203", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment", @@ -353,4 +353,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 18803504eb1..2f9f4571f28 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -150,6 +150,9 @@ class LoanRepayment(AccountsController): "status", "is_secured_loan", "total_payment", + "debit_adjustment_amount", + "credit_adjustment_amount", + "refund_amount", "loan_amount", "disbursed_amount", "total_interest_payable", @@ -399,7 +402,7 @@ class LoanRepayment(AccountsController): remarks = "Repayment against loan " + self.against_loan if self.reference_number: - remarks += "with reference no. {}".format(self.reference_number) + remarks += " with reference no. {}".format(self.reference_number) if self.repay_from_salary: payment_account = self.payroll_payable_account diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index 731b65e9a29..3f773011b29 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -58,6 +58,9 @@ class LoanSecurityUnpledge(Document): self.loan, [ "total_payment", + "debit_adjustment_amount", + "credit_adjustment_amount", + "refund_amount", "total_principal_paid", "loan_amount", "total_interest_payable", diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py index e19fd15fc84..2eeb97c8f89 100644 --- a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py +++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py @@ -9,6 +9,9 @@ from frappe.utils import cint, flt, getdate import erpnext from erpnext.accounts.general_ledger import make_gl_entries from erpnext.controllers.accounts_controller import AccountsController +from erpnext.loan_management.doctype.loan_repayment.loan_repayment import ( + get_pending_principal_amount, +) class LoanWriteOff(AccountsController): @@ -22,16 +25,26 @@ class LoanWriteOff(AccountsController): def validate_write_off_amount(self): precision = cint(frappe.db.get_default("currency_precision")) or 2 - total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value( + + loan_details = frappe.get_value( "Loan", self.loan, - ["total_payment", "total_principal_paid", "total_interest_payable", "written_off_amount"], + [ + "total_payment", + "debit_adjustment_amount", + "credit_adjustment_amount", + "refund_amount", + "total_principal_paid", + "loan_amount", + "total_interest_payable", + "written_off_amount", + "disbursed_amount", + "status", + ], + as_dict=1, ) - pending_principal_amount = flt( - flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount), - precision, - ) + pending_principal_amount = flt(get_pending_principal_amount(loan_details), precision) if self.write_off_amount > pending_principal_amount: frappe.throw(_("Write off amount cannot be greater than pending principal amount")) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9077ed662ff..cbc224f99e4 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -372,3 +372,4 @@ erpnext.patches.v13_0.show_india_localisation_deprecation_warning erpnext.patches.v13_0.fix_number_and_frequency_for_monthly_depreciation erpnext.patches.v13_0.reset_corrupt_defaults erpnext.patches.v13_0.show_hr_payroll_deprecation_warning +erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair diff --git a/erpnext/patches/v13_0/create_accounting_dimensions_for_asset_repair.py b/erpnext/patches/v13_0/create_accounting_dimensions_for_asset_repair.py new file mode 100644 index 00000000000..61a5c86386c --- /dev/null +++ b/erpnext/patches/v13_0/create_accounting_dimensions_for_asset_repair.py @@ -0,0 +1,29 @@ +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_field + + +def execute(): + accounting_dimensions = frappe.db.get_all( + "Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"] + ) + + if not accounting_dimensions: + return + + for d in accounting_dimensions: + doctype = "Asset Repair" + field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname}) + + if field: + continue + + df = { + "fieldname": d.fieldname, + "label": d.label, + "fieldtype": "Link", + "options": d.document_type, + "insert_after": "accounting_dimensions_section", + } + + create_custom_field(doctype, df, ignore_validate=True) + frappe.clear_cache(doctype=doctype) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 4db0d2c366a..a5c892687c5 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -622,7 +622,7 @@ class SalarySlip(TransactionBase): self.add_tax_components(payroll_period) def add_structure_components(self, component_type): - data = self.get_data_for_eval() + data, default_data = self.get_data_for_eval() timesheet_component = frappe.db.get_value( "Salary Structure", self.salary_structure, "salary_component" ) @@ -633,11 +633,12 @@ class SalarySlip(TransactionBase): amount = self.eval_condition_and_formula(struct_row, data) if ( - amount - or (struct_row.amount_based_on_formula and amount is not None) - and struct_row.statistical_component == 0 - ): - self.update_component_row(struct_row, amount, component_type, data=data) + amount or (struct_row.amount_based_on_formula and amount is not None) + ) and struct_row.statistical_component == 0: + default_amount = self.eval_condition_and_formula(struct_row, default_data) + self.update_component_row( + struct_row, amount, component_type, data=data, default_amount=default_amount + ) def get_data_for_eval(self): """Returns data for evaluating formula""" @@ -681,11 +682,15 @@ class SalarySlip(TransactionBase): for sc in salary_components: data.setdefault(sc.salary_component_abbr, 0) + # shallow copy of data to store default amounts (without payment days) for tax calculation + default_data = data.copy() + for key in ("earnings", "deductions"): for d in self.get(key): + default_data[d.abbr] = d.default_amount data[d.abbr] = d.amount - return data + return data, default_data def eval_condition_and_formula(self, d, data): try: @@ -791,7 +796,14 @@ class SalarySlip(TransactionBase): 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, data=None + self, + component_data, + amount, + component_type, + additional_salary=None, + is_recurring=0, + data=None, + default_amount=None, ): component_row = None for d in self.get(component_type): @@ -852,7 +864,7 @@ class SalarySlip(TransactionBase): additional_salary.deduct_full_tax_on_selected_payroll_date ) else: - component_row.default_amount = amount + component_row.default_amount = default_amount or amount component_row.additional_amount = 0 component_row.deduct_full_tax_on_selected_payroll_date = ( component_data.deduct_full_tax_on_selected_payroll_date @@ -1285,7 +1297,7 @@ class SalarySlip(TransactionBase): )[0].total_amount def calculate_tax_by_tax_slab(self, annual_taxable_earning, tax_slab): - data = self.get_data_for_eval() + data, default_data = self.get_data_for_eval() data.update({"annual_taxable_earning": annual_taxable_earning}) tax_amount = 0 for slab in tax_slab.slabs: diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 425a03b3ba6..987c1ac281e 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -7,7 +7,7 @@ import unittest import frappe from frappe.model.document import Document -from frappe.tests.utils import change_settings +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import ( add_days, add_months, @@ -35,13 +35,12 @@ from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_detail from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip -class TestSalarySlip(unittest.TestCase): +class TestSalarySlip(FrappeTestCase): def setUp(self): setup_test() frappe.flags.pop("via_payroll_entry", None) def tearDown(self): - frappe.db.rollback() frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0) frappe.set_user("Administrator") @@ -372,13 +371,19 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(salary_slip.payment_days, days_in_month - no_of_holidays - 1) - # gross pay calculation based on attendance (payment days) - gross_pay = 78100 - ( - (78000 / (days_in_month - no_of_holidays)) - * flt(salary_slip.leave_without_pay + salary_slip.absent_days) + # component calculation based on attendance (payment days) + amount, precision = None, None + + for row in salary_slip.earnings: + if row.salary_component == "Basic Salary": + amount = row.amount + precision = row.precision("amount") + break + expected_amount = flt( + (50000 * salary_slip.payment_days / salary_slip.total_working_days), precision ) - self.assertEqual(salary_slip.gross_pay, flt(gross_pay, 2)) + self.assertEqual(amount, expected_amount) @change_settings("Payroll Settings", {"payroll_based_on": "Attendance"}) def test_component_amount_dependent_on_another_payment_days_based_component(self): @@ -919,6 +924,41 @@ class TestSalarySlip(unittest.TestCase): # undelete fixture data frappe.db.rollback() + @change_settings( + "Payroll Settings", + { + "payroll_based_on": "Attendance", + "consider_unmarked_attendance_as": "Present", + "include_holidays_in_total_working_days": True, + }, + ) + def test_default_amount(self): + # Special Allowance (SA) uses another component Basic (BS) in it's formula : BD * .5 + # Basic has "Depends on Payment Days" enabled + # Test default amount for SA is based on default amount for BS (irrespective of PD) + # Test amount for SA is based on amount for BS (based on PD) + from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure + + month_start_date = get_first_day(nowdate()) + joining_date = add_days(month_start_date, 3) + employee = make_employee("test_tax_for_mid_joinee@salary.com", date_of_joining=joining_date) + + salary_structure = make_salary_structure( + "Stucture to test tax", + "Monthly", + test_tax=True, + from_date=joining_date, + employee=employee, + ) + + ss = make_salary_slip(salary_structure.name, employee=employee) + + # default amount for SA (special allowance = BS*0.5) should be based on default amount for basic + self.assertEqual(ss.earnings[2].default_amount, 25000) + self.assertEqual( + ss.earnings[2].amount, flt(ss.earnings[0].amount * 0.5, ss.earnings[0].precision("amount")) + ) + def test_tax_for_recurring_additional_salary(self): frappe.db.sql("""delete from `tabPayroll Period`""") frappe.db.sql("""delete from `tabSalary Component`""") @@ -981,6 +1021,16 @@ class TestSalarySlip(unittest.TestCase): frappe.db.rollback() + def test_do_not_show_statistical_component_in_slip(self): + make_employee("test_statistical_component@salary.com") + new_ss = make_employee_salary_slip( + "test_statistical_component@salary.com", + "Monthly", + "Test Payment Based On Attendence", + ) + components = [row.salary_component for row in new_ss.get("earnings")] + self.assertNotIn("Statistical Component", components) + def make_activity_for_employee(self): activity_type = frappe.get_doc("Activity Type", "_Test Activity Type") activity_type.billing_rate = 50 @@ -1038,7 +1088,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None, po def make_salary_component(salary_components, test_tax, company_list=None): for salary_component in salary_components: if frappe.db.exists("Salary Component", salary_component["salary_component"]): - continue + frappe.delete_doc("Salary Component", salary_component["salary_component"], force=True) if test_tax: if salary_component["type"] == "Earning": @@ -1122,6 +1172,13 @@ def make_earning_salary_component( "depends_on_payment_days": 0, }, {"salary_component": "Leave Encashment", "abbr": "LE", "type": "Earning"}, + { + "salary_component": "Statistical Component", + "abbr": "SC", + "type": "Earning", + "statistical_component": 1, + "amount": 500, + }, ] if include_flexi_benefits: data.extend( @@ -1419,6 +1476,10 @@ def setup_test(): "Salary Slip", "Attendance", "Additional Salary", + "Employee Tax Exemption Declaration", + "Employee Tax Exemption Proof Submission", + "Employee Benefit Claim", + "Salary Structure Assignment", ]: frappe.db.sql("delete from `tab%s`" % dt) diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index acc416fca3f..c598904bbe4 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -150,6 +150,7 @@ def make_salary_structure( currency=erpnext.get_default_currency(), payroll_period=None, include_flexi_benefits=False, + base=None, ): if test_tax: frappe.db.sql("""delete from `tabSalary Structure` where name=%s""", (salary_structure)) @@ -200,6 +201,7 @@ def make_salary_structure( company=company, currency=currency, payroll_period=payroll_period, + base=base, ) return salary_structure_doc diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py index f171b6e9cbe..68815bf1edf 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -23,24 +23,31 @@ def _execute(filters=None): if not filters: filters = {} columns = get_columns() + output_gst_accounts = get_output_gst_accounts(filters.company) company_currency = erpnext.get_company_currency(filters.company) item_list = get_items(filters) if item_list: - itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency) + itemised_tax, tax_columns = get_tax_accounts( + item_list, columns, company_currency, output_gst_accounts + ) data = [] added_item = [] for d in item_list: if (d.parent, d.item_code) not in added_item: - row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty, d.tax_rate or 0] + row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty] total_tax = 0 + tax_rate = 0 for tax in tax_columns: item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {}) + if item_tax.get("is_gst_tax"): + tax_rate += flt(item_tax.get("tax_rate", 0)) total_tax += flt(item_tax.get("tax_amount", 0)) - row += [d.base_net_amount + total_tax] - row += [d.base_net_amount] + row += [tax_rate] + row += [d.taxable_value + total_tax] + row += [d.taxable_value] for tax in tax_columns: item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {}) row += [item_tax.get("tax_amount", 0)] @@ -51,6 +58,40 @@ def _execute(filters=None): return columns, data +def get_output_gst_accounts(company): + + accounts = frappe.qb.DocType("Account") + gst_accounts = frappe.qb.DocType("GST Account") + + accounts_query = ( + frappe.qb.from_(accounts) + .select(accounts.name) + .where((accounts.account_type == "Tax") & (accounts.root_type == "Liability")) + ) + + gst_accounts_query = ( + frappe.qb.from_(gst_accounts) + .select( + gst_accounts.cgst_account, + gst_accounts.sgst_account, + gst_accounts.igst_account, + gst_accounts.utgst_account, + gst_accounts.cess_account, + ) + .where((gst_accounts.is_reverse_charge_account == 0) & (gst_accounts.company == company)) + ) + + gst_accounts_list = [ + account for sublist in gst_accounts_query.run() for account in sublist if account + ] + + tax_accounts_list = [account[0] for account in accounts_query.run() if account] + + output_tax_list = [account for account in gst_accounts_list if account in tax_accounts_list] + + return output_tax_list + + def get_columns(): columns = [ { @@ -99,44 +140,24 @@ def get_items(filters): match_conditions = " and {0} ".format(match_conditions) items = frappe.db.sql( - """ + f""" SELECT `tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.stock_uom, - sum( - `tabSales Invoice Item`.stock_qty - ) as stock_qty, - sum( - `tabSales Invoice Item`.base_net_amount - ) as base_net_amount, - sum( - `tabSales Invoice Item`.base_price_list_rate - ) as base_price_list_rate, - + sum(`tabSales Invoice Item`.stock_qty) AS stock_qty, + sum(`tabSales Invoice Item`.taxable_value) AS taxable_value, + sum(`tabSales Invoice Item`.base_price_list_rate) AS base_price_list_rate, `tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code, - `tabGST HSN Code`.description, - json_extract( - `tabSales Taxes and Charges`.item_wise_tax_detail, - concat( - '$."', `tabSales Invoice Item`.item_code, - '"[0]' - ) - ) * count( - distinct `tabSales Taxes and Charges`.name - ) as tax_rate + `tabGST HSN Code`.description FROM `tabSales Invoice` - INNER JOIN - `tabSales Invoice Item` ON `tabSales Invoice`.name = `tabSales Invoice Item`.parent - INNER JOIN - `tabGST HSN Code` ON `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name % s % s - LEFT JOIN - `tabSales Taxes and Charges` ON `tabSales Taxes and Charges`.parent = `tabSales Invoice`.name + INNER JOIN `tabSales Invoice Item` ON `tabSales Invoice`.name = `tabSales Invoice Item`.parent + INNER JOIN `tabGST HSN Code` ON `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name WHERE `tabSales Invoice`.docstatus = 1 - AND - `tabSales Invoice Item`.gst_hsn_code is not NULL + AND `tabSales Invoice Item`.gst_hsn_code IS NOT NULL + {conditions} GROUP BY `tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code, @@ -145,8 +166,9 @@ def get_items(filters): ORDER BY `tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.uom - """ - % (conditions, match_conditions), + """.format( + conditions=conditions + ), filters, as_dict=1, ) @@ -158,6 +180,7 @@ def get_tax_accounts( item_list, columns, company_currency, + output_gst_accounts, doctype="Sales Invoice", tax_doctype="Sales Taxes and Charges", ): @@ -197,7 +220,7 @@ def get_tax_accounts( for parent, account_head, item_wise_tax_detail, tax_amount in tax_details: - if account_head not in tax_columns and tax_amount: + if account_head in output_gst_accounts and account_head not in tax_columns and tax_amount: # as description is text editor earlier and markup can break the column convention in reports tax_columns.append(account_head) @@ -210,29 +233,40 @@ def get_tax_accounts( continue itemised_tax.setdefault(item_code, frappe._dict()) if isinstance(tax_data, list): + tax_rate = 0 + is_gst_tax = 0 + if account_head in output_gst_accounts: + is_gst_tax = 1 + tax_rate = tax_data[0] tax_amount = tax_data[1] else: + tax_rate = 0 tax_amount = 0 for d in item_row_map.get(parent, {}).get(item_code, []): item_tax_amount = tax_amount if item_tax_amount: itemised_tax.setdefault((parent, item_code), {})[account_head] = frappe._dict( - {"tax_amount": flt(item_tax_amount, tax_amount_precision)} + { + "tax_rate": flt(tax_rate, 2), + "is_gst_tax": is_gst_tax, + "tax_amount": flt(item_tax_amount, tax_amount_precision), + } ) except ValueError: continue tax_columns.sort() for account_head in tax_columns: - columns.append( - { - "label": account_head, - "fieldname": frappe.scrub(account_head), - "fieldtype": "Float", - "width": 110, - } - ) + if account_head in output_gst_accounts: + columns.append( + { + "label": account_head, + "fieldname": frappe.scrub(account_head), + "fieldtype": "Float", + "width": 110, + } + ) return itemised_tax, tax_columns diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index b4ece46e6e1..539a0278e2f 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -161,13 +161,14 @@ erpnext.PointOfSale.Payment = class { frappe.ui.form.on('POS Invoice', 'contact_mobile', (frm) => { const contact = frm.doc.contact_mobile; + if (!this.request_for_payment_field) return; const request_button = $(this.request_for_payment_field.$input[0]); if (contact) { request_button.removeClass('btn-default').addClass('btn-primary'); } else { request_button.removeClass('btn-primary').addClass('btn-default'); - } - }); + } + }); frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => { if (frm.doc.coupon_code && !frm.applying_pos_coupon_code) { diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 769b2d88085..a4e23267cd4 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -143,10 +143,6 @@ def get_item_for_list_in_html(context): if (context.get("website_image") or "").startswith("files/"): context["website_image"] = "/" + quote(context["website_image"]) - context["show_availability_status"] = cint( - frappe.db.get_single_value("E Commerce Settings", "show_availability_status") - ) - products_template = "templates/includes/products_as_list.html" return frappe.get_template(products_template).render(context) diff --git a/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py b/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py index dbf6cf05e79..d488150eef3 100644 --- a/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py +++ b/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py @@ -6,8 +6,6 @@ import frappe from frappe import _ from frappe.utils.dashboard import cache_source -from erpnext.stock.utils import get_stock_value_from_bin - @frappe.whitelist() @cache_source @@ -30,26 +28,24 @@ def get( warehouse_filters.append(["company", "=", filters.get("company")]) warehouses = frappe.get_list( - "Warehouse", fields=["name"], filters=warehouse_filters, order_by="name" + "Warehouse", pluck="name", filters=warehouse_filters, order_by="name" ) - for wh in warehouses: - balance = get_stock_value_from_bin(warehouse=wh.name) - wh["balance"] = balance[0][0] - - warehouses = [x for x in warehouses if not (x.get("balance") == None)] + warehouses = frappe.get_list( + "Bin", + fields=["warehouse", "sum(stock_value) stock_value"], + filters={"warehouse": ["IN", warehouses], "stock_value": [">", 0]}, + group_by="warehouse", + order_by="stock_value DESC", + limit_page_length=10, + ) if not warehouses: return [] - sorted_warehouse_map = sorted(warehouses, key=lambda i: i["balance"], reverse=True) - - if len(sorted_warehouse_map) > 10: - sorted_warehouse_map = sorted_warehouse_map[:10] - - for warehouse in sorted_warehouse_map: - labels.append(_(warehouse.get("name"))) - datapoints.append(warehouse.get("balance")) + for warehouse in warehouses: + labels.append(_(warehouse.get("warehouse"))) + datapoints.append(warehouse.get("stock_value")) return { "labels": labels, diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 4e64b7b1833..270acb869c5 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -635,6 +635,24 @@ class update_entries_after(object): voucher_detail_no=sle.voucher_detail_no, sle=sle, ) + + elif ( + sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] + and sle.actual_qty > 0 + and frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_internal_supplier") + ): + sle_details = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": sle.voucher_type, + "voucher_no": sle.voucher_no, + "dependant_sle_voucher_detail_no": sle.voucher_detail_no, + }, + ["stock_value_difference", "actual_qty"], + as_dict=1, + ) + + rate = abs(sle_details.stock_value_difference / sle.actual_qty) else: if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"): rate_field = "valuation_rate"