diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index f8eeba84662..81937469f21 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -41,6 +41,11 @@ class AccountingDimension(Document): self.set_fieldname_and_label() def validate(self): + self.validate_doctype() + validate_column_name(self.fieldname) + self.validate_dimension_defaults() + + def validate_doctype(self): if self.document_type in ( *core_doctypes_list, "Accounting Dimension", @@ -62,9 +67,6 @@ class AccountingDimension(Document): if not self.is_new(): self.validate_document_type_change() - validate_column_name(self.fieldname) - self.validate_dimension_defaults() - def validate_document_type_change(self): doctype_before_save = frappe.db.get_value("Accounting Dimension", self.name, "document_type") if doctype_before_save != self.document_type: @@ -103,6 +105,7 @@ class AccountingDimension(Document): def on_update(self): frappe.flags.accounting_dimensions = None + frappe.flags.accounting_dimensions_details = None def make_dimension_in_accounting_doctypes(doc, doclist=None): @@ -263,7 +266,7 @@ def get_checks_for_pl_and_bs_accounts(): frappe.flags.accounting_dimensions_details = frappe.db.sql( """SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c - WHERE p.name = c.parent""", + WHERE p.name = c.parent AND p.disabled = 0""", as_dict=1, ) diff --git a/erpnext/accounts/doctype/gl_entry/test_gl_entry.py b/erpnext/accounts/doctype/gl_entry/test_gl_entry.py index f6ed163bff5..6ff378559c0 100644 --- a/erpnext/accounts/doctype/gl_entry/test_gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/test_gl_entry.py @@ -124,3 +124,20 @@ class TestGLEntry(unittest.TestCase): str(e), "Party Type and Party can only be set for Receivable / Payable account_Test Account Cost for Goods Sold - _TC", ) + + def test_validate_account_party_type_shareholder(self): + jv = make_journal_entry( + "Opening Balance Equity - _TC", + "Cash - _TC", + 100, + "_Test Cost Center - _TC", + save=False, + submit=False, + ) + + for row in jv.accounts: + row.party_type = "Shareholder" + break + + jv.save().submit() + self.assertEqual(1, jv.docstatus) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index f84f094e464..84e44621784 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -313,6 +313,7 @@ class PaymentRequest(Document): "payer_name": data.customer_name, "order_id": self.name, "currency": self.currency, + "payment_gateway": self.payment_gateway, } ) @@ -774,7 +775,10 @@ def get_existing_paid_amount(doctype, name): frappe.qb.from_(PL) .left_join(PER) .on( - (PER.reference_doctype == PL.against_voucher_type) & (PER.reference_name == PL.against_voucher_no) + (PL.against_voucher_type == PER.reference_doctype) + & (PL.against_voucher_no == PER.reference_name) + & (PL.voucher_type == PER.parenttype) + & (PL.voucher_no == PER.parent) ) .select(Abs(Sum(PL.amount)).as_("total_paid_amount")) .where(PL.against_voucher_type.eq(doctype)) diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index ed940470d6c..7ce6cc0b7b7 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -542,6 +542,45 @@ class TestPaymentRequest(FrappeTestCase): self.assertEqual(pr.grand_total, si.outstanding_amount) + def test_partial_paid_invoice_with_more_payment_entry(self): + pi = make_purchase_invoice(currency="INR", qty=1, rate=500) + pi.submit() + pi_1 = make_purchase_invoice(currency="INR", qty=1, rate=300) + pi_1.submit() + + pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1, submit_doc=0, return_doc=1) + pr.grand_total = 200 + pr.submit() + pr.create_payment_entry() + pr_1 = make_payment_request( + dt="Purchase Invoice", dn=pi.name, mute_email=1, submit_doc=0, return_doc=1 + ) + pr_1.grand_total = 200 + pr_1.submit() + pr_1.create_payment_entry() + + pe = get_payment_entry(dt="Purchase Invoice", dn=pi.name) + pe.paid_amount = 200 + pe.references[0].reference_doctype = pi.doctype + pe.references[0].reference_name = pi.name + pe.references[0].grand_total = pi.grand_total + pe.references[0].outstanding_amount = pi.outstanding_amount + pe.references[0].allocated_amount = 100 + pe.append( + "references", + { + "reference_doctype": pi_1.doctype, + "reference_name": pi_1.name, + "grand_total": pi_1.grand_total, + "outstanding_amount": pi_1.outstanding_amount, + "allocated_amount": 100, + }, + ) + + pr_2 = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1) + pi.load_from_db() + self.assertEqual(pr_2.grand_total, pi.outstanding_amount) + def test_partial_paid_invoice_with_submitted_payment_entry(self): pi = make_purchase_invoice(currency="INR", qty=1, rate=5000) diff --git a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py index 5dd6259c585..34585ec200d 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py @@ -39,10 +39,12 @@ class TestPOSClosingEntry(unittest.TestCase): pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1) pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500}) + pos_inv1.save() pos_inv1.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) + pos_inv2.save() pos_inv2.submit() pcv_doc = make_closing_entry_from_opening(opening_entry) @@ -68,6 +70,7 @@ class TestPOSClosingEntry(unittest.TestCase): pos_inv = create_pos_invoice(rate=3500, do_not_submit=1, item_name="Test Item", without_item_code=1) pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500}) + pos_inv.save() pos_inv.submit() pcv_doc = make_closing_entry_from_opening(opening_entry) @@ -86,10 +89,12 @@ class TestPOSClosingEntry(unittest.TestCase): pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1) pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500}) + pos_inv1.save() pos_inv1.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) + pos_inv2.save() pos_inv2.submit() # make return entry of pos_inv2 @@ -111,10 +116,12 @@ class TestPOSClosingEntry(unittest.TestCase): pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1) pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500}) + pos_inv1.save() pos_inv1.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) + pos_inv2.save() pos_inv2.submit() pcv_doc = make_closing_entry_from_opening(opening_entry) @@ -165,6 +172,7 @@ class TestPOSClosingEntry(unittest.TestCase): opening_entry = create_opening_entry(pos_profile, test_user.name) pos_inv1 = create_pos_invoice(rate=350, do_not_submit=1, pos_profile=pos_profile.name) pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500}) + pos_inv1.save() pos_inv1.submit() # if in between a mandatory accounting dimension is added to the POS Profile then @@ -218,11 +226,27 @@ class TestPOSClosingEntry(unittest.TestCase): opening_entry = create_opening_entry(pos_profile, test_user.name) pos_inv = create_pos_invoice( - item_code=item_code, qty=5, rate=300, use_serial_batch_fields=1, batch_no=batch_no + item_code=item_code, + qty=5, + rate=300, + use_serial_batch_fields=1, + batch_no=batch_no, + do_not_submit=True, ) + pos_inv.payments[0].amount = pos_inv.grand_total + pos_inv.save() + pos_inv.submit() pos_inv2 = create_pos_invoice( - item_code=item_code, qty=5, rate=300, use_serial_batch_fields=1, batch_no=batch_no + item_code=item_code, + qty=5, + rate=300, + use_serial_batch_fields=1, + batch_no=batch_no, + do_not_submit=True, ) + pos_inv2.payments[0].amount = pos_inv2.grand_total + pos_inv2.save() + pos_inv2.submit() batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code) self.assertEqual(batch_qty_with_pos, 0.0) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index ab5a4092c33..a8a733ac42c 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -20,6 +20,10 @@ from erpnext.controllers.queries import item_query as _item_query from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos +class PartialPaymentValidationError(frappe.ValidationError): + pass + + class POSInvoice(SalesInvoice): # begin: auto-generated types # This code is auto-generated. Do not modify anything in this block. @@ -210,6 +214,7 @@ class POSInvoice(SalesInvoice): self.validate_payment_amount() self.validate_loyalty_transaction() self.validate_company_with_pos_company() + self.validate_full_payment() if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code @@ -477,6 +482,20 @@ class POSInvoice(SalesInvoice): if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points: validate_loyalty_points(self, self.loyalty_points) + def validate_full_payment(self): + invoice_total = flt(self.rounded_total) or flt(self.grand_total) + + if self.docstatus == 1: + if self.is_return and self.paid_amount != invoice_total: + frappe.throw( + msg=_("Partial Payment in POS Invoice is not allowed."), exc=PartialPaymentValidationError + ) + + if self.paid_amount < invoice_total: + frappe.throw( + msg=_("Partial Payment in POS Invoice is not allowed."), exc=PartialPaymentValidationError + ) + def set_status(self, update=False, status=None, update_modified=True): if self.is_new(): if self.get("amended_from"): diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 7b6b8b50543..09c9443bdd9 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -7,7 +7,7 @@ import unittest import frappe from frappe import _ -from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return +from erpnext.accounts.doctype.pos_invoice.pos_invoice import PartialPaymentValidationError, make_sales_return from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.stock.doctype.item.test_item import make_item @@ -313,7 +313,7 @@ class TestPOSInvoice(unittest.TestCase): ) pos.append( - "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1} + "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2000, "default": 1} ) pos.insert() @@ -324,6 +324,11 @@ class TestPOSInvoice(unittest.TestCase): # partial return 1 pos_return1.get("items")[0].qty = -1 + pos_return1.set("payments", []) + pos_return1.append( + "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -1000, "default": 1} + ) + pos_return1.paid_amount = -1000 pos_return1.submit() pos_return1.reload() @@ -338,6 +343,11 @@ class TestPOSInvoice(unittest.TestCase): # partial return 2 pos_return2 = make_sales_return(pos.name) + pos_return2.set("payments", []) + pos_return2.append( + "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -1000, "default": 1} + ) + pos_return2.paid_amount = -1000 pos_return2.submit() self.assertEqual(pos_return2.get("items")[0].qty, -1) @@ -373,6 +383,15 @@ class TestPOSInvoice(unittest.TestCase): inv.payments = [] self.assertRaises(frappe.ValidationError, inv.insert) + def test_partial_payment(self): + pos_inv = create_pos_invoice(rate=10000, do_not_save=1) + pos_inv.append( + "payments", + {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 9000}, + ) + pos_inv.insert() + self.assertRaises(PartialPaymentValidationError, pos_inv.submit) + def test_serialized_item_transaction(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item @@ -581,7 +600,13 @@ class TestPOSInvoice(unittest.TestCase): "Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty" ) - inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000) + inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1) + inv.append( + "payments", + {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000}, + ) + inv.insert() + inv.submit() lpe = frappe.get_doc( "Loyalty Point Entry", @@ -607,7 +632,13 @@ class TestPOSInvoice(unittest.TestCase): ) # add 10 loyalty points - create_pos_invoice(customer="Test Loyalty Customer", rate=10000) + pos_inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1) + pos_inv.append( + "payments", + {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000}, + ) + pos_inv.paid_amount = 10000 + pos_inv.submit() before_lp_details = get_loyalty_program_details_with_points( "Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty" @@ -641,10 +672,12 @@ class TestPOSInvoice(unittest.TestCase): test_user, pos_profile = init_user_and_profile() pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1) pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 270}) + pos_inv.save() pos_inv.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) + pos_inv2.save() pos_inv2.submit() consolidate_pos_invoices() @@ -676,6 +709,7 @@ class TestPOSInvoice(unittest.TestCase): "included_in_print_rate": 1, }, ) + pos_inv.save() pos_inv.submit() pos_inv2 = create_pos_invoice(rate=300, qty=2, do_not_submit=1) @@ -692,6 +726,7 @@ class TestPOSInvoice(unittest.TestCase): "included_in_print_rate": 1, }, ) + pos_inv2.save() pos_inv2.submit() consolidate_pos_invoices() @@ -744,6 +779,7 @@ class TestPOSInvoice(unittest.TestCase): "included_in_print_rate": 1, }, ) + pos_inv2.save() pos_inv2.submit() consolidate_pos_invoices() @@ -774,7 +810,10 @@ class TestPOSInvoice(unittest.TestCase): # POS Invoice 1, for the batch without bundle pos_inv1 = create_pos_invoice(item="_BATCH ITEM Test For Reserve", rate=300, qty=15, do_not_save=1) - + pos_inv1.append( + "payments", + {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 4500}, + ) pos_inv1.items[0].batch_no = batch_no pos_inv1.save() pos_inv1.submit() @@ -790,8 +829,14 @@ class TestPOSInvoice(unittest.TestCase): # POS Invoice 2, for the batch with bundle pos_inv2 = create_pos_invoice( - item="_BATCH ITEM Test For Reserve", rate=300, qty=10, batch_no=batch_no + item="_BATCH ITEM Test For Reserve", rate=300, qty=10, batch_no=batch_no, do_not_save=1 ) + pos_inv2.append( + "payments", + {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3000}, + ) + pos_inv2.save() + pos_inv2.submit() pos_inv2.reload() self.assertTrue(pos_inv2.items[0].serial_and_batch_bundle) @@ -826,6 +871,10 @@ class TestPOSInvoice(unittest.TestCase): pos_inv1 = create_pos_invoice( item=item.name, rate=300, qty=1, do_not_submit=1, batch_no="TestBatch 01" ) + pos_inv1.append( + "payments", + {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300}, + ) pos_inv1.save() pos_inv1.submit() diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 18136a033b0..a8dd4c41c92 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -12,7 +12,9 @@ from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime from frappe.utils.background_jobs import enqueue, is_job_enqueued from frappe.utils.scheduler import is_scheduler_inactive -from erpnext.accounts.doctype.pos_profile.pos_profile import required_accounting_dimensions +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_checks_for_pl_and_bs_accounts, +) class POSInvoiceMergeLog(Document): @@ -292,22 +294,23 @@ class POSInvoiceMergeLog(Document): invoice.disable_rounded_total = cint( frappe.db.get_value("POS Profile", invoice.pos_profile, "disable_rounded_total") ) - accounting_dimensions = required_accounting_dimensions() + accounting_dimensions = get_checks_for_pl_and_bs_accounts() + accounting_dimensions_fields = [d.fieldname for d in accounting_dimensions] dimension_values = frappe.db.get_value( - "POS Profile", {"name": invoice.pos_profile}, accounting_dimensions, as_dict=1 + "POS Profile", {"name": invoice.pos_profile}, accounting_dimensions_fields, as_dict=1 ) for dimension in accounting_dimensions: - dimension_value = dimension_values.get(dimension) + dimension_value = dimension_values.get(dimension.fieldname) - if not dimension_value: + if not dimension_value and (dimension.mandatory_for_pl or dimension.mandatory_for_bs): frappe.throw( _("Please set Accounting Dimension {} in {}").format( - frappe.bold(frappe.unscrub(dimension)), + frappe.bold(dimension.label), frappe.get_desk_link("POS Profile", invoice.pos_profile), ) ) - invoice.set(dimension, dimension_value) + invoice.set(dimension.fieldname, dimension_value) if self.merge_invoices_based_on == "Customer Group": invoice.flags.ignore_pos_profile = True diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index 904d8e83b9c..e0d37436be5 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py @@ -28,14 +28,17 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): pos_inv = create_pos_invoice(rate=300, do_not_submit=1) pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300}) + pos_inv.save() pos_inv.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) + pos_inv2.save() pos_inv2.submit() pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300}) + pos_inv3.save() pos_inv3.submit() consolidate_pos_invoices() @@ -61,14 +64,17 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): pos_inv = create_pos_invoice(rate=300, do_not_submit=1) pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300}) + pos_inv.save() pos_inv.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) + pos_inv2.save() pos_inv2.submit() pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300}) + pos_inv3.save() pos_inv3.submit() pos_inv_cn = make_sales_return(pos_inv.name) @@ -122,6 +128,8 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): }, ) inv.insert() + inv.payments[0].amount = inv.grand_total + inv.save() inv.submit() inv2 = create_pos_invoice(qty=1, rate=100, do_not_save=True) @@ -138,6 +146,8 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): }, ) inv2.insert() + inv2.payments[0].amount = inv.grand_total + inv2.save() inv2.submit() consolidate_pos_invoices() @@ -272,7 +282,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): inv2.submit() inv3 = create_pos_invoice(qty=3, rate=600, do_not_save=True) - inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000}) + inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1800}) inv3.insert() inv3.submit() @@ -280,8 +290,8 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): inv.load_from_db() consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice) - self.assertEqual(consolidated_invoice.outstanding_amount, 800) - self.assertNotEqual(consolidated_invoice.status, "Paid") + self.assertNotEqual(consolidated_invoice.outstanding_amount, 800) + self.assertEqual(consolidated_invoice.status, "Paid") finally: frappe.set_user("Administrator") @@ -416,6 +426,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): do_not_submit=1, ) pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) + pos_inv.save() pos_inv.submit() pos_inv_cn = make_sales_return(pos_inv.name) @@ -430,6 +441,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): do_not_submit=1, ) pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) + pos_inv2.save() pos_inv2.submit() consolidate_pos_invoices() diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index 22f2965b86e..c38f9563eb2 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -29,6 +29,7 @@ "ignore_pricing_rule", "allow_rate_change", "allow_discount_change", + "disable_grand_total_to_default_mop", "section_break_23", "item_groups", "column_break_25", @@ -382,6 +383,12 @@ "fieldname": "print_receipt_on_order_complete", "fieldtype": "Check", "label": "Print Receipt on Order Complete" + }, + { + "default": "0", + "fieldname": "disable_grand_total_to_default_mop", + "fieldtype": "Check", + "label": "Disable auto setting Grand Total to default Payment Mode" } ], "icon": "icon-cog", @@ -409,7 +416,7 @@ "link_fieldname": "pos_profile" } ], - "modified": "2025-01-01 11:07:03.161950", + "modified": "2025-01-29 13:12:30.796630", "modified_by": "Administrator", "module": "Accounts", "name": "POS Profile", diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py index ea27116e91c..a8c4a4d8d72 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py @@ -7,6 +7,10 @@ from frappe import _, msgprint, scrub, unscrub from frappe.model.document import Document from frappe.utils import get_link_to_form, now +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_checks_for_pl_and_bs_accounts, +) + class POSProfile(Document): # begin: auto-generated types @@ -36,6 +40,7 @@ class POSProfile(Document): currency: DF.Link customer: DF.Link | None customer_groups: DF.Table[POSCustomerGroup] + disable_grand_total_to_default_mop: DF.Check disable_rounded_total: DF.Check disabled: DF.Check expense_account: DF.Link | None @@ -69,15 +74,19 @@ class POSProfile(Document): self.validate_accounting_dimensions() def validate_accounting_dimensions(self): - acc_dim_names = required_accounting_dimensions() - for acc_dim in acc_dim_names: - if not self.get(acc_dim): + acc_dims = get_checks_for_pl_and_bs_accounts() + for acc_dim in acc_dims: + if ( + self.company == acc_dim.company + and not self.get(acc_dim.fieldname) + and (acc_dim.mandatory_for_pl or acc_dim.mandatory_for_bs) + ): frappe.throw( _( "{0} is a mandatory Accounting Dimension.
" "Please set a value for {0} in Accounting Dimensions section." ).format( - unscrub(frappe.bold(acc_dim)), + frappe.bold(acc_dim.label), ), title=_("Mandatory Accounting Dimension"), ) @@ -215,23 +224,6 @@ def get_child_nodes(group_type, root): ) -def required_accounting_dimensions(): - p = frappe.qb.DocType("Accounting Dimension") - c = frappe.qb.DocType("Accounting Dimension Detail") - - acc_dim_doc = ( - frappe.qb.from_(p) - .inner_join(c) - .on(p.name == c.parent) - .select(c.parent) - .where((c.mandatory_for_bs == 1) | (c.mandatory_for_pl == 1)) - .where(p.disabled == 0) - ).run(as_dict=1) - - acc_dim_names = [scrub(d.parent) for d in acc_dim_doc] - return acc_dim_names - - @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def pos_profile_query(doctype, txt, searchfield, start, page_len, filters): diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index ee9dd2be8c3..c4825a6d519 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -53,6 +53,7 @@ "column_break_42", "free_item_uom", "round_free_qty", + "dont_enforce_free_item_qty", "is_recursive", "recurse_for", "apply_recursion_over", @@ -643,12 +644,19 @@ "fieldname": "has_priority", "fieldtype": "Check", "label": "Has Priority" + }, + { + "default": "0", + "depends_on": "eval:doc.price_or_product_discount == 'Product'", + "fieldname": "dont_enforce_free_item_qty", + "fieldtype": "Check", + "label": "Don't Enforce Free Item Qty" } ], "icon": "fa fa-gift", "idx": 1, "links": [], - "modified": "2024-09-16 18:14:51.314765", + "modified": "2025-02-17 18:15:39.824639", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 73cb2483811..7a895f6818b 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -60,6 +60,7 @@ class PricingRule(Document): disable: DF.Check discount_amount: DF.Currency discount_percentage: DF.Float + dont_enforce_free_item_qty: DF.Check for_price_list: DF.Link | None free_item: DF.Link | None free_item_rate: DF.Currency @@ -645,7 +646,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, ra if pricing_rule.margin_type in ["Percentage", "Amount"]: item_details.margin_rate_or_amount = 0.0 item_details.margin_type = None - elif pricing_rule.get("free_item"): + elif pricing_rule.get("free_item") and not pricing_rule.get("dont_enforce_free_item_qty"): item_details.remove_free_item = ( item_code if pricing_rule.get("same_item") else pricing_rule.get("free_item") ) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 965e2b267a3..3ad3d45ee47 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -428,6 +428,54 @@ class TestPricingRule(FrappeTestCase): self.assertEqual(so.items[1].is_free_item, 1) self.assertEqual(so.items[1].item_code, "_Test Item 2") + def test_dont_enforce_free_item_qty(self): + # this test is only for testing non-enforcement as all other tests in this file already test with enforcement + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule") + test_record = { + "doctype": "Pricing Rule", + "title": "_Test Pricing Rule", + "apply_on": "Item Code", + "currency": "USD", + "items": [ + { + "item_code": "_Test Item", + } + ], + "selling": 1, + "rate_or_discount": "Discount Percentage", + "rate": 0, + "min_qty": 0, + "max_qty": 7, + "discount_percentage": 17.5, + "price_or_product_discount": "Product", + "same_item": 0, + "free_item": "_Test Item 2", + "free_qty": 1, + "company": "_Test Company", + } + pricing_rule = frappe.get_doc(test_record.copy()).insert() + + # With enforcement + so = make_sales_order(item_code="_Test Item", qty=1, do_not_submit=True) + self.assertEqual(so.items[1].is_free_item, 1) + self.assertEqual(so.items[1].item_code, "_Test Item 2") + + # Test 1 : Saving a document with an item with pricing list without it's corresponding free item will cause it the free item to be refetched on save + so.items.pop(1) + so.save() + so.reload() + self.assertEqual(len(so.items), 2) + + # Without enforcement + pricing_rule.dont_enforce_free_item_qty = 1 + pricing_rule.save() + + # Test 2 : Deleted free item will not be fetched again on save without enforcement + so.items.pop(1) + so.save() + so.reload() + self.assertEqual(len(so.items), 1) + def test_cumulative_pricing_rule(self): frappe.delete_doc_if_exists("Pricing Rule", "_Test Cumulative Pricing Rule") test_record = { @@ -1451,6 +1499,7 @@ def make_pricing_rule(**args): "discount_amount": args.discount_amount or 0.0, "apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0, "has_priority": args.has_priority or 0, + "enforce_free_item_qty": args.dont_enforce_free_item_qty or 0, } ) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 551eaa3d1ce..bbf73e80809 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -713,7 +713,10 @@ def apply_pricing_rule_for_free_items(doc, pricing_rule_args): args.pop((item.item_code, item.pricing_rules)) for free_item in args.values(): - doc.append("items", free_item) + if doc.is_new() or not frappe.get_value( + "Pricing Rule", free_item["pricing_rules"], "dont_enforce_free_item_qty" + ): + doc.append("items", free_item) def get_pricing_rule_items(pr_doc, other_items=False) -> list: diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index cee7a238df0..48f400e1a46 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -236,17 +236,21 @@ def get_ar_filters(doc, entry): def get_html(doc, filters, entry, col, res, ageing): base_template_path = "frappe/www/printview.html" - template_path = ( - "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html" - if doc.report == "General Ledger" - else "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html" - ) + template_path = "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html" + if doc.report == "General Ledger": + template_path = ( + "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html" + ) + + process_soa_html = frappe.get_hooks("process_soa_html") + # fetching custom print format for Process Statement of Accounts + if process_soa_html and process_soa_html.get(doc.report): + template_path = process_soa_html[doc.report][-1] if doc.letter_head: from frappe.www.printview import get_letter_head letter_head = get_letter_head(doc, 0) - html = frappe.render_template( template_path, { @@ -262,7 +266,6 @@ def get_html(doc, filters, entry, col, res, ageing): else None, }, ) - html = frappe.render_template( base_template_path, {"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer}, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 5753eba8cc1..7345a5ef78d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -460,6 +460,8 @@ class SalesInvoice(SellingController): self.make_bundle_for_sales_purchase_return(table_name) self.make_bundle_using_old_serial_batch_fields(table_name) + + self.update_stock_reservation_entries() self.update_stock_ledger() # this sequence because outstanding may get -ve @@ -561,6 +563,7 @@ class SalesInvoice(SellingController): self.make_gl_entries_on_cancel() if self.update_stock == 1: + self.update_stock_reservation_entries() self.repost_future_sle_and_gle() self.db_set("status", "Cancelled") diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 0df9dcb0683..f2e86ef2b2e 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -765,7 +765,7 @@ def validate_account_party_type(self): if self.party_type and self.party: account_type = frappe.get_cached_value("Account", self.account, "account_type") - if account_type and (account_type not in ["Receivable", "Payable"]): + if account_type and (account_type not in ["Receivable", "Payable", "Equity"]): frappe.throw( _( "Party Type and Party can only be set for Receivable / Payable account

" "{0}" diff --git a/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html b/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html index c1c611ee3a3..8a6968ee373 100644 --- a/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html +++ b/erpnext/accounts/print_format/journal_auditing_voucher/journal_auditing_voucher.html @@ -44,7 +44,7 @@ {% endfor %} Total (debit) - {{ gl | sum(attribute='debit') }} + {{ gl | sum(attribute='debit') | round(2) }} Credit @@ -61,7 +61,7 @@ {% endfor %} Total (credit) - {{ gl | sum(attribute='credit') }} + {{ gl | sum(attribute='credit') | round(2) }} Narration: {{ gl[0].remarks }} diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 445e532183b..c13197613d2 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -89,6 +89,7 @@ frappe.query_reports["Accounts Payable"] = { fieldname: "party", label: __("Party"), fieldtype: "MultiSelectList", + options: "party_type", get_data: function (txt) { if (!frappe.query_report.filters) return; diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js index cf7a62c6b69..e46af2657b5 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -66,6 +66,7 @@ frappe.query_reports["Accounts Payable Summary"] = { fieldname: "party", label: __("Party"), fieldtype: "MultiSelectList", + options: "party_type", get_data: function (txt) { if (!frappe.query_report.filters) return; diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index 9f15bbc333d..01f5a205cea 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -56,6 +56,7 @@ frappe.query_reports["Accounts Receivable"] = { fieldname: "party", label: __("Party"), fieldtype: "MultiSelectList", + options: "party_type", get_data: function (txt) { if (!frappe.query_report.filters) return; diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js index e36f40169b3..17ee5e0b323 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -66,6 +66,7 @@ frappe.query_reports["Accounts Receivable Summary"] = { fieldname: "party", label: __("Party"), fieldtype: "MultiSelectList", + options: "party_type", get_data: function (txt) { if (!frappe.query_report.filters) return; 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 83bd48c71f3..c74450191aa 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js @@ -91,6 +91,7 @@ function get_filters() { fieldname: "budget_against_filter", label: __("Dimension Filter"), fieldtype: "MultiSelectList", + options: "budget_against", get_data: function (txt) { if (!frappe.query_report.filters) return; diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index e202d5dfffd..54d6fb2e2f6 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -73,6 +73,7 @@ frappe.query_reports["General Ledger"] = { fieldname: "party", label: __("Party"), fieldtype: "MultiSelectList", + options: "party_type", get_data: function (txt) { if (!frappe.query_report.filters) return; @@ -151,6 +152,7 @@ frappe.query_reports["General Ledger"] = { fieldname: "cost_center", label: __("Cost Center"), fieldtype: "MultiSelectList", + options: "Cost Center", get_data: function (txt) { return frappe.db.get_link_options("Cost Center", txt, { company: frappe.query_report.get_filter_value("company"), @@ -161,6 +163,7 @@ frappe.query_reports["General Ledger"] = { fieldname: "project", label: __("Project"), fieldtype: "MultiSelectList", + options: "Project", get_data: function (txt) { return frappe.db.get_link_options("Project", txt, { company: frappe.query_report.get_filter_value("company"), diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js index ad194ee90a2..0ddb95fff2f 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.js +++ b/erpnext/accounts/report/gross_profit/gross_profit.js @@ -67,6 +67,7 @@ frappe.query_reports["Gross Profit"] = { fieldname: "cost_center", label: __("Cost Center"), fieldtype: "MultiSelectList", + options: "Cost Center", get_data: function (txt) { return frappe.db.get_link_options("Cost Center", txt, { company: frappe.query_report.get_filter_value("company"), @@ -77,6 +78,7 @@ frappe.query_reports["Gross Profit"] = { fieldname: "project", label: __("Project"), fieldtype: "MultiSelectList", + options: "Project", get_data: function (txt) { return frappe.db.get_link_options("Project", txt, { company: frappe.query_report.get_filter_value("company"), diff --git a/erpnext/accounts/report/payment_ledger/payment_ledger.js b/erpnext/accounts/report/payment_ledger/payment_ledger.js index 8d1f227fe6d..7ffd26c9700 100644 --- a/erpnext/accounts/report/payment_ledger/payment_ledger.js +++ b/erpnext/accounts/report/payment_ledger/payment_ledger.js @@ -50,6 +50,7 @@ function get_filters() { fieldname: "party", label: __("Party"), fieldtype: "MultiSelectList", + options: "party_type", get_data: function (txt) { if (!frappe.query_report.filters) return; diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js index 50578d314e3..62482ac162c 100644 --- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js +++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js @@ -68,16 +68,12 @@ frappe.query_reports["Trial Balance for Party"] = { { fieldname: "account", label: __("Account"), - fieldtype: "Link", + fieldtype: "MultiSelectList", options: "Account", - get_query: function () { - var company = frappe.query_report.get_filter_value("company"); - return { - doctype: "Account", - filters: { - company: company, - }, - }; + get_data: function (txt) { + return frappe.db.get_link_options("Account", txt, { + company: frappe.query_report.get_filter_value("company"), + }); }, }, { diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py index dd1a12514e2..f6c79eb6c45 100644 --- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py +++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py @@ -4,8 +4,10 @@ import frappe from frappe import _ +from frappe.query_builder.functions import Sum from frappe.utils import cint, flt +from erpnext.accounts.report.general_ledger.general_ledger import get_accounts_with_children from erpnext.accounts.report.trial_balance.trial_balance import validate_filters @@ -35,9 +37,14 @@ def get_data(filters, show_party_name): filters=party_filters, order_by="name", ) + + account_filter = [] + if filters.get("account"): + account_filter = get_accounts_with_children(filters.get("account")) + company_currency = frappe.get_cached_value("Company", filters.company, "default_currency") - opening_balances = get_opening_balances(filters) - balances_within_period = get_balances_within_period(filters) + opening_balances = get_opening_balances(filters, account_filter) + balances_within_period = get_balances_within_period(filters, account_filter) data = [] # total_debit, total_credit = 0, 0 @@ -89,30 +96,34 @@ def get_data(filters, show_party_name): return data -def get_opening_balances(filters): - account_filter = "" - if filters.get("account"): - account_filter = "and account = %s" % (frappe.db.escape(filters.get("account"))) +def get_opening_balances(filters, account_filter=None): + GL_Entry = frappe.qb.DocType("GL Entry") - gle = frappe.db.sql( - f""" - select party, sum(debit) as opening_debit, sum(credit) as opening_credit - from `tabGL Entry` - where company=%(company)s - and is_cancelled=0 - and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' - and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s)) - {account_filter} - group by party""", - { - "company": filters.company, - "from_date": filters.from_date, - "to_date": filters.to_date, - "party_type": filters.party_type, - }, - as_dict=True, + query = ( + frappe.qb.from_(GL_Entry) + .select( + GL_Entry.party, + Sum(GL_Entry.debit).as_("opening_debit"), + Sum(GL_Entry.credit).as_("opening_credit"), + ) + .where( + (GL_Entry.company == filters.company) + & (GL_Entry.is_cancelled == 0) + & (GL_Entry.party_type == filters.party_type) + & (GL_Entry.party != "") + & ( + (GL_Entry.posting_date < filters.from_date) + | ((GL_Entry.is_opening == "Yes") & (GL_Entry.posting_date <= filters.to_date)) + ) + ) + .groupby(GL_Entry.party) ) + if account_filter: + query = query.where(GL_Entry.account.isin(account_filter)) + + gle = query.run(as_dict=True) + opening = frappe._dict() for d in gle: opening_debit, opening_credit = toggle_debit_credit(d.opening_debit, d.opening_credit) @@ -121,31 +132,33 @@ def get_opening_balances(filters): return opening -def get_balances_within_period(filters): - account_filter = "" - if filters.get("account"): - account_filter = "and account = %s" % (frappe.db.escape(filters.get("account"))) +def get_balances_within_period(filters, account_filter=None): + GL_Entry = frappe.qb.DocType("GL Entry") - gle = frappe.db.sql( - f""" - select party, sum(debit) as debit, sum(credit) as credit - from `tabGL Entry` - where company=%(company)s - and is_cancelled = 0 - and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' - and posting_date >= %(from_date)s and posting_date <= %(to_date)s - and ifnull(is_opening, 'No') = 'No' - {account_filter} - group by party""", - { - "company": filters.company, - "from_date": filters.from_date, - "to_date": filters.to_date, - "party_type": filters.party_type, - }, - as_dict=True, + query = ( + frappe.qb.from_(GL_Entry) + .select( + GL_Entry.party, + Sum(GL_Entry.debit).as_("debit"), + Sum(GL_Entry.credit).as_("credit"), + ) + .where( + (GL_Entry.company == filters.company) + & (GL_Entry.is_cancelled == 0) + & (GL_Entry.party_type == filters.party_type) + & (GL_Entry.party != "") + & (GL_Entry.posting_date >= filters.from_date) + & (GL_Entry.posting_date <= filters.to_date) + & (GL_Entry.is_opening == "No") + ) + .groupby(GL_Entry.party) ) + if account_filter: + query = query.where(GL_Entry.account.isin(account_filter)) + + gle = query.run(as_dict=True) + balances_within_period = frappe._dict() for d in gle: balances_within_period.setdefault(d.party, [d.debit, d.credit]) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 21e307b480c..b9ea888faf7 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -609,9 +609,7 @@ frappe.ui.form.on("Asset", { frm.trigger("toggle_reference_doc"); if (frm.doc.purchase_receipt) { if (frm.doc.item_code) { - frappe.db.get_doc("Purchase Receipt", frm.doc.purchase_receipt).then((pr_doc) => { - frm.events.set_values_from_purchase_doc(frm, "Purchase Receipt", pr_doc); - }); + frm.events.set_values_from_purchase_doc(frm, "Purchase Receipt"); } else { frm.set_value("purchase_receipt", ""); frappe.msgprint({ @@ -626,9 +624,7 @@ frappe.ui.form.on("Asset", { frm.trigger("toggle_reference_doc"); if (frm.doc.purchase_invoice) { if (frm.doc.item_code) { - frappe.db.get_doc("Purchase Invoice", frm.doc.purchase_invoice).then((pi_doc) => { - frm.events.set_values_from_purchase_doc(frm, "Purchase Invoice", pi_doc); - }); + frm.events.set_values_from_purchase_doc(frm, "Purchase Invoice"); } else { frm.set_value("purchase_invoice", ""); frappe.msgprint({ @@ -639,45 +635,35 @@ frappe.ui.form.on("Asset", { } }, - set_values_from_purchase_doc: function (frm, doctype, purchase_doc) { - frm.set_value("company", purchase_doc.company); - if (purchase_doc.bill_date) { - frm.set_value("purchase_date", purchase_doc.bill_date); - } else { - frm.set_value("purchase_date", purchase_doc.posting_date); - } - if (!frm.doc.is_existing_asset && !frm.doc.available_for_use_date) { - frm.set_value("available_for_use_date", frm.doc.purchase_date); - } - const item = purchase_doc.items.find((item) => item.item_code === frm.doc.item_code); - if (!item) { - let doctype_field = frappe.scrub(doctype); - frm.set_value(doctype_field, ""); - frappe.msgprint({ - title: __("Invalid {0}", [__(doctype)]), - message: __("The selected {0} does not contain the selected Asset Item.", [__(doctype)]), - indicator: "red", - }); - } - frappe.db.get_value("Item", item.item_code, "is_grouped_asset", (r) => { - var asset_quantity = r.is_grouped_asset ? item.qty : 1; - var purchase_amount = flt( - item.valuation_rate * asset_quantity, - precision("gross_purchase_amount") - ); + set_values_from_purchase_doc: (frm, doctype) => { + frappe.call({ + method: "erpnext.assets.doctype.asset.asset.get_values_from_purchase_doc", + args: { + purchase_doc_name: frm.doc.purchase_receipt || frm.doc.purchase_invoice, + item_code: frm.doc.item_code, + doctype: doctype, + }, + callback: (r) => { + if (r.message) { + let data = r.message; + frm.set_value("company", data.company); + frm.set_value("purchase_date", data.purchase_date); + frm.set_value("gross_purchase_amount", data.gross_purchase_amount); + frm.set_value("purchase_amount", data.gross_purchase_amount); + frm.set_value("asset_quantity", data.asset_quantity); + frm.set_value("cost_center", data.cost_center); - frm.set_value("gross_purchase_amount", purchase_amount); - frm.set_value("purchase_amount", purchase_amount); - frm.set_value("asset_quantity", asset_quantity); - frm.set_value("cost_center", item.cost_center || purchase_doc.cost_center); - if (item.asset_location) { - frm.set_value("location", item.asset_location); - } - if (doctype === "Purchase Receipt") { - frm.set_value("purchase_receipt_item", item.name); - } else if (doctype === "Purchase Invoice") { - frm.set_value("purchase_invoice_item", item.name); - } + if (doctype === "Purchase Receipt") { + frm.set_value("purchase_receipt_item", data.purchase_receipt_item); + } else { + frm.set_value("purchase_invoice_item", data.purchase_invoice_item); + } + + let is_editable = !data.is_multiple_items; // if multiple items, then fields should be read-only + frm.set_df_property("gross_purchase_amount", "read_only", is_editable); + frm.set_df_property("asset_quantity", "read_only", is_editable); + } + }, }); }, diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index e28eab9ed13..06b851f2cca 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -227,8 +227,7 @@ "fieldtype": "Currency", "label": "Gross Purchase Amount", "mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)", - "options": "Company:company:default_currency", - "read_only_depends_on": "eval:!doc.is_existing_asset" + "options": "Company:company:default_currency" }, { "fieldname": "available_for_use_date", @@ -470,8 +469,7 @@ "default": "1", "fieldname": "asset_quantity", "fieldtype": "Int", - "label": "Asset Quantity", - "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset" + "label": "Asset Quantity" }, { "fieldname": "depr_entry_posting_status", @@ -541,14 +539,13 @@ }, { "fieldname": "purchase_receipt_item", - "fieldtype": "Link", + "fieldtype": "Data", "hidden": 1, - "label": "Purchase Receipt Item", - "options": "Purchase Receipt Item" + "label": "Purchase Receipt Item" }, { "fieldname": "purchase_invoice_item", - "fieldtype": "Link", + "fieldtype": "Data", "hidden": 1, "label": "Purchase Invoice Item", "options": "Purchase Invoice Item" @@ -595,7 +592,7 @@ "link_fieldname": "target_asset" } ], - "modified": "2024-12-26 14:23:20.968882", + "modified": "2025-02-11 16:01:56.140904", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 994f36ae1f2..9b3cb022bd8 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -95,9 +95,9 @@ class Asset(AccountsController): purchase_amount: DF.Currency purchase_date: DF.Date | None purchase_invoice: DF.Link | None - purchase_invoice_item: DF.Link | None + purchase_invoice_item: DF.Data | None purchase_receipt: DF.Link | None - purchase_receipt_item: DF.Link | None + purchase_receipt_item: DF.Data | None split_from: DF.Link | None status: DF.Literal[ "Draft", @@ -121,6 +121,7 @@ class Asset(AccountsController): def validate(self): self.validate_precision() + self.set_purchase_doc_row_item() self.validate_asset_values() self.validate_asset_and_reference() self.validate_item() @@ -199,6 +200,38 @@ class Asset(AccountsController): def after_delete(self): add_asset_activity(self.name, _("Asset deleted")) + def set_purchase_doc_row_item(self): + if self.is_existing_asset or self.is_composite_asset: + return + + self.purchase_amount = self.gross_purchase_amount + purchase_doc_type = "Purchase Receipt" if self.purchase_receipt else "Purchase Invoice" + purchase_doc = self.purchase_receipt or self.purchase_invoice + + if not purchase_doc: + return + + linked_item = self.get_linked_item(purchase_doc_type, purchase_doc) + + if linked_item: + if purchase_doc_type == "Purchase Receipt": + self.purchase_receipt_item = linked_item + else: + self.purchase_invoice_item = linked_item + + def get_linked_item(self, purchase_doc_type, purchase_doc): + purchase_doc = frappe.get_doc(purchase_doc_type, purchase_doc) + + for item in purchase_doc.items: + if self.asset_quantity > 1: + if item.base_net_amount == self.gross_purchase_amount and item.qty == self.asset_quantity: + return item.name + elif item.qty == self.asset_quantity: + return item.name + else: + if item.base_net_rate == self.gross_purchase_amount and item.qty == self.asset_quantity: + return item.name + def validate_asset_and_reference(self): if self.purchase_invoice or self.purchase_receipt: reference_doc = "Purchase Invoice" if self.purchase_invoice else "Purchase Receipt" @@ -1125,6 +1158,30 @@ def has_active_capitalization(asset): return active_capitalizations > 0 +@frappe.whitelist() +def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype): + purchase_doc = frappe.get_doc(doctype, purchase_doc_name) + matching_items = [item for item in purchase_doc.items if item.item_code == item_code] + + if not matching_items: + frappe.throw(_(f"Selected {doctype} does not contain the Item Code {item_code}")) + + first_item = matching_items[0] + is_multiple_items = len(matching_items) > 1 + + return { + "company": purchase_doc.company, + "purchase_date": purchase_doc.get("bill_date") or purchase_doc.get("posting_date"), + "gross_purchase_amount": flt(first_item.base_net_amount), + "asset_quantity": first_item.qty, + "cost_center": first_item.cost_center or purchase_doc.get("cost_center"), + "asset_location": first_item.get("asset_location"), + "is_multiple_items": is_multiple_items, + "purchase_receipt_item": first_item.name if doctype == "Purchase Receipt" else None, + "purchase_invoice_item": first_item.name if doctype == "Purchase Invoice" else None, + } + + @frappe.whitelist() def split_asset(asset_name, split_qty): asset = frappe.get_doc("Asset", asset_name) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 6ad0631135a..6a1410ac0f9 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -444,9 +444,9 @@ def scrap_asset(asset_name): notes = _("This schedule was created when Asset {0} was scrapped.").format( get_link_to_form(asset.doctype, asset.name) ) - - depreciate_asset(asset, date, notes) - asset.reload() + if asset.status != "Fully Depreciated": + depreciate_asset(asset, date, notes) + asset.reload() depreciation_series = frappe.get_cached_value("Company", asset.company, "series_for_depreciation_entry") diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 4fc20594ffa..1724e3cc99c 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -917,6 +917,7 @@ "fieldtype": "Float", "label": "Subcontracted Quantity", "no_copy": 1, + "non_negative": 1, "read_only": 1 } ], @@ -924,7 +925,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-12-10 12:11:18.536089", + "modified": "2025-02-18 12:35:04.432636", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js index d8c91babdfd..58657da9168 100644 --- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js @@ -52,6 +52,7 @@ frappe.query_reports["Purchase Order Analysis"] = { label: __("Status"), fieldtype: "MultiSelectList", width: "80", + options: ["To Pay", "To Bill", "To Receive", "To Receive and Bill", "Completed"], get_data: function (txt) { let status = ["To Bill", "To Receive", "To Receive and Bill", "Completed"]; let options = []; diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js index 9701e147f05..2c0be8f70d7 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js @@ -50,6 +50,7 @@ frappe.query_reports["Supplier Quotation Comparison"] = { fieldname: "supplier", label: __("Supplier"), fieldtype: "MultiSelectList", + options: "Supplier", get_data: function (txt) { return frappe.db.get_link_options("Supplier", txt); }, @@ -58,6 +59,7 @@ frappe.query_reports["Supplier Quotation Comparison"] = { fieldtype: "MultiSelectList", label: __("Supplier Quotation"), fieldname: "supplier_quotation", + options: "Supplier Quotation", default: "", get_data: function (txt) { return frappe.db.get_link_options("Supplier Quotation", txt, { docstatus: ["<", 2] }); diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index e75fdc126e6..bb0a4070981 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -170,7 +170,7 @@ class AccountsController(TransactionBase): self.validate_qty_is_not_zero() if ( - self.doctype in ["Sales Invoice", "Purchase Invoice"] + self.doctype in ["Sales Invoice", "Purchase Invoice", "POS Invoice"] and self.get("is_return") and self.get("update_stock") ): diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 8da22785b94..9643110b76a 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -779,8 +779,10 @@ class BuyingController(SubcontractingController): is_plural = "s" if len(created_assets) != 1 else "" messages.append( - _("Asset{} {assets_link} created for {}").format( - is_plural, frappe.bold(d.item_code), assets_link=assets_link + _("Asset{is_plural} {assets_link} created for {item_code}").format( + is_plural=is_plural, + assets_link=assets_link, + item_code=frappe.bold(d.item_code), ) ) else: diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 66ec8851727..a9007208a2a 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -258,7 +258,7 @@ def get_already_returned_items(doc): field = ( frappe.scrub(doc.doctype) + "_item" - if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Sales Invoice"] + if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Sales Invoice", "POS Invoice"] else "dn_detail" ) data = frappe.db.sql( @@ -770,6 +770,7 @@ def get_return_against_item_fields(voucher_type): "Delivery Note": "dn_detail", "Sales Invoice": "sales_invoice_item", "Subcontracting Receipt": "subcontracting_receipt_item", + "POS Invoice": "sales_invoice_item", } return return_against_item_fields[voucher_type] diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index f79d83d5b09..8ba8f06c65f 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -596,12 +596,13 @@ class SellingController(StockController): if not self.is_internal_transfer() or self.docstatus == 1 else None ) - if serial_and_batch_bundle and self.is_internal_transfer() and self.is_return: - if self.docstatus == 1: + + if self.is_internal_transfer(): + if serial_and_batch_bundle and self.docstatus == 1 and self.is_return: serial_and_batch_bundle = self.make_package_for_transfer( serial_and_batch_bundle, item_row.warehouse, type_of_transaction="Inward" ) - else: + elif not serial_and_batch_bundle: serial_and_batch_bundle = frappe.db.get_value( "Stock Ledger Entry", {"voucher_detail_no": item_row.name, "warehouse": item_row.warehouse}, @@ -791,6 +792,154 @@ class SellingController(StockController): validate_item_type(self, "is_sales_item", "sales") + def update_stock_reservation_entries(self) -> None: + """Updates Delivered Qty in Stock Reservation Entries.""" + + if not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"): + return + + # Don't update Delivered Qty on Return. + if self.is_return: + return + + so_field = "sales_order" if self.doctype == "Sales Invoice" else "against_sales_order" + + if self._action == "submit": + for item in self.get("items"): + # Skip if `Sales Order` or `Sales Order Item` reference is not set. + if not item.get(so_field) or not item.so_detail: + continue + + sre_list = frappe.db.get_all( + "Stock Reservation Entry", + { + "docstatus": 1, + "voucher_type": "Sales Order", + "voucher_no": item.get(so_field), + "voucher_detail_no": item.so_detail, + "warehouse": item.warehouse, + "status": ["not in", ["Delivered", "Cancelled"]], + }, + order_by="creation", + ) + + # Skip if no Stock Reservation Entries. + if not sre_list: + continue + + qty_to_deliver = item.stock_qty + for sre in sre_list: + if qty_to_deliver <= 0: + break + + sre_doc = frappe.get_doc("Stock Reservation Entry", sre) + + qty_can_be_deliver = 0 + if sre_doc.reservation_based_on == "Serial and Batch" and item.serial_and_batch_bundle: + sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle) + if sre_doc.has_serial_no: + delivered_serial_nos = [d.serial_no for d in sbb.entries] + for entry in sre_doc.sb_entries: + if entry.serial_no in delivered_serial_nos: + entry.delivered_qty = 1 # Qty will always be 0 or 1 for Serial No. + entry.db_update() + qty_can_be_deliver += 1 + delivered_serial_nos.remove(entry.serial_no) + else: + delivered_batch_qty = {d.batch_no: -1 * d.qty for d in sbb.entries} + for entry in sre_doc.sb_entries: + if entry.batch_no in delivered_batch_qty: + delivered_qty = min( + (entry.qty - entry.delivered_qty), delivered_batch_qty[entry.batch_no] + ) + entry.delivered_qty += delivered_qty + entry.db_update() + qty_can_be_deliver += delivered_qty + delivered_batch_qty[entry.batch_no] -= delivered_qty + else: + # `Delivered Qty` should be less than or equal to `Reserved Qty`. + qty_can_be_deliver = min( + (sre_doc.reserved_qty - sre_doc.delivered_qty), qty_to_deliver + ) + + sre_doc.delivered_qty += qty_can_be_deliver + sre_doc.db_update() + + # Update Stock Reservation Entry `Status` based on `Delivered Qty`. + sre_doc.update_status() + + # Update Reserved Stock in Bin. + sre_doc.update_reserved_stock_in_bin() + + qty_to_deliver -= qty_can_be_deliver + + if self._action == "cancel": + for item in self.get("items"): + # Skip if `Sales Order` or `Sales Order Item` reference is not set. + if not item.get(so_field) or not item.so_detail: + continue + + sre_list = frappe.db.get_all( + "Stock Reservation Entry", + { + "docstatus": 1, + "voucher_type": "Sales Order", + "voucher_no": item.get(so_field), + "voucher_detail_no": item.so_detail, + "warehouse": item.warehouse, + "status": ["in", ["Partially Delivered", "Delivered"]], + }, + order_by="creation", + ) + + # Skip if no Stock Reservation Entries. + if not sre_list: + continue + + qty_to_undelivered = item.stock_qty + for sre in sre_list: + if qty_to_undelivered <= 0: + break + + sre_doc = frappe.get_doc("Stock Reservation Entry", sre) + + qty_can_be_undelivered = 0 + if sre_doc.reservation_based_on == "Serial and Batch" and item.serial_and_batch_bundle: + sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle) + if sre_doc.has_serial_no: + serial_nos_to_undelivered = [d.serial_no for d in sbb.entries] + for entry in sre_doc.sb_entries: + if entry.serial_no in serial_nos_to_undelivered: + entry.delivered_qty = 0 # Qty will always be 0 or 1 for Serial No. + entry.db_update() + qty_can_be_undelivered += 1 + serial_nos_to_undelivered.remove(entry.serial_no) + else: + batch_qty_to_undelivered = {d.batch_no: -1 * d.qty for d in sbb.entries} + for entry in sre_doc.sb_entries: + if entry.batch_no in batch_qty_to_undelivered: + undelivered_qty = min( + entry.delivered_qty, batch_qty_to_undelivered[entry.batch_no] + ) + entry.delivered_qty -= undelivered_qty + entry.db_update() + qty_can_be_undelivered += undelivered_qty + batch_qty_to_undelivered[entry.batch_no] -= undelivered_qty + else: + # `Qty to Undelivered` should be less than or equal to `Delivered Qty`. + qty_can_be_undelivered = min(sre_doc.delivered_qty, qty_to_undelivered) + + sre_doc.delivered_qty -= qty_can_be_undelivered + sre_doc.db_update() + + # Update Stock Reservation Entry `Status` based on `Delivered Qty`. + sre_doc.update_status() + + # Update Reserved Stock in Bin. + sre_doc.update_reserved_stock_in_bin() + + qty_to_undelivered -= qty_can_be_undelivered + def set_default_income_account_for_item(obj): for d in obj.get("items"): diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index e1cd0a1c340..e1e4c4ce8f2 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import comma_or, flt, get_link_to_form, getdate, now, nowdate +from frappe.utils import comma_or, flt, get_link_to_form, getdate, now, nowdate, safe_div class OverAllowanceError(frappe.ValidationError): @@ -543,7 +543,7 @@ class StatusUpdater(Document): )[0][0] ) - per_billed = (min(ref_doc_qty, billed_qty) / ref_doc_qty) * 100 + per_billed = safe_div(min(ref_doc_qty, billed_qty), ref_doc_qty) * 100 ref_doc = frappe.get_doc(ref_dt, ref_dn) diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js index 36361e531df..f7c605fdd2e 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js @@ -31,6 +31,7 @@ frappe.query_reports["Opportunity Summary by Sales Stage"] = { fieldname: "status", label: __("Status"), fieldtype: "MultiSelectList", + options: ["Open", "Converted", "Quotation", "Replied"], get_data: function () { return [ { value: "Open", description: "Status" }, diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 1b4840d14b5..90ccff720e8 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -216,7 +216,7 @@ class JobCard(Document): open_job_cards = [] if d.get("employee"): - open_job_cards = self.get_open_job_cards(d.get("employee")) + open_job_cards = self.get_open_job_cards(d.get("employee"), workstation=self.workstation) data = self.get_overlap_for(d, open_job_cards=open_job_cards) if data: @@ -257,9 +257,13 @@ class JobCard(Document): frappe.get_cached_value("Workstation", self.workstation, "production_capacity") or 1 ) - if args.get("employee"): - # override capacity for employee - production_capacity = 1 + if self.get_open_job_cards(args.get("employee")): + frappe.throw( + _( + "Employee {0} is currently working on another workstation. Please assign another employee." + ).format(args.get("employee")), + OverlapError, + ) if not self.has_overlap(production_capacity, time_logs): return {} @@ -366,7 +370,7 @@ class JobCard(Document): return time_logs - def get_open_job_cards(self, employee): + def get_open_job_cards(self, employee, workstation=None): jc = frappe.qb.DocType("Job Card") jctl = frappe.qb.DocType("Job Card Time Log") @@ -377,13 +381,15 @@ class JobCard(Document): .select(jc.name) .where( (jctl.parent == jc.name) - & (jc.workstation == self.workstation) & (jctl.employee == employee) & (jc.docstatus < 1) & (jc.name != self.name) ) ) + if workstation: + query = query.where(jc.workstation == workstation) + jobs = query.run(as_dict=True) return [job.get("name") for job in jobs] if jobs else [] diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js index 2196500cfb0..b4b5955891e 100644 --- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js @@ -56,6 +56,7 @@ frappe.query_reports["Job Card Summary"] = { label: __("Work Orders"), fieldname: "work_order", fieldtype: "MultiSelectList", + options: "Work Order", get_data: function (txt) { return frappe.db.get_link_options("Work Order", txt); }, @@ -64,6 +65,7 @@ frappe.query_reports["Job Card Summary"] = { label: __("Production Item"), fieldname: "production_item", fieldtype: "MultiSelectList", + options: "Item", get_data: function (txt) { return frappe.db.get_link_options("Item", txt); }, diff --git a/erpnext/manufacturing/report/production_planning_report/production_planning_report.js b/erpnext/manufacturing/report/production_planning_report/production_planning_report.js index d6ca248abb5..40d87b8b0a2 100644 --- a/erpnext/manufacturing/report/production_planning_report/production_planning_report.js +++ b/erpnext/manufacturing/report/production_planning_report/production_planning_report.js @@ -41,7 +41,7 @@ frappe.query_reports["Production Planning Report"] = { fieldname: "docnames", label: __("Document Name"), fieldtype: "MultiSelectList", - options: "Sales Order", + options: "based_on", get_data: function (txt) { if (!frappe.query_report.filters) return; diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.js b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js index f48f8b7ea18..74fbf47bb23 100644 --- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.js +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js @@ -42,6 +42,7 @@ frappe.query_reports["Work Order Summary"] = { label: __("Sales Orders"), fieldname: "sales_order", fieldtype: "MultiSelectList", + options: "Sales Order", get_data: function (txt) { return frappe.db.get_link_options("Sales Order", txt); }, @@ -50,6 +51,7 @@ frappe.query_reports["Work Order Summary"] = { label: __("Production Item"), fieldname: "production_item", fieldtype: "MultiSelectList", + options: "Item", get_data: function (txt) { return frappe.db.get_link_options("Item", txt); }, diff --git a/erpnext/patches.txt b/erpnext/patches.txt index bb76f948f3a..cb10fcce1c4 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -393,3 +393,4 @@ erpnext.patches.v15_0.sync_auto_reconcile_config execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment") erpnext.patches.v14_0.disable_add_row_in_gross_profit erpnext.patches.v15_0.set_difference_amount_in_asset_value_adjustment +erpnext.patches.v14_0.update_posting_datetime diff --git a/erpnext/patches/v14_0/update_posting_datetime.py b/erpnext/patches/v14_0/update_posting_datetime.py new file mode 100644 index 00000000000..cb28193b74b --- /dev/null +++ b/erpnext/patches/v14_0/update_posting_datetime.py @@ -0,0 +1,10 @@ +import frappe + + +def execute(): + frappe.db.sql( + """ + UPDATE `tabStock Ledger Entry` + SET posting_datetime = timestamp(posting_date, posting_time) + """ + ) diff --git a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py index 523b559d734..ff77fbb91ec 100644 --- a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py +++ b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py @@ -1,74 +1,29 @@ import frappe +from frappe.utils import cstr def execute(): - frappe.reload_doc("assets", "doctype", "Asset Depreciation Schedule") - frappe.reload_doc("assets", "doctype", "Asset Finance Book") - frappe.reload_doc("assets", "doctype", "Asset") - - assets = get_details_of_draft_or_submitted_depreciable_assets() - asset_finance_books_map = get_asset_finance_books_map() - asset_depreciation_schedules_map = get_asset_depreciation_schedules_map() - for asset in assets: - if not asset_depreciation_schedules_map.get(asset.name): + for key, fb_row in asset_finance_books_map.items(): + depreciation_schedules = asset_depreciation_schedules_map.get(key) + if not depreciation_schedules: continue - depreciation_schedules = asset_depreciation_schedules_map[asset.name] + asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule") + asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(fb_row, fb_row) + asset_depr_schedule_doc.flags.ignore_validate = True + asset_depr_schedule_doc.insert() - for fb_row in asset_finance_books_map[asset.name]: - asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule") + if fb_row.docstatus == 1: + frappe.db.set_value( + "Asset Depreciation Schedule", + asset_depr_schedule_doc.name, + {"docstatus": 1, "status": "Active"}, + ) - asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(asset, fb_row) - - asset_depr_schedule_doc.insert() - - if asset.docstatus == 1: - asset_depr_schedule_doc.submit() - - depreciation_schedules_of_fb_row = [ - ds for ds in depreciation_schedules if ds["finance_book_id"] == str(fb_row.idx) - ] - - update_depreciation_schedules(depreciation_schedules_of_fb_row, asset_depr_schedule_doc.name) - - -def get_details_of_draft_or_submitted_depreciable_assets(): - asset = frappe.qb.DocType("Asset") - - records = ( - frappe.qb.from_(asset) - .select( - asset.name, - asset.opening_accumulated_depreciation, - asset.gross_purchase_amount, - asset.opening_number_of_booked_depreciations, - asset.docstatus, - ) - .where(asset.calculate_depreciation == 1) - .where(asset.docstatus < 2) - ).run(as_dict=True) - - return records - - -def group_records_by_asset_name(records): - grouped_dict = {} - - for item in records: - key = next(iter(item.keys())) - value = item[key] - - if value not in grouped_dict: - grouped_dict[value] = [] - - del item["asset_name"] - - grouped_dict[value].append(item) - - return grouped_dict + update_depreciation_schedules(depreciation_schedules, asset_depr_schedule_doc.name) def get_asset_finance_books_map(): @@ -90,12 +45,20 @@ def get_asset_finance_books_map(): afb.expected_value_after_useful_life, afb.daily_prorata_based, afb.shift_based, + asset.docstatus, + asset.name, + asset.opening_accumulated_depreciation, + asset.gross_purchase_amount, + asset.opening_number_of_booked_depreciations, ) .where(asset.docstatus < 2) + .where(asset.calculate_depreciation == 1) .orderby(afb.idx) ).run(as_dict=True) - asset_finance_books_map = group_records_by_asset_name(records) + asset_finance_books_map = frappe._dict() + for d in records: + asset_finance_books_map.setdefault((d.asset_name, cstr(d.finance_book)), d) return asset_finance_books_map @@ -111,13 +74,17 @@ def get_asset_depreciation_schedules_map(): .select( asset.name.as_("asset_name"), ds.name, + ds.finance_book, ds.finance_book_id, ) .where(asset.docstatus < 2) + .where(asset.calculate_depreciation == 1) .orderby(ds.idx) ).run(as_dict=True) - asset_depreciation_schedules_map = group_records_by_asset_name(records) + asset_depreciation_schedules_map = frappe._dict() + for d in records: + asset_depreciation_schedules_map.setdefault((d.asset_name, cstr(d.finance_book)), []).append(d) return asset_depreciation_schedules_map diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 875e6980e1e..30ebb3d97ce 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -894,10 +894,16 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { this.frm.refresh_fields(); } - set_default_payment(total_amount_to_pay, update_paid_amount) { + async set_default_payment(total_amount_to_pay, update_paid_amount) { var me = this; var payment_status = true; if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) { + let r = await frappe.db.get_value("POS Profile", this.frm.doc.pos_profile, "disable_grand_total_to_default_mop"); + + if (r.message.disable_grand_total_to_default_mop) { + return; + } + $.each(this.frm.doc['payments'] || [], function(index, data) { if(data.default && payment_status && total_amount_to_pay > 0) { let base_amount, amount; diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 7311857e350..03665b48a85 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -353,6 +353,26 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. ); dialog.show(); } + + currency() { + super.currency(); + let me = this; + const company_currency = this.get_company_currency(); + if (this.frm.doc.currency && this.frm.doc.currency !== company_currency) { + this.get_exchange_rate( + this.frm.doc.transaction_date, + this.frm.doc.currency, + company_currency, + function (exchange_rate) { + if (exchange_rate != me.frm.doc.conversion_rate) { + me.set_margin_amount_based_on_currency(exchange_rate); + me.set_actual_charges_based_on_currency(exchange_rate); + me.frm.set_value("conversion_rate", exchange_rate); + } + } + ); + } + } }; cur_frm.script_manager.make(erpnext.selling.QuotationController); diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index fb9e895ccb7..500a7657176 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -106,7 +106,11 @@ "purchase_order", "column_break_89", "material_request_item", - "purchase_order_item" + "purchase_order_item", + "accounting_dimensions_section", + "cost_center", + "column_break_ihdh", + "project" ], "fields": [ { @@ -926,12 +930,42 @@ "fieldname": "available_quantity_section", "fieldtype": "Section Break", "label": "Available Quantity" + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "allow_on_submit": 1, + "default": ":Company", + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center", + "print_hide": 1, + "print_width": "120px", + "reqd": 1, + "width": "120px" + }, + { + "fieldname": "column_break_ihdh", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project", + "search_index": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2024-11-21 14:21:29.743474", + "modified": "2025-02-06 13:29:24.619850", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.py b/erpnext/selling/doctype/sales_order_item/sales_order_item.py index 888ea755e2e..7f2a37b1616 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.py +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.py @@ -32,6 +32,7 @@ class SalesOrderItem(Document): brand: DF.Link | None company_total_stock: DF.Float conversion_factor: DF.Float + cost_center: DF.Link customer_item_code: DF.Data | None delivered_by_supplier: DF.Check delivered_qty: DF.Float @@ -68,6 +69,7 @@ class SalesOrderItem(Document): pricing_rules: DF.SmallText | None produced_qty: DF.Float production_plan_qty: DF.Float + project: DF.Link | None projected_qty: DF.Float purchase_order: DF.Link | None purchase_order_item: DF.Data | None diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 33dd0489ba2..fc8b75031c8 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -340,11 +340,19 @@ erpnext.PointOfSale.Payment = class { // pass } - render_payment_section() { + async render_payment_section() { this.render_payment_mode_dom(); this.make_invoice_fields_control(); this.update_totals_section(); - this.focus_on_default_mop(); + let r = await frappe.db.get_value( + "POS Profile", + this.frm.doc.pos_profile, + "disable_grand_total_to_default_mop" + ); + + if (!r.message.disable_grand_total_to_default_mop) { + this.focus_on_default_mop(); + } } after_render() { diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js index 99ef5e40f3b..e4e9ec07b3f 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js @@ -87,6 +87,7 @@ function get_filters() { fieldname: "status", label: __("Status"), fieldtype: "MultiSelectList", + options: ["Overdue", "Unpaid", "Completed", "Partly Paid"], width: 100, get_data: function (txt) { let status = ["Overdue", "Unpaid", "Completed", "Partly Paid"]; diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js index e47e5f8ae4d..5866fcbc845 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js @@ -53,6 +53,7 @@ frappe.query_reports["Sales Order Analysis"] = { fieldname: "status", label: __("Status"), fieldtype: "MultiSelectList", + options: ["To Pay", "To Bill", "To Deliver", "To Deliver and Bill", "Completed"], width: "80", get_data: function (txt) { let status = ["To Bill", "To Deliver", "To Deliver and Bill", "Completed"]; diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 39527c9ec47..ba04abce8f3 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -491,149 +491,6 @@ class DeliveryNote(SellingController): self.delete_auto_created_batches() - def update_stock_reservation_entries(self) -> None: - """Updates Delivered Qty in Stock Reservation Entries.""" - - # Don't update Delivered Qty on Return. - if self.is_return: - return - - if self._action == "submit": - for item in self.get("items"): - # Skip if `Sales Order` or `Sales Order Item` reference is not set. - if not item.against_sales_order or not item.so_detail: - continue - - sre_list = frappe.db.get_all( - "Stock Reservation Entry", - { - "docstatus": 1, - "voucher_type": "Sales Order", - "voucher_no": item.against_sales_order, - "voucher_detail_no": item.so_detail, - "warehouse": item.warehouse, - "status": ["not in", ["Delivered", "Cancelled"]], - }, - order_by="creation", - ) - - # Skip if no Stock Reservation Entries. - if not sre_list: - continue - - qty_to_deliver = item.stock_qty - for sre in sre_list: - if qty_to_deliver <= 0: - break - - sre_doc = frappe.get_doc("Stock Reservation Entry", sre) - - qty_can_be_deliver = 0 - if sre_doc.reservation_based_on == "Serial and Batch": - sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle) - if sre_doc.has_serial_no: - delivered_serial_nos = [d.serial_no for d in sbb.entries] - for entry in sre_doc.sb_entries: - if entry.serial_no in delivered_serial_nos: - entry.delivered_qty = 1 # Qty will always be 0 or 1 for Serial No. - entry.db_update() - qty_can_be_deliver += 1 - delivered_serial_nos.remove(entry.serial_no) - else: - delivered_batch_qty = {d.batch_no: -1 * d.qty for d in sbb.entries} - for entry in sre_doc.sb_entries: - if entry.batch_no in delivered_batch_qty: - delivered_qty = min( - (entry.qty - entry.delivered_qty), delivered_batch_qty[entry.batch_no] - ) - entry.delivered_qty += delivered_qty - entry.db_update() - qty_can_be_deliver += delivered_qty - delivered_batch_qty[entry.batch_no] -= delivered_qty - else: - # `Delivered Qty` should be less than or equal to `Reserved Qty`. - qty_can_be_deliver = min( - (sre_doc.reserved_qty - sre_doc.delivered_qty), qty_to_deliver - ) - - sre_doc.delivered_qty += qty_can_be_deliver - sre_doc.db_update() - - # Update Stock Reservation Entry `Status` based on `Delivered Qty`. - sre_doc.update_status() - - # Update Reserved Stock in Bin. - sre_doc.update_reserved_stock_in_bin() - - qty_to_deliver -= qty_can_be_deliver - - if self._action == "cancel": - for item in self.get("items"): - # Skip if `Sales Order` or `Sales Order Item` reference is not set. - if not item.against_sales_order or not item.so_detail: - continue - - sre_list = frappe.db.get_all( - "Stock Reservation Entry", - { - "docstatus": 1, - "voucher_type": "Sales Order", - "voucher_no": item.against_sales_order, - "voucher_detail_no": item.so_detail, - "warehouse": item.warehouse, - "status": ["in", ["Partially Delivered", "Delivered"]], - }, - order_by="creation", - ) - - # Skip if no Stock Reservation Entries. - if not sre_list: - continue - - qty_to_undelivered = item.stock_qty - for sre in sre_list: - if qty_to_undelivered <= 0: - break - - sre_doc = frappe.get_doc("Stock Reservation Entry", sre) - - qty_can_be_undelivered = 0 - if sre_doc.reservation_based_on == "Serial and Batch": - sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle) - if sre_doc.has_serial_no: - serial_nos_to_undelivered = [d.serial_no for d in sbb.entries] - for entry in sre_doc.sb_entries: - if entry.serial_no in serial_nos_to_undelivered: - entry.delivered_qty = 0 # Qty will always be 0 or 1 for Serial No. - entry.db_update() - qty_can_be_undelivered += 1 - serial_nos_to_undelivered.remove(entry.serial_no) - else: - batch_qty_to_undelivered = {d.batch_no: -1 * d.qty for d in sbb.entries} - for entry in sre_doc.sb_entries: - if entry.batch_no in batch_qty_to_undelivered: - undelivered_qty = min( - entry.delivered_qty, batch_qty_to_undelivered[entry.batch_no] - ) - entry.delivered_qty -= undelivered_qty - entry.db_update() - qty_can_be_undelivered += undelivered_qty - batch_qty_to_undelivered[entry.batch_no] -= undelivered_qty - else: - # `Qty to Undelivered` should be less than or equal to `Delivered Qty`. - qty_can_be_undelivered = min(sre_doc.delivered_qty, qty_to_undelivered) - - sre_doc.delivered_qty -= qty_can_be_undelivered - sre_doc.db_update() - - # Update Stock Reservation Entry `Status` based on `Delivered Qty`. - sre_doc.update_status() - - # Update Reserved Stock in Bin. - sre_doc.update_reserved_stock_in_bin() - - qty_to_undelivered -= qty_can_be_undelivered - def validate_against_stock_reservation_entries(self): """Validates if Stock Reservation Entries are available for the Sales Order Item reference.""" diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 4afaffe6fd4..f86798d17e8 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -769,6 +769,48 @@ class TestDeliveryNote(FrappeTestCase): {"warehouse": "_Test Warehouse - _TC"}, ) + def test_delivery_note_internal_transfer_serial_no_status(self): + from erpnext.selling.doctype.customer.test_customer import create_internal_customer + + item = make_item( + "_Test Item for Internal Transfer With Serial No Status", + properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "INT-SN-.####"}, + ).name + + warehouse = "_Test Warehouse - _TC" + target = "Stores - _TC" + company = "_Test Company" + customer = create_internal_customer(represents_company=company) + rate = 42 + + se = make_stock_entry(target=warehouse, qty=5, basic_rate=rate, item_code=item) + serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle) + + dn = create_delivery_note( + item_code=item, + company=company, + customer=customer, + qty=5, + rate=500, + warehouse=warehouse, + target_warehouse=target, + ignore_pricing_rule=0, + use_serial_batch_fields=1, + serial_no="\n".join(serial_nos), + ) + + for serial_no in serial_nos: + sn = frappe.db.get_value("Serial No", serial_no, ["status", "warehouse"], as_dict=1) + self.assertEqual(sn.status, "Active") + self.assertEqual(sn.warehouse, target) + + dn.cancel() + + for serial_no in serial_nos: + sn = frappe.db.get_value("Serial No", serial_no, ["status", "warehouse"], as_dict=1) + self.assertEqual(sn.status, "Active") + self.assertEqual(sn.warehouse, warehouse) + def test_delivery_of_bundled_items_to_target_warehouse(self): from erpnext.selling.doctype.customer.test_customer import create_internal_customer diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 2c56b6a80bc..16ac4fd1017 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -1,6 +1,5 @@ { "actions": [], - "allow_guest_to_view": 1, "allow_import": 1, "allow_rename": 1, "autoname": "field:item_code", @@ -888,10 +887,9 @@ "icon": "fa fa-tag", "idx": 2, "image_field": "image", - "index_web_pages_for_search": 1, "links": [], "make_attachments_public": 1, - "modified": "2024-04-30 13:46:39.098753", + "modified": "2025-02-03 23:43:57.253667", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index 1daf6791d40..cb415e3813a 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -202,7 +202,8 @@ "oldfieldname": "parent_detail_docname", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "depends_on": "batch_no", @@ -295,7 +296,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-02-04 16:30:44.263964", + "modified": "2025-02-18 13:07:02.789654", "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", @@ -305,4 +306,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 624b169b5ae..c3043bbf1b5 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -1242,6 +1242,7 @@ class TestPickList(FrappeTestCase): "is_recursive": 1, "recurse_for": 2, "free_qty": 1, + "dont_enforce_free_item_qty": 0, "company": "_Test Company", "customer": "_Test Customer", } diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json index ebedd5b864b..9b440eabcfc 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json @@ -34,7 +34,9 @@ "verified_by", "column_break_17", "remarks", - "amended_from" + "amended_from", + "print_settings_section", + "letter_head" ], "fields": [ { @@ -255,6 +257,20 @@ "fieldtype": "Link", "label": "Company", "options": "Company" + }, + { + "fieldname": "print_settings_section", + "fieldtype": "Section Break", + "label": "Print Settings" + }, + { + "allow_on_submit": 1, + "fetch_from": "company.default_letter_head", + "fetch_if_empty": 1, + "fieldname": "letter_head", + "fieldtype": "Link", + "label": "Letter Head", + "options": "Letter Head" } ], "icon": "fa fa-search", @@ -262,7 +278,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-01-16 17:00:48.774532", + "modified": "2025-02-17 13:20:17.583094", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection", diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 1eb565b5338..714db645ca6 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -37,6 +37,7 @@ class QualityInspection(Document): item_code: DF.Link item_name: DF.Data | None item_serial_no: DF.Link | None + letter_head: DF.Link | None manual_inspection: DF.Check naming_series: DF.Literal["MAT-QA-.YYYY.-"] quality_inspection_template: DF.Link | None diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js index ad8360ea75c..404abbd21bc 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js @@ -134,6 +134,13 @@ frappe.ui.form.on("Serial and Batch Bundle", { }, toggle_fields(frm) { + let show_naming_series_field = + frappe.user_defaults.set_serial_and_batch_bundle_naming_based_on_naming_series; + frm.toggle_display("naming_series", cint(show_naming_series_field)); + frm.toggle_reqd("naming_series", cint(show_naming_series_field)); + + frm.toggle_display("naming_series", frm.doc.__islocal ? true : false); + if (frm.doc.has_serial_no) { frm.doc.entries.forEach((row) => { if (Math.abs(row.qty) !== 1) { diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json index 3ff76ee818b..02ecc11f44a 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json @@ -7,6 +7,7 @@ "engine": "InnoDB", "field_order": [ "item_details_tab", + "naming_series", "company", "item_name", "has_serial_no", @@ -242,12 +243,20 @@ "fieldtype": "Data", "label": "Returned Against", "read_only": 1 + }, + { + "default": "SABB-.########", + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "\nSABB-.########", + "set_only_once": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-02-12 10:53:32.090309", + "modified": "2025-02-17 18:22:36.056205", "modified_by": "Administrator", "module": "Stock", "name": "Serial and Batch Bundle", diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index c37d2464ff0..c37051e1e2b 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -8,6 +8,7 @@ from collections import Counter, defaultdict import frappe from frappe import _, _dict, bold from frappe.model.document import Document +from frappe.model.naming import make_autoname from frappe.query_builder.functions import CombineDatetime, Sum from frappe.utils import ( add_days, @@ -68,6 +69,7 @@ class SerialandBatchBundle(Document): item_code: DF.Link item_group: DF.Link | None item_name: DF.Data | None + naming_series: DF.Literal["", "SABB-.########"] posting_date: DF.Date | None posting_time: DF.Time | None returned_against: DF.Data | None @@ -80,6 +82,24 @@ class SerialandBatchBundle(Document): warehouse: DF.Link | None # end: auto-generated types + def autoname(self): + if frappe.db.get_single_value( + "Stock Settings", "set_serial_and_batch_bundle_naming_based_on_naming_series" + ): + if not self.naming_series: + frappe.throw(_("Naming Series is mandatory")) + + naming_series = self.naming_series + if "#" not in naming_series: + naming_series += ".#####" + + self.name = make_autoname(self.naming_series) + else: + try: + self.name = frappe.generate_hash(length=20) + except frappe.DuplicateEntryError: + self.autoname() + def validate(self): if self.docstatus == 1 and self.voucher_detail_no: self.validate_voucher_detail_no() diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index 0a4352e1ea9..a3bd20b80ba 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -17,6 +17,67 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry class TestSerialandBatchBundle(FrappeTestCase): + def test_naming_for_sabb(self): + frappe.db.set_single_value( + "Stock Settings", "set_serial_and_batch_bundle_naming_based_on_naming_series", 1 + ) + + serial_item_code = "New Serial No Valuation 11" + make_item( + serial_item_code, + { + "has_serial_no": 1, + "serial_no_series": "TEST-A-SER-VAL-.#####", + "is_stock_item": 1, + }, + ) + + for sn in ["TEST-A-SER-VAL-00001", "TEST-A-SER-VAL-00002"]: + if not frappe.db.exists("Serial No", sn): + frappe.get_doc( + { + "doctype": "Serial No", + "serial_no": sn, + "item_code": serial_item_code, + } + ).insert(ignore_permissions=True) + + bundle_doc = make_serial_batch_bundle( + { + "item_code": serial_item_code, + "warehouse": "_Test Warehouse - _TC", + "voucher_type": "Stock Entry", + "posting_date": today(), + "posting_time": nowtime(), + "qty": 10, + "serial_nos": ["TEST-A-SER-VAL-00001", "TEST-A-SER-VAL-00002"], + "type_of_transaction": "Inward", + "do_not_submit": True, + } + ) + + self.assertTrue(bundle_doc.name.startswith("SABB-")) + + frappe.db.set_single_value( + "Stock Settings", "set_serial_and_batch_bundle_naming_based_on_naming_series", 0 + ) + + bundle_doc = make_serial_batch_bundle( + { + "item_code": serial_item_code, + "warehouse": "_Test Warehouse - _TC", + "voucher_type": "Stock Entry", + "posting_date": today(), + "posting_time": nowtime(), + "qty": 10, + "serial_nos": ["TEST-A-SER-VAL-00001", "TEST-A-SER-VAL-00002"], + "type_of_transaction": "Inward", + "do_not_submit": True, + } + ) + + self.assertFalse(bundle_doc.name.startswith("SABB-")) + def test_inward_outward_serial_valuation(self): from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 95d6d7079b7..65db490e7b5 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2417,7 +2417,7 @@ class StockEntry(StockController): item_row = item_dict[d] child_qty = flt(item_row["qty"], precision) - if not self.is_return and child_qty <= 0: + if not self.is_return and child_qty <= 0 and not item_row.get("is_scrap_item"): continue se_child = self.append("items") diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index e542a1582e3..22a24d1bfa1 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -56,6 +56,8 @@ "use_serial_batch_fields", "do_not_update_serial_batch_on_creation_of_auto_bundle", "allow_existing_serial_no", + "serial_and_batch_bundle_section", + "set_serial_and_batch_bundle_naming_based_on_naming_series", "stock_planning_tab", "auto_material_request", "auto_indent", @@ -467,6 +469,17 @@ "fieldname": "allow_existing_serial_no", "fieldtype": "Check", "label": "Allow existing Serial No to be Manufactured/Received again" + }, + { + "fieldname": "serial_and_batch_bundle_section", + "fieldtype": "Section Break", + "label": "Serial and Batch Bundle" + }, + { + "default": "0", + "fieldname": "set_serial_and_batch_bundle_naming_based_on_naming_series", + "fieldtype": "Check", + "label": "Set Serial and Batch Bundle Naming Based on Naming Series" } ], "icon": "icon-cog", @@ -474,7 +487,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-12-09 17:52:36.030456", + "modified": "2025-02-17 14:36:36.177743", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index b7a317cd66a..8589004c8bf 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -55,6 +55,7 @@ class StockSettings(Document): role_allowed_to_create_edit_back_dated_transactions: DF.Link | None role_allowed_to_over_deliver_receive: DF.Link | None sample_retention_warehouse: DF.Link | None + set_serial_and_batch_bundle_naming_based_on_naming_series: DF.Check show_barcode_field: DF.Check stock_auth_role: DF.Link | None stock_frozen_upto: DF.Date | None @@ -75,6 +76,7 @@ class StockSettings(Document): "default_warehouse", "set_qty_in_transactions_based_on_serial_no_input", "use_serial_batch_fields", + "set_serial_and_batch_bundle_naming_based_on_naming_series", ]: frappe.db.set_default(key, self.get(key, "")) diff --git a/erpnext/stock/report/item_shortage_report/item_shortage_report.js b/erpnext/stock/report/item_shortage_report/item_shortage_report.js index d293b895e5b..8ce71ddadcb 100644 --- a/erpnext/stock/report/item_shortage_report/item_shortage_report.js +++ b/erpnext/stock/report/item_shortage_report/item_shortage_report.js @@ -16,6 +16,7 @@ frappe.query_reports["Item Shortage Report"] = { fieldname: "warehouse", label: __("Warehouse"), fieldtype: "MultiSelectList", + options: "Warehouse", width: "100", get_data: function (txt) { return frappe.db.get_link_options("Warehouse", txt); diff --git a/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.js b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.js index 3b66cb036a7..ab1e2babc80 100644 --- a/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.js +++ b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.js @@ -50,6 +50,7 @@ frappe.query_reports["Serial and Batch Summary"] = { fieldname: "voucher_no", label: __("Voucher No"), fieldtype: "MultiSelectList", + options: "voucher_type", get_data: function (txt) { if (!frappe.query_report.filters) return; diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 6369562a62d..f1055b114cf 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -663,4 +663,4 @@ def get_combine_datetime(posting_date, posting_time): if isinstance(posting_time, datetime.timedelta): posting_time = (datetime.datetime.min + posting_time).time() - return datetime.datetime.combine(posting_date, posting_time).replace(microsecond=0) + return datetime.datetime.combine(posting_date, posting_time) diff --git a/erpnext/templates/utils.py b/erpnext/templates/utils.py index 57750a56f6f..15af9f0f014 100644 --- a/erpnext/templates/utils.py +++ b/erpnext/templates/utils.py @@ -3,6 +3,7 @@ import frappe +from frappe.utils import escape_html @frappe.whitelist(allow_guest=True) @@ -11,6 +12,8 @@ def send_message(sender, message, subject="Website Query"): website_send_message(sender, message, subject) + message = escape_html(message) + lead = customer = None customer = frappe.db.sql( """select distinct dl.link_name from `tabDynamic Link` dl