mirror of
https://github.com/frappe/erpnext.git
synced 2026-02-19 01:25:07 +00:00
Merge pull request #45981 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -41,6 +41,11 @@ class AccountingDimension(Document):
|
||||
self.set_fieldname_and_label()
|
||||
|
||||
def validate(self):
|
||||
self.validate_doctype()
|
||||
validate_column_name(self.fieldname)
|
||||
self.validate_dimension_defaults()
|
||||
|
||||
def validate_doctype(self):
|
||||
if self.document_type in (
|
||||
*core_doctypes_list,
|
||||
"Accounting Dimension",
|
||||
@@ -62,9 +67,6 @@ class AccountingDimension(Document):
|
||||
if not self.is_new():
|
||||
self.validate_document_type_change()
|
||||
|
||||
validate_column_name(self.fieldname)
|
||||
self.validate_dimension_defaults()
|
||||
|
||||
def validate_document_type_change(self):
|
||||
doctype_before_save = frappe.db.get_value("Accounting Dimension", self.name, "document_type")
|
||||
if doctype_before_save != self.document_type:
|
||||
@@ -103,6 +105,7 @@ class AccountingDimension(Document):
|
||||
|
||||
def on_update(self):
|
||||
frappe.flags.accounting_dimensions = None
|
||||
frappe.flags.accounting_dimensions_details = None
|
||||
|
||||
|
||||
def make_dimension_in_accounting_doctypes(doc, doclist=None):
|
||||
@@ -263,7 +266,7 @@ def get_checks_for_pl_and_bs_accounts():
|
||||
frappe.flags.accounting_dimensions_details = frappe.db.sql(
|
||||
"""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
|
||||
FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c
|
||||
WHERE p.name = c.parent""",
|
||||
WHERE p.name = c.parent AND p.disabled = 0""",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
@@ -124,3 +124,20 @@ class TestGLEntry(unittest.TestCase):
|
||||
str(e),
|
||||
"Party Type and Party can only be set for Receivable / Payable account_Test Account Cost for Goods Sold - _TC",
|
||||
)
|
||||
|
||||
def test_validate_account_party_type_shareholder(self):
|
||||
jv = make_journal_entry(
|
||||
"Opening Balance Equity - _TC",
|
||||
"Cash - _TC",
|
||||
100,
|
||||
"_Test Cost Center - _TC",
|
||||
save=False,
|
||||
submit=False,
|
||||
)
|
||||
|
||||
for row in jv.accounts:
|
||||
row.party_type = "Shareholder"
|
||||
break
|
||||
|
||||
jv.save().submit()
|
||||
self.assertEqual(1, jv.docstatus)
|
||||
|
||||
@@ -313,6 +313,7 @@ class PaymentRequest(Document):
|
||||
"payer_name": data.customer_name,
|
||||
"order_id": self.name,
|
||||
"currency": self.currency,
|
||||
"payment_gateway": self.payment_gateway,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -774,7 +775,10 @@ def get_existing_paid_amount(doctype, name):
|
||||
frappe.qb.from_(PL)
|
||||
.left_join(PER)
|
||||
.on(
|
||||
(PER.reference_doctype == PL.against_voucher_type) & (PER.reference_name == PL.against_voucher_no)
|
||||
(PL.against_voucher_type == PER.reference_doctype)
|
||||
& (PL.against_voucher_no == PER.reference_name)
|
||||
& (PL.voucher_type == PER.parenttype)
|
||||
& (PL.voucher_no == PER.parent)
|
||||
)
|
||||
.select(Abs(Sum(PL.amount)).as_("total_paid_amount"))
|
||||
.where(PL.against_voucher_type.eq(doctype))
|
||||
|
||||
@@ -542,6 +542,45 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
|
||||
self.assertEqual(pr.grand_total, si.outstanding_amount)
|
||||
|
||||
def test_partial_paid_invoice_with_more_payment_entry(self):
|
||||
pi = make_purchase_invoice(currency="INR", qty=1, rate=500)
|
||||
pi.submit()
|
||||
pi_1 = make_purchase_invoice(currency="INR", qty=1, rate=300)
|
||||
pi_1.submit()
|
||||
|
||||
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1, submit_doc=0, return_doc=1)
|
||||
pr.grand_total = 200
|
||||
pr.submit()
|
||||
pr.create_payment_entry()
|
||||
pr_1 = make_payment_request(
|
||||
dt="Purchase Invoice", dn=pi.name, mute_email=1, submit_doc=0, return_doc=1
|
||||
)
|
||||
pr_1.grand_total = 200
|
||||
pr_1.submit()
|
||||
pr_1.create_payment_entry()
|
||||
|
||||
pe = get_payment_entry(dt="Purchase Invoice", dn=pi.name)
|
||||
pe.paid_amount = 200
|
||||
pe.references[0].reference_doctype = pi.doctype
|
||||
pe.references[0].reference_name = pi.name
|
||||
pe.references[0].grand_total = pi.grand_total
|
||||
pe.references[0].outstanding_amount = pi.outstanding_amount
|
||||
pe.references[0].allocated_amount = 100
|
||||
pe.append(
|
||||
"references",
|
||||
{
|
||||
"reference_doctype": pi_1.doctype,
|
||||
"reference_name": pi_1.name,
|
||||
"grand_total": pi_1.grand_total,
|
||||
"outstanding_amount": pi_1.outstanding_amount,
|
||||
"allocated_amount": 100,
|
||||
},
|
||||
)
|
||||
|
||||
pr_2 = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
|
||||
pi.load_from_db()
|
||||
self.assertEqual(pr_2.grand_total, pi.outstanding_amount)
|
||||
|
||||
|
||||
def test_partial_paid_invoice_with_submitted_payment_entry(self):
|
||||
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)
|
||||
|
||||
@@ -39,10 +39,12 @@ class TestPOSClosingEntry(unittest.TestCase):
|
||||
|
||||
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
|
||||
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
|
||||
pos_inv1.save()
|
||||
pos_inv1.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
pcv_doc = make_closing_entry_from_opening(opening_entry)
|
||||
@@ -68,6 +70,7 @@ class TestPOSClosingEntry(unittest.TestCase):
|
||||
|
||||
pos_inv = create_pos_invoice(rate=3500, do_not_submit=1, item_name="Test Item", without_item_code=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
pcv_doc = make_closing_entry_from_opening(opening_entry)
|
||||
@@ -86,10 +89,12 @@ class TestPOSClosingEntry(unittest.TestCase):
|
||||
|
||||
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
|
||||
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
|
||||
pos_inv1.save()
|
||||
pos_inv1.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
# make return entry of pos_inv2
|
||||
@@ -111,10 +116,12 @@ class TestPOSClosingEntry(unittest.TestCase):
|
||||
|
||||
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
|
||||
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
|
||||
pos_inv1.save()
|
||||
pos_inv1.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
pcv_doc = make_closing_entry_from_opening(opening_entry)
|
||||
@@ -165,6 +172,7 @@ class TestPOSClosingEntry(unittest.TestCase):
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
pos_inv1 = create_pos_invoice(rate=350, do_not_submit=1, pos_profile=pos_profile.name)
|
||||
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
|
||||
pos_inv1.save()
|
||||
pos_inv1.submit()
|
||||
|
||||
# if in between a mandatory accounting dimension is added to the POS Profile then
|
||||
@@ -218,11 +226,27 @@ class TestPOSClosingEntry(unittest.TestCase):
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
pos_inv = create_pos_invoice(
|
||||
item_code=item_code, qty=5, rate=300, use_serial_batch_fields=1, batch_no=batch_no
|
||||
item_code=item_code,
|
||||
qty=5,
|
||||
rate=300,
|
||||
use_serial_batch_fields=1,
|
||||
batch_no=batch_no,
|
||||
do_not_submit=True,
|
||||
)
|
||||
pos_inv.payments[0].amount = pos_inv.grand_total
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
pos_inv2 = create_pos_invoice(
|
||||
item_code=item_code, qty=5, rate=300, use_serial_batch_fields=1, batch_no=batch_no
|
||||
item_code=item_code,
|
||||
qty=5,
|
||||
rate=300,
|
||||
use_serial_batch_fields=1,
|
||||
batch_no=batch_no,
|
||||
do_not_submit=True,
|
||||
)
|
||||
pos_inv2.payments[0].amount = pos_inv2.grand_total
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
|
||||
self.assertEqual(batch_qty_with_pos, 0.0)
|
||||
|
||||
@@ -20,6 +20,10 @@ from erpnext.controllers.queries import item_query as _item_query
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
|
||||
class PartialPaymentValidationError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class POSInvoice(SalesInvoice):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
@@ -210,6 +214,7 @@ class POSInvoice(SalesInvoice):
|
||||
self.validate_payment_amount()
|
||||
self.validate_loyalty_transaction()
|
||||
self.validate_company_with_pos_company()
|
||||
self.validate_full_payment()
|
||||
if self.coupon_code:
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
|
||||
|
||||
@@ -477,6 +482,20 @@ class POSInvoice(SalesInvoice):
|
||||
if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points:
|
||||
validate_loyalty_points(self, self.loyalty_points)
|
||||
|
||||
def validate_full_payment(self):
|
||||
invoice_total = flt(self.rounded_total) or flt(self.grand_total)
|
||||
|
||||
if self.docstatus == 1:
|
||||
if self.is_return and self.paid_amount != invoice_total:
|
||||
frappe.throw(
|
||||
msg=_("Partial Payment in POS Invoice is not allowed."), exc=PartialPaymentValidationError
|
||||
)
|
||||
|
||||
if self.paid_amount < invoice_total:
|
||||
frappe.throw(
|
||||
msg=_("Partial Payment in POS Invoice is not allowed."), exc=PartialPaymentValidationError
|
||||
)
|
||||
|
||||
def set_status(self, update=False, status=None, update_modified=True):
|
||||
if self.is_new():
|
||||
if self.get("amended_from"):
|
||||
|
||||
@@ -7,7 +7,7 @@ import unittest
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import PartialPaymentValidationError, make_sales_return
|
||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
@@ -313,7 +313,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
)
|
||||
|
||||
pos.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1}
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2000, "default": 1}
|
||||
)
|
||||
|
||||
pos.insert()
|
||||
@@ -324,6 +324,11 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
# partial return 1
|
||||
pos_return1.get("items")[0].qty = -1
|
||||
pos_return1.set("payments", [])
|
||||
pos_return1.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -1000, "default": 1}
|
||||
)
|
||||
pos_return1.paid_amount = -1000
|
||||
pos_return1.submit()
|
||||
pos_return1.reload()
|
||||
|
||||
@@ -338,6 +343,11 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
# partial return 2
|
||||
pos_return2 = make_sales_return(pos.name)
|
||||
pos_return2.set("payments", [])
|
||||
pos_return2.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -1000, "default": 1}
|
||||
)
|
||||
pos_return2.paid_amount = -1000
|
||||
pos_return2.submit()
|
||||
|
||||
self.assertEqual(pos_return2.get("items")[0].qty, -1)
|
||||
@@ -373,6 +383,15 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
inv.payments = []
|
||||
self.assertRaises(frappe.ValidationError, inv.insert)
|
||||
|
||||
def test_partial_payment(self):
|
||||
pos_inv = create_pos_invoice(rate=10000, do_not_save=1)
|
||||
pos_inv.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 9000},
|
||||
)
|
||||
pos_inv.insert()
|
||||
self.assertRaises(PartialPaymentValidationError, pos_inv.submit)
|
||||
|
||||
def test_serialized_item_transaction(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||
|
||||
@@ -581,7 +600,13 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
"Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty"
|
||||
)
|
||||
|
||||
inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000)
|
||||
inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1)
|
||||
inv.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000},
|
||||
)
|
||||
inv.insert()
|
||||
inv.submit()
|
||||
|
||||
lpe = frappe.get_doc(
|
||||
"Loyalty Point Entry",
|
||||
@@ -607,7 +632,13 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
)
|
||||
|
||||
# add 10 loyalty points
|
||||
create_pos_invoice(customer="Test Loyalty Customer", rate=10000)
|
||||
pos_inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1)
|
||||
pos_inv.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000},
|
||||
)
|
||||
pos_inv.paid_amount = 10000
|
||||
pos_inv.submit()
|
||||
|
||||
before_lp_details = get_loyalty_program_details_with_points(
|
||||
"Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty"
|
||||
@@ -641,10 +672,12 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 270})
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
@@ -676,6 +709,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
"included_in_print_rate": 1,
|
||||
},
|
||||
)
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=300, qty=2, do_not_submit=1)
|
||||
@@ -692,6 +726,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
"included_in_print_rate": 1,
|
||||
},
|
||||
)
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
@@ -744,6 +779,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
"included_in_print_rate": 1,
|
||||
},
|
||||
)
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
@@ -774,7 +810,10 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
# POS Invoice 1, for the batch without bundle
|
||||
pos_inv1 = create_pos_invoice(item="_BATCH ITEM Test For Reserve", rate=300, qty=15, do_not_save=1)
|
||||
|
||||
pos_inv1.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 4500},
|
||||
)
|
||||
pos_inv1.items[0].batch_no = batch_no
|
||||
pos_inv1.save()
|
||||
pos_inv1.submit()
|
||||
@@ -790,8 +829,14 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
# POS Invoice 2, for the batch with bundle
|
||||
pos_inv2 = create_pos_invoice(
|
||||
item="_BATCH ITEM Test For Reserve", rate=300, qty=10, batch_no=batch_no
|
||||
item="_BATCH ITEM Test For Reserve", rate=300, qty=10, batch_no=batch_no, do_not_save=1
|
||||
)
|
||||
pos_inv2.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3000},
|
||||
)
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
pos_inv2.reload()
|
||||
self.assertTrue(pos_inv2.items[0].serial_and_batch_bundle)
|
||||
|
||||
@@ -826,6 +871,10 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
pos_inv1 = create_pos_invoice(
|
||||
item=item.name, rate=300, qty=1, do_not_submit=1, batch_no="TestBatch 01"
|
||||
)
|
||||
pos_inv1.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300},
|
||||
)
|
||||
pos_inv1.save()
|
||||
pos_inv1.submit()
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime
|
||||
from frappe.utils.background_jobs import enqueue, is_job_enqueued
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
from erpnext.accounts.doctype.pos_profile.pos_profile import required_accounting_dimensions
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
)
|
||||
|
||||
|
||||
class POSInvoiceMergeLog(Document):
|
||||
@@ -292,22 +294,23 @@ class POSInvoiceMergeLog(Document):
|
||||
invoice.disable_rounded_total = cint(
|
||||
frappe.db.get_value("POS Profile", invoice.pos_profile, "disable_rounded_total")
|
||||
)
|
||||
accounting_dimensions = required_accounting_dimensions()
|
||||
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
|
||||
accounting_dimensions_fields = [d.fieldname for d in accounting_dimensions]
|
||||
dimension_values = frappe.db.get_value(
|
||||
"POS Profile", {"name": invoice.pos_profile}, accounting_dimensions, as_dict=1
|
||||
"POS Profile", {"name": invoice.pos_profile}, accounting_dimensions_fields, as_dict=1
|
||||
)
|
||||
for dimension in accounting_dimensions:
|
||||
dimension_value = dimension_values.get(dimension)
|
||||
dimension_value = dimension_values.get(dimension.fieldname)
|
||||
|
||||
if not dimension_value:
|
||||
if not dimension_value and (dimension.mandatory_for_pl or dimension.mandatory_for_bs):
|
||||
frappe.throw(
|
||||
_("Please set Accounting Dimension {} in {}").format(
|
||||
frappe.bold(frappe.unscrub(dimension)),
|
||||
frappe.bold(dimension.label),
|
||||
frappe.get_desk_link("POS Profile", invoice.pos_profile),
|
||||
)
|
||||
)
|
||||
|
||||
invoice.set(dimension, dimension_value)
|
||||
invoice.set(dimension.fieldname, dimension_value)
|
||||
|
||||
if self.merge_invoices_based_on == "Customer Group":
|
||||
invoice.flags.ignore_pos_profile = True
|
||||
|
||||
@@ -28,14 +28,17 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
|
||||
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
|
||||
pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300})
|
||||
pos_inv3.save()
|
||||
pos_inv3.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
@@ -61,14 +64,17 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
|
||||
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
|
||||
pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300})
|
||||
pos_inv3.save()
|
||||
pos_inv3.submit()
|
||||
|
||||
pos_inv_cn = make_sales_return(pos_inv.name)
|
||||
@@ -122,6 +128,8 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
},
|
||||
)
|
||||
inv.insert()
|
||||
inv.payments[0].amount = inv.grand_total
|
||||
inv.save()
|
||||
inv.submit()
|
||||
|
||||
inv2 = create_pos_invoice(qty=1, rate=100, do_not_save=True)
|
||||
@@ -138,6 +146,8 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
},
|
||||
)
|
||||
inv2.insert()
|
||||
inv2.payments[0].amount = inv.grand_total
|
||||
inv2.save()
|
||||
inv2.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
@@ -272,7 +282,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
inv2.submit()
|
||||
|
||||
inv3 = create_pos_invoice(qty=3, rate=600, do_not_save=True)
|
||||
inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000})
|
||||
inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1800})
|
||||
inv3.insert()
|
||||
inv3.submit()
|
||||
|
||||
@@ -280,8 +290,8 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
|
||||
inv.load_from_db()
|
||||
consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice)
|
||||
self.assertEqual(consolidated_invoice.outstanding_amount, 800)
|
||||
self.assertNotEqual(consolidated_invoice.status, "Paid")
|
||||
self.assertNotEqual(consolidated_invoice.outstanding_amount, 800)
|
||||
self.assertEqual(consolidated_invoice.status, "Paid")
|
||||
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
@@ -416,6 +426,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
do_not_submit=1,
|
||||
)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
pos_inv_cn = make_sales_return(pos_inv.name)
|
||||
@@ -430,6 +441,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
do_not_submit=1,
|
||||
)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"ignore_pricing_rule",
|
||||
"allow_rate_change",
|
||||
"allow_discount_change",
|
||||
"disable_grand_total_to_default_mop",
|
||||
"section_break_23",
|
||||
"item_groups",
|
||||
"column_break_25",
|
||||
@@ -382,6 +383,12 @@
|
||||
"fieldname": "print_receipt_on_order_complete",
|
||||
"fieldtype": "Check",
|
||||
"label": "Print Receipt on Order Complete"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disable_grand_total_to_default_mop",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable auto setting Grand Total to default Payment Mode"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -409,7 +416,7 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2025-01-01 11:07:03.161950",
|
||||
"modified": "2025-01-29 13:12:30.796630",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
|
||||
@@ -7,6 +7,10 @@ from frappe import _, msgprint, scrub, unscrub
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_link_to_form, now
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
)
|
||||
|
||||
|
||||
class POSProfile(Document):
|
||||
# begin: auto-generated types
|
||||
@@ -36,6 +40,7 @@ class POSProfile(Document):
|
||||
currency: DF.Link
|
||||
customer: DF.Link | None
|
||||
customer_groups: DF.Table[POSCustomerGroup]
|
||||
disable_grand_total_to_default_mop: DF.Check
|
||||
disable_rounded_total: DF.Check
|
||||
disabled: DF.Check
|
||||
expense_account: DF.Link | None
|
||||
@@ -69,15 +74,19 @@ class POSProfile(Document):
|
||||
self.validate_accounting_dimensions()
|
||||
|
||||
def validate_accounting_dimensions(self):
|
||||
acc_dim_names = required_accounting_dimensions()
|
||||
for acc_dim in acc_dim_names:
|
||||
if not self.get(acc_dim):
|
||||
acc_dims = get_checks_for_pl_and_bs_accounts()
|
||||
for acc_dim in acc_dims:
|
||||
if (
|
||||
self.company == acc_dim.company
|
||||
and not self.get(acc_dim.fieldname)
|
||||
and (acc_dim.mandatory_for_pl or acc_dim.mandatory_for_bs)
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} is a mandatory Accounting Dimension. <br>"
|
||||
"Please set a value for {0} in Accounting Dimensions section."
|
||||
).format(
|
||||
unscrub(frappe.bold(acc_dim)),
|
||||
frappe.bold(acc_dim.label),
|
||||
),
|
||||
title=_("Mandatory Accounting Dimension"),
|
||||
)
|
||||
@@ -215,23 +224,6 @@ def get_child_nodes(group_type, root):
|
||||
)
|
||||
|
||||
|
||||
def required_accounting_dimensions():
|
||||
p = frappe.qb.DocType("Accounting Dimension")
|
||||
c = frappe.qb.DocType("Accounting Dimension Detail")
|
||||
|
||||
acc_dim_doc = (
|
||||
frappe.qb.from_(p)
|
||||
.inner_join(c)
|
||||
.on(p.name == c.parent)
|
||||
.select(c.parent)
|
||||
.where((c.mandatory_for_bs == 1) | (c.mandatory_for_pl == 1))
|
||||
.where(p.disabled == 0)
|
||||
).run(as_dict=1)
|
||||
|
||||
acc_dim_names = [scrub(d.parent) for d in acc_dim_doc]
|
||||
return acc_dim_names
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"column_break_42",
|
||||
"free_item_uom",
|
||||
"round_free_qty",
|
||||
"dont_enforce_free_item_qty",
|
||||
"is_recursive",
|
||||
"recurse_for",
|
||||
"apply_recursion_over",
|
||||
@@ -643,12 +644,19 @@
|
||||
"fieldname": "has_priority",
|
||||
"fieldtype": "Check",
|
||||
"label": "Has Priority"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.price_or_product_discount == 'Product'",
|
||||
"fieldname": "dont_enforce_free_item_qty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Don't Enforce Free Item Qty"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-gift",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2024-09-16 18:14:51.314765",
|
||||
"modified": "2025-02-17 18:15:39.824639",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule",
|
||||
|
||||
@@ -60,6 +60,7 @@ class PricingRule(Document):
|
||||
disable: DF.Check
|
||||
discount_amount: DF.Currency
|
||||
discount_percentage: DF.Float
|
||||
dont_enforce_free_item_qty: DF.Check
|
||||
for_price_list: DF.Link | None
|
||||
free_item: DF.Link | None
|
||||
free_item_rate: DF.Currency
|
||||
@@ -645,7 +646,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, ra
|
||||
if pricing_rule.margin_type in ["Percentage", "Amount"]:
|
||||
item_details.margin_rate_or_amount = 0.0
|
||||
item_details.margin_type = None
|
||||
elif pricing_rule.get("free_item"):
|
||||
elif pricing_rule.get("free_item") and not pricing_rule.get("dont_enforce_free_item_qty"):
|
||||
item_details.remove_free_item = (
|
||||
item_code if pricing_rule.get("same_item") else pricing_rule.get("free_item")
|
||||
)
|
||||
|
||||
@@ -428,6 +428,54 @@ class TestPricingRule(FrappeTestCase):
|
||||
self.assertEqual(so.items[1].is_free_item, 1)
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item 2")
|
||||
|
||||
def test_dont_enforce_free_item_qty(self):
|
||||
# this test is only for testing non-enforcement as all other tests in this file already test with enforcement
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"rate": 0,
|
||||
"min_qty": 0,
|
||||
"max_qty": 7,
|
||||
"discount_percentage": 17.5,
|
||||
"price_or_product_discount": "Product",
|
||||
"same_item": 0,
|
||||
"free_item": "_Test Item 2",
|
||||
"free_qty": 1,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
pricing_rule = frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
# With enforcement
|
||||
so = make_sales_order(item_code="_Test Item", qty=1, do_not_submit=True)
|
||||
self.assertEqual(so.items[1].is_free_item, 1)
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item 2")
|
||||
|
||||
# Test 1 : Saving a document with an item with pricing list without it's corresponding free item will cause it the free item to be refetched on save
|
||||
so.items.pop(1)
|
||||
so.save()
|
||||
so.reload()
|
||||
self.assertEqual(len(so.items), 2)
|
||||
|
||||
# Without enforcement
|
||||
pricing_rule.dont_enforce_free_item_qty = 1
|
||||
pricing_rule.save()
|
||||
|
||||
# Test 2 : Deleted free item will not be fetched again on save without enforcement
|
||||
so.items.pop(1)
|
||||
so.save()
|
||||
so.reload()
|
||||
self.assertEqual(len(so.items), 1)
|
||||
|
||||
def test_cumulative_pricing_rule(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Cumulative Pricing Rule")
|
||||
test_record = {
|
||||
@@ -1451,6 +1499,7 @@ def make_pricing_rule(**args):
|
||||
"discount_amount": args.discount_amount or 0.0,
|
||||
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0,
|
||||
"has_priority": args.has_priority or 0,
|
||||
"enforce_free_item_qty": args.dont_enforce_free_item_qty or 0,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -713,7 +713,10 @@ def apply_pricing_rule_for_free_items(doc, pricing_rule_args):
|
||||
args.pop((item.item_code, item.pricing_rules))
|
||||
|
||||
for free_item in args.values():
|
||||
doc.append("items", free_item)
|
||||
if doc.is_new() or not frappe.get_value(
|
||||
"Pricing Rule", free_item["pricing_rules"], "dont_enforce_free_item_qty"
|
||||
):
|
||||
doc.append("items", free_item)
|
||||
|
||||
|
||||
def get_pricing_rule_items(pr_doc, other_items=False) -> list:
|
||||
|
||||
@@ -236,17 +236,21 @@ def get_ar_filters(doc, entry):
|
||||
|
||||
def get_html(doc, filters, entry, col, res, ageing):
|
||||
base_template_path = "frappe/www/printview.html"
|
||||
template_path = (
|
||||
"erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
|
||||
if doc.report == "General Ledger"
|
||||
else "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html"
|
||||
)
|
||||
template_path = "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html"
|
||||
if doc.report == "General Ledger":
|
||||
template_path = (
|
||||
"erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
|
||||
)
|
||||
|
||||
process_soa_html = frappe.get_hooks("process_soa_html")
|
||||
# fetching custom print format for Process Statement of Accounts
|
||||
if process_soa_html and process_soa_html.get(doc.report):
|
||||
template_path = process_soa_html[doc.report][-1]
|
||||
|
||||
if doc.letter_head:
|
||||
from frappe.www.printview import get_letter_head
|
||||
|
||||
letter_head = get_letter_head(doc, 0)
|
||||
|
||||
html = frappe.render_template(
|
||||
template_path,
|
||||
{
|
||||
@@ -262,7 +266,6 @@ def get_html(doc, filters, entry, col, res, ageing):
|
||||
else None,
|
||||
},
|
||||
)
|
||||
|
||||
html = frappe.render_template(
|
||||
base_template_path,
|
||||
{"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer},
|
||||
|
||||
@@ -460,6 +460,8 @@ class SalesInvoice(SellingController):
|
||||
|
||||
self.make_bundle_for_sales_purchase_return(table_name)
|
||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||
|
||||
self.update_stock_reservation_entries()
|
||||
self.update_stock_ledger()
|
||||
|
||||
# this sequence because outstanding may get -ve
|
||||
@@ -561,6 +563,7 @@ class SalesInvoice(SellingController):
|
||||
self.make_gl_entries_on_cancel()
|
||||
|
||||
if self.update_stock == 1:
|
||||
self.update_stock_reservation_entries()
|
||||
self.repost_future_sle_and_gle()
|
||||
|
||||
self.db_set("status", "Cancelled")
|
||||
|
||||
@@ -765,7 +765,7 @@ def validate_account_party_type(self):
|
||||
|
||||
if self.party_type and self.party:
|
||||
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
||||
if account_type and (account_type not in ["Receivable", "Payable"]):
|
||||
if account_type and (account_type not in ["Receivable", "Payable", "Equity"]):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Party Type and Party can only be set for Receivable / Payable account<br><br>" "{0}"
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<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>
|
||||
<td class="top-bottom" colspan="5"><strong>Credit</strong></td>
|
||||
@@ -61,7 +61,7 @@
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<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>
|
||||
<td class="top-bottom" colspan="5"><b>Narration: </b>{{ gl[0].remarks }}</td>
|
||||
|
||||
@@ -89,6 +89,7 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ function get_filters() {
|
||||
fieldname: "budget_against_filter",
|
||||
label: __("Dimension Filter"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "budget_against",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
@@ -151,6 +152,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
fieldname: "cost_center",
|
||||
label: __("Cost Center"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Cost Center",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Cost Center", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
@@ -161,6 +163,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Project",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Project", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
|
||||
@@ -67,6 +67,7 @@ frappe.query_reports["Gross Profit"] = {
|
||||
fieldname: "cost_center",
|
||||
label: __("Cost Center"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Cost Center",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Cost Center", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
@@ -77,6 +78,7 @@ frappe.query_reports["Gross Profit"] = {
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Project",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Project", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
|
||||
@@ -50,6 +50,7 @@ function get_filters() {
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -68,16 +68,12 @@ frappe.query_reports["Trial Balance for Party"] = {
|
||||
{
|
||||
fieldname: "account",
|
||||
label: __("Account"),
|
||||
fieldtype: "Link",
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Account",
|
||||
get_query: function () {
|
||||
var company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
doctype: "Account",
|
||||
filters: {
|
||||
company: company,
|
||||
},
|
||||
};
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Account", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import get_accounts_with_children
|
||||
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
|
||||
|
||||
|
||||
@@ -35,9 +37,14 @@ def get_data(filters, show_party_name):
|
||||
filters=party_filters,
|
||||
order_by="name",
|
||||
)
|
||||
|
||||
account_filter = []
|
||||
if filters.get("account"):
|
||||
account_filter = get_accounts_with_children(filters.get("account"))
|
||||
|
||||
company_currency = frappe.get_cached_value("Company", filters.company, "default_currency")
|
||||
opening_balances = get_opening_balances(filters)
|
||||
balances_within_period = get_balances_within_period(filters)
|
||||
opening_balances = get_opening_balances(filters, account_filter)
|
||||
balances_within_period = get_balances_within_period(filters, account_filter)
|
||||
|
||||
data = []
|
||||
# total_debit, total_credit = 0, 0
|
||||
@@ -89,30 +96,34 @@ def get_data(filters, show_party_name):
|
||||
return data
|
||||
|
||||
|
||||
def get_opening_balances(filters):
|
||||
account_filter = ""
|
||||
if filters.get("account"):
|
||||
account_filter = "and account = %s" % (frappe.db.escape(filters.get("account")))
|
||||
def get_opening_balances(filters, account_filter=None):
|
||||
GL_Entry = frappe.qb.DocType("GL Entry")
|
||||
|
||||
gle = frappe.db.sql(
|
||||
f"""
|
||||
select party, sum(debit) as opening_debit, sum(credit) as opening_credit
|
||||
from `tabGL Entry`
|
||||
where company=%(company)s
|
||||
and is_cancelled=0
|
||||
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
|
||||
and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s))
|
||||
{account_filter}
|
||||
group by party""",
|
||||
{
|
||||
"company": filters.company,
|
||||
"from_date": filters.from_date,
|
||||
"to_date": filters.to_date,
|
||||
"party_type": filters.party_type,
|
||||
},
|
||||
as_dict=True,
|
||||
query = (
|
||||
frappe.qb.from_(GL_Entry)
|
||||
.select(
|
||||
GL_Entry.party,
|
||||
Sum(GL_Entry.debit).as_("opening_debit"),
|
||||
Sum(GL_Entry.credit).as_("opening_credit"),
|
||||
)
|
||||
.where(
|
||||
(GL_Entry.company == filters.company)
|
||||
& (GL_Entry.is_cancelled == 0)
|
||||
& (GL_Entry.party_type == filters.party_type)
|
||||
& (GL_Entry.party != "")
|
||||
& (
|
||||
(GL_Entry.posting_date < filters.from_date)
|
||||
| ((GL_Entry.is_opening == "Yes") & (GL_Entry.posting_date <= filters.to_date))
|
||||
)
|
||||
)
|
||||
.groupby(GL_Entry.party)
|
||||
)
|
||||
|
||||
if account_filter:
|
||||
query = query.where(GL_Entry.account.isin(account_filter))
|
||||
|
||||
gle = query.run(as_dict=True)
|
||||
|
||||
opening = frappe._dict()
|
||||
for d in gle:
|
||||
opening_debit, opening_credit = toggle_debit_credit(d.opening_debit, d.opening_credit)
|
||||
@@ -121,31 +132,33 @@ def get_opening_balances(filters):
|
||||
return opening
|
||||
|
||||
|
||||
def get_balances_within_period(filters):
|
||||
account_filter = ""
|
||||
if filters.get("account"):
|
||||
account_filter = "and account = %s" % (frappe.db.escape(filters.get("account")))
|
||||
def get_balances_within_period(filters, account_filter=None):
|
||||
GL_Entry = frappe.qb.DocType("GL Entry")
|
||||
|
||||
gle = frappe.db.sql(
|
||||
f"""
|
||||
select party, sum(debit) as debit, sum(credit) as credit
|
||||
from `tabGL Entry`
|
||||
where company=%(company)s
|
||||
and is_cancelled = 0
|
||||
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
|
||||
and posting_date >= %(from_date)s and posting_date <= %(to_date)s
|
||||
and ifnull(is_opening, 'No') = 'No'
|
||||
{account_filter}
|
||||
group by party""",
|
||||
{
|
||||
"company": filters.company,
|
||||
"from_date": filters.from_date,
|
||||
"to_date": filters.to_date,
|
||||
"party_type": filters.party_type,
|
||||
},
|
||||
as_dict=True,
|
||||
query = (
|
||||
frappe.qb.from_(GL_Entry)
|
||||
.select(
|
||||
GL_Entry.party,
|
||||
Sum(GL_Entry.debit).as_("debit"),
|
||||
Sum(GL_Entry.credit).as_("credit"),
|
||||
)
|
||||
.where(
|
||||
(GL_Entry.company == filters.company)
|
||||
& (GL_Entry.is_cancelled == 0)
|
||||
& (GL_Entry.party_type == filters.party_type)
|
||||
& (GL_Entry.party != "")
|
||||
& (GL_Entry.posting_date >= filters.from_date)
|
||||
& (GL_Entry.posting_date <= filters.to_date)
|
||||
& (GL_Entry.is_opening == "No")
|
||||
)
|
||||
.groupby(GL_Entry.party)
|
||||
)
|
||||
|
||||
if account_filter:
|
||||
query = query.where(GL_Entry.account.isin(account_filter))
|
||||
|
||||
gle = query.run(as_dict=True)
|
||||
|
||||
balances_within_period = frappe._dict()
|
||||
for d in gle:
|
||||
balances_within_period.setdefault(d.party, [d.debit, d.credit])
|
||||
|
||||
@@ -609,9 +609,7 @@ frappe.ui.form.on("Asset", {
|
||||
frm.trigger("toggle_reference_doc");
|
||||
if (frm.doc.purchase_receipt) {
|
||||
if (frm.doc.item_code) {
|
||||
frappe.db.get_doc("Purchase Receipt", frm.doc.purchase_receipt).then((pr_doc) => {
|
||||
frm.events.set_values_from_purchase_doc(frm, "Purchase Receipt", pr_doc);
|
||||
});
|
||||
frm.events.set_values_from_purchase_doc(frm, "Purchase Receipt");
|
||||
} else {
|
||||
frm.set_value("purchase_receipt", "");
|
||||
frappe.msgprint({
|
||||
@@ -626,9 +624,7 @@ frappe.ui.form.on("Asset", {
|
||||
frm.trigger("toggle_reference_doc");
|
||||
if (frm.doc.purchase_invoice) {
|
||||
if (frm.doc.item_code) {
|
||||
frappe.db.get_doc("Purchase Invoice", frm.doc.purchase_invoice).then((pi_doc) => {
|
||||
frm.events.set_values_from_purchase_doc(frm, "Purchase Invoice", pi_doc);
|
||||
});
|
||||
frm.events.set_values_from_purchase_doc(frm, "Purchase Invoice");
|
||||
} else {
|
||||
frm.set_value("purchase_invoice", "");
|
||||
frappe.msgprint({
|
||||
@@ -639,45 +635,35 @@ frappe.ui.form.on("Asset", {
|
||||
}
|
||||
},
|
||||
|
||||
set_values_from_purchase_doc: function (frm, doctype, purchase_doc) {
|
||||
frm.set_value("company", purchase_doc.company);
|
||||
if (purchase_doc.bill_date) {
|
||||
frm.set_value("purchase_date", purchase_doc.bill_date);
|
||||
} else {
|
||||
frm.set_value("purchase_date", purchase_doc.posting_date);
|
||||
}
|
||||
if (!frm.doc.is_existing_asset && !frm.doc.available_for_use_date) {
|
||||
frm.set_value("available_for_use_date", frm.doc.purchase_date);
|
||||
}
|
||||
const item = purchase_doc.items.find((item) => item.item_code === frm.doc.item_code);
|
||||
if (!item) {
|
||||
let doctype_field = frappe.scrub(doctype);
|
||||
frm.set_value(doctype_field, "");
|
||||
frappe.msgprint({
|
||||
title: __("Invalid {0}", [__(doctype)]),
|
||||
message: __("The selected {0} does not contain the selected Asset Item.", [__(doctype)]),
|
||||
indicator: "red",
|
||||
});
|
||||
}
|
||||
frappe.db.get_value("Item", item.item_code, "is_grouped_asset", (r) => {
|
||||
var asset_quantity = r.is_grouped_asset ? item.qty : 1;
|
||||
var purchase_amount = flt(
|
||||
item.valuation_rate * asset_quantity,
|
||||
precision("gross_purchase_amount")
|
||||
);
|
||||
set_values_from_purchase_doc: (frm, doctype) => {
|
||||
frappe.call({
|
||||
method: "erpnext.assets.doctype.asset.asset.get_values_from_purchase_doc",
|
||||
args: {
|
||||
purchase_doc_name: frm.doc.purchase_receipt || frm.doc.purchase_invoice,
|
||||
item_code: frm.doc.item_code,
|
||||
doctype: doctype,
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
let data = r.message;
|
||||
frm.set_value("company", data.company);
|
||||
frm.set_value("purchase_date", data.purchase_date);
|
||||
frm.set_value("gross_purchase_amount", data.gross_purchase_amount);
|
||||
frm.set_value("purchase_amount", data.gross_purchase_amount);
|
||||
frm.set_value("asset_quantity", data.asset_quantity);
|
||||
frm.set_value("cost_center", data.cost_center);
|
||||
|
||||
frm.set_value("gross_purchase_amount", purchase_amount);
|
||||
frm.set_value("purchase_amount", purchase_amount);
|
||||
frm.set_value("asset_quantity", asset_quantity);
|
||||
frm.set_value("cost_center", item.cost_center || purchase_doc.cost_center);
|
||||
if (item.asset_location) {
|
||||
frm.set_value("location", item.asset_location);
|
||||
}
|
||||
if (doctype === "Purchase Receipt") {
|
||||
frm.set_value("purchase_receipt_item", item.name);
|
||||
} else if (doctype === "Purchase Invoice") {
|
||||
frm.set_value("purchase_invoice_item", item.name);
|
||||
}
|
||||
if (doctype === "Purchase Receipt") {
|
||||
frm.set_value("purchase_receipt_item", data.purchase_receipt_item);
|
||||
} else {
|
||||
frm.set_value("purchase_invoice_item", data.purchase_invoice_item);
|
||||
}
|
||||
|
||||
let is_editable = !data.is_multiple_items; // if multiple items, then fields should be read-only
|
||||
frm.set_df_property("gross_purchase_amount", "read_only", is_editable);
|
||||
frm.set_df_property("asset_quantity", "read_only", is_editable);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -227,8 +227,7 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Gross Purchase Amount",
|
||||
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only_depends_on": "eval:!doc.is_existing_asset"
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "available_for_use_date",
|
||||
@@ -470,8 +469,7 @@
|
||||
"default": "1",
|
||||
"fieldname": "asset_quantity",
|
||||
"fieldtype": "Int",
|
||||
"label": "Asset Quantity",
|
||||
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset"
|
||||
"label": "Asset Quantity"
|
||||
},
|
||||
{
|
||||
"fieldname": "depr_entry_posting_status",
|
||||
@@ -541,14 +539,13 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_receipt_item",
|
||||
"fieldtype": "Link",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Purchase Receipt Item",
|
||||
"options": "Purchase Receipt Item"
|
||||
"label": "Purchase Receipt Item"
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_invoice_item",
|
||||
"fieldtype": "Link",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Purchase Invoice Item",
|
||||
"options": "Purchase Invoice Item"
|
||||
@@ -595,7 +592,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2024-12-26 14:23:20.968882",
|
||||
"modified": "2025-02-11 16:01:56.140904",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -95,9 +95,9 @@ class Asset(AccountsController):
|
||||
purchase_amount: DF.Currency
|
||||
purchase_date: DF.Date | None
|
||||
purchase_invoice: DF.Link | None
|
||||
purchase_invoice_item: DF.Link | None
|
||||
purchase_invoice_item: DF.Data | None
|
||||
purchase_receipt: DF.Link | None
|
||||
purchase_receipt_item: DF.Link | None
|
||||
purchase_receipt_item: DF.Data | None
|
||||
split_from: DF.Link | None
|
||||
status: DF.Literal[
|
||||
"Draft",
|
||||
@@ -121,6 +121,7 @@ class Asset(AccountsController):
|
||||
|
||||
def validate(self):
|
||||
self.validate_precision()
|
||||
self.set_purchase_doc_row_item()
|
||||
self.validate_asset_values()
|
||||
self.validate_asset_and_reference()
|
||||
self.validate_item()
|
||||
@@ -199,6 +200,38 @@ class Asset(AccountsController):
|
||||
def after_delete(self):
|
||||
add_asset_activity(self.name, _("Asset deleted"))
|
||||
|
||||
def set_purchase_doc_row_item(self):
|
||||
if self.is_existing_asset or self.is_composite_asset:
|
||||
return
|
||||
|
||||
self.purchase_amount = self.gross_purchase_amount
|
||||
purchase_doc_type = "Purchase Receipt" if self.purchase_receipt else "Purchase Invoice"
|
||||
purchase_doc = self.purchase_receipt or self.purchase_invoice
|
||||
|
||||
if not purchase_doc:
|
||||
return
|
||||
|
||||
linked_item = self.get_linked_item(purchase_doc_type, purchase_doc)
|
||||
|
||||
if linked_item:
|
||||
if purchase_doc_type == "Purchase Receipt":
|
||||
self.purchase_receipt_item = linked_item
|
||||
else:
|
||||
self.purchase_invoice_item = linked_item
|
||||
|
||||
def get_linked_item(self, purchase_doc_type, purchase_doc):
|
||||
purchase_doc = frappe.get_doc(purchase_doc_type, purchase_doc)
|
||||
|
||||
for item in purchase_doc.items:
|
||||
if self.asset_quantity > 1:
|
||||
if item.base_net_amount == self.gross_purchase_amount and item.qty == self.asset_quantity:
|
||||
return item.name
|
||||
elif item.qty == self.asset_quantity:
|
||||
return item.name
|
||||
else:
|
||||
if item.base_net_rate == self.gross_purchase_amount and item.qty == self.asset_quantity:
|
||||
return item.name
|
||||
|
||||
def validate_asset_and_reference(self):
|
||||
if self.purchase_invoice or self.purchase_receipt:
|
||||
reference_doc = "Purchase Invoice" if self.purchase_invoice else "Purchase Receipt"
|
||||
@@ -1125,6 +1158,30 @@ def has_active_capitalization(asset):
|
||||
return active_capitalizations > 0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
|
||||
purchase_doc = frappe.get_doc(doctype, purchase_doc_name)
|
||||
matching_items = [item for item in purchase_doc.items if item.item_code == item_code]
|
||||
|
||||
if not matching_items:
|
||||
frappe.throw(_(f"Selected {doctype} does not contain the Item Code {item_code}"))
|
||||
|
||||
first_item = matching_items[0]
|
||||
is_multiple_items = len(matching_items) > 1
|
||||
|
||||
return {
|
||||
"company": purchase_doc.company,
|
||||
"purchase_date": purchase_doc.get("bill_date") or purchase_doc.get("posting_date"),
|
||||
"gross_purchase_amount": flt(first_item.base_net_amount),
|
||||
"asset_quantity": first_item.qty,
|
||||
"cost_center": first_item.cost_center or purchase_doc.get("cost_center"),
|
||||
"asset_location": first_item.get("asset_location"),
|
||||
"is_multiple_items": is_multiple_items,
|
||||
"purchase_receipt_item": first_item.name if doctype == "Purchase Receipt" else None,
|
||||
"purchase_invoice_item": first_item.name if doctype == "Purchase Invoice" else None,
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def split_asset(asset_name, split_qty):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
|
||||
@@ -444,9 +444,9 @@ def scrap_asset(asset_name):
|
||||
notes = _("This schedule was created when Asset {0} was scrapped.").format(
|
||||
get_link_to_form(asset.doctype, asset.name)
|
||||
)
|
||||
|
||||
depreciate_asset(asset, date, notes)
|
||||
asset.reload()
|
||||
if asset.status != "Fully Depreciated":
|
||||
depreciate_asset(asset, date, notes)
|
||||
asset.reload()
|
||||
|
||||
depreciation_series = frappe.get_cached_value("Company", asset.company, "series_for_depreciation_entry")
|
||||
|
||||
|
||||
@@ -917,6 +917,7 @@
|
||||
"fieldtype": "Float",
|
||||
"label": "Subcontracted Quantity",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
@@ -924,7 +925,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-12-10 12:11:18.536089",
|
||||
"modified": "2025-02-18 12:35:04.432636",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
||||
@@ -52,6 +52,7 @@ frappe.query_reports["Purchase Order Analysis"] = {
|
||||
label: __("Status"),
|
||||
fieldtype: "MultiSelectList",
|
||||
width: "80",
|
||||
options: ["To Pay", "To Bill", "To Receive", "To Receive and Bill", "Completed"],
|
||||
get_data: function (txt) {
|
||||
let status = ["To Bill", "To Receive", "To Receive and Bill", "Completed"];
|
||||
let options = [];
|
||||
|
||||
@@ -50,6 +50,7 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
|
||||
fieldname: "supplier",
|
||||
label: __("Supplier"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Supplier",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Supplier", txt);
|
||||
},
|
||||
@@ -58,6 +59,7 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
|
||||
fieldtype: "MultiSelectList",
|
||||
label: __("Supplier Quotation"),
|
||||
fieldname: "supplier_quotation",
|
||||
options: "Supplier Quotation",
|
||||
default: "",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Supplier Quotation", txt, { docstatus: ["<", 2] });
|
||||
|
||||
@@ -170,7 +170,7 @@ class AccountsController(TransactionBase):
|
||||
self.validate_qty_is_not_zero()
|
||||
|
||||
if (
|
||||
self.doctype in ["Sales Invoice", "Purchase Invoice"]
|
||||
self.doctype in ["Sales Invoice", "Purchase Invoice", "POS Invoice"]
|
||||
and self.get("is_return")
|
||||
and self.get("update_stock")
|
||||
):
|
||||
|
||||
@@ -779,8 +779,10 @@ class BuyingController(SubcontractingController):
|
||||
|
||||
is_plural = "s" if len(created_assets) != 1 else ""
|
||||
messages.append(
|
||||
_("Asset{} {assets_link} created for {}").format(
|
||||
is_plural, frappe.bold(d.item_code), assets_link=assets_link
|
||||
_("Asset{is_plural} {assets_link} created for {item_code}").format(
|
||||
is_plural=is_plural,
|
||||
assets_link=assets_link,
|
||||
item_code=frappe.bold(d.item_code),
|
||||
)
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -258,7 +258,7 @@ def get_already_returned_items(doc):
|
||||
|
||||
field = (
|
||||
frappe.scrub(doc.doctype) + "_item"
|
||||
if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Sales Invoice"]
|
||||
if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Sales Invoice", "POS Invoice"]
|
||||
else "dn_detail"
|
||||
)
|
||||
data = frappe.db.sql(
|
||||
@@ -770,6 +770,7 @@ def get_return_against_item_fields(voucher_type):
|
||||
"Delivery Note": "dn_detail",
|
||||
"Sales Invoice": "sales_invoice_item",
|
||||
"Subcontracting Receipt": "subcontracting_receipt_item",
|
||||
"POS Invoice": "sales_invoice_item",
|
||||
}
|
||||
return return_against_item_fields[voucher_type]
|
||||
|
||||
|
||||
@@ -596,12 +596,13 @@ class SellingController(StockController):
|
||||
if not self.is_internal_transfer() or self.docstatus == 1
|
||||
else None
|
||||
)
|
||||
if serial_and_batch_bundle and self.is_internal_transfer() and self.is_return:
|
||||
if self.docstatus == 1:
|
||||
|
||||
if self.is_internal_transfer():
|
||||
if serial_and_batch_bundle and self.docstatus == 1 and self.is_return:
|
||||
serial_and_batch_bundle = self.make_package_for_transfer(
|
||||
serial_and_batch_bundle, item_row.warehouse, type_of_transaction="Inward"
|
||||
)
|
||||
else:
|
||||
elif not serial_and_batch_bundle:
|
||||
serial_and_batch_bundle = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_detail_no": item_row.name, "warehouse": item_row.warehouse},
|
||||
@@ -791,6 +792,154 @@ class SellingController(StockController):
|
||||
|
||||
validate_item_type(self, "is_sales_item", "sales")
|
||||
|
||||
def update_stock_reservation_entries(self) -> None:
|
||||
"""Updates Delivered Qty in Stock Reservation Entries."""
|
||||
|
||||
if not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"):
|
||||
return
|
||||
|
||||
# Don't update Delivered Qty on Return.
|
||||
if self.is_return:
|
||||
return
|
||||
|
||||
so_field = "sales_order" if self.doctype == "Sales Invoice" else "against_sales_order"
|
||||
|
||||
if self._action == "submit":
|
||||
for item in self.get("items"):
|
||||
# Skip if `Sales Order` or `Sales Order Item` reference is not set.
|
||||
if not item.get(so_field) or not item.so_detail:
|
||||
continue
|
||||
|
||||
sre_list = frappe.db.get_all(
|
||||
"Stock Reservation Entry",
|
||||
{
|
||||
"docstatus": 1,
|
||||
"voucher_type": "Sales Order",
|
||||
"voucher_no": item.get(so_field),
|
||||
"voucher_detail_no": item.so_detail,
|
||||
"warehouse": item.warehouse,
|
||||
"status": ["not in", ["Delivered", "Cancelled"]],
|
||||
},
|
||||
order_by="creation",
|
||||
)
|
||||
|
||||
# Skip if no Stock Reservation Entries.
|
||||
if not sre_list:
|
||||
continue
|
||||
|
||||
qty_to_deliver = item.stock_qty
|
||||
for sre in sre_list:
|
||||
if qty_to_deliver <= 0:
|
||||
break
|
||||
|
||||
sre_doc = frappe.get_doc("Stock Reservation Entry", sre)
|
||||
|
||||
qty_can_be_deliver = 0
|
||||
if sre_doc.reservation_based_on == "Serial and Batch" and item.serial_and_batch_bundle:
|
||||
sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
|
||||
if sre_doc.has_serial_no:
|
||||
delivered_serial_nos = [d.serial_no for d in sbb.entries]
|
||||
for entry in sre_doc.sb_entries:
|
||||
if entry.serial_no in delivered_serial_nos:
|
||||
entry.delivered_qty = 1 # Qty will always be 0 or 1 for Serial No.
|
||||
entry.db_update()
|
||||
qty_can_be_deliver += 1
|
||||
delivered_serial_nos.remove(entry.serial_no)
|
||||
else:
|
||||
delivered_batch_qty = {d.batch_no: -1 * d.qty for d in sbb.entries}
|
||||
for entry in sre_doc.sb_entries:
|
||||
if entry.batch_no in delivered_batch_qty:
|
||||
delivered_qty = min(
|
||||
(entry.qty - entry.delivered_qty), delivered_batch_qty[entry.batch_no]
|
||||
)
|
||||
entry.delivered_qty += delivered_qty
|
||||
entry.db_update()
|
||||
qty_can_be_deliver += delivered_qty
|
||||
delivered_batch_qty[entry.batch_no] -= delivered_qty
|
||||
else:
|
||||
# `Delivered Qty` should be less than or equal to `Reserved Qty`.
|
||||
qty_can_be_deliver = min(
|
||||
(sre_doc.reserved_qty - sre_doc.delivered_qty), qty_to_deliver
|
||||
)
|
||||
|
||||
sre_doc.delivered_qty += qty_can_be_deliver
|
||||
sre_doc.db_update()
|
||||
|
||||
# Update Stock Reservation Entry `Status` based on `Delivered Qty`.
|
||||
sre_doc.update_status()
|
||||
|
||||
# Update Reserved Stock in Bin.
|
||||
sre_doc.update_reserved_stock_in_bin()
|
||||
|
||||
qty_to_deliver -= qty_can_be_deliver
|
||||
|
||||
if self._action == "cancel":
|
||||
for item in self.get("items"):
|
||||
# Skip if `Sales Order` or `Sales Order Item` reference is not set.
|
||||
if not item.get(so_field) or not item.so_detail:
|
||||
continue
|
||||
|
||||
sre_list = frappe.db.get_all(
|
||||
"Stock Reservation Entry",
|
||||
{
|
||||
"docstatus": 1,
|
||||
"voucher_type": "Sales Order",
|
||||
"voucher_no": item.get(so_field),
|
||||
"voucher_detail_no": item.so_detail,
|
||||
"warehouse": item.warehouse,
|
||||
"status": ["in", ["Partially Delivered", "Delivered"]],
|
||||
},
|
||||
order_by="creation",
|
||||
)
|
||||
|
||||
# Skip if no Stock Reservation Entries.
|
||||
if not sre_list:
|
||||
continue
|
||||
|
||||
qty_to_undelivered = item.stock_qty
|
||||
for sre in sre_list:
|
||||
if qty_to_undelivered <= 0:
|
||||
break
|
||||
|
||||
sre_doc = frappe.get_doc("Stock Reservation Entry", sre)
|
||||
|
||||
qty_can_be_undelivered = 0
|
||||
if sre_doc.reservation_based_on == "Serial and Batch" and item.serial_and_batch_bundle:
|
||||
sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
|
||||
if sre_doc.has_serial_no:
|
||||
serial_nos_to_undelivered = [d.serial_no for d in sbb.entries]
|
||||
for entry in sre_doc.sb_entries:
|
||||
if entry.serial_no in serial_nos_to_undelivered:
|
||||
entry.delivered_qty = 0 # Qty will always be 0 or 1 for Serial No.
|
||||
entry.db_update()
|
||||
qty_can_be_undelivered += 1
|
||||
serial_nos_to_undelivered.remove(entry.serial_no)
|
||||
else:
|
||||
batch_qty_to_undelivered = {d.batch_no: -1 * d.qty for d in sbb.entries}
|
||||
for entry in sre_doc.sb_entries:
|
||||
if entry.batch_no in batch_qty_to_undelivered:
|
||||
undelivered_qty = min(
|
||||
entry.delivered_qty, batch_qty_to_undelivered[entry.batch_no]
|
||||
)
|
||||
entry.delivered_qty -= undelivered_qty
|
||||
entry.db_update()
|
||||
qty_can_be_undelivered += undelivered_qty
|
||||
batch_qty_to_undelivered[entry.batch_no] -= undelivered_qty
|
||||
else:
|
||||
# `Qty to Undelivered` should be less than or equal to `Delivered Qty`.
|
||||
qty_can_be_undelivered = min(sre_doc.delivered_qty, qty_to_undelivered)
|
||||
|
||||
sre_doc.delivered_qty -= qty_can_be_undelivered
|
||||
sre_doc.db_update()
|
||||
|
||||
# Update Stock Reservation Entry `Status` based on `Delivered Qty`.
|
||||
sre_doc.update_status()
|
||||
|
||||
# Update Reserved Stock in Bin.
|
||||
sre_doc.update_reserved_stock_in_bin()
|
||||
|
||||
qty_to_undelivered -= qty_can_be_undelivered
|
||||
|
||||
|
||||
def set_default_income_account_for_item(obj):
|
||||
for d in obj.get("items"):
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import comma_or, flt, get_link_to_form, getdate, now, nowdate
|
||||
from frappe.utils import comma_or, flt, get_link_to_form, getdate, now, nowdate, safe_div
|
||||
|
||||
|
||||
class OverAllowanceError(frappe.ValidationError):
|
||||
@@ -543,7 +543,7 @@ class StatusUpdater(Document):
|
||||
)[0][0]
|
||||
)
|
||||
|
||||
per_billed = (min(ref_doc_qty, billed_qty) / ref_doc_qty) * 100
|
||||
per_billed = safe_div(min(ref_doc_qty, billed_qty), ref_doc_qty) * 100
|
||||
|
||||
ref_doc = frappe.get_doc(ref_dt, ref_dn)
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ frappe.query_reports["Opportunity Summary by Sales Stage"] = {
|
||||
fieldname: "status",
|
||||
label: __("Status"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: ["Open", "Converted", "Quotation", "Replied"],
|
||||
get_data: function () {
|
||||
return [
|
||||
{ value: "Open", description: "Status" },
|
||||
|
||||
@@ -216,7 +216,7 @@ class JobCard(Document):
|
||||
|
||||
open_job_cards = []
|
||||
if d.get("employee"):
|
||||
open_job_cards = self.get_open_job_cards(d.get("employee"))
|
||||
open_job_cards = self.get_open_job_cards(d.get("employee"), workstation=self.workstation)
|
||||
|
||||
data = self.get_overlap_for(d, open_job_cards=open_job_cards)
|
||||
if data:
|
||||
@@ -257,9 +257,13 @@ class JobCard(Document):
|
||||
frappe.get_cached_value("Workstation", self.workstation, "production_capacity") or 1
|
||||
)
|
||||
|
||||
if args.get("employee"):
|
||||
# override capacity for employee
|
||||
production_capacity = 1
|
||||
if self.get_open_job_cards(args.get("employee")):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Employee {0} is currently working on another workstation. Please assign another employee."
|
||||
).format(args.get("employee")),
|
||||
OverlapError,
|
||||
)
|
||||
|
||||
if not self.has_overlap(production_capacity, time_logs):
|
||||
return {}
|
||||
@@ -366,7 +370,7 @@ class JobCard(Document):
|
||||
|
||||
return time_logs
|
||||
|
||||
def get_open_job_cards(self, employee):
|
||||
def get_open_job_cards(self, employee, workstation=None):
|
||||
jc = frappe.qb.DocType("Job Card")
|
||||
jctl = frappe.qb.DocType("Job Card Time Log")
|
||||
|
||||
@@ -377,13 +381,15 @@ class JobCard(Document):
|
||||
.select(jc.name)
|
||||
.where(
|
||||
(jctl.parent == jc.name)
|
||||
& (jc.workstation == self.workstation)
|
||||
& (jctl.employee == employee)
|
||||
& (jc.docstatus < 1)
|
||||
& (jc.name != self.name)
|
||||
)
|
||||
)
|
||||
|
||||
if workstation:
|
||||
query = query.where(jc.workstation == workstation)
|
||||
|
||||
jobs = query.run(as_dict=True)
|
||||
return [job.get("name") for job in jobs] if jobs else []
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ frappe.query_reports["Job Card Summary"] = {
|
||||
label: __("Work Orders"),
|
||||
fieldname: "work_order",
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Work Order",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Work Order", txt);
|
||||
},
|
||||
@@ -64,6 +65,7 @@ frappe.query_reports["Job Card Summary"] = {
|
||||
label: __("Production Item"),
|
||||
fieldname: "production_item",
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Item",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Item", txt);
|
||||
},
|
||||
|
||||
@@ -41,7 +41,7 @@ frappe.query_reports["Production Planning Report"] = {
|
||||
fieldname: "docnames",
|
||||
label: __("Document Name"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Sales Order",
|
||||
options: "based_on",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ frappe.query_reports["Work Order Summary"] = {
|
||||
label: __("Sales Orders"),
|
||||
fieldname: "sales_order",
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Sales Order",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Sales Order", txt);
|
||||
},
|
||||
@@ -50,6 +51,7 @@ frappe.query_reports["Work Order Summary"] = {
|
||||
label: __("Production Item"),
|
||||
fieldname: "production_item",
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Item",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Item", txt);
|
||||
},
|
||||
|
||||
@@ -393,3 +393,4 @@ erpnext.patches.v15_0.sync_auto_reconcile_config
|
||||
execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment")
|
||||
erpnext.patches.v14_0.disable_add_row_in_gross_profit
|
||||
erpnext.patches.v15_0.set_difference_amount_in_asset_value_adjustment
|
||||
erpnext.patches.v14_0.update_posting_datetime
|
||||
|
||||
10
erpnext/patches/v14_0/update_posting_datetime.py
Normal file
10
erpnext/patches/v14_0/update_posting_datetime.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE `tabStock Ledger Entry`
|
||||
SET posting_datetime = timestamp(posting_date, posting_time)
|
||||
"""
|
||||
)
|
||||
@@ -1,74 +1,29 @@
|
||||
import frappe
|
||||
from frappe.utils import cstr
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("assets", "doctype", "Asset Depreciation Schedule")
|
||||
frappe.reload_doc("assets", "doctype", "Asset Finance Book")
|
||||
frappe.reload_doc("assets", "doctype", "Asset")
|
||||
|
||||
assets = get_details_of_draft_or_submitted_depreciable_assets()
|
||||
|
||||
asset_finance_books_map = get_asset_finance_books_map()
|
||||
|
||||
asset_depreciation_schedules_map = get_asset_depreciation_schedules_map()
|
||||
|
||||
for asset in assets:
|
||||
if not asset_depreciation_schedules_map.get(asset.name):
|
||||
for key, fb_row in asset_finance_books_map.items():
|
||||
depreciation_schedules = asset_depreciation_schedules_map.get(key)
|
||||
if not depreciation_schedules:
|
||||
continue
|
||||
|
||||
depreciation_schedules = asset_depreciation_schedules_map[asset.name]
|
||||
asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
|
||||
asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(fb_row, fb_row)
|
||||
asset_depr_schedule_doc.flags.ignore_validate = True
|
||||
asset_depr_schedule_doc.insert()
|
||||
|
||||
for fb_row in asset_finance_books_map[asset.name]:
|
||||
asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
|
||||
if fb_row.docstatus == 1:
|
||||
frappe.db.set_value(
|
||||
"Asset Depreciation Schedule",
|
||||
asset_depr_schedule_doc.name,
|
||||
{"docstatus": 1, "status": "Active"},
|
||||
)
|
||||
|
||||
asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(asset, fb_row)
|
||||
|
||||
asset_depr_schedule_doc.insert()
|
||||
|
||||
if asset.docstatus == 1:
|
||||
asset_depr_schedule_doc.submit()
|
||||
|
||||
depreciation_schedules_of_fb_row = [
|
||||
ds for ds in depreciation_schedules if ds["finance_book_id"] == str(fb_row.idx)
|
||||
]
|
||||
|
||||
update_depreciation_schedules(depreciation_schedules_of_fb_row, asset_depr_schedule_doc.name)
|
||||
|
||||
|
||||
def get_details_of_draft_or_submitted_depreciable_assets():
|
||||
asset = frappe.qb.DocType("Asset")
|
||||
|
||||
records = (
|
||||
frappe.qb.from_(asset)
|
||||
.select(
|
||||
asset.name,
|
||||
asset.opening_accumulated_depreciation,
|
||||
asset.gross_purchase_amount,
|
||||
asset.opening_number_of_booked_depreciations,
|
||||
asset.docstatus,
|
||||
)
|
||||
.where(asset.calculate_depreciation == 1)
|
||||
.where(asset.docstatus < 2)
|
||||
).run(as_dict=True)
|
||||
|
||||
return records
|
||||
|
||||
|
||||
def group_records_by_asset_name(records):
|
||||
grouped_dict = {}
|
||||
|
||||
for item in records:
|
||||
key = next(iter(item.keys()))
|
||||
value = item[key]
|
||||
|
||||
if value not in grouped_dict:
|
||||
grouped_dict[value] = []
|
||||
|
||||
del item["asset_name"]
|
||||
|
||||
grouped_dict[value].append(item)
|
||||
|
||||
return grouped_dict
|
||||
update_depreciation_schedules(depreciation_schedules, asset_depr_schedule_doc.name)
|
||||
|
||||
|
||||
def get_asset_finance_books_map():
|
||||
@@ -90,12 +45,20 @@ def get_asset_finance_books_map():
|
||||
afb.expected_value_after_useful_life,
|
||||
afb.daily_prorata_based,
|
||||
afb.shift_based,
|
||||
asset.docstatus,
|
||||
asset.name,
|
||||
asset.opening_accumulated_depreciation,
|
||||
asset.gross_purchase_amount,
|
||||
asset.opening_number_of_booked_depreciations,
|
||||
)
|
||||
.where(asset.docstatus < 2)
|
||||
.where(asset.calculate_depreciation == 1)
|
||||
.orderby(afb.idx)
|
||||
).run(as_dict=True)
|
||||
|
||||
asset_finance_books_map = group_records_by_asset_name(records)
|
||||
asset_finance_books_map = frappe._dict()
|
||||
for d in records:
|
||||
asset_finance_books_map.setdefault((d.asset_name, cstr(d.finance_book)), d)
|
||||
|
||||
return asset_finance_books_map
|
||||
|
||||
@@ -111,13 +74,17 @@ def get_asset_depreciation_schedules_map():
|
||||
.select(
|
||||
asset.name.as_("asset_name"),
|
||||
ds.name,
|
||||
ds.finance_book,
|
||||
ds.finance_book_id,
|
||||
)
|
||||
.where(asset.docstatus < 2)
|
||||
.where(asset.calculate_depreciation == 1)
|
||||
.orderby(ds.idx)
|
||||
).run(as_dict=True)
|
||||
|
||||
asset_depreciation_schedules_map = group_records_by_asset_name(records)
|
||||
asset_depreciation_schedules_map = frappe._dict()
|
||||
for d in records:
|
||||
asset_depreciation_schedules_map.setdefault((d.asset_name, cstr(d.finance_book)), []).append(d)
|
||||
|
||||
return asset_depreciation_schedules_map
|
||||
|
||||
|
||||
@@ -894,10 +894,16 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
this.frm.refresh_fields();
|
||||
}
|
||||
|
||||
set_default_payment(total_amount_to_pay, update_paid_amount) {
|
||||
async set_default_payment(total_amount_to_pay, update_paid_amount) {
|
||||
var me = this;
|
||||
var payment_status = true;
|
||||
if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) {
|
||||
let r = await frappe.db.get_value("POS Profile", this.frm.doc.pos_profile, "disable_grand_total_to_default_mop");
|
||||
|
||||
if (r.message.disable_grand_total_to_default_mop) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.each(this.frm.doc['payments'] || [], function(index, data) {
|
||||
if(data.default && payment_status && total_amount_to_pay > 0) {
|
||||
let base_amount, amount;
|
||||
|
||||
@@ -353,6 +353,26 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
||||
);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
currency() {
|
||||
super.currency();
|
||||
let me = this;
|
||||
const company_currency = this.get_company_currency();
|
||||
if (this.frm.doc.currency && this.frm.doc.currency !== company_currency) {
|
||||
this.get_exchange_rate(
|
||||
this.frm.doc.transaction_date,
|
||||
this.frm.doc.currency,
|
||||
company_currency,
|
||||
function (exchange_rate) {
|
||||
if (exchange_rate != me.frm.doc.conversion_rate) {
|
||||
me.set_margin_amount_based_on_currency(exchange_rate);
|
||||
me.set_actual_charges_based_on_currency(exchange_rate);
|
||||
me.frm.set_value("conversion_rate", exchange_rate);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
cur_frm.script_manager.make(erpnext.selling.QuotationController);
|
||||
|
||||
@@ -106,7 +106,11 @@
|
||||
"purchase_order",
|
||||
"column_break_89",
|
||||
"material_request_item",
|
||||
"purchase_order_item"
|
||||
"purchase_order_item",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"column_break_ihdh",
|
||||
"project"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -926,12 +930,42 @@
|
||||
"fieldname": "available_quantity_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Available Quantity"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": ":Company",
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center",
|
||||
"print_hide": 1,
|
||||
"print_width": "120px",
|
||||
"reqd": 1,
|
||||
"width": "120px"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ihdh",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project",
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-21 14:21:29.743474",
|
||||
"modified": "2025-02-06 13:29:24.619850",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order Item",
|
||||
|
||||
@@ -32,6 +32,7 @@ class SalesOrderItem(Document):
|
||||
brand: DF.Link | None
|
||||
company_total_stock: DF.Float
|
||||
conversion_factor: DF.Float
|
||||
cost_center: DF.Link
|
||||
customer_item_code: DF.Data | None
|
||||
delivered_by_supplier: DF.Check
|
||||
delivered_qty: DF.Float
|
||||
@@ -68,6 +69,7 @@ class SalesOrderItem(Document):
|
||||
pricing_rules: DF.SmallText | None
|
||||
produced_qty: DF.Float
|
||||
production_plan_qty: DF.Float
|
||||
project: DF.Link | None
|
||||
projected_qty: DF.Float
|
||||
purchase_order: DF.Link | None
|
||||
purchase_order_item: DF.Data | None
|
||||
|
||||
@@ -340,11 +340,19 @@ erpnext.PointOfSale.Payment = class {
|
||||
// pass
|
||||
}
|
||||
|
||||
render_payment_section() {
|
||||
async render_payment_section() {
|
||||
this.render_payment_mode_dom();
|
||||
this.make_invoice_fields_control();
|
||||
this.update_totals_section();
|
||||
this.focus_on_default_mop();
|
||||
let r = await frappe.db.get_value(
|
||||
"POS Profile",
|
||||
this.frm.doc.pos_profile,
|
||||
"disable_grand_total_to_default_mop"
|
||||
);
|
||||
|
||||
if (!r.message.disable_grand_total_to_default_mop) {
|
||||
this.focus_on_default_mop();
|
||||
}
|
||||
}
|
||||
|
||||
after_render() {
|
||||
|
||||
@@ -87,6 +87,7 @@ function get_filters() {
|
||||
fieldname: "status",
|
||||
label: __("Status"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: ["Overdue", "Unpaid", "Completed", "Partly Paid"],
|
||||
width: 100,
|
||||
get_data: function (txt) {
|
||||
let status = ["Overdue", "Unpaid", "Completed", "Partly Paid"];
|
||||
|
||||
@@ -53,6 +53,7 @@ frappe.query_reports["Sales Order Analysis"] = {
|
||||
fieldname: "status",
|
||||
label: __("Status"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: ["To Pay", "To Bill", "To Deliver", "To Deliver and Bill", "Completed"],
|
||||
width: "80",
|
||||
get_data: function (txt) {
|
||||
let status = ["To Bill", "To Deliver", "To Deliver and Bill", "Completed"];
|
||||
|
||||
@@ -491,149 +491,6 @@ class DeliveryNote(SellingController):
|
||||
|
||||
self.delete_auto_created_batches()
|
||||
|
||||
def update_stock_reservation_entries(self) -> None:
|
||||
"""Updates Delivered Qty in Stock Reservation Entries."""
|
||||
|
||||
# Don't update Delivered Qty on Return.
|
||||
if self.is_return:
|
||||
return
|
||||
|
||||
if self._action == "submit":
|
||||
for item in self.get("items"):
|
||||
# Skip if `Sales Order` or `Sales Order Item` reference is not set.
|
||||
if not item.against_sales_order or not item.so_detail:
|
||||
continue
|
||||
|
||||
sre_list = frappe.db.get_all(
|
||||
"Stock Reservation Entry",
|
||||
{
|
||||
"docstatus": 1,
|
||||
"voucher_type": "Sales Order",
|
||||
"voucher_no": item.against_sales_order,
|
||||
"voucher_detail_no": item.so_detail,
|
||||
"warehouse": item.warehouse,
|
||||
"status": ["not in", ["Delivered", "Cancelled"]],
|
||||
},
|
||||
order_by="creation",
|
||||
)
|
||||
|
||||
# Skip if no Stock Reservation Entries.
|
||||
if not sre_list:
|
||||
continue
|
||||
|
||||
qty_to_deliver = item.stock_qty
|
||||
for sre in sre_list:
|
||||
if qty_to_deliver <= 0:
|
||||
break
|
||||
|
||||
sre_doc = frappe.get_doc("Stock Reservation Entry", sre)
|
||||
|
||||
qty_can_be_deliver = 0
|
||||
if sre_doc.reservation_based_on == "Serial and Batch":
|
||||
sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
|
||||
if sre_doc.has_serial_no:
|
||||
delivered_serial_nos = [d.serial_no for d in sbb.entries]
|
||||
for entry in sre_doc.sb_entries:
|
||||
if entry.serial_no in delivered_serial_nos:
|
||||
entry.delivered_qty = 1 # Qty will always be 0 or 1 for Serial No.
|
||||
entry.db_update()
|
||||
qty_can_be_deliver += 1
|
||||
delivered_serial_nos.remove(entry.serial_no)
|
||||
else:
|
||||
delivered_batch_qty = {d.batch_no: -1 * d.qty for d in sbb.entries}
|
||||
for entry in sre_doc.sb_entries:
|
||||
if entry.batch_no in delivered_batch_qty:
|
||||
delivered_qty = min(
|
||||
(entry.qty - entry.delivered_qty), delivered_batch_qty[entry.batch_no]
|
||||
)
|
||||
entry.delivered_qty += delivered_qty
|
||||
entry.db_update()
|
||||
qty_can_be_deliver += delivered_qty
|
||||
delivered_batch_qty[entry.batch_no] -= delivered_qty
|
||||
else:
|
||||
# `Delivered Qty` should be less than or equal to `Reserved Qty`.
|
||||
qty_can_be_deliver = min(
|
||||
(sre_doc.reserved_qty - sre_doc.delivered_qty), qty_to_deliver
|
||||
)
|
||||
|
||||
sre_doc.delivered_qty += qty_can_be_deliver
|
||||
sre_doc.db_update()
|
||||
|
||||
# Update Stock Reservation Entry `Status` based on `Delivered Qty`.
|
||||
sre_doc.update_status()
|
||||
|
||||
# Update Reserved Stock in Bin.
|
||||
sre_doc.update_reserved_stock_in_bin()
|
||||
|
||||
qty_to_deliver -= qty_can_be_deliver
|
||||
|
||||
if self._action == "cancel":
|
||||
for item in self.get("items"):
|
||||
# Skip if `Sales Order` or `Sales Order Item` reference is not set.
|
||||
if not item.against_sales_order or not item.so_detail:
|
||||
continue
|
||||
|
||||
sre_list = frappe.db.get_all(
|
||||
"Stock Reservation Entry",
|
||||
{
|
||||
"docstatus": 1,
|
||||
"voucher_type": "Sales Order",
|
||||
"voucher_no": item.against_sales_order,
|
||||
"voucher_detail_no": item.so_detail,
|
||||
"warehouse": item.warehouse,
|
||||
"status": ["in", ["Partially Delivered", "Delivered"]],
|
||||
},
|
||||
order_by="creation",
|
||||
)
|
||||
|
||||
# Skip if no Stock Reservation Entries.
|
||||
if not sre_list:
|
||||
continue
|
||||
|
||||
qty_to_undelivered = item.stock_qty
|
||||
for sre in sre_list:
|
||||
if qty_to_undelivered <= 0:
|
||||
break
|
||||
|
||||
sre_doc = frappe.get_doc("Stock Reservation Entry", sre)
|
||||
|
||||
qty_can_be_undelivered = 0
|
||||
if sre_doc.reservation_based_on == "Serial and Batch":
|
||||
sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
|
||||
if sre_doc.has_serial_no:
|
||||
serial_nos_to_undelivered = [d.serial_no for d in sbb.entries]
|
||||
for entry in sre_doc.sb_entries:
|
||||
if entry.serial_no in serial_nos_to_undelivered:
|
||||
entry.delivered_qty = 0 # Qty will always be 0 or 1 for Serial No.
|
||||
entry.db_update()
|
||||
qty_can_be_undelivered += 1
|
||||
serial_nos_to_undelivered.remove(entry.serial_no)
|
||||
else:
|
||||
batch_qty_to_undelivered = {d.batch_no: -1 * d.qty for d in sbb.entries}
|
||||
for entry in sre_doc.sb_entries:
|
||||
if entry.batch_no in batch_qty_to_undelivered:
|
||||
undelivered_qty = min(
|
||||
entry.delivered_qty, batch_qty_to_undelivered[entry.batch_no]
|
||||
)
|
||||
entry.delivered_qty -= undelivered_qty
|
||||
entry.db_update()
|
||||
qty_can_be_undelivered += undelivered_qty
|
||||
batch_qty_to_undelivered[entry.batch_no] -= undelivered_qty
|
||||
else:
|
||||
# `Qty to Undelivered` should be less than or equal to `Delivered Qty`.
|
||||
qty_can_be_undelivered = min(sre_doc.delivered_qty, qty_to_undelivered)
|
||||
|
||||
sre_doc.delivered_qty -= qty_can_be_undelivered
|
||||
sre_doc.db_update()
|
||||
|
||||
# Update Stock Reservation Entry `Status` based on `Delivered Qty`.
|
||||
sre_doc.update_status()
|
||||
|
||||
# Update Reserved Stock in Bin.
|
||||
sre_doc.update_reserved_stock_in_bin()
|
||||
|
||||
qty_to_undelivered -= qty_can_be_undelivered
|
||||
|
||||
def validate_against_stock_reservation_entries(self):
|
||||
"""Validates if Stock Reservation Entries are available for the Sales Order Item reference."""
|
||||
|
||||
|
||||
@@ -769,6 +769,48 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
{"warehouse": "_Test Warehouse - _TC"},
|
||||
)
|
||||
|
||||
def test_delivery_note_internal_transfer_serial_no_status(self):
|
||||
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||
|
||||
item = make_item(
|
||||
"_Test Item for Internal Transfer With Serial No Status",
|
||||
properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "INT-SN-.####"},
|
||||
).name
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
target = "Stores - _TC"
|
||||
company = "_Test Company"
|
||||
customer = create_internal_customer(represents_company=company)
|
||||
rate = 42
|
||||
|
||||
se = make_stock_entry(target=warehouse, qty=5, basic_rate=rate, item_code=item)
|
||||
serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)
|
||||
|
||||
dn = create_delivery_note(
|
||||
item_code=item,
|
||||
company=company,
|
||||
customer=customer,
|
||||
qty=5,
|
||||
rate=500,
|
||||
warehouse=warehouse,
|
||||
target_warehouse=target,
|
||||
ignore_pricing_rule=0,
|
||||
use_serial_batch_fields=1,
|
||||
serial_no="\n".join(serial_nos),
|
||||
)
|
||||
|
||||
for serial_no in serial_nos:
|
||||
sn = frappe.db.get_value("Serial No", serial_no, ["status", "warehouse"], as_dict=1)
|
||||
self.assertEqual(sn.status, "Active")
|
||||
self.assertEqual(sn.warehouse, target)
|
||||
|
||||
dn.cancel()
|
||||
|
||||
for serial_no in serial_nos:
|
||||
sn = frappe.db.get_value("Serial No", serial_no, ["status", "warehouse"], as_dict=1)
|
||||
self.assertEqual(sn.status, "Active")
|
||||
self.assertEqual(sn.warehouse, warehouse)
|
||||
|
||||
def test_delivery_of_bundled_items_to_target_warehouse(self):
|
||||
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_guest_to_view": 1,
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:item_code",
|
||||
@@ -888,10 +887,9 @@
|
||||
"icon": "fa fa-tag",
|
||||
"idx": 2,
|
||||
"image_field": "image",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2024-04-30 13:46:39.098753",
|
||||
"modified": "2025-02-03 23:43:57.253667",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item",
|
||||
|
||||
@@ -202,7 +202,8 @@
|
||||
"oldfieldname": "parent_detail_docname",
|
||||
"oldfieldtype": "Data",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "batch_no",
|
||||
@@ -295,7 +296,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-04 16:30:44.263964",
|
||||
"modified": "2025-02-18 13:07:02.789654",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Packed Item",
|
||||
@@ -305,4 +306,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1242,6 +1242,7 @@ class TestPickList(FrappeTestCase):
|
||||
"is_recursive": 1,
|
||||
"recurse_for": 2,
|
||||
"free_qty": 1,
|
||||
"dont_enforce_free_item_qty": 0,
|
||||
"company": "_Test Company",
|
||||
"customer": "_Test Customer",
|
||||
}
|
||||
|
||||
@@ -34,7 +34,9 @@
|
||||
"verified_by",
|
||||
"column_break_17",
|
||||
"remarks",
|
||||
"amended_from"
|
||||
"amended_from",
|
||||
"print_settings_section",
|
||||
"letter_head"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -255,6 +257,20 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
},
|
||||
{
|
||||
"fieldname": "print_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Print Settings"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fetch_from": "company.default_letter_head",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "letter_head",
|
||||
"fieldtype": "Link",
|
||||
"label": "Letter Head",
|
||||
"options": "Letter Head"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-search",
|
||||
@@ -262,7 +278,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-16 17:00:48.774532",
|
||||
"modified": "2025-02-17 13:20:17.583094",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Quality Inspection",
|
||||
|
||||
@@ -37,6 +37,7 @@ class QualityInspection(Document):
|
||||
item_code: DF.Link
|
||||
item_name: DF.Data | None
|
||||
item_serial_no: DF.Link | None
|
||||
letter_head: DF.Link | None
|
||||
manual_inspection: DF.Check
|
||||
naming_series: DF.Literal["MAT-QA-.YYYY.-"]
|
||||
quality_inspection_template: DF.Link | None
|
||||
|
||||
@@ -134,6 +134,13 @@ frappe.ui.form.on("Serial and Batch Bundle", {
|
||||
},
|
||||
|
||||
toggle_fields(frm) {
|
||||
let show_naming_series_field =
|
||||
frappe.user_defaults.set_serial_and_batch_bundle_naming_based_on_naming_series;
|
||||
frm.toggle_display("naming_series", cint(show_naming_series_field));
|
||||
frm.toggle_reqd("naming_series", cint(show_naming_series_field));
|
||||
|
||||
frm.toggle_display("naming_series", frm.doc.__islocal ? true : false);
|
||||
|
||||
if (frm.doc.has_serial_no) {
|
||||
frm.doc.entries.forEach((row) => {
|
||||
if (Math.abs(row.qty) !== 1) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"item_details_tab",
|
||||
"naming_series",
|
||||
"company",
|
||||
"item_name",
|
||||
"has_serial_no",
|
||||
@@ -242,12 +243,20 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Returned Against",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "SABB-.########",
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"label": "Naming Series",
|
||||
"options": "\nSABB-.########",
|
||||
"set_only_once": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-02-12 10:53:32.090309",
|
||||
"modified": "2025-02-17 18:22:36.056205",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Serial and Batch Bundle",
|
||||
|
||||
@@ -8,6 +8,7 @@ from collections import Counter, defaultdict
|
||||
import frappe
|
||||
from frappe import _, _dict, bold
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.query_builder.functions import CombineDatetime, Sum
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
@@ -68,6 +69,7 @@ class SerialandBatchBundle(Document):
|
||||
item_code: DF.Link
|
||||
item_group: DF.Link | None
|
||||
item_name: DF.Data | None
|
||||
naming_series: DF.Literal["", "SABB-.########"]
|
||||
posting_date: DF.Date | None
|
||||
posting_time: DF.Time | None
|
||||
returned_against: DF.Data | None
|
||||
@@ -80,6 +82,24 @@ class SerialandBatchBundle(Document):
|
||||
warehouse: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
def autoname(self):
|
||||
if frappe.db.get_single_value(
|
||||
"Stock Settings", "set_serial_and_batch_bundle_naming_based_on_naming_series"
|
||||
):
|
||||
if not self.naming_series:
|
||||
frappe.throw(_("Naming Series is mandatory"))
|
||||
|
||||
naming_series = self.naming_series
|
||||
if "#" not in naming_series:
|
||||
naming_series += ".#####"
|
||||
|
||||
self.name = make_autoname(self.naming_series)
|
||||
else:
|
||||
try:
|
||||
self.name = frappe.generate_hash(length=20)
|
||||
except frappe.DuplicateEntryError:
|
||||
self.autoname()
|
||||
|
||||
def validate(self):
|
||||
if self.docstatus == 1 and self.voucher_detail_no:
|
||||
self.validate_voucher_detail_no()
|
||||
|
||||
@@ -17,6 +17,67 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
|
||||
class TestSerialandBatchBundle(FrappeTestCase):
|
||||
def test_naming_for_sabb(self):
|
||||
frappe.db.set_single_value(
|
||||
"Stock Settings", "set_serial_and_batch_bundle_naming_based_on_naming_series", 1
|
||||
)
|
||||
|
||||
serial_item_code = "New Serial No Valuation 11"
|
||||
make_item(
|
||||
serial_item_code,
|
||||
{
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "TEST-A-SER-VAL-.#####",
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
for sn in ["TEST-A-SER-VAL-00001", "TEST-A-SER-VAL-00002"]:
|
||||
if not frappe.db.exists("Serial No", sn):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Serial No",
|
||||
"serial_no": sn,
|
||||
"item_code": serial_item_code,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
bundle_doc = make_serial_batch_bundle(
|
||||
{
|
||||
"item_code": serial_item_code,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"voucher_type": "Stock Entry",
|
||||
"posting_date": today(),
|
||||
"posting_time": nowtime(),
|
||||
"qty": 10,
|
||||
"serial_nos": ["TEST-A-SER-VAL-00001", "TEST-A-SER-VAL-00002"],
|
||||
"type_of_transaction": "Inward",
|
||||
"do_not_submit": True,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertTrue(bundle_doc.name.startswith("SABB-"))
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Stock Settings", "set_serial_and_batch_bundle_naming_based_on_naming_series", 0
|
||||
)
|
||||
|
||||
bundle_doc = make_serial_batch_bundle(
|
||||
{
|
||||
"item_code": serial_item_code,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"voucher_type": "Stock Entry",
|
||||
"posting_date": today(),
|
||||
"posting_time": nowtime(),
|
||||
"qty": 10,
|
||||
"serial_nos": ["TEST-A-SER-VAL-00001", "TEST-A-SER-VAL-00002"],
|
||||
"type_of_transaction": "Inward",
|
||||
"do_not_submit": True,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertFalse(bundle_doc.name.startswith("SABB-"))
|
||||
|
||||
def test_inward_outward_serial_valuation(self):
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
@@ -2417,7 +2417,7 @@ class StockEntry(StockController):
|
||||
item_row = item_dict[d]
|
||||
|
||||
child_qty = flt(item_row["qty"], precision)
|
||||
if not self.is_return and child_qty <= 0:
|
||||
if not self.is_return and child_qty <= 0 and not item_row.get("is_scrap_item"):
|
||||
continue
|
||||
|
||||
se_child = self.append("items")
|
||||
|
||||
@@ -56,6 +56,8 @@
|
||||
"use_serial_batch_fields",
|
||||
"do_not_update_serial_batch_on_creation_of_auto_bundle",
|
||||
"allow_existing_serial_no",
|
||||
"serial_and_batch_bundle_section",
|
||||
"set_serial_and_batch_bundle_naming_based_on_naming_series",
|
||||
"stock_planning_tab",
|
||||
"auto_material_request",
|
||||
"auto_indent",
|
||||
@@ -467,6 +469,17 @@
|
||||
"fieldname": "allow_existing_serial_no",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow existing Serial No to be Manufactured/Received again"
|
||||
},
|
||||
{
|
||||
"fieldname": "serial_and_batch_bundle_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Serial and Batch Bundle"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "set_serial_and_batch_bundle_naming_based_on_naming_series",
|
||||
"fieldtype": "Check",
|
||||
"label": "Set Serial and Batch Bundle Naming Based on Naming Series"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -474,7 +487,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-12-09 17:52:36.030456",
|
||||
"modified": "2025-02-17 14:36:36.177743",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Settings",
|
||||
|
||||
@@ -55,6 +55,7 @@ class StockSettings(Document):
|
||||
role_allowed_to_create_edit_back_dated_transactions: DF.Link | None
|
||||
role_allowed_to_over_deliver_receive: DF.Link | None
|
||||
sample_retention_warehouse: DF.Link | None
|
||||
set_serial_and_batch_bundle_naming_based_on_naming_series: DF.Check
|
||||
show_barcode_field: DF.Check
|
||||
stock_auth_role: DF.Link | None
|
||||
stock_frozen_upto: DF.Date | None
|
||||
@@ -75,6 +76,7 @@ class StockSettings(Document):
|
||||
"default_warehouse",
|
||||
"set_qty_in_transactions_based_on_serial_no_input",
|
||||
"use_serial_batch_fields",
|
||||
"set_serial_and_batch_bundle_naming_based_on_naming_series",
|
||||
]:
|
||||
frappe.db.set_default(key, self.get(key, ""))
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ frappe.query_reports["Item Shortage Report"] = {
|
||||
fieldname: "warehouse",
|
||||
label: __("Warehouse"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Warehouse",
|
||||
width: "100",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Warehouse", txt);
|
||||
|
||||
@@ -50,6 +50,7 @@ frappe.query_reports["Serial and Batch Summary"] = {
|
||||
fieldname: "voucher_no",
|
||||
label: __("Voucher No"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "voucher_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -663,4 +663,4 @@ def get_combine_datetime(posting_date, posting_time):
|
||||
if isinstance(posting_time, datetime.timedelta):
|
||||
posting_time = (datetime.datetime.min + posting_time).time()
|
||||
|
||||
return datetime.datetime.combine(posting_date, posting_time).replace(microsecond=0)
|
||||
return datetime.datetime.combine(posting_date, posting_time)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.utils import escape_html
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@@ -11,6 +12,8 @@ def send_message(sender, message, subject="Website Query"):
|
||||
|
||||
website_send_message(sender, message, subject)
|
||||
|
||||
message = escape_html(message)
|
||||
|
||||
lead = customer = None
|
||||
customer = frappe.db.sql(
|
||||
"""select distinct dl.link_name from `tabDynamic Link` dl
|
||||
|
||||
Reference in New Issue
Block a user