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