diff --git a/erpnext/accounts/doctype/account/account.js b/erpnext/accounts/doctype/account/account.js index 320e1cab7c3..7d63b257faf 100644 --- a/erpnext/accounts/doctype/account/account.js +++ b/erpnext/accounts/doctype/account/account.js @@ -117,9 +117,6 @@ frappe.ui.form.on('Account', { args: { old: frm.doc.name, new: data.name, - is_group: frm.doc.is_group, - root_type: frm.doc.root_type, - company: frm.doc.company }, callback: function(r) { if(!r.exc) { diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index bbe4c54a71a..22ddc2ffae3 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -18,6 +18,10 @@ class BalanceMismatchError(frappe.ValidationError): pass +class InvalidAccountMergeError(frappe.ValidationError): + pass + + class Account(NestedSet): nsm_parent_field = "parent_account" @@ -444,24 +448,35 @@ def update_account_number(name, account_name, account_number=None, from_descenda @frappe.whitelist() -def merge_account(old, new, is_group, root_type, company): +def merge_account(old, new): # Validate properties before merging - if not frappe.db.exists("Account", new): + new_account = frappe.get_cached_doc("Account", new) + old_account = frappe.get_cached_doc("Account", old) + + if not new_account: throw(_("Account {0} does not exist").format(new)) - val = list(frappe.db.get_value("Account", new, ["is_group", "root_type", "company"])) - - if val != [cint(is_group), root_type, company]: + if ( + cint(new_account.is_group), + new_account.root_type, + new_account.company, + cstr(new_account.account_currency), + ) != ( + cint(old_account.is_group), + old_account.root_type, + old_account.company, + cstr(old_account.account_currency), + ): throw( - _( - """Merging is only possible if following properties are same in both records. Is Group, Root Type, Company""" - ) + msg=_( + """Merging is only possible if following properties are same in both records. Is Group, Root Type, Company and Account Currency""" + ), + title=("Invalid Accounts"), + exc=InvalidAccountMergeError, ) - if is_group and frappe.db.get_value("Account", new, "parent_account") == old: - frappe.db.set_value( - "Account", new, "parent_account", frappe.db.get_value("Account", old, "parent_account") - ) + if old_account.is_group and new_account.parent_account == old: + new_account.db_set("parent_account", frappe.get_cached_value("Account", old, "parent_account")) frappe.rename_doc("Account", old, new, merge=1, force=1) diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js index 8ae90ceb383..d537adfcbfd 100644 --- a/erpnext/accounts/doctype/account/account_tree.js +++ b/erpnext/accounts/doctype/account/account_tree.js @@ -56,36 +56,41 @@ frappe.treeview_settings["Account"] = { accounts = nodes; } - const get_balances = frappe.call({ - method: 'erpnext.accounts.utils.get_account_balances', - args: { - accounts: accounts, - company: cur_tree.args.company - }, - }); + frappe.db.get_single_value("Accounts Settings", "show_balance_in_coa").then((value) => { + if(value) { - get_balances.then(r => { - if (!r.message || r.message.length == 0) return; + const get_balances = frappe.call({ + method: 'erpnext.accounts.utils.get_account_balances', + args: { + accounts: accounts, + company: cur_tree.args.company + }, + }); - for (let account of r.message) { + get_balances.then(r => { + if (!r.message || r.message.length == 0) return; - const node = cur_tree.nodes && cur_tree.nodes[account.value]; - if (!node || node.is_root) continue; + for (let account of r.message) { - // show Dr if positive since balance is calculated as debit - credit else show Cr - const balance = account.balance_in_account_currency || account.balance; - const dr_or_cr = balance > 0 ? "Dr": "Cr"; - const format = (value, currency) => format_currency(Math.abs(value), currency); + const node = cur_tree.nodes && cur_tree.nodes[account.value]; + if (!node || node.is_root) continue; - if (account.balance!==undefined) { - node.parent && node.parent.find('.balance-area').remove(); - $('' - + (account.balance_in_account_currency ? - (format(account.balance_in_account_currency, account.account_currency) + " / ") : "") - + format(account.balance, account.company_currency) - + " " + dr_or_cr - + '').insertBefore(node.$ul); - } + // show Dr if positive since balance is calculated as debit - credit else show Cr + const balance = account.balance_in_account_currency || account.balance; + const dr_or_cr = balance > 0 ? "Dr": "Cr"; + const format = (value, currency) => format_currency(Math.abs(value), currency); + + if (account.balance!==undefined) { + node.parent && node.parent.find('.balance-area').remove(); + $('' + + (account.balance_in_account_currency ? + (format(account.balance_in_account_currency, account.account_currency) + " / ") : "") + + format(account.balance, account.company_currency) + + " " + dr_or_cr + + '').insertBefore(node.$ul); + } + } + }); } }); }, diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py index 62303bd723f..30eebef7fba 100644 --- a/erpnext/accounts/doctype/account/test_account.py +++ b/erpnext/accounts/doctype/account/test_account.py @@ -7,7 +7,11 @@ import unittest import frappe from frappe.test_runner import make_test_records -from erpnext.accounts.doctype.account.account import merge_account, update_account_number +from erpnext.accounts.doctype.account.account import ( + InvalidAccountMergeError, + merge_account, + update_account_number, +) from erpnext.stock import get_company_default_inventory_account, get_warehouse_account test_dependencies = ["Company"] @@ -47,49 +51,53 @@ class TestAccount(unittest.TestCase): frappe.delete_doc("Account", "1211-11-4 - 6 - Debtors 1 - Test - - _TC") def test_merge_account(self): - if not frappe.db.exists("Account", "Current Assets - _TC"): - acc = frappe.new_doc("Account") - acc.account_name = "Current Assets" - acc.is_group = 1 - acc.parent_account = "Application of Funds (Assets) - _TC" - acc.company = "_Test Company" - acc.insert() - if not frappe.db.exists("Account", "Securities and Deposits - _TC"): - acc = frappe.new_doc("Account") - acc.account_name = "Securities and Deposits" - acc.parent_account = "Current Assets - _TC" - acc.is_group = 1 - acc.company = "_Test Company" - acc.insert() - if not frappe.db.exists("Account", "Earnest Money - _TC"): - acc = frappe.new_doc("Account") - acc.account_name = "Earnest Money" - acc.parent_account = "Securities and Deposits - _TC" - acc.company = "_Test Company" - acc.insert() - if not frappe.db.exists("Account", "Cash In Hand - _TC"): - acc = frappe.new_doc("Account") - acc.account_name = "Cash In Hand" - acc.is_group = 1 - acc.parent_account = "Current Assets - _TC" - acc.company = "_Test Company" - acc.insert() - if not frappe.db.exists("Account", "Accumulated Depreciation - _TC"): - acc = frappe.new_doc("Account") - acc.account_name = "Accumulated Depreciation" - acc.parent_account = "Fixed Assets - _TC" - acc.company = "_Test Company" - acc.account_type = "Accumulated Depreciation" - acc.insert() + create_account( + account_name="Current Assets", + is_group=1, + parent_account="Application of Funds (Assets) - _TC", + company="_Test Company", + ) + + create_account( + account_name="Securities and Deposits", + is_group=1, + parent_account="Current Assets - _TC", + company="_Test Company", + ) + + create_account( + account_name="Earnest Money", + parent_account="Securities and Deposits - _TC", + company="_Test Company", + ) + + create_account( + account_name="Cash In Hand", + is_group=1, + parent_account="Current Assets - _TC", + company="_Test Company", + ) + + create_account( + account_name="Receivable INR", + parent_account="Current Assets - _TC", + company="_Test Company", + account_currency="INR", + ) + + create_account( + account_name="Receivable USD", + parent_account="Current Assets - _TC", + company="_Test Company", + account_currency="USD", + ) - doc = frappe.get_doc("Account", "Securities and Deposits - _TC") parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account") self.assertEqual(parent, "Securities and Deposits - _TC") - merge_account( - "Securities and Deposits - _TC", "Cash In Hand - _TC", doc.is_group, doc.root_type, doc.company - ) + merge_account("Securities and Deposits - _TC", "Cash In Hand - _TC") + parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account") # Parent account of the child account changes after merging @@ -98,30 +106,28 @@ class TestAccount(unittest.TestCase): # Old account doesn't exist after merging self.assertFalse(frappe.db.exists("Account", "Securities and Deposits - _TC")) - doc = frappe.get_doc("Account", "Current Assets - _TC") - # Raise error as is_group property doesn't match self.assertRaises( - frappe.ValidationError, + InvalidAccountMergeError, merge_account, "Current Assets - _TC", "Accumulated Depreciation - _TC", - doc.is_group, - doc.root_type, - doc.company, ) - doc = frappe.get_doc("Account", "Capital Stock - _TC") - # Raise error as root_type property doesn't match self.assertRaises( - frappe.ValidationError, + InvalidAccountMergeError, merge_account, "Capital Stock - _TC", "Softwares - _TC", - doc.is_group, - doc.root_type, - doc.company, + ) + + # Raise error as currency doesn't match + self.assertRaises( + InvalidAccountMergeError, + merge_account, + "Receivable INR - _TC", + "Receivable USD - _TC", ) def test_account_sync(self): @@ -400,11 +406,20 @@ def create_account(**kwargs): "Account", filters={"account_name": kwargs.get("account_name"), "company": kwargs.get("company")} ) if account: - return account + account = frappe.get_doc("Account", account) + account.update( + dict( + is_group=kwargs.get("is_group", 0), + parent_account=kwargs.get("parent_account"), + ) + ) + account.save() + return account.name else: account = frappe.get_doc( dict( doctype="Account", + is_group=kwargs.get("is_group", 0), account_name=kwargs.get("account_name"), account_type=kwargs.get("account_type"), parent_account=kwargs.get("parent_account"), diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 467c68f1021..3ab9d2b60d5 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -66,7 +66,9 @@ "report_settings_sb", "banking_tab", "enable_party_matching", - "enable_fuzzy_matching" + "enable_fuzzy_matching", + "tab_break_dpet", + "show_balance_in_coa" ], "fields": [ { @@ -416,6 +418,17 @@ "fieldname": "ignore_account_closing_balance", "fieldtype": "Check", "label": "Ignore Account Closing Balance" + }, + { + "fieldname": "tab_break_dpet", + "fieldtype": "Tab Break", + "label": "Chart Of Accounts" + }, + { + "default": "1", + "fieldname": "show_balance_in_coa", + "fieldtype": "Check", + "label": "Show Balances in Chart Of Accounts" } ], "icon": "icon-cog", diff --git a/erpnext/accounts/doctype/ledger_merge/ledger_merge.py b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py index 18e5a1ac85b..6e89d039932 100644 --- a/erpnext/accounts/doctype/ledger_merge/ledger_merge.py +++ b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py @@ -49,9 +49,6 @@ def start_merge(docname): merge_account( row.account, ledger_merge.account, - ledger_merge.is_group, - ledger_merge.root_type, - ledger_merge.company, ) row.db_set("merged", 1) frappe.db.commit() diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 0e2de4ac395..c5501a58306 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1455,6 +1455,14 @@ def get_outstanding_reference_documents(args): fieldname, args.get(date_fields[0]), args.get(date_fields[1]) ) posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])]) + elif args.get(date_fields[0]): + # if only from date is supplied + condition += " and {0} >= '{1}'".format(fieldname, args.get(date_fields[0])) + posting_and_due_date.append(ple[fieldname].gte(args.get(date_fields[0]))) + elif args.get(date_fields[1]): + # if only to date is supplied + condition += " and {0} <= '{1}'".format(fieldname, args.get(date_fields[1])) + posting_and_due_date.append(ple[fieldname].lte(args.get(date_fields[1]))) if args.get("company"): condition += " and company = {0}".format(frappe.db.escape(args.get("company"))) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index e7a7ae24ceb..7c013b6abef 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -1,5 +1,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and contributors # For license information, please see license.txt + + import collections import frappe @@ -43,7 +45,6 @@ class POSInvoice(SalesInvoice): self.validate_debit_to_acc() self.validate_write_off_account() self.validate_change_amount() - self.validate_duplicate_serial_and_batch_no() self.validate_change_account() self.validate_item_cost_centers() self.validate_warehouse() @@ -56,6 +57,7 @@ class POSInvoice(SalesInvoice): self.validate_payment_amount() self.validate_loyalty_transaction() self.validate_company_with_pos_company() + self.validate_duplicate_serial_no() if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code @@ -156,27 +158,18 @@ class POSInvoice(SalesInvoice): title=_("Item Unavailable"), ) - def validate_duplicate_serial_and_batch_no(self): + def validate_duplicate_serial_no(self): serial_nos = [] - batch_nos = [] for row in self.get("items"): if row.serial_no: serial_nos = row.serial_no.split("\n") - if row.batch_no and not row.serial_no: - batch_nos.append(row.batch_no) - if serial_nos: for key, value in collections.Counter(serial_nos).items(): if value > 1: frappe.throw(_("Duplicate Serial No {0} found").format("key")) - if batch_nos: - for key, value in collections.Counter(batch_nos).items(): - if value > 1: - frappe.throw(_("Duplicate Batch No {0} found").format("key")) - def validate_pos_reserved_batch_qty(self, item): filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no": item.batch_no} @@ -683,7 +676,7 @@ def get_bundle_availability(bundle_item_code, warehouse): item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse) available_qty = item_bin_qty - item_pos_reserved_qty - max_available_bundles = available_qty / item.stock_qty + max_available_bundles = available_qty / item.qty if bundle_bin_qty > max_available_bundles and frappe.get_value( "Item", item.item_code, "is_stock_item" ): diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 3132fdd259a..bd2fee3b0ad 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -464,6 +464,37 @@ class TestPOSInvoice(unittest.TestCase): pos2.insert() self.assertRaises(frappe.ValidationError, pos2.submit) + def test_pos_invoice_with_duplicate_serial_no(self): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + + se = make_serialized_item( + company="_Test Company", + target_warehouse="Stores - _TC", + cost_center="Main - _TC", + expense_account="Cost of Goods Sold - _TC", + ) + + serial_nos = get_serial_nos(se.get("items")[0].serial_no) + + pos = create_pos_invoice( + company="_Test Company", + debit_to="Debtors - _TC", + account_for_change_amount="Cash - _TC", + warehouse="Stores - _TC", + income_account="Sales - _TC", + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + item=se.get("items")[0].item_code, + rate=1000, + qty=2, + do_not_save=1, + ) + + pos.get("items")[0].has_serial_no = 1 + pos.get("items")[0].serial_no = serial_nos[0] + "\n" + serial_nos[0] + self.assertRaises(frappe.ValidationError, pos.submit) + def test_invalid_serial_no_validation(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 0f8e77952cf..30265aeb50e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1153,7 +1153,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): item = create_item("_Test Item for Deferred Accounting", is_purchase_item=True) item.enable_deferred_expense = 1 - item.deferred_expense_account = deferred_account + item.item_defaults[0].deferred_expense_account = deferred_account item.save() pi = make_purchase_invoice(item=item.name, qty=1, rate=100, do_not_save=True) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py index 6fdcf263a55..fd95c1fe0e5 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py @@ -15,9 +15,11 @@ def get_data(): }, "internal_links": { "Sales Order": ["items", "sales_order"], - "Delivery Note": ["items", "delivery_note"], "Timesheet": ["timesheets", "time_sheet"], }, + "internal_and_external_links": { + "Delivery Note": ["items", "delivery_note"], + }, "transactions": [ { "label": _("Payment"), diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index eee99dcfde0..378be113e7c 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2322,7 +2322,7 @@ class TestSalesInvoice(unittest.TestCase): item = create_item("_Test Item for Deferred Accounting") item.enable_deferred_revenue = 1 - item.deferred_revenue_account = deferred_account + item.item_defaults[0].deferred_revenue_account = deferred_account item.no_of_months = 12 item.save() @@ -3102,7 +3102,7 @@ class TestSalesInvoice(unittest.TestCase): item = create_item("_Test Item for Deferred Accounting") item.enable_deferred_expense = 1 - item.deferred_revenue_account = deferred_account + item.item_defaults[0].deferred_revenue_account = deferred_account item.save() si = create_sales_invoice( diff --git a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py index cb84cf4fc0a..3cf93cc8659 100644 --- a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py +++ b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py @@ -24,7 +24,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): def tearDown(self): frappe.db.rollback() - def test_accounts_receivable_with_supplier(self): + def test_accounts_payable_for_foreign_currency_supplier(self): pi = self.create_purchase_invoice(do_not_submit=True) pi.currency = "USD" pi.conversion_rate = 80 diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index cb8ec876e9e..bb00d616dbc 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -38,32 +38,31 @@ frappe.query_reports["Accounts Receivable"] = { } }, { - "fieldname": "customer", - "label": __("Customer"), + "fieldname": "party_type", + "label": __("Party Type"), "fieldtype": "Link", - "options": "Customer", + "options": "Party Type", + "Default": "Customer", + get_query: () => { + return { + filters: { + 'account_type': 'Receivable' + } + }; + }, on_change: () => { - var customer = frappe.query_report.get_filter_value('customer'); - var company = frappe.query_report.get_filter_value('company'); - if (customer) { - frappe.db.get_value('Customer', customer, ["customer_name", "payment_terms"], function(value) { - frappe.query_report.set_filter_value('customer_name', value["customer_name"]); - frappe.query_report.set_filter_value('payment_terms', value["payment_terms"]); - }); + frappe.query_report.set_filter_value('party', ""); + let party_type = frappe.query_report.get_filter_value('party_type'); + frappe.query_report.toggle_filter_display('customer_group', frappe.query_report.get_filter_value('party_type') !== "Customer"); - frappe.db.get_value('Customer Credit Limit', {'parent': customer, 'company': company}, - ["credit_limit"], function(value) { - if (value) { - frappe.query_report.set_filter_value('credit_limit', value["credit_limit"]); - } - }, "Customer"); - } else { - frappe.query_report.set_filter_value('customer_name', ""); - frappe.query_report.set_filter_value('credit_limit', ""); - frappe.query_report.set_filter_value('payment_terms', ""); - } } }, + { + "fieldname":"party", + "label": __("Party"), + "fieldtype": "Dynamic Link", + "options": "party_type", + }, { "fieldname": "party_account", "label": __("Receivable Account"), @@ -174,24 +173,6 @@ frappe.query_reports["Accounts Receivable"] = { "fieldname": "show_remarks", "label": __("Show Remarks"), "fieldtype": "Check", - }, - { - "fieldname": "customer_name", - "label": __("Customer Name"), - "fieldtype": "Data", - "hidden": 1 - }, - { - "fieldname": "payment_terms", - "label": __("Payment Tems"), - "fieldtype": "Data", - "hidden": 1 - }, - { - "fieldname": "credit_limit", - "label": __("Credit Limit"), - "fieldtype": "Currency", - "hidden": 1 } ], diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 14f8993727a..79424023652 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -769,15 +769,12 @@ class ReceivablePayableReport(object): self.or_filters = [] for party_type in self.party_type: - party_type_field = scrub(party_type) - self.or_filters.append(self.ple.party_type == party_type) + self.add_common_filters() - self.add_common_filters(party_type_field=party_type_field) - - if party_type_field == "customer": + if self.account_type == "Receivable": self.add_customer_filters() - elif party_type_field == "supplier": + elif self.account_type == "Payable": self.add_supplier_filters() if self.filters.cost_center: @@ -793,16 +790,13 @@ class ReceivablePayableReport(object): ] self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list)) - def add_common_filters(self, party_type_field): + def add_common_filters(self): if self.filters.company: self.qb_selection_filter.append(self.ple.company == self.filters.company) if self.filters.finance_book: self.qb_selection_filter.append(self.ple.finance_book == self.filters.finance_book) - if self.filters.get(party_type_field): - self.qb_selection_filter.append(self.ple.party == self.filters.get(party_type_field)) - if self.filters.get("party_type"): self.qb_selection_filter.append(self.filters.party_type == self.ple.party_type) @@ -969,6 +963,20 @@ class ReceivablePayableReport(object): fieldtype="Link", options="Contact", ) + if self.filters.party_type == "Customer": + self.add_column( + _("Customer Name"), + fieldname="customer_name", + fieldtype="Link", + options="Customer", + ) + elif self.filters.party_type == "Supplier": + self.add_column( + _("Supplier Name"), + fieldname="supplier_name", + fieldtype="Link", + options="Supplier", + ) self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data") self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data") diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 0c7d931d2d5..b98916ee443 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -568,3 +568,40 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): row.account_currency, ], ) + + def test_usd_customer_filter(self): + filters = { + "company": self.company, + "party_type": "Customer", + "party": self.customer, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + + si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True) + si.currency = "USD" + si.conversion_rate = 80 + si.debit_to = self.debtors_usd + si.save().submit() + name = si.name + + # check invoice grand total and invoiced column's value for 3 payment terms + report = execute(filters) + + expected = { + "voucher_type": si.doctype, + "voucher_no": si.name, + "party_account": self.debtors_usd, + "customer_name": self.customer, + "invoiced": 100.0, + "outstanding": 100.0, + "account_currency": "USD", + } + self.assertEqual(len(report[1]), 1) + report_output = report[1][0] + for field in expected: + with self.subTest(field=field): + self.assertEqual(report_output.get(field), expected.get(field)) diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py index 28d0c20a918..7b1a9027780 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py @@ -81,7 +81,7 @@ class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin): self.create_item("_Test Internet Subscription", 0, self.warehouse, self.company) item = frappe.get_doc("Item", self.item) item.enable_deferred_revenue = 1 - item.deferred_revenue_account = self.deferred_revenue_account + item.item_defaults[0].deferred_revenue_account = self.deferred_revenue_account item.no_of_months = 3 item.save() @@ -150,7 +150,7 @@ class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin): self.create_item("_Test Office Desk", 0, self.warehouse, self.company) item = frappe.get_doc("Item", self.item) item.enable_deferred_expense = 1 - item.deferred_expense_account = self.deferred_expense_account + item.item_defaults[0].deferred_expense_account = self.deferred_expense_account item.no_of_months_exp = 3 item.save() diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 23403a4b15b..d670a356975 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -272,20 +272,19 @@ def get_conditions(filters): if match_conditions: conditions.append(match_conditions) - if filters.get("include_dimensions"): - accounting_dimensions = get_accounting_dimensions(as_list=False) + accounting_dimensions = get_accounting_dimensions(as_list=False) - if accounting_dimensions: - for dimension in accounting_dimensions: - if not dimension.disabled: - if filters.get(dimension.fieldname): - if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): - filters[dimension.fieldname] = get_dimension_with_children( - dimension.document_type, filters.get(dimension.fieldname) - ) - conditions.append("{0} in %({0})s".format(dimension.fieldname)) - else: - conditions.append("{0} in %({0})s".format(dimension.fieldname)) + if accounting_dimensions: + for dimension in accounting_dimensions: + if not dimension.disabled: + if filters.get(dimension.fieldname): + if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): + filters[dimension.fieldname] = get_dimension_with_children( + dimension.document_type, filters.get(dimension.fieldname) + ) + conditions.append("{0} in %({0})s".format(dimension.fieldname)) + else: + conditions.append("{0} in %({0})s".format(dimension.fieldname)) return "and {}".format(" and ".join(conditions)) if conditions else "" diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 645abf25a8f..bacd98bea77 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -1171,6 +1171,7 @@ "depends_on": "is_internal_supplier", "fieldname": "set_from_warehouse", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Set From Warehouse", "options": "Warehouse" }, @@ -1271,7 +1272,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2023-05-24 11:16:41.195340", + "modified": "2023-09-13 16:21:07.361700", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", 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 c645b04e129..fe1b9702539 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -878,6 +878,7 @@ "depends_on": "eval:parent.is_internal_supplier", "fieldname": "from_warehouse", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "From Warehouse", "options": "Warehouse" }, @@ -902,7 +903,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-11-29 16:47:41.364387", + "modified": "2023-09-13 16:22:40.825092", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index b9fc344647b..7c7467e6f64 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -195,6 +195,9 @@ class TestSupplier(FrappeTestCase): def create_supplier(**args): args = frappe._dict(args) + if not args.supplier_name: + args.supplier_name = frappe.generate_hash() + if frappe.db.exists("Supplier", args.supplier_name): return frappe.get_doc("Supplier", args.supplier_name) @@ -202,6 +205,7 @@ def create_supplier(**args): { "doctype": "Supplier", "supplier_name": args.supplier_name, + "default_currency": args.default_currency, "supplier_group": args.supplier_group or "Services", "supplier_type": args.supplier_type or "Company", "tax_withholding_category": args.tax_withholding_category, diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py index 21241e08603..07187352eb7 100644 --- a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py +++ b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py @@ -7,7 +7,7 @@ import copy import frappe from frappe import _ from frappe.query_builder.functions import Coalesce, Sum -from frappe.utils import date_diff, flt, getdate +from frappe.utils import cint, date_diff, flt, getdate def execute(filters=None): @@ -47,8 +47,10 @@ def get_data(filters): mr.transaction_date.as_("date"), mr_item.schedule_date.as_("required_date"), mr_item.item_code.as_("item_code"), - Sum(Coalesce(mr_item.stock_qty, 0)).as_("qty"), - Coalesce(mr_item.stock_uom, "").as_("uom"), + Sum(Coalesce(mr_item.qty, 0)).as_("qty"), + Sum(Coalesce(mr_item.stock_qty, 0)).as_("stock_qty"), + Coalesce(mr_item.uom, "").as_("uom"), + Coalesce(mr_item.stock_uom, "").as_("stock_uom"), Sum(Coalesce(mr_item.ordered_qty, 0)).as_("ordered_qty"), Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"), (Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.received_qty, 0))).as_( @@ -96,7 +98,7 @@ def get_conditions(filters, query, mr, mr_item): def update_qty_columns(row_to_update, data_row): - fields = ["qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"] + fields = ["qty", "stock_qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"] for field in fields: row_to_update[field] += flt(data_row[field]) @@ -104,16 +106,20 @@ def update_qty_columns(row_to_update, data_row): def prepare_data(data, filters): """Prepare consolidated Report data and Chart data""" material_request_map, item_qty_map = {}, {} + precision = cint(frappe.db.get_default("float_precision")) or 2 for row in data: # item wise map for charts if not row["item_code"] in item_qty_map: item_qty_map[row["item_code"]] = { - "qty": row["qty"], - "ordered_qty": row["ordered_qty"], - "received_qty": row["received_qty"], - "qty_to_receive": row["qty_to_receive"], - "qty_to_order": row["qty_to_order"], + "qty": flt(row["stock_qty"], precision), + "stock_qty": flt(row["stock_qty"], precision), + "stock_uom": row["stock_uom"], + "uom": row["uom"], + "ordered_qty": flt(row["ordered_qty"], precision), + "received_qty": flt(row["received_qty"], precision), + "qty_to_receive": flt(row["qty_to_receive"], precision), + "qty_to_order": flt(row["qty_to_order"], precision), } else: item_entry = item_qty_map[row["item_code"]] @@ -200,21 +206,34 @@ def get_columns(filters): {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 100}, {"label": _("Description"), "fieldname": "description", "fieldtype": "Data", "width": 200}, { - "label": _("Stock UOM"), + "label": _("UOM"), "fieldname": "uom", "fieldtype": "Data", "width": 100, }, + { + "label": _("Stock UOM"), + "fieldname": "stock_uom", + "fieldtype": "Data", + "width": 100, + }, ] ) columns.extend( [ { - "label": _("Stock Qty"), + "label": _("Qty"), "fieldname": "qty", "fieldtype": "Float", - "width": 120, + "width": 140, + "convertible": "qty", + }, + { + "label": _("Qty in Stock UOM"), + "fieldname": "stock_qty", + "fieldtype": "Float", + "width": 140, "convertible": "qty", }, { diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index df77e5260f5..bd2354e7452 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -168,7 +168,6 @@ class AccountsController(TransactionBase): self.validate_value("base_grand_total", ">=", 0) validate_return(self) - self.set_total_in_words() self.validate_all_documents_schedule() @@ -207,6 +206,8 @@ class AccountsController(TransactionBase): if self.doctype != "Material Request" and not self.ignore_pricing_rule: apply_pricing_rule_on_transaction(self) + self.set_total_in_words() + def before_cancel(self): validate_einvoice_fields(self) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 1de39967730..01990a3a268 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -162,10 +162,13 @@ class BuyingController(SubcontractingController): purchase_doc_field = ( "purchase_receipt" if self.doctype == "Purchase Receipt" else "purchase_invoice" ) - not_cancelled_asset = [ - d.name - for d in frappe.db.get_all("Asset", {purchase_doc_field: self.return_against, "docstatus": 1}) - ] + not_cancelled_asset = [] + if self.return_against: + not_cancelled_asset = [ + d.name + for d in frappe.db.get_all("Asset", {purchase_doc_field: self.return_against, "docstatus": 1}) + ] + if self.is_return and len(not_cancelled_asset): frappe.throw( _( diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py index 4f9088e8c08..57746a234bc 100644 --- a/erpnext/e_commerce/shopping_cart/cart.py +++ b/erpnext/e_commerce/shopping_cart/cart.py @@ -634,7 +634,6 @@ def get_applicable_shipping_rules(party=None, quotation=None): shipping_rules = get_shipping_rules(quotation) if shipping_rules: - rule_label_map = frappe.db.get_values("Shipping Rule", shipping_rules, "label") # we need this in sorted order as per the position of the rule in the settings page return [[rule, rule] for rule in shipping_rules] diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.js b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.js index 82a2d802b80..53581339fae 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.js +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.js @@ -6,7 +6,14 @@ frappe.ui.form.on('Loan Repayment', { // refresh: function(frm) { - // } + // }, + + setup: function(frm) { + if (frappe.meta.has_field("Loan Repayment", "repay_from_salary")) { + frm.add_fetch("against_loan", "repay_from_salary", "repay_from_salary"); + } + }, + onload: function(frm) { frm.set_query('against_loan', function() { return { diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index f0e113b0e7b..0d16c7f0c50 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -80,6 +80,12 @@ class LoanRepayment(AccountsController): if amounts.get("due_date"): self.due_date = amounts.get("due_date") + if hasattr(self, "repay_from_salary") and hasattr(self, "payroll_payable_account"): + if self.repay_from_salary and not self.payroll_payable_account: + frappe.throw(_("Please set Payroll Payable Account in Loan Repayment")) + elif not self.repay_from_salary and self.payroll_payable_account: + self.repay_from_salary = 1 + def check_future_entries(self): future_repayment_date = frappe.db.get_value( "Loan Repayment", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 46c554c1e80..72438ddceea 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -476,6 +476,15 @@ frappe.ui.form.on("Material Request Plan Item", { } }) } + }, + + material_request_type(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + + if (row.from_warehouse && + row.material_request_type !== "Material Transfer") { + frappe.model.set_value(cdt, cdn, 'from_warehouse', ''); + } } }); diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 795cb97fffa..fcaae79d6c5 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -40,6 +40,12 @@ class ProductionPlan(Document): self._rename_temporary_references() validate_uom_is_integer(self, "stock_uom", "planned_qty") self.validate_sales_orders() + self.validate_material_request_type() + + def validate_material_request_type(self): + for row in self.get("mr_items"): + if row.from_warehouse and row.material_request_type != "Material Transfer": + row.from_warehouse = "" @frappe.whitelist() def validate_sales_orders(self, sales_order=None): @@ -749,7 +755,9 @@ class ProductionPlan(Document): "items", { "item_code": item.item_code, - "from_warehouse": item.from_warehouse, + "from_warehouse": item.from_warehouse + if material_request_type == "Material Transfer" + else None, "qty": item.quantity, "schedule_date": schedule_date, "warehouse": item.warehouse, diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index f5778847b9c..78080645050 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1087,6 +1087,41 @@ class TestProductionPlan(FrappeTestCase): ) self.assertEqual(reserved_qty_after_mr, before_qty) + def test_from_warehouse_for_purchase_material_request(self): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + from erpnext.stock.utils import get_or_make_bin + + create_item("RM-TEST-123 For Purchase", valuation_rate=100) + bin_name = get_or_make_bin("RM-TEST-123 For Purchase", "_Test Warehouse - _TC") + t_warehouse = create_warehouse("_Test Store - _TC") + make_stock_entry( + item_code="Raw Material Item 1", + qty=5, + rate=100, + target=t_warehouse, + ) + + plan = create_production_plan(item_code="Test Production Item 1", do_not_save=1) + mr_items = get_items_for_material_requests( + plan.as_dict(), warehouses=[{"warehouse": t_warehouse}] + ) + + for d in mr_items: + plan.append("mr_items", d) + + plan.save() + + for row in plan.mr_items: + if row.material_request_type == "Material Transfer": + self.assertEqual(row.from_warehouse, t_warehouse) + + row.material_request_type = "Purchase" + + plan.save() + + for row in plan.mr_items: + self.assertFalse(row.from_warehouse) + def test_skip_available_qty_for_sub_assembly_items(self): from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom diff --git a/erpnext/patches.txt b/erpnext/patches.txt index cde484eb0fa..19f8dab9a17 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -339,5 +339,6 @@ erpnext.patches.v14_0.update_closing_balances #15-07-2023 execute:frappe.defaults.clear_default("fiscal_year") execute:frappe.db.set_single_value('Selling Settings', 'allow_negative_rates_for_items', 0) erpnext.patches.v14_0.correct_asset_value_if_je_with_workflow +erpnext.patches.v14_0.migrate_deferred_accounts_to_item_defaults # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger diff --git a/erpnext/patches/v14_0/delete_education_doctypes.py b/erpnext/patches/v14_0/delete_education_doctypes.py index 76b2300fd2a..55b64eaabd8 100644 --- a/erpnext/patches/v14_0/delete_education_doctypes.py +++ b/erpnext/patches/v14_0/delete_education_doctypes.py @@ -46,6 +46,17 @@ def execute(): for doctype in doctypes: frappe.delete_doc("DocType", doctype, ignore_missing=True) + titles = [ + "Fees", + "Student Admission", + "Grant Application", + "Chapter", + "Certification Application", + ] + items = frappe.get_all("Portal Menu Item", filters=[["title", "in", titles]], pluck="name") + for item in items: + frappe.delete_doc("Portal Menu Item", item, ignore_missing=True, force=True) + frappe.delete_doc("Module Def", "Education", ignore_missing=True, force=True) click.secho( diff --git a/erpnext/patches/v14_0/delete_healthcare_doctypes.py b/erpnext/patches/v14_0/delete_healthcare_doctypes.py index 2c699e4a9f2..896a4409507 100644 --- a/erpnext/patches/v14_0/delete_healthcare_doctypes.py +++ b/erpnext/patches/v14_0/delete_healthcare_doctypes.py @@ -41,7 +41,7 @@ def execute(): for card in cards: frappe.delete_doc("Number Card", card, ignore_missing=True, force=True) - titles = ["Lab Test", "Prescription", "Patient Appointment"] + titles = ["Lab Test", "Prescription", "Patient Appointment", "Patient"] items = frappe.get_all("Portal Menu Item", filters=[["title", "in", titles]], pluck="name") for item in items: frappe.delete_doc("Portal Menu Item", item, ignore_missing=True, force=True) diff --git a/erpnext/patches/v14_0/migrate_deferred_accounts_to_item_defaults.py b/erpnext/patches/v14_0/migrate_deferred_accounts_to_item_defaults.py new file mode 100644 index 00000000000..44b830babb2 --- /dev/null +++ b/erpnext/patches/v14_0/migrate_deferred_accounts_to_item_defaults.py @@ -0,0 +1,39 @@ +import frappe + + +def execute(): + try: + item_dict = get_deferred_accounts() + add_to_item_defaults(item_dict) + except Exception: + frappe.db.rollback() + frappe.log_error("Failed to migrate deferred accounts in Item Defaults.") + + +def get_deferred_accounts(): + item = frappe.qb.DocType("Item") + return ( + frappe.qb.from_(item) + .select(item.name, item.deferred_expense_account, item.deferred_revenue_account) + .where((item.enable_deferred_expense == 1) | (item.enable_deferred_revenue == 1)) + .run(as_dict=True) + ) + + +def add_to_item_defaults(item_dict): + for item in item_dict: + add_company_wise_item_default(item, "deferred_expense_account") + add_company_wise_item_default(item, "deferred_revenue_account") + + +def add_company_wise_item_default(item, account_type): + company = frappe.get_cached_value("Account", item[account_type], "company") + if company and item[account_type]: + item_defaults = frappe.get_cached_value("Item", item["name"], "item_defaults") + for item_row in item_defaults: + if item_row.company == company: + frappe.set_value("Item Default", item_row.name, account_type, item[account_type]) + break + else: + item_defaults.append({"company": company, account_type: item[account_type]}) + frappe.set_value("Item", item["name"], "item_defaults", item_defaults) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index b6b6e2e5ad6..fe24b18098a 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -119,19 +119,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } }); - if(this.frm.fields_dict["items"].grid.get_field('batch_no')) { - this.frm.set_query("batch_no", "items", function(doc, cdt, cdn) { + if(this.frm.fields_dict['items'].grid.get_field('batch_no')) { + this.frm.set_query('batch_no', 'items', function(doc, cdt, cdn) { return me.set_query_for_batch(doc, cdt, cdn); }); - - let batch_field = this.frm.get_docfield('items', 'batch_no'); - if (batch_field) { - batch_field.get_route_options_for_new_doc = (row) => { - return { - 'item': row.doc.item_code - } - }; - } } if( @@ -196,14 +187,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }); } - let batch_no_field = this.frm.get_docfield("items", "batch_no"); - if (batch_no_field) { - batch_no_field.get_route_options_for_new_doc = function(row) { - return { - "item": row.doc.item_code - } - }; - } if (this.frm.fields_dict["items"].grid.get_field('blanket_order')) { this.frm.set_query("blanket_order", "items", function(doc, cdt, cdn) { @@ -257,6 +240,17 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } ]); } + + if(this.frm.fields_dict['items'].grid.get_field('batch_no')) { + let batch_field = this.frm.get_docfield('items', 'batch_no'); + if (batch_field) { + batch_field.get_route_options_for_new_doc = (row) => { + return { + 'item': row.doc.item_code + } + }; + } + } } is_return() { diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 608e23a8268..799ad555a52 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1998,6 +1998,61 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(len(dn.packed_items), 1) self.assertEqual(dn.items[0].item_code, "_Test Product Bundle Item Partial 2") + @change_settings("Selling Settings", {"editable_bundle_item_rates": 1}) + def test_expired_rate_for_packed_item(self): + bundle = "_Test Product Bundle 1" + packed_item = "_Packed Item 1" + + # test Update Items with product bundle + for product_bundle in [bundle]: + if not frappe.db.exists("Item", product_bundle): + bundle_item = make_item(product_bundle, {"is_stock_item": 0}) + bundle_item.append( + "item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"} + ) + bundle_item.save(ignore_permissions=True) + + for product_bundle in [packed_item]: + if not frappe.db.exists("Item", product_bundle): + make_item(product_bundle, {"is_stock_item": 0, "stock_uom": "Nos"}) + + make_product_bundle(bundle, [packed_item], 1) + + for scenario in [ + {"valid_upto": add_days(nowdate(), -1), "expected_rate": 0.0}, + {"valid_upto": add_days(nowdate(), 1), "expected_rate": 111.0}, + ]: + with self.subTest(scenario=scenario): + frappe.get_doc( + { + "doctype": "Item Price", + "item_code": packed_item, + "selling": 1, + "price_list": "_Test Price List", + "valid_from": add_days(nowdate(), -1), + "valid_upto": scenario.get("valid_upto"), + "price_list_rate": 111, + } + ).save() + + so = frappe.new_doc("Sales Order") + so.transaction_date = nowdate() + so.delivery_date = nowdate() + so.set_warehouse = "" + so.company = "_Test Company" + so.customer = "_Test Customer" + so.currency = "INR" + so.selling_price_list = "_Test Price List" + so.append("items", {"item_code": bundle, "qty": 1}) + so.save() + + self.assertEqual(len(so.items), 1) + self.assertEqual(len(so.packed_items), 1) + self.assertEqual(so.items[0].item_code, bundle) + self.assertEqual(so.packed_items[0].item_code, packed_item) + self.assertEqual(so.items[0].rate, scenario.get("expected_rate")) + self.assertEqual(so.packed_items[0].rate, scenario.get("expected_rate")) + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index ff0e1b66bf8..11f2cafc35d 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -1252,6 +1252,7 @@ "depends_on": "eval: doc.is_internal_customer", "fieldname": "set_target_warehouse", "fieldtype": "Link", + "ignore_user_permissions": 1, "in_standard_filter": 1, "label": "Set Target Warehouse", "no_copy": 1, @@ -1399,7 +1400,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2023-06-16 14:58:55.066602", + "modified": "2023-09-04 14:15:28.363184", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 3a056500b54..ba6e247d2e4 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -138,6 +138,7 @@ class DeliveryNote(SellingController): self.validate_uom_is_integer("stock_uom", "stock_qty") self.validate_uom_is_integer("uom", "qty") self.validate_with_previous_doc() + self.validate_duplicate_serial_nos() from erpnext.stock.doctype.packed_item.packed_item import make_packing_list @@ -412,6 +413,21 @@ class DeliveryNote(SellingController): pluck="name", ) + def validate_duplicate_serial_nos(self): + serial_nos = [] + for item in self.items: + if not item.serial_no: + continue + + for serial_no in item.serial_no.split("\n"): + if serial_no in serial_nos: + frappe.throw( + _("Row #{0}: Serial No {1} is already selected.").format(item.idx, serial_no), + title=_("Duplicate Serial No"), + ) + else: + serial_nos.append(serial_no) + def update_billed_amount_based_on_so(so_detail, update_modified=True): from frappe.query_builder.functions import Sum diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py index e66c23324da..d4a574da73f 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py @@ -11,10 +11,12 @@ def get_data(): }, "internal_links": { "Sales Order": ["items", "against_sales_order"], - "Sales Invoice": ["items", "against_sales_invoice"], "Material Request": ["items", "material_request"], "Purchase Order": ["items", "purchase_order"], }, + "internal_and_external_links": { + "Sales Invoice": ["items", "against_sales_invoice"], + }, "transactions": [ {"label": _("Related"), "items": ["Sales Invoice", "Packing Slip", "Delivery Trip"]}, {"label": _("Reference"), "items": ["Sales Order", "Shipment", "Quality Inspection"]}, diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 2565d1b76d1..2acfd84d944 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1211,6 +1211,38 @@ class TestDeliveryNote(FrappeTestCase): self.assertTrue(return_dn.docstatus == 1) + def test_duplicate_serial_no_in_delivery_note(self): + # Step - 1: Create Serial Item + serial_item = make_item( + properties={ + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": frappe.generate_hash("", 10) + ".###", + } + ).name + + # Step - 2: Inward Stock + se = make_stock_entry(item_code=serial_item, target="_Test Warehouse - _TC", qty=4) + + # Step - 3: Create Delivery Note with Duplicare Serial Nos + serial_nos = se.items[0].serial_no.split("\n") + dn = create_delivery_note( + item_code=serial_item, + warehouse="_Test Warehouse - _TC", + qty=2, + do_not_save=True, + ) + dn.items[0].serial_no = "\n".join(serial_nos[:2]) + dn.append("items", dn.items[0].as_dict()) + + # Test - 1: ValidationError should be raised + self.assertRaises(frappe.ValidationError, dn.save) + + # Step - 4: Submit Delivery Note with unique Serial Nos + dn.items[1].serial_no = "\n".join(serial_nos[2:]) + dn.save() + dn.submit() + def tearDown(self): frappe.db.rollback() frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 86f9af25e7a..b306a41bb83 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -350,18 +350,20 @@ $.extend(erpnext.item, { } } - frm.fields_dict['deferred_revenue_account'].get_query = function() { + frm.fields_dict["item_defaults"].grid.get_field("deferred_revenue_account").get_query = function(doc, cdt, cdn) { return { filters: { + "company": locals[cdt][cdn].company, 'root_type': 'Liability', "is_group": 0 } } } - frm.fields_dict['deferred_expense_account'].get_query = function() { + frm.fields_dict["item_defaults"].grid.get_field("deferred_expense_account").get_query = function(doc, cdt, cdn) { return { filters: { + "company": locals[cdt][cdn].company, 'root_type': 'Asset', "is_group": 0 } diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 7f4ba032e86..7d0a387f43e 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -70,6 +70,13 @@ "variant_based_on", "attributes", "accounting", + "deferred_accounting_section", + "enable_deferred_expense", + "no_of_months_exp", + "column_break_9s9o", + "enable_deferred_revenue", + "no_of_months", + "section_break_avcp", "item_defaults", "purchasing_tab", "purchase_uom", @@ -85,10 +92,6 @@ "delivered_by_supplier", "column_break2", "supplier_items", - "deferred_expense_section", - "enable_deferred_expense", - "deferred_expense_account", - "no_of_months_exp", "foreign_trade_details", "country_of_origin", "column_break_59", @@ -99,10 +102,6 @@ "is_sales_item", "column_break3", "max_discount", - "deferred_revenue", - "enable_deferred_revenue", - "deferred_revenue_account", - "no_of_months", "customer_details", "customer_items", "item_tax_section_break", @@ -657,20 +656,6 @@ "oldfieldname": "max_discount", "oldfieldtype": "Currency" }, - { - "collapsible": 1, - "fieldname": "deferred_revenue", - "fieldtype": "Section Break", - "label": "Deferred Revenue" - }, - { - "depends_on": "enable_deferred_revenue", - "fieldname": "deferred_revenue_account", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Deferred Revenue Account", - "options": "Account" - }, { "default": "0", "fieldname": "enable_deferred_revenue", @@ -681,21 +666,7 @@ "depends_on": "enable_deferred_revenue", "fieldname": "no_of_months", "fieldtype": "Int", - "label": "No of Months" - }, - { - "collapsible": 1, - "fieldname": "deferred_expense_section", - "fieldtype": "Section Break", - "label": "Deferred Expense" - }, - { - "depends_on": "enable_deferred_expense", - "fieldname": "deferred_expense_account", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Deferred Expense Account", - "options": "Account" + "label": "No of Months (Revenue)" }, { "default": "0", @@ -904,6 +875,20 @@ "fieldname": "accounting", "fieldtype": "Tab Break", "label": "Accounting" + }, + { + "fieldname": "column_break_9s9o", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_avcp", + "fieldtype": "Section Break" + }, + { + "collapsible": 1, + "fieldname": "deferred_accounting_section", + "fieldtype": "Section Break", + "label": "Deferred Accounting" } ], "icon": "fa fa-tag", @@ -912,7 +897,7 @@ "index_web_pages_for_search": 1, "links": [], "make_attachments_public": 1, - "modified": "2023-07-14 17:18:18.658942", + "modified": "2023-09-11 13:46:32.688051", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item_default/item_default.json b/erpnext/stock/doctype/item_default/item_default.json index 042d398256a..28956612762 100644 --- a/erpnext/stock/doctype/item_default/item_default.json +++ b/erpnext/stock/doctype/item_default/item_default.json @@ -19,7 +19,11 @@ "selling_defaults", "selling_cost_center", "column_break_12", - "income_account" + "income_account", + "deferred_accounting_defaults_section", + "deferred_expense_account", + "column_break_kwad", + "deferred_revenue_account" ], "fields": [ { @@ -108,11 +112,34 @@ "fieldtype": "Link", "label": "Default Provisional Account", "options": "Account" + }, + { + "fieldname": "deferred_accounting_defaults_section", + "fieldtype": "Section Break", + "label": "Deferred Accounting Defaults" + }, + { + "depends_on": "eval: parent.enable_deferred_expense", + "fieldname": "deferred_expense_account", + "fieldtype": "Link", + "label": "Deferred Expense Account", + "options": "Account" + }, + { + "depends_on": "eval: parent.enable_deferred_revenue", + "fieldname": "deferred_revenue_account", + "fieldtype": "Link", + "label": "Deferred Revenue Account", + "options": "Account" + }, + { + "fieldname": "column_break_kwad", + "fieldtype": "Column Break" } ], "istable": 1, "links": [], - "modified": "2022-04-10 20:18:54.148195", + "modified": "2023-09-04 12:33:14.607267", "modified_by": "Administrator", "module": "Stock", "name": "Item Default", diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index ffec57ca1df..25c765bbced 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -296,6 +296,7 @@ "depends_on": "eval:doc.material_request_type == 'Material Transfer'", "fieldname": "set_from_warehouse", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Set Source Warehouse", "options": "Warehouse" }, @@ -356,7 +357,7 @@ "idx": 70, "is_submittable": 1, "links": [], - "modified": "2023-07-25 17:19:31.662662", + "modified": "2023-09-15 12:07:24.789471", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index dbd8de4fcb0..a9e9ad1a639 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -207,6 +207,9 @@ def update_packed_item_price_data(pi_row, item_data, doc): "conversion_rate": doc.get("conversion_rate"), } ) + if not row_data.get("transaction_date"): + row_data.update({"transaction_date": doc.get("transaction_date")}) + rate = get_price_list_rate(row_data, item_doc).get("price_list_rate") pi_row.rate = rate or item_data.get("valuation_rate") or 0.0 diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 4f6c6364a47..1873efc711a 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -605,7 +605,7 @@ class PurchaseReceipt(BuyingController): account=provisional_account, cost_center=item.cost_center, debit=0.0, - credit=multiplication_factor * item.amount, + credit=multiplication_factor * item.base_amount, remarks=remarks, against_account=expense_account, account_currency=credit_currency, @@ -619,7 +619,7 @@ class PurchaseReceipt(BuyingController): gl_entries=gl_entries, account=expense_account, cost_center=item.cost_center, - debit=multiplication_factor * item.amount, + debit=multiplication_factor * item.base_amount, credit=0.0, remarks=remarks, against_account=provisional_account, diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index c9433cf5106..2f46809f49d 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2024,6 +2024,49 @@ class TestPurchaseReceipt(FrappeTestCase): ste7.reload() self.assertEqual(ste7.items[0].valuation_rate, valuation_rate) + def test_purchase_receipt_provisional_accounting(self): + # Step - 1: Create Supplier with Default Currency as USD + from erpnext.buying.doctype.supplier.test_supplier import create_supplier + + supplier = create_supplier(default_currency="USD") + + # Step - 2: Setup Company for Provisional Accounting + from erpnext.accounts.doctype.account.test_account import create_account + + provisional_account = create_account( + account_name="Provision Account", + parent_account="Current Liabilities - _TC", + company="_Test Company", + ) + company = frappe.get_doc("Company", "_Test Company") + company.enable_provisional_accounting_for_non_stock_items = 1 + company.default_provisional_account = provisional_account + company.save() + + # Step - 3: Create Non-Stock Item + item = make_item(properties={"is_stock_item": 0}) + + # Step - 4: Create Purchase Receipt + pr = make_purchase_receipt( + qty=2, + item_code=item.name, + company=company.name, + supplier=supplier.name, + currency=supplier.default_currency, + ) + + # Test - 1: Total and Base Total should not be the same as the currency is different + self.assertNotEqual(flt(pr.total, 2), flt(pr.base_total, 2)) + self.assertEqual(flt(pr.total * pr.conversion_rate, 2), flt(pr.base_total, 2)) + + # Test - 2: Sum of Debit or Credit should be equal to Purchase Receipt Base Total + amount = frappe.db.get_value("GL Entry", {"docstatus": 1, "voucher_no": pr.name}, ["sum(debit)"]) + expected_amount = pr.base_total + self.assertEqual(amount, expected_amount) + + company.enable_provisional_accounting_for_non_stock_items = 0 + company.save() + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index dd08ef4d1e3..6dd0a58645c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -101,15 +101,6 @@ frappe.ui.form.on('Stock Entry', { } }); - let batch_field = frm.get_docfield('items', 'batch_no'); - if (batch_field) { - batch_field.get_route_options_for_new_doc = (row) => { - return { - 'item': row.doc.item_code - } - }; - } - frm.add_fetch("bom_no", "inspection_required", "inspection_required"); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); @@ -345,6 +336,15 @@ frappe.ui.form.on('Stock Entry', { if(!check_should_not_attach_bom_items(frm.doc.bom_no)) { erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); } + + let batch_field = frm.get_docfield('items', 'batch_no'); + if (batch_field) { + batch_field.get_route_options_for_new_doc = (row) => { + return { + 'item': row.doc.item_code + } + }; + } }, get_items_from_transit_entry: function(frm) { diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index f3adefb3e74..f7eb859f830 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -729,7 +729,11 @@ def get_default_discount_account(args, item): def get_default_deferred_account(args, item, fieldname=None): if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): return ( - item.get(fieldname) + frappe.get_cached_value( + "Item Default", + {"parent": args.item_code, "company": args.get("company")}, + fieldname, + ) or args.get(fieldname) or frappe.get_cached_value("Company", args.company, "default_" + fieldname) ) diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js index 31f389f236e..74849058c3c 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js @@ -3,23 +3,23 @@ /* eslint-disable */ const DIFFERNCE_FIELD_NAMES = [ - "difference_in_qty", - "fifo_qty_diff", - "fifo_value_diff", - "fifo_valuation_diff", - "valuation_diff", - "fifo_difference_diff", - "diff_value_diff" + 'difference_in_qty', + 'fifo_qty_diff', + 'fifo_value_diff', + 'fifo_valuation_diff', + 'valuation_diff', + 'fifo_difference_diff', + 'diff_value_diff' ]; -frappe.query_reports["Stock Ledger Invariant Check"] = { - "filters": [ +frappe.query_reports['Stock Ledger Invariant Check'] = { + 'filters': [ { - "fieldname": "item_code", - "fieldtype": "Link", - "label": "Item", - "mandatory": 1, - "options": "Item", + 'fieldname': 'item_code', + 'fieldtype': 'Link', + 'label': 'Item', + 'mandatory': 1, + 'options': 'Item', get_query: function() { return { filters: {is_stock_item: 1, has_serial_no: 0} @@ -27,18 +27,61 @@ frappe.query_reports["Stock Ledger Invariant Check"] = { } }, { - "fieldname": "warehouse", - "fieldtype": "Link", - "label": "Warehouse", - "mandatory": 1, - "options": "Warehouse", + 'fieldname': 'warehouse', + 'fieldtype': 'Link', + 'label': 'Warehouse', + 'mandatory': 1, + 'options': 'Warehouse', } ], + formatter (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); if (DIFFERNCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) { - value = "" + value + ""; + value = '' + value + ''; } return value; }, + + get_datatable_options(options) { + return Object.assign(options, { + checkboxColumn: true, + }); + }, + + onload(report) { + report.page.add_inner_button(__('Create Reposting Entry'), () => { + let message = ` +
+ Reposting Entry will change the value of + accounts Stock In Hand, and Stock Expenses + in the Trial Balance report and will also change + the Balance Value in the Stock Balance report. +
+Are you sure you want to create a Reposting Entry?
+