Merge pull request #45981 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2025-02-19 17:19:18 +05:30
committed by GitHub
74 changed files with 955 additions and 409 deletions

View File

@@ -41,6 +41,11 @@ class AccountingDimension(Document):
self.set_fieldname_and_label() self.set_fieldname_and_label()
def validate(self): def validate(self):
self.validate_doctype()
validate_column_name(self.fieldname)
self.validate_dimension_defaults()
def validate_doctype(self):
if self.document_type in ( if self.document_type in (
*core_doctypes_list, *core_doctypes_list,
"Accounting Dimension", "Accounting Dimension",
@@ -62,9 +67,6 @@ class AccountingDimension(Document):
if not self.is_new(): if not self.is_new():
self.validate_document_type_change() self.validate_document_type_change()
validate_column_name(self.fieldname)
self.validate_dimension_defaults()
def validate_document_type_change(self): def validate_document_type_change(self):
doctype_before_save = frappe.db.get_value("Accounting Dimension", self.name, "document_type") doctype_before_save = frappe.db.get_value("Accounting Dimension", self.name, "document_type")
if doctype_before_save != self.document_type: if doctype_before_save != self.document_type:
@@ -103,6 +105,7 @@ class AccountingDimension(Document):
def on_update(self): def on_update(self):
frappe.flags.accounting_dimensions = None frappe.flags.accounting_dimensions = None
frappe.flags.accounting_dimensions_details = None
def make_dimension_in_accounting_doctypes(doc, doclist=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( 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 """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 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, as_dict=1,
) )

View File

@@ -124,3 +124,20 @@ class TestGLEntry(unittest.TestCase):
str(e), str(e),
"Party Type and Party can only be set for Receivable / Payable account_Test Account Cost for Goods Sold - _TC", "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)

View File

@@ -313,6 +313,7 @@ class PaymentRequest(Document):
"payer_name": data.customer_name, "payer_name": data.customer_name,
"order_id": self.name, "order_id": self.name,
"currency": self.currency, "currency": self.currency,
"payment_gateway": self.payment_gateway,
} }
) )
@@ -774,7 +775,10 @@ def get_existing_paid_amount(doctype, name):
frappe.qb.from_(PL) frappe.qb.from_(PL)
.left_join(PER) .left_join(PER)
.on( .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")) .select(Abs(Sum(PL.amount)).as_("total_paid_amount"))
.where(PL.against_voucher_type.eq(doctype)) .where(PL.against_voucher_type.eq(doctype))

View File

@@ -542,6 +542,45 @@ class TestPaymentRequest(FrappeTestCase):
self.assertEqual(pr.grand_total, si.outstanding_amount) 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): def test_partial_paid_invoice_with_submitted_payment_entry(self):
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000) pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)

View File

@@ -39,10 +39,12 @@ class TestPOSClosingEntry(unittest.TestCase):
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1) 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.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
pos_inv1.save()
pos_inv1.submit() pos_inv1.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) 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.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
pos_inv2.save()
pos_inv2.submit() pos_inv2.submit()
pcv_doc = make_closing_entry_from_opening(opening_entry) 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 = 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.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
pos_inv.save()
pos_inv.submit() pos_inv.submit()
pcv_doc = make_closing_entry_from_opening(opening_entry) 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 = create_pos_invoice(rate=3500, do_not_submit=1)
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500}) pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
pos_inv1.save()
pos_inv1.submit() pos_inv1.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) 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.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
pos_inv2.save()
pos_inv2.submit() pos_inv2.submit()
# make return entry of pos_inv2 # 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 = create_pos_invoice(rate=3500, do_not_submit=1)
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500}) pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
pos_inv1.save()
pos_inv1.submit() pos_inv1.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) 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.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
pos_inv2.save()
pos_inv2.submit() pos_inv2.submit()
pcv_doc = make_closing_entry_from_opening(opening_entry) 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) 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 = 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.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
pos_inv1.save()
pos_inv1.submit() pos_inv1.submit()
# if in between a mandatory accounting dimension is added to the POS Profile then # 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) opening_entry = create_opening_entry(pos_profile, test_user.name)
pos_inv = create_pos_invoice( 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( 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) batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
self.assertEqual(batch_qty_with_pos, 0.0) self.assertEqual(batch_qty_with_pos, 0.0)

View File

@@ -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 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
class PartialPaymentValidationError(frappe.ValidationError):
pass
class POSInvoice(SalesInvoice): class POSInvoice(SalesInvoice):
# begin: auto-generated types # begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block. # 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_payment_amount()
self.validate_loyalty_transaction() self.validate_loyalty_transaction()
self.validate_company_with_pos_company() self.validate_company_with_pos_company()
self.validate_full_payment()
if self.coupon_code: if self.coupon_code:
from erpnext.accounts.doctype.pricing_rule.utils import validate_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: if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points:
validate_loyalty_points(self, 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): def set_status(self, update=False, status=None, update_modified=True):
if self.is_new(): if self.is_new():
if self.get("amended_from"): if self.get("amended_from"):

View File

@@ -7,7 +7,7 @@ import unittest
import frappe import frappe
from frappe import _ 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.pos_profile.test_pos_profile import make_pos_profile
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
@@ -313,7 +313,7 @@ class TestPOSInvoice(unittest.TestCase):
) )
pos.append( 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() pos.insert()
@@ -324,6 +324,11 @@ class TestPOSInvoice(unittest.TestCase):
# partial return 1 # partial return 1
pos_return1.get("items")[0].qty = -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.submit()
pos_return1.reload() pos_return1.reload()
@@ -338,6 +343,11 @@ class TestPOSInvoice(unittest.TestCase):
# partial return 2 # partial return 2
pos_return2 = make_sales_return(pos.name) 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() pos_return2.submit()
self.assertEqual(pos_return2.get("items")[0].qty, -1) self.assertEqual(pos_return2.get("items")[0].qty, -1)
@@ -373,6 +383,15 @@ class TestPOSInvoice(unittest.TestCase):
inv.payments = [] inv.payments = []
self.assertRaises(frappe.ValidationError, inv.insert) 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): def test_serialized_item_transaction(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item 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" "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( lpe = frappe.get_doc(
"Loyalty Point Entry", "Loyalty Point Entry",
@@ -607,7 +632,13 @@ class TestPOSInvoice(unittest.TestCase):
) )
# add 10 loyalty points # 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( before_lp_details = get_loyalty_program_details_with_points(
"Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty" "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() 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 = 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.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 270})
pos_inv.save()
pos_inv.submit() pos_inv.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) 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.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
pos_inv2.save()
pos_inv2.submit() pos_inv2.submit()
consolidate_pos_invoices() consolidate_pos_invoices()
@@ -676,6 +709,7 @@ class TestPOSInvoice(unittest.TestCase):
"included_in_print_rate": 1, "included_in_print_rate": 1,
}, },
) )
pos_inv.save()
pos_inv.submit() pos_inv.submit()
pos_inv2 = create_pos_invoice(rate=300, qty=2, do_not_submit=1) 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, "included_in_print_rate": 1,
}, },
) )
pos_inv2.save()
pos_inv2.submit() pos_inv2.submit()
consolidate_pos_invoices() consolidate_pos_invoices()
@@ -744,6 +779,7 @@ class TestPOSInvoice(unittest.TestCase):
"included_in_print_rate": 1, "included_in_print_rate": 1,
}, },
) )
pos_inv2.save()
pos_inv2.submit() pos_inv2.submit()
consolidate_pos_invoices() consolidate_pos_invoices()
@@ -774,7 +810,10 @@ class TestPOSInvoice(unittest.TestCase):
# POS Invoice 1, for the batch without bundle # 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 = 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.items[0].batch_no = batch_no
pos_inv1.save() pos_inv1.save()
pos_inv1.submit() pos_inv1.submit()
@@ -790,8 +829,14 @@ class TestPOSInvoice(unittest.TestCase):
# POS Invoice 2, for the batch with bundle # POS Invoice 2, for the batch with bundle
pos_inv2 = create_pos_invoice( 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() pos_inv2.reload()
self.assertTrue(pos_inv2.items[0].serial_and_batch_bundle) self.assertTrue(pos_inv2.items[0].serial_and_batch_bundle)
@@ -826,6 +871,10 @@ class TestPOSInvoice(unittest.TestCase):
pos_inv1 = create_pos_invoice( pos_inv1 = create_pos_invoice(
item=item.name, rate=300, qty=1, do_not_submit=1, batch_no="TestBatch 01" 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.save()
pos_inv1.submit() pos_inv1.submit()

View File

@@ -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.background_jobs import enqueue, is_job_enqueued
from frappe.utils.scheduler import is_scheduler_inactive 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): class POSInvoiceMergeLog(Document):
@@ -292,22 +294,23 @@ class POSInvoiceMergeLog(Document):
invoice.disable_rounded_total = cint( invoice.disable_rounded_total = cint(
frappe.db.get_value("POS Profile", invoice.pos_profile, "disable_rounded_total") 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( 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: 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( frappe.throw(
_("Please set Accounting Dimension {} in {}").format( _("Please set Accounting Dimension {} in {}").format(
frappe.bold(frappe.unscrub(dimension)), frappe.bold(dimension.label),
frappe.get_desk_link("POS Profile", invoice.pos_profile), 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": if self.merge_invoices_based_on == "Customer Group":
invoice.flags.ignore_pos_profile = True invoice.flags.ignore_pos_profile = True

View File

@@ -28,14 +28,17 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
pos_inv = create_pos_invoice(rate=300, do_not_submit=1) 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.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
pos_inv.save()
pos_inv.submit() pos_inv.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) 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.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
pos_inv2.save()
pos_inv2.submit() pos_inv2.submit()
pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) 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.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300})
pos_inv3.save()
pos_inv3.submit() pos_inv3.submit()
consolidate_pos_invoices() consolidate_pos_invoices()
@@ -61,14 +64,17 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
pos_inv = create_pos_invoice(rate=300, do_not_submit=1) 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.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
pos_inv.save()
pos_inv.submit() pos_inv.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) 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.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
pos_inv2.save()
pos_inv2.submit() pos_inv2.submit()
pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) 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.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300})
pos_inv3.save()
pos_inv3.submit() pos_inv3.submit()
pos_inv_cn = make_sales_return(pos_inv.name) pos_inv_cn = make_sales_return(pos_inv.name)
@@ -122,6 +128,8 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
}, },
) )
inv.insert() inv.insert()
inv.payments[0].amount = inv.grand_total
inv.save()
inv.submit() inv.submit()
inv2 = create_pos_invoice(qty=1, rate=100, do_not_save=True) inv2 = create_pos_invoice(qty=1, rate=100, do_not_save=True)
@@ -138,6 +146,8 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
}, },
) )
inv2.insert() inv2.insert()
inv2.payments[0].amount = inv.grand_total
inv2.save()
inv2.submit() inv2.submit()
consolidate_pos_invoices() consolidate_pos_invoices()
@@ -272,7 +282,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
inv2.submit() inv2.submit()
inv3 = create_pos_invoice(qty=3, rate=600, do_not_save=True) 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.insert()
inv3.submit() inv3.submit()
@@ -280,8 +290,8 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
inv.load_from_db() inv.load_from_db()
consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice) consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice)
self.assertEqual(consolidated_invoice.outstanding_amount, 800) self.assertNotEqual(consolidated_invoice.outstanding_amount, 800)
self.assertNotEqual(consolidated_invoice.status, "Paid") self.assertEqual(consolidated_invoice.status, "Paid")
finally: finally:
frappe.set_user("Administrator") frappe.set_user("Administrator")
@@ -416,6 +426,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
do_not_submit=1, do_not_submit=1,
) )
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
pos_inv.save()
pos_inv.submit() pos_inv.submit()
pos_inv_cn = make_sales_return(pos_inv.name) pos_inv_cn = make_sales_return(pos_inv.name)
@@ -430,6 +441,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
do_not_submit=1, do_not_submit=1,
) )
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
pos_inv2.save()
pos_inv2.submit() pos_inv2.submit()
consolidate_pos_invoices() consolidate_pos_invoices()

View File

@@ -29,6 +29,7 @@
"ignore_pricing_rule", "ignore_pricing_rule",
"allow_rate_change", "allow_rate_change",
"allow_discount_change", "allow_discount_change",
"disable_grand_total_to_default_mop",
"section_break_23", "section_break_23",
"item_groups", "item_groups",
"column_break_25", "column_break_25",
@@ -382,6 +383,12 @@
"fieldname": "print_receipt_on_order_complete", "fieldname": "print_receipt_on_order_complete",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Print Receipt on Order Complete" "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", "icon": "icon-cog",
@@ -409,7 +416,7 @@
"link_fieldname": "pos_profile" "link_fieldname": "pos_profile"
} }
], ],
"modified": "2025-01-01 11:07:03.161950", "modified": "2025-01-29 13:12:30.796630",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Profile", "name": "POS Profile",

View File

@@ -7,6 +7,10 @@ from frappe import _, msgprint, scrub, unscrub
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import get_link_to_form, now 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): class POSProfile(Document):
# begin: auto-generated types # begin: auto-generated types
@@ -36,6 +40,7 @@ class POSProfile(Document):
currency: DF.Link currency: DF.Link
customer: DF.Link | None customer: DF.Link | None
customer_groups: DF.Table[POSCustomerGroup] customer_groups: DF.Table[POSCustomerGroup]
disable_grand_total_to_default_mop: DF.Check
disable_rounded_total: DF.Check disable_rounded_total: DF.Check
disabled: DF.Check disabled: DF.Check
expense_account: DF.Link | None expense_account: DF.Link | None
@@ -69,15 +74,19 @@ class POSProfile(Document):
self.validate_accounting_dimensions() self.validate_accounting_dimensions()
def validate_accounting_dimensions(self): def validate_accounting_dimensions(self):
acc_dim_names = required_accounting_dimensions() acc_dims = get_checks_for_pl_and_bs_accounts()
for acc_dim in acc_dim_names: for acc_dim in acc_dims:
if not self.get(acc_dim): 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( frappe.throw(
_( _(
"{0} is a mandatory Accounting Dimension. <br>" "{0} is a mandatory Accounting Dimension. <br>"
"Please set a value for {0} in Accounting Dimensions section." "Please set a value for {0} in Accounting Dimensions section."
).format( ).format(
unscrub(frappe.bold(acc_dim)), frappe.bold(acc_dim.label),
), ),
title=_("Mandatory Accounting Dimension"), 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.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def pos_profile_query(doctype, txt, searchfield, start, page_len, filters): def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):

View File

@@ -53,6 +53,7 @@
"column_break_42", "column_break_42",
"free_item_uom", "free_item_uom",
"round_free_qty", "round_free_qty",
"dont_enforce_free_item_qty",
"is_recursive", "is_recursive",
"recurse_for", "recurse_for",
"apply_recursion_over", "apply_recursion_over",
@@ -643,12 +644,19 @@
"fieldname": "has_priority", "fieldname": "has_priority",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Has Priority" "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", "icon": "fa fa-gift",
"idx": 1, "idx": 1,
"links": [], "links": [],
"modified": "2024-09-16 18:14:51.314765", "modified": "2025-02-17 18:15:39.824639",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Pricing Rule", "name": "Pricing Rule",

View File

@@ -60,6 +60,7 @@ class PricingRule(Document):
disable: DF.Check disable: DF.Check
discount_amount: DF.Currency discount_amount: DF.Currency
discount_percentage: DF.Float discount_percentage: DF.Float
dont_enforce_free_item_qty: DF.Check
for_price_list: DF.Link | None for_price_list: DF.Link | None
free_item: DF.Link | None free_item: DF.Link | None
free_item_rate: DF.Currency 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"]: if pricing_rule.margin_type in ["Percentage", "Amount"]:
item_details.margin_rate_or_amount = 0.0 item_details.margin_rate_or_amount = 0.0
item_details.margin_type = None 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_details.remove_free_item = (
item_code if pricing_rule.get("same_item") else pricing_rule.get("free_item") item_code if pricing_rule.get("same_item") else pricing_rule.get("free_item")
) )

View File

@@ -428,6 +428,54 @@ class TestPricingRule(FrappeTestCase):
self.assertEqual(so.items[1].is_free_item, 1) self.assertEqual(so.items[1].is_free_item, 1)
self.assertEqual(so.items[1].item_code, "_Test Item 2") 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): def test_cumulative_pricing_rule(self):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Cumulative Pricing Rule") frappe.delete_doc_if_exists("Pricing Rule", "_Test Cumulative Pricing Rule")
test_record = { test_record = {
@@ -1451,6 +1499,7 @@ def make_pricing_rule(**args):
"discount_amount": args.discount_amount or 0.0, "discount_amount": args.discount_amount or 0.0,
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0, "apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0,
"has_priority": args.has_priority or 0, "has_priority": args.has_priority or 0,
"enforce_free_item_qty": args.dont_enforce_free_item_qty or 0,
} }
) )

View File

@@ -713,7 +713,10 @@ def apply_pricing_rule_for_free_items(doc, pricing_rule_args):
args.pop((item.item_code, item.pricing_rules)) args.pop((item.item_code, item.pricing_rules))
for free_item in args.values(): 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: def get_pricing_rule_items(pr_doc, other_items=False) -> list:

View File

@@ -236,17 +236,21 @@ def get_ar_filters(doc, entry):
def get_html(doc, filters, entry, col, res, ageing): def get_html(doc, filters, entry, col, res, ageing):
base_template_path = "frappe/www/printview.html" base_template_path = "frappe/www/printview.html"
template_path = ( template_path = "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html"
"erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html" if doc.report == "General Ledger":
if doc.report == "General Ledger" template_path = (
else "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html" "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: if doc.letter_head:
from frappe.www.printview import get_letter_head from frappe.www.printview import get_letter_head
letter_head = get_letter_head(doc, 0) letter_head = get_letter_head(doc, 0)
html = frappe.render_template( html = frappe.render_template(
template_path, template_path,
{ {
@@ -262,7 +266,6 @@ def get_html(doc, filters, entry, col, res, ageing):
else None, else None,
}, },
) )
html = frappe.render_template( html = frappe.render_template(
base_template_path, base_template_path,
{"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer}, {"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer},

View File

@@ -460,6 +460,8 @@ class SalesInvoice(SellingController):
self.make_bundle_for_sales_purchase_return(table_name) self.make_bundle_for_sales_purchase_return(table_name)
self.make_bundle_using_old_serial_batch_fields(table_name) self.make_bundle_using_old_serial_batch_fields(table_name)
self.update_stock_reservation_entries()
self.update_stock_ledger() self.update_stock_ledger()
# this sequence because outstanding may get -ve # this sequence because outstanding may get -ve
@@ -561,6 +563,7 @@ class SalesInvoice(SellingController):
self.make_gl_entries_on_cancel() self.make_gl_entries_on_cancel()
if self.update_stock == 1: if self.update_stock == 1:
self.update_stock_reservation_entries()
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
self.db_set("status", "Cancelled") self.db_set("status", "Cancelled")

View File

@@ -765,7 +765,7 @@ def validate_account_party_type(self):
if self.party_type and self.party: if self.party_type and self.party:
account_type = frappe.get_cached_value("Account", self.account, "account_type") 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( frappe.throw(
_( _(
"Party Type and Party can only be set for Receivable / Payable account<br><br>" "{0}" "Party Type and Party can only be set for Receivable / Payable account<br><br>" "{0}"

View File

@@ -44,7 +44,7 @@
{% endfor %} {% endfor %}
<tr> <tr>
<td class="right" colspan="3" ><strong>Total (debit) </strong></td> <td class="right" colspan="3" ><strong>Total (debit) </strong></td>
<td class="left" >{{ gl | sum(attribute='debit') }}</td> <td class="left" >{{ gl | sum(attribute='debit') | round(2) }}</td>
</tr> </tr>
<tr> <tr>
<td class="top-bottom" colspan="5"><strong>Credit</strong></td> <td class="top-bottom" colspan="5"><strong>Credit</strong></td>
@@ -61,7 +61,7 @@
{% endfor %} {% endfor %}
<tr> <tr>
<td class="right" colspan="3"><strong>Total (credit) </strong></td> <td class="right" colspan="3"><strong>Total (credit) </strong></td>
<td class="left" >{{ gl | sum(attribute='credit') }}</td> <td class="left" >{{ gl | sum(attribute='credit') | round(2) }}</td>
</tr> </tr>
<tr> <tr>
<td class="top-bottom" colspan="5"><b>Narration: </b>{{ gl[0].remarks }}</td> <td class="top-bottom" colspan="5"><b>Narration: </b>{{ gl[0].remarks }}</td>

View File

@@ -89,6 +89,7 @@ frappe.query_reports["Accounts Payable"] = {
fieldname: "party", fieldname: "party",
label: __("Party"), label: __("Party"),
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: "party_type",
get_data: function (txt) { get_data: function (txt) {
if (!frappe.query_report.filters) return; if (!frappe.query_report.filters) return;

View File

@@ -66,6 +66,7 @@ frappe.query_reports["Accounts Payable Summary"] = {
fieldname: "party", fieldname: "party",
label: __("Party"), label: __("Party"),
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: "party_type",
get_data: function (txt) { get_data: function (txt) {
if (!frappe.query_report.filters) return; if (!frappe.query_report.filters) return;

View File

@@ -56,6 +56,7 @@ frappe.query_reports["Accounts Receivable"] = {
fieldname: "party", fieldname: "party",
label: __("Party"), label: __("Party"),
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: "party_type",
get_data: function (txt) { get_data: function (txt) {
if (!frappe.query_report.filters) return; if (!frappe.query_report.filters) return;

View File

@@ -66,6 +66,7 @@ frappe.query_reports["Accounts Receivable Summary"] = {
fieldname: "party", fieldname: "party",
label: __("Party"), label: __("Party"),
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: "party_type",
get_data: function (txt) { get_data: function (txt) {
if (!frappe.query_report.filters) return; if (!frappe.query_report.filters) return;

View File

@@ -91,6 +91,7 @@ function get_filters() {
fieldname: "budget_against_filter", fieldname: "budget_against_filter",
label: __("Dimension Filter"), label: __("Dimension Filter"),
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: "budget_against",
get_data: function (txt) { get_data: function (txt) {
if (!frappe.query_report.filters) return; if (!frappe.query_report.filters) return;

View File

@@ -73,6 +73,7 @@ frappe.query_reports["General Ledger"] = {
fieldname: "party", fieldname: "party",
label: __("Party"), label: __("Party"),
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: "party_type",
get_data: function (txt) { get_data: function (txt) {
if (!frappe.query_report.filters) return; if (!frappe.query_report.filters) return;
@@ -151,6 +152,7 @@ frappe.query_reports["General Ledger"] = {
fieldname: "cost_center", fieldname: "cost_center",
label: __("Cost Center"), label: __("Cost Center"),
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: "Cost Center",
get_data: function (txt) { get_data: function (txt) {
return frappe.db.get_link_options("Cost Center", txt, { return frappe.db.get_link_options("Cost Center", txt, {
company: frappe.query_report.get_filter_value("company"), company: frappe.query_report.get_filter_value("company"),
@@ -161,6 +163,7 @@ frappe.query_reports["General Ledger"] = {
fieldname: "project", fieldname: "project",
label: __("Project"), label: __("Project"),
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) { get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, { return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"), company: frappe.query_report.get_filter_value("company"),

View File

@@ -67,6 +67,7 @@ frappe.query_reports["Gross Profit"] = {
fieldname: "cost_center", fieldname: "cost_center",
label: __("Cost Center"), label: __("Cost Center"),
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: "Cost Center",
get_data: function (txt) { get_data: function (txt) {
return frappe.db.get_link_options("Cost Center", txt, { return frappe.db.get_link_options("Cost Center", txt, {
company: frappe.query_report.get_filter_value("company"), company: frappe.query_report.get_filter_value("company"),
@@ -77,6 +78,7 @@ frappe.query_reports["Gross Profit"] = {
fieldname: "project", fieldname: "project",
label: __("Project"), label: __("Project"),
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) { get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, { return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"), company: frappe.query_report.get_filter_value("company"),

View File

@@ -50,6 +50,7 @@ function get_filters() {
fieldname: "party", fieldname: "party",
label: __("Party"), label: __("Party"),
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: "party_type",
get_data: function (txt) { get_data: function (txt) {
if (!frappe.query_report.filters) return; if (!frappe.query_report.filters) return;

View File

@@ -68,16 +68,12 @@ frappe.query_reports["Trial Balance for Party"] = {
{ {
fieldname: "account", fieldname: "account",
label: __("Account"), label: __("Account"),
fieldtype: "Link", fieldtype: "MultiSelectList",
options: "Account", options: "Account",
get_query: function () { get_data: function (txt) {
var company = frappe.query_report.get_filter_value("company"); return frappe.db.get_link_options("Account", txt, {
return { company: frappe.query_report.get_filter_value("company"),
doctype: "Account", });
filters: {
company: company,
},
};
}, },
}, },
{ {

View File

@@ -4,8 +4,10 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.query_builder.functions import Sum
from frappe.utils import cint, flt 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 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, filters=party_filters,
order_by="name", 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") company_currency = frappe.get_cached_value("Company", filters.company, "default_currency")
opening_balances = get_opening_balances(filters) opening_balances = get_opening_balances(filters, account_filter)
balances_within_period = get_balances_within_period(filters) balances_within_period = get_balances_within_period(filters, account_filter)
data = [] data = []
# total_debit, total_credit = 0, 0 # total_debit, total_credit = 0, 0
@@ -89,30 +96,34 @@ def get_data(filters, show_party_name):
return data return data
def get_opening_balances(filters): def get_opening_balances(filters, account_filter=None):
account_filter = "" GL_Entry = frappe.qb.DocType("GL Entry")
if filters.get("account"):
account_filter = "and account = %s" % (frappe.db.escape(filters.get("account")))
gle = frappe.db.sql( query = (
f""" frappe.qb.from_(GL_Entry)
select party, sum(debit) as opening_debit, sum(credit) as opening_credit .select(
from `tabGL Entry` GL_Entry.party,
where company=%(company)s Sum(GL_Entry.debit).as_("opening_debit"),
and is_cancelled=0 Sum(GL_Entry.credit).as_("opening_credit"),
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)) .where(
{account_filter} (GL_Entry.company == filters.company)
group by party""", & (GL_Entry.is_cancelled == 0)
{ & (GL_Entry.party_type == filters.party_type)
"company": filters.company, & (GL_Entry.party != "")
"from_date": filters.from_date, & (
"to_date": filters.to_date, (GL_Entry.posting_date < filters.from_date)
"party_type": filters.party_type, | ((GL_Entry.is_opening == "Yes") & (GL_Entry.posting_date <= filters.to_date))
}, )
as_dict=True, )
.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() opening = frappe._dict()
for d in gle: for d in gle:
opening_debit, opening_credit = toggle_debit_credit(d.opening_debit, d.opening_credit) opening_debit, opening_credit = toggle_debit_credit(d.opening_debit, d.opening_credit)
@@ -121,31 +132,33 @@ def get_opening_balances(filters):
return opening return opening
def get_balances_within_period(filters): def get_balances_within_period(filters, account_filter=None):
account_filter = "" GL_Entry = frappe.qb.DocType("GL Entry")
if filters.get("account"):
account_filter = "and account = %s" % (frappe.db.escape(filters.get("account")))
gle = frappe.db.sql( query = (
f""" frappe.qb.from_(GL_Entry)
select party, sum(debit) as debit, sum(credit) as credit .select(
from `tabGL Entry` GL_Entry.party,
where company=%(company)s Sum(GL_Entry.debit).as_("debit"),
and is_cancelled = 0 Sum(GL_Entry.credit).as_("credit"),
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' )
and posting_date >= %(from_date)s and posting_date <= %(to_date)s .where(
and ifnull(is_opening, 'No') = 'No' (GL_Entry.company == filters.company)
{account_filter} & (GL_Entry.is_cancelled == 0)
group by party""", & (GL_Entry.party_type == filters.party_type)
{ & (GL_Entry.party != "")
"company": filters.company, & (GL_Entry.posting_date >= filters.from_date)
"from_date": filters.from_date, & (GL_Entry.posting_date <= filters.to_date)
"to_date": filters.to_date, & (GL_Entry.is_opening == "No")
"party_type": filters.party_type, )
}, .groupby(GL_Entry.party)
as_dict=True,
) )
if account_filter:
query = query.where(GL_Entry.account.isin(account_filter))
gle = query.run(as_dict=True)
balances_within_period = frappe._dict() balances_within_period = frappe._dict()
for d in gle: for d in gle:
balances_within_period.setdefault(d.party, [d.debit, d.credit]) balances_within_period.setdefault(d.party, [d.debit, d.credit])

View File

@@ -609,9 +609,7 @@ frappe.ui.form.on("Asset", {
frm.trigger("toggle_reference_doc"); frm.trigger("toggle_reference_doc");
if (frm.doc.purchase_receipt) { if (frm.doc.purchase_receipt) {
if (frm.doc.item_code) { 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");
frm.events.set_values_from_purchase_doc(frm, "Purchase Receipt", pr_doc);
});
} else { } else {
frm.set_value("purchase_receipt", ""); frm.set_value("purchase_receipt", "");
frappe.msgprint({ frappe.msgprint({
@@ -626,9 +624,7 @@ frappe.ui.form.on("Asset", {
frm.trigger("toggle_reference_doc"); frm.trigger("toggle_reference_doc");
if (frm.doc.purchase_invoice) { if (frm.doc.purchase_invoice) {
if (frm.doc.item_code) { 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");
frm.events.set_values_from_purchase_doc(frm, "Purchase Invoice", pi_doc);
});
} else { } else {
frm.set_value("purchase_invoice", ""); frm.set_value("purchase_invoice", "");
frappe.msgprint({ frappe.msgprint({
@@ -639,45 +635,35 @@ frappe.ui.form.on("Asset", {
} }
}, },
set_values_from_purchase_doc: function (frm, doctype, purchase_doc) { set_values_from_purchase_doc: (frm, doctype) => {
frm.set_value("company", purchase_doc.company); frappe.call({
if (purchase_doc.bill_date) { method: "erpnext.assets.doctype.asset.asset.get_values_from_purchase_doc",
frm.set_value("purchase_date", purchase_doc.bill_date); args: {
} else { purchase_doc_name: frm.doc.purchase_receipt || frm.doc.purchase_invoice,
frm.set_value("purchase_date", purchase_doc.posting_date); item_code: frm.doc.item_code,
} doctype: doctype,
if (!frm.doc.is_existing_asset && !frm.doc.available_for_use_date) { },
frm.set_value("available_for_use_date", frm.doc.purchase_date); callback: (r) => {
} if (r.message) {
const item = purchase_doc.items.find((item) => item.item_code === frm.doc.item_code); let data = r.message;
if (!item) { frm.set_value("company", data.company);
let doctype_field = frappe.scrub(doctype); frm.set_value("purchase_date", data.purchase_date);
frm.set_value(doctype_field, ""); frm.set_value("gross_purchase_amount", data.gross_purchase_amount);
frappe.msgprint({ frm.set_value("purchase_amount", data.gross_purchase_amount);
title: __("Invalid {0}", [__(doctype)]), frm.set_value("asset_quantity", data.asset_quantity);
message: __("The selected {0} does not contain the selected Asset Item.", [__(doctype)]), frm.set_value("cost_center", data.cost_center);
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")
);
frm.set_value("gross_purchase_amount", purchase_amount); if (doctype === "Purchase Receipt") {
frm.set_value("purchase_amount", purchase_amount); frm.set_value("purchase_receipt_item", data.purchase_receipt_item);
frm.set_value("asset_quantity", asset_quantity); } else {
frm.set_value("cost_center", item.cost_center || purchase_doc.cost_center); frm.set_value("purchase_invoice_item", data.purchase_invoice_item);
if (item.asset_location) { }
frm.set_value("location", item.asset_location);
} let is_editable = !data.is_multiple_items; // if multiple items, then fields should be read-only
if (doctype === "Purchase Receipt") { frm.set_df_property("gross_purchase_amount", "read_only", is_editable);
frm.set_value("purchase_receipt_item", item.name); frm.set_df_property("asset_quantity", "read_only", is_editable);
} else if (doctype === "Purchase Invoice") { }
frm.set_value("purchase_invoice_item", item.name); },
}
}); });
}, },

View File

@@ -227,8 +227,7 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Gross Purchase Amount", "label": "Gross Purchase Amount",
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)", "mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
"options": "Company:company:default_currency", "options": "Company:company:default_currency"
"read_only_depends_on": "eval:!doc.is_existing_asset"
}, },
{ {
"fieldname": "available_for_use_date", "fieldname": "available_for_use_date",
@@ -470,8 +469,7 @@
"default": "1", "default": "1",
"fieldname": "asset_quantity", "fieldname": "asset_quantity",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Asset Quantity", "label": "Asset Quantity"
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset"
}, },
{ {
"fieldname": "depr_entry_posting_status", "fieldname": "depr_entry_posting_status",
@@ -541,14 +539,13 @@
}, },
{ {
"fieldname": "purchase_receipt_item", "fieldname": "purchase_receipt_item",
"fieldtype": "Link", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Purchase Receipt Item", "label": "Purchase Receipt Item"
"options": "Purchase Receipt Item"
}, },
{ {
"fieldname": "purchase_invoice_item", "fieldname": "purchase_invoice_item",
"fieldtype": "Link", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Purchase Invoice Item", "label": "Purchase Invoice Item",
"options": "Purchase Invoice Item" "options": "Purchase Invoice Item"
@@ -595,7 +592,7 @@
"link_fieldname": "target_asset" "link_fieldname": "target_asset"
} }
], ],
"modified": "2024-12-26 14:23:20.968882", "modified": "2025-02-11 16:01:56.140904",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset", "name": "Asset",

View File

@@ -95,9 +95,9 @@ class Asset(AccountsController):
purchase_amount: DF.Currency purchase_amount: DF.Currency
purchase_date: DF.Date | None purchase_date: DF.Date | None
purchase_invoice: DF.Link | 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: DF.Link | None
purchase_receipt_item: DF.Link | None purchase_receipt_item: DF.Data | None
split_from: DF.Link | None split_from: DF.Link | None
status: DF.Literal[ status: DF.Literal[
"Draft", "Draft",
@@ -121,6 +121,7 @@ class Asset(AccountsController):
def validate(self): def validate(self):
self.validate_precision() self.validate_precision()
self.set_purchase_doc_row_item()
self.validate_asset_values() self.validate_asset_values()
self.validate_asset_and_reference() self.validate_asset_and_reference()
self.validate_item() self.validate_item()
@@ -199,6 +200,38 @@ class Asset(AccountsController):
def after_delete(self): def after_delete(self):
add_asset_activity(self.name, _("Asset deleted")) 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): def validate_asset_and_reference(self):
if self.purchase_invoice or self.purchase_receipt: if self.purchase_invoice or self.purchase_receipt:
reference_doc = "Purchase Invoice" if self.purchase_invoice else "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 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() @frappe.whitelist()
def split_asset(asset_name, split_qty): def split_asset(asset_name, split_qty):
asset = frappe.get_doc("Asset", asset_name) asset = frappe.get_doc("Asset", asset_name)

View File

@@ -444,9 +444,9 @@ def scrap_asset(asset_name):
notes = _("This schedule was created when Asset {0} was scrapped.").format( notes = _("This schedule was created when Asset {0} was scrapped.").format(
get_link_to_form(asset.doctype, asset.name) get_link_to_form(asset.doctype, asset.name)
) )
if asset.status != "Fully Depreciated":
depreciate_asset(asset, date, notes) depreciate_asset(asset, date, notes)
asset.reload() asset.reload()
depreciation_series = frappe.get_cached_value("Company", asset.company, "series_for_depreciation_entry") depreciation_series = frappe.get_cached_value("Company", asset.company, "series_for_depreciation_entry")

View File

@@ -917,6 +917,7 @@
"fieldtype": "Float", "fieldtype": "Float",
"label": "Subcontracted Quantity", "label": "Subcontracted Quantity",
"no_copy": 1, "no_copy": 1,
"non_negative": 1,
"read_only": 1 "read_only": 1
} }
], ],
@@ -924,7 +925,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-12-10 12:11:18.536089", "modified": "2025-02-18 12:35:04.432636",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Item", "name": "Purchase Order Item",

View File

@@ -52,6 +52,7 @@ frappe.query_reports["Purchase Order Analysis"] = {
label: __("Status"), label: __("Status"),
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
width: "80", width: "80",
options: ["To Pay", "To Bill", "To Receive", "To Receive and Bill", "Completed"],
get_data: function (txt) { get_data: function (txt) {
let status = ["To Bill", "To Receive", "To Receive and Bill", "Completed"]; let status = ["To Bill", "To Receive", "To Receive and Bill", "Completed"];
let options = []; let options = [];

View File

@@ -50,6 +50,7 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
fieldname: "supplier", fieldname: "supplier",
label: __("Supplier"), label: __("Supplier"),
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: "Supplier",
get_data: function (txt) { get_data: function (txt) {
return frappe.db.get_link_options("Supplier", txt); return frappe.db.get_link_options("Supplier", txt);
}, },
@@ -58,6 +59,7 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
label: __("Supplier Quotation"), label: __("Supplier Quotation"),
fieldname: "supplier_quotation", fieldname: "supplier_quotation",
options: "Supplier Quotation",
default: "", default: "",
get_data: function (txt) { get_data: function (txt) {
return frappe.db.get_link_options("Supplier Quotation", txt, { docstatus: ["<", 2] }); return frappe.db.get_link_options("Supplier Quotation", txt, { docstatus: ["<", 2] });

View File

@@ -170,7 +170,7 @@ class AccountsController(TransactionBase):
self.validate_qty_is_not_zero() self.validate_qty_is_not_zero()
if ( 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("is_return")
and self.get("update_stock") and self.get("update_stock")
): ):

View File

@@ -779,8 +779,10 @@ class BuyingController(SubcontractingController):
is_plural = "s" if len(created_assets) != 1 else "" is_plural = "s" if len(created_assets) != 1 else ""
messages.append( messages.append(
_("Asset{} {assets_link} created for {}").format( _("Asset{is_plural} {assets_link} created for {item_code}").format(
is_plural, frappe.bold(d.item_code), assets_link=assets_link is_plural=is_plural,
assets_link=assets_link,
item_code=frappe.bold(d.item_code),
) )
) )
else: else:

View File

@@ -258,7 +258,7 @@ def get_already_returned_items(doc):
field = ( field = (
frappe.scrub(doc.doctype) + "_item" 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" else "dn_detail"
) )
data = frappe.db.sql( data = frappe.db.sql(
@@ -770,6 +770,7 @@ def get_return_against_item_fields(voucher_type):
"Delivery Note": "dn_detail", "Delivery Note": "dn_detail",
"Sales Invoice": "sales_invoice_item", "Sales Invoice": "sales_invoice_item",
"Subcontracting Receipt": "subcontracting_receipt_item", "Subcontracting Receipt": "subcontracting_receipt_item",
"POS Invoice": "sales_invoice_item",
} }
return return_against_item_fields[voucher_type] return return_against_item_fields[voucher_type]

View File

@@ -596,12 +596,13 @@ class SellingController(StockController):
if not self.is_internal_transfer() or self.docstatus == 1 if not self.is_internal_transfer() or self.docstatus == 1
else None 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 = self.make_package_for_transfer(
serial_and_batch_bundle, item_row.warehouse, type_of_transaction="Inward" 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( serial_and_batch_bundle = frappe.db.get_value(
"Stock Ledger Entry", "Stock Ledger Entry",
{"voucher_detail_no": item_row.name, "warehouse": item_row.warehouse}, {"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") 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): def set_default_income_account_for_item(obj):
for d in obj.get("items"): for d in obj.get("items"):

View File

@@ -5,7 +5,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document 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): class OverAllowanceError(frappe.ValidationError):
@@ -543,7 +543,7 @@ class StatusUpdater(Document):
)[0][0] )[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) ref_doc = frappe.get_doc(ref_dt, ref_dn)

View File

@@ -31,6 +31,7 @@ frappe.query_reports["Opportunity Summary by Sales Stage"] = {
fieldname: "status", fieldname: "status",
label: __("Status"), label: __("Status"),
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: ["Open", "Converted", "Quotation", "Replied"],
get_data: function () { get_data: function () {
return [ return [
{ value: "Open", description: "Status" }, { value: "Open", description: "Status" },

View File

@@ -216,7 +216,7 @@ class JobCard(Document):
open_job_cards = [] open_job_cards = []
if d.get("employee"): 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) data = self.get_overlap_for(d, open_job_cards=open_job_cards)
if data: if data:
@@ -257,9 +257,13 @@ class JobCard(Document):
frappe.get_cached_value("Workstation", self.workstation, "production_capacity") or 1 frappe.get_cached_value("Workstation", self.workstation, "production_capacity") or 1
) )
if args.get("employee"): if self.get_open_job_cards(args.get("employee")):
# override capacity for employee frappe.throw(
production_capacity = 1 _(
"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): if not self.has_overlap(production_capacity, time_logs):
return {} return {}
@@ -366,7 +370,7 @@ class JobCard(Document):
return time_logs 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") jc = frappe.qb.DocType("Job Card")
jctl = frappe.qb.DocType("Job Card Time Log") jctl = frappe.qb.DocType("Job Card Time Log")
@@ -377,13 +381,15 @@ class JobCard(Document):
.select(jc.name) .select(jc.name)
.where( .where(
(jctl.parent == jc.name) (jctl.parent == jc.name)
& (jc.workstation == self.workstation)
& (jctl.employee == employee) & (jctl.employee == employee)
& (jc.docstatus < 1) & (jc.docstatus < 1)
& (jc.name != self.name) & (jc.name != self.name)
) )
) )
if workstation:
query = query.where(jc.workstation == workstation)
jobs = query.run(as_dict=True) jobs = query.run(as_dict=True)
return [job.get("name") for job in jobs] if jobs else [] return [job.get("name") for job in jobs] if jobs else []

View File

@@ -56,6 +56,7 @@ frappe.query_reports["Job Card Summary"] = {
label: __("Work Orders"), label: __("Work Orders"),
fieldname: "work_order", fieldname: "work_order",
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: "Work Order",
get_data: function (txt) { get_data: function (txt) {
return frappe.db.get_link_options("Work Order", txt); return frappe.db.get_link_options("Work Order", txt);
}, },
@@ -64,6 +65,7 @@ frappe.query_reports["Job Card Summary"] = {
label: __("Production Item"), label: __("Production Item"),
fieldname: "production_item", fieldname: "production_item",
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: "Item",
get_data: function (txt) { get_data: function (txt) {
return frappe.db.get_link_options("Item", txt); return frappe.db.get_link_options("Item", txt);
}, },

View File

@@ -41,7 +41,7 @@ frappe.query_reports["Production Planning Report"] = {
fieldname: "docnames", fieldname: "docnames",
label: __("Document Name"), label: __("Document Name"),
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: "Sales Order", options: "based_on",
get_data: function (txt) { get_data: function (txt) {
if (!frappe.query_report.filters) return; if (!frappe.query_report.filters) return;

View File

@@ -42,6 +42,7 @@ frappe.query_reports["Work Order Summary"] = {
label: __("Sales Orders"), label: __("Sales Orders"),
fieldname: "sales_order", fieldname: "sales_order",
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: "Sales Order",
get_data: function (txt) { get_data: function (txt) {
return frappe.db.get_link_options("Sales Order", txt); return frappe.db.get_link_options("Sales Order", txt);
}, },
@@ -50,6 +51,7 @@ frappe.query_reports["Work Order Summary"] = {
label: __("Production Item"), label: __("Production Item"),
fieldname: "production_item", fieldname: "production_item",
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: "Item",
get_data: function (txt) { get_data: function (txt) {
return frappe.db.get_link_options("Item", txt); return frappe.db.get_link_options("Item", txt);
}, },

View File

@@ -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") 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.v14_0.disable_add_row_in_gross_profit
erpnext.patches.v15_0.set_difference_amount_in_asset_value_adjustment erpnext.patches.v15_0.set_difference_amount_in_asset_value_adjustment
erpnext.patches.v14_0.update_posting_datetime

View File

@@ -0,0 +1,10 @@
import frappe
def execute():
frappe.db.sql(
"""
UPDATE `tabStock Ledger Entry`
SET posting_datetime = timestamp(posting_date, posting_time)
"""
)

View File

@@ -1,74 +1,29 @@
import frappe import frappe
from frappe.utils import cstr
def execute(): 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_finance_books_map = get_asset_finance_books_map()
asset_depreciation_schedules_map = get_asset_depreciation_schedules_map() asset_depreciation_schedules_map = get_asset_depreciation_schedules_map()
for asset in assets: for key, fb_row in asset_finance_books_map.items():
if not asset_depreciation_schedules_map.get(asset.name): depreciation_schedules = asset_depreciation_schedules_map.get(key)
if not depreciation_schedules:
continue 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]: if fb_row.docstatus == 1:
asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule") 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) update_depreciation_schedules(depreciation_schedules, asset_depr_schedule_doc.name)
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
def get_asset_finance_books_map(): def get_asset_finance_books_map():
@@ -90,12 +45,20 @@ def get_asset_finance_books_map():
afb.expected_value_after_useful_life, afb.expected_value_after_useful_life,
afb.daily_prorata_based, afb.daily_prorata_based,
afb.shift_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.docstatus < 2)
.where(asset.calculate_depreciation == 1)
.orderby(afb.idx) .orderby(afb.idx)
).run(as_dict=True) ).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 return asset_finance_books_map
@@ -111,13 +74,17 @@ def get_asset_depreciation_schedules_map():
.select( .select(
asset.name.as_("asset_name"), asset.name.as_("asset_name"),
ds.name, ds.name,
ds.finance_book,
ds.finance_book_id, ds.finance_book_id,
) )
.where(asset.docstatus < 2) .where(asset.docstatus < 2)
.where(asset.calculate_depreciation == 1)
.orderby(ds.idx) .orderby(ds.idx)
).run(as_dict=True) ).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 return asset_depreciation_schedules_map

View File

@@ -894,10 +894,16 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
this.frm.refresh_fields(); 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 me = this;
var payment_status = true; var payment_status = true;
if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) { 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) { $.each(this.frm.doc['payments'] || [], function(index, data) {
if(data.default && payment_status && total_amount_to_pay > 0) { if(data.default && payment_status && total_amount_to_pay > 0) {
let base_amount, amount; let base_amount, amount;

View File

@@ -353,6 +353,26 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
); );
dialog.show(); 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); cur_frm.script_manager.make(erpnext.selling.QuotationController);

View File

@@ -106,7 +106,11 @@
"purchase_order", "purchase_order",
"column_break_89", "column_break_89",
"material_request_item", "material_request_item",
"purchase_order_item" "purchase_order_item",
"accounting_dimensions_section",
"cost_center",
"column_break_ihdh",
"project"
], ],
"fields": [ "fields": [
{ {
@@ -926,12 +930,42 @@
"fieldname": "available_quantity_section", "fieldname": "available_quantity_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Available Quantity" "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, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-11-21 14:21:29.743474", "modified": "2025-02-06 13:29:24.619850",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order Item", "name": "Sales Order Item",

View File

@@ -32,6 +32,7 @@ class SalesOrderItem(Document):
brand: DF.Link | None brand: DF.Link | None
company_total_stock: DF.Float company_total_stock: DF.Float
conversion_factor: DF.Float conversion_factor: DF.Float
cost_center: DF.Link
customer_item_code: DF.Data | None customer_item_code: DF.Data | None
delivered_by_supplier: DF.Check delivered_by_supplier: DF.Check
delivered_qty: DF.Float delivered_qty: DF.Float
@@ -68,6 +69,7 @@ class SalesOrderItem(Document):
pricing_rules: DF.SmallText | None pricing_rules: DF.SmallText | None
produced_qty: DF.Float produced_qty: DF.Float
production_plan_qty: DF.Float production_plan_qty: DF.Float
project: DF.Link | None
projected_qty: DF.Float projected_qty: DF.Float
purchase_order: DF.Link | None purchase_order: DF.Link | None
purchase_order_item: DF.Data | None purchase_order_item: DF.Data | None

View File

@@ -340,11 +340,19 @@ erpnext.PointOfSale.Payment = class {
// pass // pass
} }
render_payment_section() { async render_payment_section() {
this.render_payment_mode_dom(); this.render_payment_mode_dom();
this.make_invoice_fields_control(); this.make_invoice_fields_control();
this.update_totals_section(); 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() { after_render() {

View File

@@ -87,6 +87,7 @@ function get_filters() {
fieldname: "status", fieldname: "status",
label: __("Status"), label: __("Status"),
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: ["Overdue", "Unpaid", "Completed", "Partly Paid"],
width: 100, width: 100,
get_data: function (txt) { get_data: function (txt) {
let status = ["Overdue", "Unpaid", "Completed", "Partly Paid"]; let status = ["Overdue", "Unpaid", "Completed", "Partly Paid"];

View File

@@ -53,6 +53,7 @@ frappe.query_reports["Sales Order Analysis"] = {
fieldname: "status", fieldname: "status",
label: __("Status"), label: __("Status"),
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: ["To Pay", "To Bill", "To Deliver", "To Deliver and Bill", "Completed"],
width: "80", width: "80",
get_data: function (txt) { get_data: function (txt) {
let status = ["To Bill", "To Deliver", "To Deliver and Bill", "Completed"]; let status = ["To Bill", "To Deliver", "To Deliver and Bill", "Completed"];

View File

@@ -491,149 +491,6 @@ class DeliveryNote(SellingController):
self.delete_auto_created_batches() 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): def validate_against_stock_reservation_entries(self):
"""Validates if Stock Reservation Entries are available for the Sales Order Item reference.""" """Validates if Stock Reservation Entries are available for the Sales Order Item reference."""

View File

@@ -769,6 +769,48 @@ class TestDeliveryNote(FrappeTestCase):
{"warehouse": "_Test Warehouse - _TC"}, {"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): def test_delivery_of_bundled_items_to_target_warehouse(self):
from erpnext.selling.doctype.customer.test_customer import create_internal_customer from erpnext.selling.doctype.customer.test_customer import create_internal_customer

View File

@@ -1,6 +1,5 @@
{ {
"actions": [], "actions": [],
"allow_guest_to_view": 1,
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "field:item_code", "autoname": "field:item_code",
@@ -888,10 +887,9 @@
"icon": "fa fa-tag", "icon": "fa fa-tag",
"idx": 2, "idx": 2,
"image_field": "image", "image_field": "image",
"index_web_pages_for_search": 1,
"links": [], "links": [],
"make_attachments_public": 1, "make_attachments_public": 1,
"modified": "2024-04-30 13:46:39.098753", "modified": "2025-02-03 23:43:57.253667",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item", "name": "Item",

View File

@@ -202,7 +202,8 @@
"oldfieldname": "parent_detail_docname", "oldfieldname": "parent_detail_docname",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1,
"search_index": 1
}, },
{ {
"depends_on": "batch_no", "depends_on": "batch_no",
@@ -295,7 +296,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-02-04 16:30:44.263964", "modified": "2025-02-18 13:07:02.789654",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Packed Item", "name": "Packed Item",
@@ -305,4 +306,4 @@
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -1242,6 +1242,7 @@ class TestPickList(FrappeTestCase):
"is_recursive": 1, "is_recursive": 1,
"recurse_for": 2, "recurse_for": 2,
"free_qty": 1, "free_qty": 1,
"dont_enforce_free_item_qty": 0,
"company": "_Test Company", "company": "_Test Company",
"customer": "_Test Customer", "customer": "_Test Customer",
} }

View File

@@ -34,7 +34,9 @@
"verified_by", "verified_by",
"column_break_17", "column_break_17",
"remarks", "remarks",
"amended_from" "amended_from",
"print_settings_section",
"letter_head"
], ],
"fields": [ "fields": [
{ {
@@ -255,6 +257,20 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Company", "label": "Company",
"options": "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", "icon": "fa fa-search",
@@ -262,7 +278,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2025-01-16 17:00:48.774532", "modified": "2025-02-17 13:20:17.583094",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Quality Inspection", "name": "Quality Inspection",

View File

@@ -37,6 +37,7 @@ class QualityInspection(Document):
item_code: DF.Link item_code: DF.Link
item_name: DF.Data | None item_name: DF.Data | None
item_serial_no: DF.Link | None item_serial_no: DF.Link | None
letter_head: DF.Link | None
manual_inspection: DF.Check manual_inspection: DF.Check
naming_series: DF.Literal["MAT-QA-.YYYY.-"] naming_series: DF.Literal["MAT-QA-.YYYY.-"]
quality_inspection_template: DF.Link | None quality_inspection_template: DF.Link | None

View File

@@ -134,6 +134,13 @@ frappe.ui.form.on("Serial and Batch Bundle", {
}, },
toggle_fields(frm) { 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) { if (frm.doc.has_serial_no) {
frm.doc.entries.forEach((row) => { frm.doc.entries.forEach((row) => {
if (Math.abs(row.qty) !== 1) { if (Math.abs(row.qty) !== 1) {

View File

@@ -7,6 +7,7 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"item_details_tab", "item_details_tab",
"naming_series",
"company", "company",
"item_name", "item_name",
"has_serial_no", "has_serial_no",
@@ -242,12 +243,20 @@
"fieldtype": "Data", "fieldtype": "Data",
"label": "Returned Against", "label": "Returned Against",
"read_only": 1 "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, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2025-02-12 10:53:32.090309", "modified": "2025-02-17 18:22:36.056205",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Serial and Batch Bundle", "name": "Serial and Batch Bundle",

View File

@@ -8,6 +8,7 @@ from collections import Counter, defaultdict
import frappe import frappe
from frappe import _, _dict, bold from frappe import _, _dict, bold
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.naming import make_autoname
from frappe.query_builder.functions import CombineDatetime, Sum from frappe.query_builder.functions import CombineDatetime, Sum
from frappe.utils import ( from frappe.utils import (
add_days, add_days,
@@ -68,6 +69,7 @@ class SerialandBatchBundle(Document):
item_code: DF.Link item_code: DF.Link
item_group: DF.Link | None item_group: DF.Link | None
item_name: DF.Data | None item_name: DF.Data | None
naming_series: DF.Literal["", "SABB-.########"]
posting_date: DF.Date | None posting_date: DF.Date | None
posting_time: DF.Time | None posting_time: DF.Time | None
returned_against: DF.Data | None returned_against: DF.Data | None
@@ -80,6 +82,24 @@ class SerialandBatchBundle(Document):
warehouse: DF.Link | None warehouse: DF.Link | None
# end: auto-generated types # 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): def validate(self):
if self.docstatus == 1 and self.voucher_detail_no: if self.docstatus == 1 and self.voucher_detail_no:
self.validate_voucher_detail_no() self.validate_voucher_detail_no()

View File

@@ -17,6 +17,67 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
class TestSerialandBatchBundle(FrappeTestCase): 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): def test_inward_outward_serial_valuation(self):
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note 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 from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt

View File

@@ -2417,7 +2417,7 @@ class StockEntry(StockController):
item_row = item_dict[d] item_row = item_dict[d]
child_qty = flt(item_row["qty"], precision) 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 continue
se_child = self.append("items") se_child = self.append("items")

View File

@@ -56,6 +56,8 @@
"use_serial_batch_fields", "use_serial_batch_fields",
"do_not_update_serial_batch_on_creation_of_auto_bundle", "do_not_update_serial_batch_on_creation_of_auto_bundle",
"allow_existing_serial_no", "allow_existing_serial_no",
"serial_and_batch_bundle_section",
"set_serial_and_batch_bundle_naming_based_on_naming_series",
"stock_planning_tab", "stock_planning_tab",
"auto_material_request", "auto_material_request",
"auto_indent", "auto_indent",
@@ -467,6 +469,17 @@
"fieldname": "allow_existing_serial_no", "fieldname": "allow_existing_serial_no",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow existing Serial No to be Manufactured/Received again" "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", "icon": "icon-cog",
@@ -474,7 +487,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2024-12-09 17:52:36.030456", "modified": "2025-02-17 14:36:36.177743",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Settings", "name": "Stock Settings",

View File

@@ -55,6 +55,7 @@ class StockSettings(Document):
role_allowed_to_create_edit_back_dated_transactions: DF.Link | None role_allowed_to_create_edit_back_dated_transactions: DF.Link | None
role_allowed_to_over_deliver_receive: DF.Link | None role_allowed_to_over_deliver_receive: DF.Link | None
sample_retention_warehouse: 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 show_barcode_field: DF.Check
stock_auth_role: DF.Link | None stock_auth_role: DF.Link | None
stock_frozen_upto: DF.Date | None stock_frozen_upto: DF.Date | None
@@ -75,6 +76,7 @@ class StockSettings(Document):
"default_warehouse", "default_warehouse",
"set_qty_in_transactions_based_on_serial_no_input", "set_qty_in_transactions_based_on_serial_no_input",
"use_serial_batch_fields", "use_serial_batch_fields",
"set_serial_and_batch_bundle_naming_based_on_naming_series",
]: ]:
frappe.db.set_default(key, self.get(key, "")) frappe.db.set_default(key, self.get(key, ""))

View File

@@ -16,6 +16,7 @@ frappe.query_reports["Item Shortage Report"] = {
fieldname: "warehouse", fieldname: "warehouse",
label: __("Warehouse"), label: __("Warehouse"),
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: "Warehouse",
width: "100", width: "100",
get_data: function (txt) { get_data: function (txt) {
return frappe.db.get_link_options("Warehouse", txt); return frappe.db.get_link_options("Warehouse", txt);

View File

@@ -50,6 +50,7 @@ frappe.query_reports["Serial and Batch Summary"] = {
fieldname: "voucher_no", fieldname: "voucher_no",
label: __("Voucher No"), label: __("Voucher No"),
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: "voucher_type",
get_data: function (txt) { get_data: function (txt) {
if (!frappe.query_report.filters) return; if (!frappe.query_report.filters) return;

View File

@@ -663,4 +663,4 @@ def get_combine_datetime(posting_date, posting_time):
if isinstance(posting_time, datetime.timedelta): if isinstance(posting_time, datetime.timedelta):
posting_time = (datetime.datetime.min + posting_time).time() 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)

View File

@@ -3,6 +3,7 @@
import frappe import frappe
from frappe.utils import escape_html
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
@@ -11,6 +12,8 @@ def send_message(sender, message, subject="Website Query"):
website_send_message(sender, message, subject) website_send_message(sender, message, subject)
message = escape_html(message)
lead = customer = None lead = customer = None
customer = frappe.db.sql( customer = frappe.db.sql(
"""select distinct dl.link_name from `tabDynamic Link` dl """select distinct dl.link_name from `tabDynamic Link` dl