diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 256bde5c719..86c3a8a9336 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -2,6 +2,7 @@ # For license information, please see license.txt import frappe +from frappe.model.docstatus import DocStatus from frappe.utils import flt from erpnext.controllers.status_updater import StatusUpdater @@ -68,7 +69,7 @@ class BankTransaction(StatusUpdater): "payment_entry": voucher["payment_name"], "allocated_amount": 0.0, # Temporary } - child = self.append("payment_entries", pe) + self.append("payment_entries", pe) added = True # runs on_update_after_submit @@ -393,3 +394,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 f900e0775ce..eb0dc74825d 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/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index c59643280e8..f86320d917a 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', "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", "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 d5eeae5ab34..6b3f46d3833 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "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 3b77614607a..0add6c57da6 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -31,7 +31,7 @@ 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"]; + this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "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 4a9153a1ebc..2af11159298 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -131,6 +131,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 b236b447d57..1e8428d9e29 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -34,7 +34,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e 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"]; + 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Bank Transaction"]; if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { // show debit_to in print format diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 0c337aa62ec..83b7da94110 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -138,6 +138,7 @@ "loyalty_amount", "column_break_77", "loyalty_program", + "dont_create_loyalty_points", "loyalty_redemption_account", "loyalty_redemption_cost_center", "contact_and_address_tab", @@ -1039,8 +1040,7 @@ "label": "Loyalty Program", "no_copy": 1, "options": "Loyalty Program", - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "allow_on_submit": 1, @@ -2153,6 +2153,14 @@ "fieldname": "update_billed_amount_in_delivery_note", "fieldtype": "Check", "label": "Update Billed Amount in Delivery Note" + }, + { + "default": "0", + "depends_on": "loyalty_program", + "fieldname": "dont_create_loyalty_points", + "fieldtype": "Check", + "label": "Don't Create Loyalty Points", + "no_copy": 1 } ], "icon": "fa fa-file-text", @@ -2165,7 +2173,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2023-11-23 16:56:29.679499", + "modified": "2024-01-02 17:25:46.027523", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 63a576b5ba8..66d9022d08f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -300,7 +300,12 @@ class SalesInvoice(SellingController): update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) # create the loyalty point ledger entry if the customer is enrolled in any loyalty program - if not self.is_return and not self.is_consolidated and self.loyalty_program: + if ( + not self.is_return + and not self.is_consolidated + and self.loyalty_program + and not self.dont_create_loyalty_points + ): self.make_loyalty_point_entry() elif ( self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program 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/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js index ca1cca13115..c9ddde9b5fb 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/consolidated_financial_statement/consolidated_financial_statement.js b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js index b7d25c41982..cb1083b06c1 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js @@ -129,7 +129,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { } value = default_formatter(value, row, column, data); - if (!data.parent_account) { + if (data && !data.parent_account) { value = $(`${value}`); var $value = $(value).css("font-weight", "bold"); 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 b2222c2d6a7..2da6d18f005 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/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py index 4eb135b0487..f745c87a00a 100644 --- a/erpnext/accounts/report/purchase_register/purchase_register.py +++ b/erpnext/accounts/report/purchase_register/purchase_register.py @@ -89,6 +89,8 @@ def _execute(filters=None, additional_table_columns=None): "payable_account": inv.credit_to, "mode_of_payment": inv.mode_of_payment, "project": ", ".join(project) if inv.doctype == "Purchase Invoice" else inv.project, + "bill_no": inv.bill_no, + "bill_date": inv.bill_date, "remarks": inv.remarks, "purchase_order": ", ".join(purchase_order), "purchase_receipt": ", ".join(purchase_receipt), @@ -433,7 +435,7 @@ def get_payments(filters): account_fieldname="paid_to", party="supplier", party_name="supplier_name", - party_account=get_party_account("Supplier", filters.supplier, filters.company), + party_account=[get_party_account("Supplier", filters.supplier, filters.company)], ) payment_entries = get_payment_entries(filters, args) journal_entries = get_journal_entries(filters, args) diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index 61b1fe2293c..1cb72f8d2d6 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -477,7 +477,7 @@ def get_payments(filters): account_fieldname="paid_from", party="customer", party_name="customer_name", - party_account=get_party_account("Customer", filters.customer, filters.company), + party_account=[get_party_account("Customer", filters.customer, filters.company)], ) payment_entries = get_payment_entries(filters, args) journal_entries = get_journal_entries(filters, args) diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index 7dd61d2c64c..795cab900c1 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -251,8 +251,9 @@ def get_journal_entries(filters, args): ) .where( (je.voucher_type == "Journal Entry") + & (je.docstatus == 1) & (journal_account.party == filters.get(args.party)) - & (journal_account.account == args.party_account) + & (journal_account.account.isin(args.party_account)) ) .orderby(je.posting_date, je.name, order=Order.desc) ) @@ -281,7 +282,9 @@ def get_payment_entries(filters, args): pe.cost_center, ) .where( - (pe.party == filters.get(args.party)) & (pe[args.account_fieldname] == args.party_account) + (pe.docstatus == 1) + & (pe.party == filters.get(args.party)) + & (pe[args.account_fieldname].isin(args.party_account)) ) .orderby(pe.posting_date, pe.name, order=Order.desc) ) @@ -365,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 3d5e5fc4ec7..660cb62d2c5 100644 --- a/erpnext/accounts/test/test_utils.py +++ b/erpnext/accounts/test/test_utils.py @@ -22,6 +22,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") @@ -125,6 +129,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 52b0c34673a..d9fb75a86d2 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1222,8 +1222,13 @@ def get_autoname_with_number(number_value, doc_title, company): def parse_naming_series_variable(doc, variable): if variable == "FY": - date = doc.get("posting_date") or doc.get("transaction_date") or getdate() - return get_fiscal_year(date=date, company=doc.get("company"))[0] + if doc: + date = doc.get("posting_date") or doc.get("transaction_date") or getdate() + company = doc.get("company") + else: + date = getdate() + company = None + return get_fiscal_year(date=date, company=company)[0] @frappe.whitelist() diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index cdccf81507b..d8b8bf18a15 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" }, { @@ -583,7 +582,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 3db4a8d18bd..e5e022802d2 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -61,6 +61,7 @@ class Asset(AccountsController): def on_cancel(self): self.validate_cancellation() self.cancel_movement_entries() + self.cancel_capitalization() self.delete_depreciation_entries() self.set_status() self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") @@ -831,6 +832,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 d in self.get("schedules"): @@ -1351,6 +1362,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_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js index 304bdf26dee..67f8421fbde 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -20,10 +20,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() { @@ -119,13 +119,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 92cb85d1b7c..d1bf15eb459 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -72,11 +72,23 @@ class AssetCapitalization(StockController): self.update_target_asset() def on_cancel(self): - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") + self.ignore_linked_doctypes = ( + "GL Entry", + "Stock Ledger Entry", + "Repost Item Valuation", + "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 @@ -782,7 +794,6 @@ def get_consumed_asset_details(args): out.cost_center = get_default_cost_center( args, item_defaults, item_group_defaults, brand_defaults ) - return out @@ -830,10 +841,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/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 5be70288c56..f070c40a589 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -188,7 +188,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", @@ -214,39 +214,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 e580748866d..cc8ee9226a4 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1347,11 +1347,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) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index b73ebf53ae8..06ea8336bd6 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -6,9 +6,12 @@ import json from collections import 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 import Criterion, CustomFunction +from frappe.query_builder.functions import Locate from frappe.utils import nowdate, unique +from pypika import Order import erpnext from erpnext.stock.get_item_details import _get_item_tax_template @@ -329,37 +332,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() 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/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index a462141a86b..5354d0d6c13 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -239,11 +239,12 @@ def new_bank_transaction(transaction): withdrawal = 0.0 tags = [] - try: - tags += transaction["category"] - tags += [f'Plaid Cat. {transaction["category_id"]}'] - except KeyError: - pass + if transaction["category"]: + try: + tags += transaction["category"] + tags += [f'Plaid Cat. {transaction["category_id"]}'] + except KeyError: + pass if not frappe.db.exists( "Bank Transaction", dict(transaction_id=transaction["transaction_id"]) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 97d71a5f2a3..481b740d775 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -16,7 +16,7 @@ app_include_js = "erpnext.bundle.js" app_include_css = "erpnext.bundle.css" web_include_js = "erpnext-web.bundle.js" web_include_css = "erpnext-web.bundle.css" -email_css = "email_erpnext.bundle.css" +email_css = "erpnext_email.bundle.scss" doctype_js = { "Address": "public/js/address.js", diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 682762678ba..4c76e2a869e 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/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py index 526bc2ba4ac..4593cbda5ee 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/holiday_list.py @@ -63,7 +63,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/item/item.js b/erpnext/stock/doctype/item/item.js index b306a41bb83..1b71017bd4d 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -630,26 +630,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 d7cefe36473..d44bb4d26d8 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1077,6 +1077,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/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 404758ce94f..b444e864e3d 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -13,7 +13,6 @@ from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse -from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction class TestPurchaseReceipt(FrappeTestCase): @@ -197,84 +196,6 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertFalse(frappe.db.get_value("Batch", {"item": item.name, "reference_name": pr.name})) self.assertFalse(frappe.db.get_all("Serial No", {"batch_no": batch_no})) - def test_duplicate_serial_nos(self): - from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note - - item = frappe.db.exists("Item", {"item_name": "Test Serialized Item 123"}) - if not item: - item = create_item("Test Serialized Item 123") - item.has_serial_no = 1 - item.serial_no_series = "TSI123-.####" - item.save() - else: - item = frappe.get_doc("Item", {"item_name": "Test Serialized Item 123"}) - - # First make purchase receipt - pr = make_purchase_receipt(item_code=item.name, qty=2, rate=500) - pr.load_from_db() - - serial_nos = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "item_code": item.name}, - "serial_no", - ) - - serial_nos = get_serial_nos(serial_nos) - - self.assertEquals(get_serial_nos(pr.items[0].serial_no), serial_nos) - - # Then tried to receive same serial nos in difference company - pr_different_company = make_purchase_receipt( - item_code=item.name, - qty=2, - rate=500, - serial_no="\n".join(serial_nos), - company="_Test Company 1", - do_not_submit=True, - warehouse="Stores - _TC1", - ) - - self.assertRaises(SerialNoDuplicateError, pr_different_company.submit) - - # Then made delivery note to remove the serial nos from stock - dn = create_delivery_note(item_code=item.name, qty=2, rate=1500, serial_no="\n".join(serial_nos)) - dn.load_from_db() - self.assertEquals(get_serial_nos(dn.items[0].serial_no), serial_nos) - - posting_date = add_days(today(), -3) - - # Try to receive same serial nos again in the same company with backdated. - pr1 = make_purchase_receipt( - item_code=item.name, - qty=2, - rate=500, - posting_date=posting_date, - serial_no="\n".join(serial_nos), - do_not_submit=True, - ) - - self.assertRaises(SerialNoExistsInFutureTransaction, pr1.submit) - - # Try to receive same serial nos with different company with backdated. - pr2 = make_purchase_receipt( - item_code=item.name, - qty=2, - rate=500, - posting_date=posting_date, - serial_no="\n".join(serial_nos), - company="_Test Company 1", - do_not_submit=True, - warehouse="Stores - _TC1", - ) - - self.assertRaises(SerialNoExistsInFutureTransaction, pr2.submit) - - # Receive the same serial nos after the delivery note posting date and time - make_purchase_receipt(item_code=item.name, qty=2, rate=500, serial_no="\n".join(serial_nos)) - - # Raise the error for backdated deliver note entry cancel - self.assertRaises(SerialNoExistsInFutureTransaction, dn.cancel) - def test_purchase_receipt_gl_entry(self): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 2a9f091bd09..002f7bf8a8c 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -68,6 +68,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 4f19643ad52..9eeb4602ab6 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -216,6 +216,33 @@ class TestQualityInspection(FrappeTestCase): qa.save() self.assertEqual(qa.status, "Accepted") + 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 6b1a8efc997..5e523aef999 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -557,7 +557,8 @@ "label": "Job Card Item", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "default": "0", @@ -572,7 +573,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_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index dd39f103cd7..9a46ae71ad2 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -634,30 +634,48 @@ class StockReconciliation(StockController): if voucher_detail_no != row.name: continue - current_qty = get_batch_qty_for_stock_reco( - row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name - ) + if row.serial_no: + item_dict = get_stock_balance_for( + row.item_code, + row.warehouse, + self.posting_date, + self.posting_time, + voucher_no=self.name, + ) + + current_qty = item_dict.get("qty") + row.current_serial_no = item_dict.get("serial_nos") + row.current_valuation_rate = item_dict.get("rate") + else: + current_qty = get_batch_qty_for_stock_reco( + row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name + ) precesion = row.precision("current_qty") if flt(current_qty, precesion) != flt(row.current_qty, precesion): - val_rate = get_valuation_rate( - row.item_code, - row.warehouse, - self.doctype, - self.name, - company=self.company, - batch_no=row.batch_no, - ) + if not row.serial_no: + val_rate = get_valuation_rate( + row.item_code, + row.warehouse, + self.doctype, + self.name, + company=self.company, + batch_no=row.batch_no, + ) + + row.current_valuation_rate = val_rate - row.current_valuation_rate = val_rate row.current_qty = current_qty - row.db_set( - { - "current_qty": row.current_qty, - "current_valuation_rate": row.current_valuation_rate, - "current_amount": flt(row.current_qty * row.current_valuation_rate), - } - ) + values_to_update = { + "current_qty": row.current_qty, + "current_valuation_rate": row.current_valuation_rate, + "current_amount": flt(row.current_qty * row.current_valuation_rate), + } + + if row.current_serial_no: + values_to_update["current_serial_no"] = row.current_serial_no + + row.db_set(values_to_update) if ( add_new_sle @@ -880,6 +898,7 @@ def get_stock_balance_for( batch_no: Optional[str] = None, with_valuation_rate: bool = True, inventory_dimensions_dict=None, + voucher_no=None, ): frappe.has_permission("Stock Reconciliation", "write", throw=True) @@ -910,6 +929,7 @@ def get_stock_balance_for( with_serial_no=has_serial_no, inventory_dimensions_dict=inventory_dimensions_dict, batch_no=batch_no, + voucher_no=voucher_no, ) if has_serial_no: diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 05c60175f51..19c04afe909 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -1055,6 +1055,74 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): self.assertEqual(sr.items[0].current_qty, se2.items[0].qty) self.assertEqual(len(sr.items[0].current_serial_no.split("\n")), sr.items[0].current_qty) + def test_backdated_purchase_receipt_with_stock_reco(self): + item_code = self.make_item( + properties={ + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "TEST-SERIAL-.###", + } + ).name + + warehouse = "_Test Warehouse - _TC" + + # Step - 1: Create a Backdated Purchase Receipt + + pr1 = make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3) + ) + pr1.reload() + + serial_nos = sorted(get_serial_nos(pr1.items[0].serial_no))[:5] + + # Step - 2: Create a Stock Reconciliation + sr1 = create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=5, + serial_no="\n".join(serial_nos), + ) + + data = frappe.get_all( + "Stock Ledger Entry", + fields=["serial_no", "actual_qty", "stock_value_difference"], + filters={"voucher_no": sr1.name, "is_cancelled": 0}, + order_by="creation", + ) + + for d in data: + if d.actual_qty < 0: + self.assertEqual(d.actual_qty, -10.0) + self.assertAlmostEqual(d.stock_value_difference, -1000.0) + else: + self.assertEqual(d.actual_qty, 5.0) + self.assertAlmostEqual(d.stock_value_difference, 500.0) + + # Step - 3: Create a Purchase Receipt before the first Purchase Receipt + make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=10, rate=200, posting_date=add_days(nowdate(), -5) + ) + + data = frappe.get_all( + "Stock Ledger Entry", + fields=["serial_no", "actual_qty", "stock_value_difference"], + filters={"voucher_no": sr1.name, "is_cancelled": 0}, + order_by="creation", + ) + + for d in data: + if d.actual_qty < 0: + self.assertEqual(d.actual_qty, -20.0) + self.assertAlmostEqual(d.stock_value_difference, -3000.0) + else: + self.assertEqual(d.actual_qty, 5.0) + self.assertAlmostEqual(d.stock_value_difference, 500.0) + + active_serial_no = frappe.get_all( + "Serial No", filters={"status": "Active", "item_code": item_code} + ) + self.assertEqual(len(active_serial_no), 5) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) 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 a04309ad48e..e3cd34725b8 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", @@ -49,6 +50,7 @@ "reqd": 1 }, { + "fetch_from": "item_code.item_name", "fieldname": "item_name", "fieldtype": "Data", "in_global_search": 1, @@ -186,11 +188,18 @@ "fieldtype": "Data", "label": "Has Item Scanned", "read_only": 1 + }, + { + "fetch_from": "item_code.item_group", + "fieldname": "item_group", + "fieldtype": "Link", + "label": "Item Group", + "options": "Item Group" } ], "istable": 1, "links": [], - "modified": "2023-07-25 11:58:44.992419", + "modified": "2024-01-14 10:04:23.599951", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation Item", @@ -201,4 +210,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index df1f544d7b1..ef1b0cda4ff 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1,7 +1,6 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -import copy import json from typing import Optional, Set, Tuple @@ -27,10 +26,6 @@ class NegativeStockError(frappe.ValidationError): pass -class SerialNoExistsInFutureTransaction(frappe.ValidationError): - pass - - def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): """Create SL entries from SL entry dicts @@ -54,9 +49,6 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc future_sle_exists(args, sl_entries) for sle in sl_entries: - if sle.serial_no and not via_landed_cost_voucher: - validate_serial_no(sle) - if cancel: sle["actual_qty"] = -flt(sle.get("actual_qty")) @@ -133,35 +125,6 @@ def get_args_for_future_sle(row): ) -def validate_serial_no(sle): - from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos - - for sn in get_serial_nos(sle.serial_no): - args = copy.deepcopy(sle) - args.serial_no = sn - args.warehouse = "" - - vouchers = [] - for row in get_stock_ledger_entries(args, ">"): - voucher_type = frappe.bold(row.voucher_type) - voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no)) - vouchers.append(f"{voucher_type} {voucher_no}") - - if vouchers: - serial_no = frappe.bold(sn) - msg = ( - f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first. - The list of the transactions are as below.""" - + "

" - - title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel" - frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction) - - def validate_cancellation(args): if args[0].get("is_cancelled"): repost_entry = frappe.db.get_value( @@ -573,7 +536,12 @@ class update_entries_after(object): if not self.args.get("sle_id"): self.get_dynamic_incoming_outgoing_rate(sle) - if sle.voucher_type == "Stock Reconciliation" and sle.batch_no and sle.voucher_detail_no: + if ( + sle.voucher_type == "Stock Reconciliation" + and not self.args.get("sle_id") + and sle.voucher_detail_no + and (sle.batch_no or sle.serial_no) + ): self.reset_actual_qty_for_stock_reco(sle) if ( @@ -651,11 +619,52 @@ class update_entries_after(object): doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty >= 0) if sle.actual_qty < 0: - sle.actual_qty = ( - flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty")) - * -1 + stock_reco_details = frappe.db.get_value( + "Stock Reconciliation Item", + sle.voucher_detail_no, + ["current_qty", "current_serial_no as sn_no"], + as_dict=True, ) + sle.actual_qty = flt(stock_reco_details.current_qty) * -1 + + if stock_reco_details.sn_no: + sle.serial_no = stock_reco_details.sn_no + sle.qty_after_transaction = 0.0 + + if sle.serial_no: + self.update_serial_no_status(sle) + + def update_serial_no_status(self, sle): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + serial_nos = get_serial_nos(sle.serial_no) + warehouse = None + status = "Delivered" + if sle.actual_qty > 0: + warehouse = sle.warehouse + status = "Active" + + sn_table = frappe.qb.DocType("Serial No") + + query = ( + frappe.qb.update(sn_table) + .set(sn_table.warehouse, warehouse) + .set(sn_table.status, status) + .where(sn_table.name.isin(serial_nos)) + ) + + if sle.actual_qty > 0: + query = query.set(sn_table.purchase_document_type, sle.voucher_type) + query = query.set(sn_table.purchase_document_no, sle.voucher_no) + query = query.set(sn_table.delivery_document_type, None) + query = query.set(sn_table.delivery_document_no, None) + else: + query = query.set(sn_table.delivery_document_type, sle.voucher_type) + query = query.set(sn_table.delivery_document_no, sle.voucher_no) + + query.run() + def validate_negative_stock(self, sle): """ validate negative stock for entries current datetime onwards @@ -1282,6 +1291,9 @@ def get_stock_ledger_entries( if operator in (">", "<=") and previous_sle.get("name"): conditions += " and name!=%(name)s" + if operator in (">", "<=") and previous_sle.get("voucher_no"): + conditions += " and voucher_no!=%(voucher_no)s" + if extra_cond: conditions += f"{extra_cond}" diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index e019c572daf..2b57a1be8fa 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -96,6 +96,7 @@ def get_stock_balance( with_serial_no=False, inventory_dimensions_dict=None, batch_no=None, + voucher_no=None, ): """Returns stock balance quantity at given warehouse on given posting date or current date. @@ -115,6 +116,9 @@ def get_stock_balance( "posting_time": posting_time, } + if voucher_no: + args["voucher_no"] = voucher_no + extra_cond = "" if inventory_dimensions_dict: for field, value in inventory_dimensions_dict.items():