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."""
- + "