diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py index 5a6bb6976f9..adf59254435 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py @@ -76,6 +76,7 @@ class TestBankReconciliationTool(AccountsTestMixin, FrappeTestCase): "deposit": 100, "bank_account": self.bank_account, "reference_number": "123", + "currency": "INR", } ) .save() diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 1d6cb8e2c09..fef3b569ed2 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -3,6 +3,7 @@ import frappe from frappe import _ +from frappe.model.docstatus import DocStatus from frappe.model.document import Document from frappe.utils import flt @@ -48,6 +49,24 @@ class BankTransaction(Document): def validate(self): self.validate_duplicate_references() + self.validate_currency() + + def validate_currency(self): + """ + Bank Transaction should be on the same currency as the Bank Account. + """ + if self.currency and self.bank_account: + account = frappe.get_cached_value("Bank Account", self.bank_account, "account") + account_currency = frappe.get_cached_value("Account", account, "account_currency") + + if self.currency != account_currency: + frappe.throw( + _( + "Transaction currency: {0} cannot be different from Bank Account({1}) currency: {2}" + ).format( + frappe.bold(self.currency), frappe.bold(self.bank_account), frappe.bold(account_currency) + ) + ) def set_status(self): if self.docstatus == 2: @@ -415,3 +434,21 @@ def unclear_reference_payment(doctype, docname, bt_name): bt = frappe.get_doc("Bank Transaction", bt_name) set_voucher_clearance(doctype, docname, None, bt) return docname + + +def remove_from_bank_transaction(doctype, docname): + """Remove a (cancelled) voucher from all Bank Transactions.""" + for bt_name in get_reconciled_bank_transactions(doctype, docname): + bt = frappe.get_doc("Bank Transaction", bt_name) + if bt.docstatus == DocStatus.cancelled(): + continue + + modified = False + + for pe in bt.payment_entries: + if pe.payment_document == doctype and pe.payment_entry == docname: + bt.remove(pe) + modified = True + + if modified: + bt.save() diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index 4a6491d086a..7bb3f4183b4 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -2,10 +2,10 @@ # See license.txt import json -import unittest import frappe from frappe import utils +from frappe.model.docstatus import DocStatus from frappe.tests.utils import FrappeTestCase from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import ( @@ -81,6 +81,29 @@ class TestBankTransaction(FrappeTestCase): clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date") self.assertFalse(clearance_date) + def test_cancel_voucher(self): + bank_transaction = frappe.get_doc( + "Bank Transaction", + dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"), + ) + payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700)) + vouchers = json.dumps( + [ + { + "payment_doctype": "Payment Entry", + "payment_name": payment.name, + "amount": bank_transaction.unallocated_amount, + } + ] + ) + reconcile_vouchers(bank_transaction.name, vouchers) + payment.reload() + payment.cancel() + bank_transaction.reload() + self.assertEqual(bank_transaction.docstatus, DocStatus.submitted()) + self.assertEqual(bank_transaction.unallocated_amount, 1700) + self.assertEqual(bank_transaction.payment_entries, []) + # Check if ERPNext can correctly filter a linked payments based on the debit/credit amount def test_debit_credit_output(self): bank_transaction = frappe.get_doc( diff --git a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py index 181406bad7c..8d437164015 100644 --- a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py +++ b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py @@ -39,7 +39,7 @@ def test_record_generator(): ] start = 2012 - end = now_datetime().year + 5 + end = now_datetime().year + 25 for year in range(start, end): test_records.append( { diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 266154d87f0..07fb5e857ca 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry"); frappe.ui.form.on("Journal Entry", { setup: function(frm) { frm.add_fetch("bank_account", "account", "account"); - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule', "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"]; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule', "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Bank Transaction"]; }, refresh: function(frm) { diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 81ffee3f6e8..9402e3da096 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -9,7 +9,7 @@ erpnext.accounts.taxes.setup_tax_filters("Advance Taxes and Charges"); frappe.ui.form.on('Payment Entry', { onload: function(frm) { - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payment', 'Unreconcile Payment Entries']; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payment', 'Unreconcile Payment Entries', "Bank Transaction"]; if(frm.doc.__islocal) { if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 44d4d816434..a2d210dc54d 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -35,7 +35,17 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. super.onload(); // Ignore linked advances - this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Serial and Batch Bundle"]; + this.frm.ignore_doctypes_on_cancel_all = [ + "Journal Entry", + "Payment Entry", + "Purchase Invoice", + "Repost Payment Ledger", + "Repost Accounting Ledger", + "Unreconcile Payment", + "Unreconcile Payment Entries", + "Serial and Batch Bundle", + "Bank Transaction", + ]; if(!this.frm.doc.__islocal) { // show credit_to in print format diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index aa52600a889..cb0b8e1fb11 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -296,6 +296,18 @@ class PurchaseInvoice(BuyingController): self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") + self.set_percentage_received() + + def set_percentage_received(self): + total_billed_qty = 0.0 + total_received_qty = 0.0 + for row in self.items: + if row.purchase_receipt and row.pr_detail and row.received_qty: + total_billed_qty += row.qty + total_received_qty += row.received_qty + + if total_billed_qty and total_received_qty: + self.per_received = total_received_qty / total_billed_qty * 100 def validate_release_date(self): if self.release_date and getdate(nowdate()) >= getdate(self.release_date): diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json index 347cae05b72..adab54b3756 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json @@ -126,7 +126,7 @@ "fieldname": "rate", "fieldtype": "Float", "in_list_view": 1, - "label": "Rate", + "label": "Tax Rate", "oldfieldname": "rate", "oldfieldtype": "Currency" }, @@ -230,7 +230,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-08-05 20:04:36.618240", + "modified": "2024-01-14 10:04:36.618240", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Taxes and Charges", @@ -239,4 +239,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index ba2cd82516f..7cda11addc2 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -36,9 +36,19 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e var me = this; super.onload(); - this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', - 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", - 'Serial and Batch Bundle' + this.frm.ignore_doctypes_on_cancel_all = [ + "POS Invoice", + "Timesheet", + "POS Invoice Merge Log", + "POS Closing Entry", + "Journal Entry", + "Payment Entry", + "Repost Payment Ledger", + "Repost Accounting Ledger", + "Unreconcile Payment", + "Unreconcile Payment Entries", + "Serial and Batch Bundle", + "Bank Transaction", ]; if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json index e236577e118..9e0a7983b74 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json +++ b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json @@ -108,7 +108,7 @@ "fieldname": "rate", "fieldtype": "Float", "in_list_view": 1, - "label": "Rate", + "label": "Tax Rate", "oldfieldname": "rate", "oldfieldtype": "Currency" }, @@ -218,7 +218,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-10-17 13:08:17.776528", + "modified": "2024-01-14 10:08:17.776528", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Taxes and Charges", @@ -227,4 +227,4 @@ "sort_field": "modified", "sort_order": "ASC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 94f5e29f068..d9132e26147 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -16,6 +16,7 @@ from frappe.utils.data import ( date_diff, flt, get_last_day, + get_link_to_form, getdate, nowdate, ) @@ -317,6 +318,37 @@ class Subscription(Document): if self.is_new(): self.set_subscription_status() + self.validate_party_billing_currency() + + def validate_party_billing_currency(self): + """ + Subscription should be of the same currency as the Party's default billing currency or company default. + """ + if self.party: + party_billing_currency = frappe.get_cached_value( + self.party_type, self.party, "default_currency" + ) or frappe.get_cached_value("Company", self.company, "default_currency") + + plans = [x.plan for x in self.plans] + subscription_plan_currencies = frappe.db.get_all( + "Subscription Plan", filters={"name": ("in", plans)}, fields=["name", "currency"] + ) + unsupported_plans = [] + for x in subscription_plan_currencies: + if x.currency != party_billing_currency: + unsupported_plans.append("{0}".format(get_link_to_form("Subscription Plan", x.name))) + + if unsupported_plans: + unsupported_plans = [ + _( + "Below Subscription Plans are of different currency to the party default billing currency/Company currency: {0}" + ).format(frappe.bold(party_billing_currency)) + ] + unsupported_plans + + frappe.throw( + unsupported_plans, frappe.ValidationError, "Unsupported Subscription Plans", as_list=True + ) + def validate_trial_period(self) -> None: """ Runs sanity checks on trial period dates for the `Subscription` @@ -563,6 +595,8 @@ class Subscription(Document): ) and self.can_generate_new_invoice(posting_date): self.generate_invoice(posting_date=posting_date) self.update_subscription_period(add_days(self.current_invoice_end, 1)) + elif posting_date and getdate(posting_date) > getdate(self.current_invoice_end): + self.update_subscription_period() if self.cancel_at_period_end and ( getdate(posting_date) >= getdate(self.current_invoice_end) diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index 785fd04b82e..a46642ad500 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -460,11 +460,13 @@ class TestSubscription(FrappeTestCase): self.assertEqual(len(subscription.invoices), 1) def test_multi_currency_subscription(self): + party = "_Test Subscription Customer" + frappe.db.set_value("Customer", party, "default_currency", "USD") subscription = create_subscription( start_date="2018-01-01", generate_invoice_at="Beginning of the current subscription period", - plans=[{"plan": "_Test Plan Multicurrency", "qty": 1}], - party="_Test Subscription Customer", + plans=[{"plan": "_Test Plan Multicurrency", "qty": 1, "currency": "USD"}], + party=party, ) subscription.process() @@ -528,13 +530,21 @@ class TestSubscription(FrappeTestCase): def make_plans(): - create_plan(plan_name="_Test Plan Name", cost=900) - create_plan(plan_name="_Test Plan Name 2", cost=1999) + create_plan(plan_name="_Test Plan Name", cost=900, currency="INR") + create_plan(plan_name="_Test Plan Name 2", cost=1999, currency="INR") create_plan( - plan_name="_Test Plan Name 3", cost=1999, billing_interval="Day", billing_interval_count=14 + plan_name="_Test Plan Name 3", + cost=1999, + billing_interval="Day", + billing_interval_count=14, + currency="INR", ) create_plan( - plan_name="_Test Plan Name 4", cost=20000, billing_interval="Month", billing_interval_count=3 + plan_name="_Test Plan Name 4", + cost=20000, + billing_interval="Month", + billing_interval_count=3, + currency="INR", ) create_plan( plan_name="_Test Plan Multicurrency", cost=50, billing_interval="Month", currency="USD" diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.json b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json index 563df79eec7..bc1f579cf07 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.json +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json @@ -41,7 +41,8 @@ "fieldname": "currency", "fieldtype": "Link", "label": "Currency", - "options": "Currency" + "options": "Currency", + "reqd": 1 }, { "fieldname": "column_break_3", @@ -148,10 +149,11 @@ } ], "links": [], - "modified": "2021-12-10 15:24:15.794477", + "modified": "2024-01-14 17:59:34.687977", "modified_by": "Administrator", "module": "Accounts", "name": "Subscription Plan", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -193,5 +195,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py index 118d2547808..cdfa3e56d9f 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py @@ -24,7 +24,7 @@ class SubscriptionPlan(Document): billing_interval_count: DF.Int cost: DF.Currency cost_center: DF.Link | None - currency: DF.Link | None + currency: DF.Link item: DF.Link payment_gateway: DF.Link | None plan_name: DF.Data diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js index 9c356bf28ea..d6a4755d6ae 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js @@ -84,10 +84,6 @@ function get_filters() { options: budget_against_options, default: "Cost Center", reqd: 1, - get_data: function() { - console.log(this.options); - return ["Emacs", "Rocks"]; - }, on_change: function() { frappe.query_report.set_filter_value("budget_against_filter", []); frappe.query_report.refresh(); diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index 0464f99d200..0f77bb38ec2 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -376,6 +376,10 @@ class PartyLedgerSummaryReport(object): if not income_or_expense_accounts: # prevent empty 'in' condition income_or_expense_accounts.append("") + else: + # escape '%' in account name + # ignoring frappe.db.escape as it replaces single quotes with double quotes + income_or_expense_accounts = [x.replace("%", "%%") for x in income_or_expense_accounts] accounts_query = ( qb.from_(gl) diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index 0912c7270de..aed338a723b 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -368,7 +368,7 @@ def filter_invoices_based_on_dimensions(filters, query, parent_doc): dimension.document_type, filters.get(dimension.fieldname) ) fieldname = dimension.fieldname - query = query.where(parent_doc[fieldname] == filters.fieldname) + query = query.where(parent_doc[fieldname].isin(filters[fieldname])) return query diff --git a/erpnext/accounts/test/test_utils.py b/erpnext/accounts/test/test_utils.py index 3cb5e42e7ae..c439d4b1904 100644 --- a/erpnext/accounts/test/test_utils.py +++ b/erpnext/accounts/test/test_utils.py @@ -23,6 +23,10 @@ class TestUtils(unittest.TestCase): super(TestUtils, cls).setUpClass() make_test_objects("Address", ADDRESS_RECORDS) + @classmethod + def tearDownClass(cls): + frappe.db.rollback() + def test_get_party_shipping_address(self): address = get_party_shipping_address("Customer", "_Test Customer 1") self.assertEqual(address, "_Test Billing Address 2 Title-Billing") @@ -126,6 +130,38 @@ class TestUtils(unittest.TestCase): self.assertEqual(len(payment_entry.references), 1) self.assertEqual(payment_entry.difference_amount, 0) + def test_naming_series_variable_parsing(self): + """ + Tests parsing utility used by Naming Series Variable hook for FY + """ + from frappe.custom.doctype.property_setter.property_setter import make_property_setter + from frappe.utils import nowdate + + from erpnext.accounts.utils import get_fiscal_year + from erpnext.buying.doctype.supplier.test_supplier import create_supplier + + # Configure Supplier Naming in Buying Settings + frappe.db.set_default("supp_master_name", "Auto Name") + + # Configure Autoname in Supplier DocType + make_property_setter( + "Supplier", None, "naming_rule", "Expression", "Data", for_doctype="Doctype" + ) + make_property_setter( + "Supplier", None, "autoname", "SUP-.FY.-.#####", "Data", for_doctype="Doctype" + ) + + fiscal_year = get_fiscal_year(nowdate())[0] + + # Create Supplier + supplier = create_supplier() + + # Check Naming Series in generated Supplier ID + doc_name = supplier.name.split("-") + self.assertEqual(len(doc_name), 3) + self.assertSequenceEqual(doc_name[0:2], ("SUP", fiscal_year)) + frappe.db.set_default("supp_master_name", "Supplier Name") + ADDRESS_RECORDS = [ { diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index a34282eef18..8f899c3e764 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1263,7 +1263,7 @@ def get_autoname_with_number(number_value, doc_title, company): def parse_naming_series_variable(doc, variable): if variable == "FY": if doc: - date = doc.get("posting_date") or doc.get("transaction_date") + date = doc.get("posting_date") or doc.get("transaction_date") or getdate() company = doc.get("company") else: date = getdate() diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index d0c9350d777..39a0867d984 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -202,8 +202,7 @@ "fieldname": "purchase_date", "fieldtype": "Date", "label": "Purchase Date", - "mandatory_depends_on": "eval:!doc.is_existing_asset", - "read_only": 1, + "mandatory_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset", "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset" }, { @@ -590,7 +589,7 @@ "link_fieldname": "target_asset" } ], - "modified": "2024-01-05 17:36:53.131512", + "modified": "2024-01-15 17:35:49.226603", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index a7e6ae9afbd..73572499f23 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -162,6 +162,7 @@ class Asset(AccountsController): def on_cancel(self): self.validate_cancellation() self.cancel_movement_entries() + self.cancel_capitalization() self.delete_depreciation_entries() cancel_asset_depr_schedules(self) self.set_status() @@ -517,6 +518,16 @@ class Asset(AccountsController): movement = frappe.get_doc("Asset Movement", movement.get("name")) movement.cancel() + def cancel_capitalization(self): + asset_capitalization = frappe.db.get_value( + "Asset Capitalization", + {"target_asset": self.name, "docstatus": 1, "entry_type": "Capitalization"}, + ) + + if asset_capitalization: + asset_capitalization = frappe.get_doc("Asset Capitalization", asset_capitalization) + asset_capitalization.cancel() + def delete_depreciation_entries(self): if self.calculate_depreciation: for row in self.get("finance_books"): @@ -1027,6 +1038,8 @@ def is_cwip_accounting_enabled(asset_category): @frappe.whitelist() def get_asset_value_after_depreciation(asset_name, finance_book=None): asset = frappe.get_doc("Asset", asset_name) + if not asset.calculate_depreciation: + return flt(asset.value_after_depreciation) return asset.get_value_after_depreciation(finance_book) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 66930c0e7ce..dbb18b543f1 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -19,6 +19,7 @@ from frappe.utils import ( ) from frappe.utils.user import get_users_with_role +import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_checks_for_pl_and_bs_accounts, ) @@ -522,6 +523,13 @@ def depreciate_asset(asset_doc, date, notes): make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date) + cancel_depreciation_entries(asset_doc, date) + + +@erpnext.allow_regional +def cancel_depreciation_entries(asset_doc, date): + pass + def reset_depreciation_schedule(asset_doc, date, notes): if not asset_doc.calculate_depreciation: diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index dc80aa5c5c7..25d010500f6 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -891,7 +891,7 @@ class TestDepreciationMethods(AssetSetup): ["2030-12-31", 28630.14, 28630.14], ["2031-12-31", 35684.93, 64315.07], ["2032-12-31", 17842.46, 82157.53], - ["2033-06-06", 5342.47, 87500.0], + ["2033-06-06", 5342.46, 87499.99], ] schedules = [ @@ -1003,7 +1003,7 @@ class TestDepreciationBasics(AssetSetup): asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active") depreciation_amount = get_depreciation_amount( - asset_depr_schedule_doc, asset, 100000, asset.finance_books[0] + asset_depr_schedule_doc, asset, 100000, 100000, asset.finance_books[0] ) self.assertEqual(depreciation_amount, 30000) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js index be78d9ebdc2..2f0de979392 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -21,10 +21,10 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s this.show_stock_ledger(); } - if (this.frm.doc.stock_items && !this.frm.doc.stock_items.length && this.frm.doc.target_asset && this.frm.doc.capitalization_method === "Choose a WIP composite asset") { - this.set_consumed_stock_items_tagged_to_wip_composite_asset(this.frm.doc.target_asset); - this.get_target_asset_details(); - } + // if (this.frm.doc.stock_items && !this.frm.doc.stock_items.length && this.frm.doc.target_asset && this.frm.doc.capitalization_method === "Choose a WIP composite asset") { + // this.set_consumed_stock_items_tagged_to_wip_composite_asset(this.frm.doc.target_asset); + // this.get_target_asset_details(); + // } } setup_queries() { @@ -143,13 +143,20 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s }, callback: function (r) { if (!r.exc && r.message) { - me.frm.clear_table("stock_items"); - - for (let item of r.message) { - me.frm.add_child("stock_items", item); + if(r.message[0] && r.message[0].length) { + me.frm.clear_table("stock_items"); + for (let item of r.message[0]) { + me.frm.add_child("stock_items", item); + } + refresh_field("stock_items"); + } + if (r.message[1] && r.message[1].length) { + me.frm.clear_table("asset_items"); + for (let item of r.message[1]) { + me.frm.add_child("asset_items", item); + } + me.frm.refresh_field("asset_items"); } - - refresh_field("stock_items"); me.calculate_totals(); } diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 66997ca59cc..cad74df51e1 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -136,11 +136,19 @@ class AssetCapitalization(StockController): "Stock Ledger Entry", "Repost Item Valuation", "Serial and Batch Bundle", + "Asset", ) + self.cancel_target_asset() self.update_stock_ledger() self.make_gl_entries() self.restore_consumed_asset_items() + def cancel_target_asset(self): + if self.entry_type == "Capitalization" and self.target_asset: + asset_doc = frappe.get_doc("Asset", self.target_asset) + if asset_doc.docstatus == 1: + asset_doc.cancel() + def set_title(self): self.title = self.target_asset_name or self.target_item_name or self.target_item_code @@ -881,7 +889,6 @@ def get_consumed_asset_details(args): out.cost_center = get_default_cost_center( args, item_defaults, item_group_defaults, brand_defaults ) - return out @@ -929,10 +936,27 @@ def get_items_tagged_to_wip_composite_asset(asset): "qty", "valuation_rate", "amount", + "is_fixed_asset", + "parent", ] pr_items = frappe.get_all( - "Purchase Receipt Item", filters={"wip_composite_asset": asset}, fields=fields + "Purchase Receipt Item", filters={"wip_composite_asset": asset, "docstatus": 1}, fields=fields ) - return pr_items + stock_items = [] + asset_items = [] + for d in pr_items: + if not d.is_fixed_asset: + stock_items.append(frappe._dict(d)) + else: + asset_details = frappe.db.get_value( + "Asset", + {"item_code": d.item_code, "purchase_receipt": d.parent}, + ["name as asset", "asset_name"], + as_dict=1, + ) + d.update(asset_details) + asset_items.append(frappe._dict(d)) + + return stock_items, asset_items diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py index 4c94be53203..146c03e8c3f 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -7,6 +7,7 @@ from frappe.model.document import Document from frappe.utils import ( add_days, add_months, + add_years, cint, date_diff, flt, @@ -18,6 +19,7 @@ from frappe.utils import ( ) import erpnext +from erpnext.accounts.utils import get_fiscal_year class AssetDepreciationSchedule(Document): @@ -283,12 +285,20 @@ class AssetDepreciationSchedule(Document): depreciation_amount = 0 number_of_pending_depreciations = final_number_of_depreciations - start - + yearly_opening_wdv = value_after_depreciation + current_fiscal_year_end_date = None for n in range(start, final_number_of_depreciations): # If depreciation is already completed (for double declining balance) if skip_row: continue + schedule_date = add_months(row.depreciation_start_date, n * cint(row.frequency_of_depreciation)) + if not current_fiscal_year_end_date: + current_fiscal_year_end_date = get_fiscal_year(row.depreciation_start_date)[2] + elif getdate(schedule_date) > getdate(current_fiscal_year_end_date): + current_fiscal_year_end_date = add_years(current_fiscal_year_end_date, 1) + yearly_opening_wdv = value_after_depreciation + if n > 0 and len(self.get("depreciation_schedule")) > n - 1: prev_depreciation_amount = self.get("depreciation_schedule")[n - 1].depreciation_amount else: @@ -298,6 +308,7 @@ class AssetDepreciationSchedule(Document): self, asset_doc, value_after_depreciation, + yearly_opening_wdv, row, n, prev_depreciation_amount, @@ -341,10 +352,7 @@ class AssetDepreciationSchedule(Document): n == 0 and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata) and not self.opening_accumulated_depreciation - and get_updated_rate_of_depreciation_for_wdv_and_dd( - asset_doc, value_after_depreciation, row, False - ) - == row.rate_of_depreciation + and not self.flags.wdv_it_act_applied ): from_date = add_days( asset_doc.available_for_use_date, -1 @@ -404,8 +412,9 @@ class AssetDepreciationSchedule(Document): if not depreciation_amount: continue - value_after_depreciation -= flt( - depreciation_amount, asset_doc.precision("gross_purchase_amount") + value_after_depreciation = flt( + value_after_depreciation - flt(depreciation_amount), + asset_doc.precision("gross_purchase_amount"), ) # Adjust depreciation amount in the last period based on the expected value after useful life @@ -585,6 +594,7 @@ def get_depreciation_amount( asset_depr_schedule, asset, depreciable_value, + yearly_opening_wdv, fb_row, schedule_idx=0, prev_depreciation_amount=0, @@ -596,26 +606,18 @@ def get_depreciation_amount( asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations ) else: - rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd( - asset, depreciable_value, fb_row - ) return get_wdv_or_dd_depr_amount( + asset, + fb_row, depreciable_value, - rate_of_depreciation, - fb_row.frequency_of_depreciation, + yearly_opening_wdv, schedule_idx, prev_depreciation_amount, has_wdv_or_dd_non_yearly_pro_rata, + asset_depr_schedule, ) -@erpnext.allow_regional -def get_updated_rate_of_depreciation_for_wdv_and_dd( - asset, depreciable_value, fb_row, show_msg=True -): - return fb_row.rate_of_depreciation - - def get_straight_line_or_manual_depr_amount( asset_depr_schedule, asset, row, schedule_idx, number_of_pending_depreciations ): @@ -751,30 +753,57 @@ def get_asset_shift_factors_map(): return dict(frappe.db.get_all("Asset Shift Factor", ["shift_name", "shift_factor"], as_list=True)) +@erpnext.allow_regional def get_wdv_or_dd_depr_amount( + asset, + fb_row, depreciable_value, - rate_of_depreciation, - frequency_of_depreciation, + yearly_opening_wdv, schedule_idx, prev_depreciation_amount, has_wdv_or_dd_non_yearly_pro_rata, + asset_depr_schedule, ): - if cint(frequency_of_depreciation) == 12: - return flt(depreciable_value) * (flt(rate_of_depreciation) / 100) + return get_default_wdv_or_dd_depr_amount( + asset, + fb_row, + depreciable_value, + schedule_idx, + prev_depreciation_amount, + has_wdv_or_dd_non_yearly_pro_rata, + asset_depr_schedule, + ) + + +def get_default_wdv_or_dd_depr_amount( + asset, + fb_row, + depreciable_value, + schedule_idx, + prev_depreciation_amount, + has_wdv_or_dd_non_yearly_pro_rata, + asset_depr_schedule, +): + if cint(fb_row.frequency_of_depreciation) == 12: + return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100) else: if has_wdv_or_dd_non_yearly_pro_rata: if schedule_idx == 0: - return flt(depreciable_value) * (flt(rate_of_depreciation) / 100) - elif schedule_idx % (12 / cint(frequency_of_depreciation)) == 1: + return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100) + elif schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 1: return ( - flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200) + flt(depreciable_value) + * flt(fb_row.frequency_of_depreciation) + * (flt(fb_row.rate_of_depreciation) / 1200) ) else: return prev_depreciation_amount else: - if schedule_idx % (12 / cint(frequency_of_depreciation)) == 0: + if schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 0: return ( - flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200) + flt(depreciable_value) + * flt(fb_row.frequency_of_depreciation) + * (flt(fb_row.rate_of_depreciation) / 1200) ) else: return prev_depreciation_amount diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json index 25ae7a492c8..ba5b5f87826 100644 --- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json +++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json @@ -94,7 +94,6 @@ }, { "default": "0", - "depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"", "fieldname": "daily_prorata_based", "fieldtype": "Check", "label": "Depreciate based on daily pro-rata" @@ -110,7 +109,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-29 00:57:07.579777", + "modified": "2023-12-29 08:49:39.876439", "modified_by": "Administrator", "module": "Assets", "name": "Asset Finance Book", diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index b05de7d0b2e..ddcbd555ae8 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -214,7 +214,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-01-05 15:26:02.320942", + "modified": "2024-01-12 16:42:01.894346", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", @@ -240,39 +240,24 @@ "write": 1 }, { - "email": 1, - "print": 1, "read": 1, - "role": "Accounts User", - "share": 1 + "role": "Accounts User" }, { - "email": 1, - "print": 1, "read": 1, - "role": "Accounts Manager", - "share": 1 + "role": "Accounts Manager" }, { - "email": 1, - "print": 1, "read": 1, - "role": "Stock Manager", - "share": 1 + "role": "Stock Manager" }, { - "email": 1, - "print": 1, "read": 1, - "role": "Stock User", - "share": 1 + "role": "Stock User" }, { - "email": 1, - "print": 1, "read": 1, - "role": "Purchase User", - "share": 1 + "role": "Purchase User" } ], "sort_field": "modified", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 7986c3d4809..6b1be396d2c 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1414,11 +1414,16 @@ class AccountsController(TransactionBase): reconcile_against_document(lst) def on_cancel(self): + from erpnext.accounts.doctype.bank_transaction.bank_transaction import ( + remove_from_bank_transaction, + ) from erpnext.accounts.utils import ( cancel_exchange_gain_loss_journal, unlink_ref_doc_from_payment_entries, ) + remove_from_bank_transaction(self.doctype, self.name) + if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]: # Cancel Exchange Gain/Loss Journal before unlinking cancel_exchange_gain_loss_journal(self) @@ -1947,7 +1952,7 @@ class AccountsController(TransactionBase): self.remove(item) def set_payment_schedule(self): - if self.doctype == "Sales Invoice" and self.is_pos: + if (self.doctype == "Sales Invoice" and self.is_pos) or self.get("is_opening") == "Yes": self.payment_terms_template = "" return @@ -2130,7 +2135,7 @@ class AccountsController(TransactionBase): ) def validate_payment_schedule_amount(self): - if self.doctype == "Sales Invoice" and self.is_pos: + if (self.doctype == "Sales Invoice" and self.is_pos) or self.get("is_opening") == "Yes": return party_account_currency = self.get("party_account_currency") diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index e858820965e..604a25127cf 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -6,10 +6,12 @@ import json from collections import OrderedDict, defaultdict import frappe -from frappe import scrub +from frappe import qb, scrub from frappe.desk.reportview import get_filters_cond, get_match_cond -from frappe.query_builder.functions import Concat, Sum +from frappe.query_builder import Criterion, CustomFunction +from frappe.query_builder.functions import Concat, Locate, Sum from frappe.utils import nowdate, today, unique +from pypika import Order import erpnext from erpnext.stock.get_item_details import _get_item_tax_template @@ -339,37 +341,46 @@ 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 = "" + proj = qb.DocType("Project") + qb_filter_and_conditions = [] + qb_filter_or_conditions = [] + ifelse = CustomFunction("IF", ["condition", "then", "else"]) + if filters and filters.get("customer"): - cond = """(`tabProject`.customer = %s or - ifnull(`tabProject`.customer,"")="") and""" % ( - frappe.db.escape(filters.get("customer")) - ) + qb_filter_and_conditions.append(proj.customer == filters.get("customer")) + + qb_filter_and_conditions.append(proj.status.notin(["Completed", "Cancelled"])) + + q = qb.from_(proj) 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]) + for x in fields: + q = q.select(proj[x]) - return frappe.db.sql( - """select {fields} from `tabProject` - where - `tabProject`.status not in ('Completed', 'Cancelled') - and {cond} {scond} {match_cond} - order by - (case when locate(%(_txt)s, `tabProject`.name) > 0 then locate(%(_txt)s, `tabProject`.name) else 99999 end), - `tabProject`.idx desc, - `tabProject`.name asc - limit {page_len} offset {start}""".format( - fields=", ".join(["`tabProject`.{0}".format(f) for f in fields]), - cond=cond, - scond=searchfields, - match_cond=get_match_cond(doctype), - start=start, - page_len=page_len, - ), - {"txt": "%{0}%".format(txt), "_txt": txt.replace("%", "")}, - ) + # don't consider 'customer' and 'status' fields for pattern search, as they must be exactly matched + searchfields = [ + x for x in frappe.get_meta(doctype).get_search_fields() if x not in ["customer", "status"] + ] + + # pattern search + if txt: + for x in searchfields: + qb_filter_or_conditions.append(proj[x].like(f"%{txt}%")) + + q = q.where(Criterion.all(qb_filter_and_conditions)).where(Criterion.any(qb_filter_or_conditions)) + + # ordering + if txt: + # project_name containing search string 'txt' will be given higher precedence + q = q.orderby(ifelse(Locate(txt, proj.project_name) > 0, Locate(txt, proj.project_name), 99999)) + q = q.orderby(proj.idx, order=Order.desc).orderby(proj.name) + + if page_len: + q = q.limit(page_len) + + if start: + q = q.offset(start) + return q.run() @frappe.whitelist() @@ -416,23 +427,14 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): meta = frappe.get_meta(doctype, cached=True) searchfields = meta.get_search_fields() - query = get_batches_from_stock_ledger_entries(searchfields, txt, filters) - bundle_query = get_batches_from_serial_and_batch_bundle(searchfields, txt, filters) - - data = ( - frappe.qb.from_((query) + (bundle_query)) - .select("batch_no", "qty", "manufacturing_date", "expiry_date") - .offset(start) - .limit(page_len) + batches = get_batches_from_stock_ledger_entries(searchfields, txt, filters, start, page_len) + batches.extend( + get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start, page_len) ) - for field in searchfields: - data = data.select(field) + filtered_batches = get_filterd_batches(batches) - data = data.run() - data = get_filterd_batches(data) - - return data + return filtered_batches def get_filterd_batches(data): @@ -452,7 +454,7 @@ def get_filterd_batches(data): return filterd_batch -def get_batches_from_stock_ledger_entries(searchfields, txt, filters): +def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, page_len=100): stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry") batch_table = frappe.qb.DocType("Batch") @@ -474,6 +476,8 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters): & (stock_ledger_entry.batch_no.isnotnull()) ) .groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse) + .offset(start) + .limit(page_len) ) query = query.select( @@ -488,16 +492,16 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters): query = query.select(batch_table[field]) if txt: - txt_condition = batch_table.name.like(txt) + txt_condition = batch_table.name.like("%{0}%".format(txt)) for field in searchfields + ["name"]: - txt_condition |= batch_table[field].like(txt) + txt_condition |= batch_table[field].like("%{0}%".format(txt)) query = query.where(txt_condition) - return query + return query.run(as_list=1) or [] -def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters): +def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0, page_len=100): bundle = frappe.qb.DocType("Serial and Batch Entry") stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry") batch_table = frappe.qb.DocType("Batch") @@ -522,6 +526,8 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters): & (stock_ledger_entry.serial_and_batch_bundle.isnotnull()) ) .groupby(bundle.batch_no, bundle.warehouse) + .offset(start) + .limit(page_len) ) bundle_query = bundle_query.select( @@ -536,13 +542,13 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters): bundle_query = bundle_query.select(batch_table[field]) if txt: - txt_condition = batch_table.name.like(txt) + txt_condition = batch_table.name.like("%{0}%".format(txt)) for field in searchfields + ["name"]: - txt_condition |= batch_table[field].like(txt) + txt_condition |= batch_table[field].like("%{0}%".format(txt)) bundle_query = bundle_query.where(txt_condition) - return bundle_query + return bundle_query.run(as_list=1) @frappe.whitelist() diff --git a/erpnext/controllers/tests/test_queries.py b/erpnext/controllers/tests/test_queries.py index 60d1733021c..3a3bc1cd725 100644 --- a/erpnext/controllers/tests/test_queries.py +++ b/erpnext/controllers/tests/test_queries.py @@ -68,7 +68,7 @@ class TestQueries(unittest.TestCase): self.assertGreaterEqual(len(query(txt="_Test Item Home Desktop Manufactured")), 1) def test_project_query(self): - query = add_default_params(queries.get_project_name, "BOM") + query = add_default_params(queries.get_project_name, "Project") self.assertGreaterEqual(len(query(txt="_Test Project")), 1) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index cd92263543b..c9c474db7f0 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -173,7 +173,7 @@ frappe.ui.form.on('Production Plan', { method: "set_status", freeze: true, doc: frm.doc, - args: {close : close}, + args: {close : close, update_bin: true}, callback: function() { frm.reload_doc(); } diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 3943b13b827..51658a03a7c 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -579,7 +579,7 @@ class ProductionPlan(Document): frappe.delete_doc("Work Order", d.name) @frappe.whitelist() - def set_status(self, close=None): + def set_status(self, close=None, update_bin=False): self.status = {0: "Draft", 1: "Submitted", 2: "Cancelled"}.get(self.docstatus) if close: @@ -599,7 +599,7 @@ class ProductionPlan(Document): if close is not None: self.db_set("status", self.status) - if self.docstatus == 1 and self.status != "Completed": + if update_bin and self.docstatus == 1 and self.status != "Completed": self.update_bin_qty() def update_ordered_status(self): diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index fedeb7a4777..1c748a809b7 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1486,14 +1486,14 @@ class TestProductionPlan(FrappeTestCase): before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) pln.reload() - pln.set_status(close=True) + pln.set_status(close=True, update_bin=True) bin_name = get_or_make_bin(rm_item, rm_warehouse) after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) self.assertAlmostEqual(after_qty, before_qty - 10) pln.reload() - pln.set_status(close=False) + pln.set_status(close=False, update_bin=True) bin_name = get_or_make_bin(rm_item, rm_warehouse) after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 598167b3370..de46271e474 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -21,7 +21,7 @@ $.extend(erpnext, { }, toggle_naming_series: function() { - if(cur_frm.fields_dict.naming_series) { + if(cur_frm && cur_frm.fields_dict.naming_series) { cur_frm.toggle_display("naming_series", cur_frm.doc.__islocal?true:false); } }, diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js index bb23f1512b9..3f70c09f667 100644 --- a/erpnext/public/js/utils/dimension_tree_filter.js +++ b/erpnext/public/js/utils/dimension_tree_filter.js @@ -16,6 +16,8 @@ erpnext.accounts.dimensions = { }, callback: function(r) { me.accounting_dimensions = r.message[0]; + // Ignoring "Project" as it is already handled specifically in Sales Order and Delivery Note + me.accounting_dimensions = me.accounting_dimensions.filter(x=>{return x.document_type != "Project"}); me.default_dimensions = r.message[1]; me.setup_filters(frm, doctype); } diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index b206e3fe336..56c745c00ac 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -94,6 +94,9 @@ frappe.ui.form.on("Sales Order", { frm.set_value("reserve_stock", 0); frm.set_df_property("reserve_stock", "read_only", 1); frm.set_df_property("reserve_stock", "hidden", 1); + frm.fields_dict.items.grid.update_docfield_property('reserve_stock', 'hidden', 1); + frm.fields_dict.items.grid.update_docfield_property('reserve_stock', 'default', 0); + frm.fields_dict.items.grid.update_docfield_property('reserve_stock', 'read_only', 1); } }) } diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 95423612c85..5ef2c50146a 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -200,6 +200,7 @@ class SalesOrder(SellingController): self.validate_for_items() self.validate_warehouse() self.validate_drop_ship() + self.validate_reserved_stock() self.validate_serial_no_based_delivery() validate_against_blanket_order(self) validate_inter_company_party( @@ -660,6 +661,17 @@ class SalesOrder(SellingController): ).format(item.item_code) ) + def validate_reserved_stock(self): + """Clean reserved stock flag for non-stock Item""" + + enable_stock_reservation = frappe.db.get_single_value( + "Stock Settings", "enable_stock_reservation" + ) + + for item in self.items: + if item.reserve_stock and (not enable_stock_reservation or not cint(item.is_stock_item)): + item.reserve_stock = 0 + def has_unreserved_stock(self) -> bool: """Returns True if there is any unreserved item in the Sales Order.""" diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index d4ccfc4753d..87aeeac3683 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -10,6 +10,7 @@ "item_code", "customer_item_code", "ensure_delivery_based_on_produced_serial_no", + "is_stock_item", "reserve_stock", "col_break1", "delivery_date", @@ -867,6 +868,7 @@ { "allow_on_submit": 1, "default": "1", + "depends_on": "eval:doc.is_stock_item", "fieldname": "reserve_stock", "fieldtype": "Check", "label": "Reserve Stock", @@ -891,6 +893,16 @@ "label": "Production Plan Qty", "no_copy": 1, "read_only": 1 + }, + { + "default": "0", + "fetch_from": "item_code.is_stock_item", + "fieldname": "is_stock_item", + "fieldtype": "Check", + "hidden": 1, + "label": "Is Stock Item", + "print_hide": 1, + "report_hide": 1 } ], "idx": 1, diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py index 1afee41ac33..6381234a0b7 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/holiday_list.py @@ -87,7 +87,7 @@ class HolidayList(Document): for holiday_date, holiday_name in country_holidays( self.country, subdiv=self.subdivision, - years=[from_date.year, to_date.year], + years=list(range(from_date.year, to_date.year + 1)), language=frappe.local.lang, ).items(): if holiday_date in existing_holidays: diff --git a/erpnext/setup/doctype/holiday_list/test_holiday_list.py b/erpnext/setup/doctype/holiday_list/test_holiday_list.py index 7eeb27d864e..c0e71f5d254 100644 --- a/erpnext/setup/doctype/holiday_list/test_holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/test_holiday_list.py @@ -48,17 +48,58 @@ class TestHolidayList(unittest.TestCase): def test_local_holidays(self): holiday_list = frappe.new_doc("Holiday List") - holiday_list.from_date = "2023-04-01" - holiday_list.to_date = "2023-04-30" + holiday_list.from_date = "2022-01-01" + holiday_list.to_date = "2024-12-31" holiday_list.country = "DE" holiday_list.subdivision = "SN" holiday_list.get_local_holidays() - holidays = [holiday.holiday_date for holiday in holiday_list.holidays] - self.assertNotIn(date(2023, 1, 1), holidays) + holidays = holiday_list.get_holidays() + self.assertIn(date(2022, 1, 1), holidays) + self.assertIn(date(2022, 4, 15), holidays) + self.assertIn(date(2022, 4, 18), holidays) + self.assertIn(date(2022, 5, 1), holidays) + self.assertIn(date(2022, 5, 26), holidays) + self.assertIn(date(2022, 6, 6), holidays) + self.assertIn(date(2022, 10, 3), holidays) + self.assertIn(date(2022, 10, 31), holidays) + self.assertIn(date(2022, 11, 16), holidays) + self.assertIn(date(2022, 12, 25), holidays) + self.assertIn(date(2022, 12, 26), holidays) + self.assertIn(date(2023, 1, 1), holidays) self.assertIn(date(2023, 4, 7), holidays) self.assertIn(date(2023, 4, 10), holidays) - self.assertNotIn(date(2023, 5, 1), holidays) + self.assertIn(date(2023, 5, 1), holidays) + self.assertIn(date(2023, 5, 18), holidays) + self.assertIn(date(2023, 5, 29), holidays) + self.assertIn(date(2023, 10, 3), holidays) + self.assertIn(date(2023, 10, 31), holidays) + self.assertIn(date(2023, 11, 22), holidays) + self.assertIn(date(2023, 12, 25), holidays) + self.assertIn(date(2023, 12, 26), holidays) + self.assertIn(date(2024, 1, 1), holidays) + self.assertIn(date(2024, 3, 29), holidays) + self.assertIn(date(2024, 4, 1), holidays) + self.assertIn(date(2024, 5, 1), holidays) + self.assertIn(date(2024, 5, 9), holidays) + self.assertIn(date(2024, 5, 20), holidays) + self.assertIn(date(2024, 10, 3), holidays) + self.assertIn(date(2024, 10, 31), holidays) + self.assertIn(date(2024, 11, 20), holidays) + self.assertIn(date(2024, 12, 25), holidays) + self.assertIn(date(2024, 12, 26), holidays) + + # check some random dates that should not be local holidays + self.assertNotIn(date(2022, 1, 2), holidays) + self.assertNotIn(date(2023, 4, 16), holidays) + self.assertNotIn(date(2024, 4, 19), holidays) + self.assertNotIn(date(2022, 5, 2), holidays) + self.assertNotIn(date(2023, 5, 27), holidays) + self.assertNotIn(date(2024, 6, 7), holidays) + self.assertNotIn(date(2022, 10, 4), holidays) + self.assertNotIn(date(2023, 10, 30), holidays) + self.assertNotIn(date(2024, 11, 17), holidays) + self.assertNotIn(date(2022, 12, 24), holidays) def test_localized_country_names(self): lang = frappe.local.lang diff --git a/erpnext/stock/doctype/bin/bin.json b/erpnext/stock/doctype/bin/bin.json index 10d95113574..39e0917ce69 100644 --- a/erpnext/stock/doctype/bin/bin.json +++ b/erpnext/stock/doctype/bin/bin.json @@ -186,7 +186,7 @@ "idx": 1, "in_create": 1, "links": [], - "modified": "2023-11-01 16:51:17.079107", + "modified": "2024-01-16 15:11:46.140323", "modified_by": "Administrator", "module": "Stock", "name": "Bin", @@ -213,6 +213,21 @@ "read": 1, "report": 1, "role": "Stock User" + }, + { + "read": 1, + "report": 1, + "role": "Stock Manager" + }, + { + "read": 1, + "report": 1, + "role": "Purchase Manager" + }, + { + "read": 1, + "report": 1, + "role": "Sales Manager" } ], "quick_entry": 1, diff --git a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.json b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.json index 225da6d15ec..acb96e7a2f5 100644 --- a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.json +++ b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.json @@ -103,15 +103,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "amended_from", - "fieldtype": "Link", - "label": "Amended From", - "no_copy": 1, - "options": "Closing Stock Balance", - "print_hide": 1, - "read_only": 1 - }, { "fieldname": "include_uom", "fieldtype": "Link", @@ -122,7 +113,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-05-17 11:46:04.448220", + "modified": "2023-05-18 11:46:04.448220", "modified_by": "Administrator", "module": "Stock", "name": "Closing Stock Balance", diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 6e810e5987e..b964c843a72 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -600,26 +600,12 @@ $.extend(erpnext.item, { } }); } else { - frappe.call({ - method: "frappe.client.get", - args: { - doctype: "Item Attribute", - name: d.attribute - } - }).then((r) => { - if(r.message) { - const from = r.message.from_range; - const to = r.message.to_range; - const increment = r.message.increment; - - let values = []; - for(var i = from; i <= to; i = flt(i + increment, 6)) { - values.push(i); - } - attr_val_fields[d.attribute] = values; - resolve(); - } - }); + let values = []; + for(var i = d.from_range; i <= d.to_range; i = flt(i + d.increment, 6)) { + values.push(i); + } + attr_val_fields[d.attribute] = values; + resolve(); } }); diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 517cc0342a8..970949106c2 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1228,6 +1228,7 @@ def make_purchase_invoice(source_name, target_doc=None, args=None): "field_map": { "name": "pr_detail", "parent": "purchase_receipt", + "qty": "received_qty", "purchase_order_item": "po_detail", "purchase_order": "purchase_order", "is_fixed_asset": "is_fixed_asset", diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 4464ea820e4..8816341065a 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -111,6 +111,9 @@ class QualityInspection(Document): def on_cancel(self): self.update_qc_reference() + def on_trash(self): + self.update_qc_reference() + def validate_readings_status_mandatory(self): for reading in self.readings: if not reading.status: diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index f5f8c3afd16..c423ca84ffd 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -250,6 +250,33 @@ class TestQualityInspection(FrappeTestCase): qa.delete() dn.delete() + def test_delete_quality_inspection_linked_with_stock_entry(self): + item_code = create_item("_Test Cicuular Dependecy Item with QA").name + + se = make_stock_entry( + item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100, do_not_submit=True + ) + + se.inspection_required = 1 + se.save() + + qa = create_quality_inspection( + item_code=item_code, reference_type="Stock Entry", reference_name=se.name, do_not_submit=True + ) + + se.reload() + se.items[0].quality_inspection = qa.name + se.save() + + qa.delete() + + se.reload() + + qc = se.items[0].quality_inspection + self.assertFalse(qc) + + se.delete() + def create_quality_inspection(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 564c380017b..d45296f1310 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -104,7 +104,8 @@ "in_standard_filter": 1, "label": "Stock Entry Type", "options": "Stock Entry Type", - "reqd": 1 + "reqd": 1, + "search_index": 1 }, { "depends_on": "eval:doc.purpose == 'Material Transfer'", @@ -546,7 +547,8 @@ "label": "Job Card", "options": "Job Card", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "amended_from", @@ -679,7 +681,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-06-19 18:23:40.748114", + "modified": "2024-01-12 11:56:58.644882", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 0c08fb2ed3e..bd84a2b0d99 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -561,7 +561,8 @@ "label": "Job Card Item", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "default": "0", @@ -589,7 +590,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-05-09 12:41:18.210864", + "modified": "2024-01-12 11:56:04.626103", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py index b2fc1f52e1a..a6dd0faadfc 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py @@ -20,7 +20,6 @@ class StockEntryDetail(Document): allow_alternative_item: DF.Check allow_zero_valuation_rate: DF.Check amount: DF.Currency - attach_something_here: DF.Attach | None barcode: DF.Data | None basic_amount: DF.Currency basic_rate: DF.Currency diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json index d9cbf957104..fc4ae6a5fab 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -10,8 +10,9 @@ "has_item_scanned", "item_code", "item_name", - "warehouse", + "item_group", "column_break_6", + "warehouse", "qty", "valuation_rate", "amount", @@ -52,6 +53,7 @@ "reqd": 1 }, { + "fetch_from": "item_code.item_name", "fieldname": "item_name", "fieldtype": "Data", "in_global_search": 1, @@ -213,11 +215,18 @@ "fieldname": "add_serial_batch_bundle", "fieldtype": "Button", "label": "Add Serial / Batch No" + }, + { + "fetch_from": "item_code.item_group", + "fieldname": "item_group", + "fieldtype": "Link", + "label": "Item Group", + "options": "Item Group" } ], "istable": 1, "links": [], - "modified": "2023-11-02 15:47:07.929550", + "modified": "2024-01-14 10:04:23.599951", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation Item",