mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-05 22:48:27 +00:00
Merge pull request #43975 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Advance Payment Ledger Entry", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2024-10-16 16:57:12.085072",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"against_voucher_type",
|
||||
"against_voucher_no",
|
||||
"amount",
|
||||
"currency",
|
||||
"event"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Voucher Type",
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Voucher No",
|
||||
"options": "voucher_type",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "against_voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Against Voucher Type",
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "against_voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Against Voucher No",
|
||||
"options": "against_voucher_type",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Currency",
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "event",
|
||||
"fieldtype": "Data",
|
||||
"label": "Event",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-05 10:31:28.736671",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Payment Ledger Entry",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Auditor",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class AdvancePaymentLedgerEntry(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
against_voucher_no: DF.DynamicLink | None
|
||||
against_voucher_type: DF.Link | None
|
||||
amount: DF.Currency
|
||||
company: DF.Link | None
|
||||
currency: DF.Link | None
|
||||
event: DF.Data | None
|
||||
voucher_no: DF.DynamicLink | None
|
||||
voucher_type: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,222 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import nowdate, today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
|
||||
class TestAdvancePaymentLedgerEntry(AccountsTestMixin, FrappeTestCase):
|
||||
"""
|
||||
Integration tests for AdvancePaymentLedgerEntry.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_usd_receivable_account()
|
||||
self.create_usd_payable_account()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_sales_order(self, qty=1, rate=100, currency="INR", do_not_submit=False):
|
||||
"""
|
||||
Helper method
|
||||
"""
|
||||
so = make_sales_order(
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
currency=currency,
|
||||
item=self.item,
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
transaction_date=today(),
|
||||
do_not_submit=do_not_submit,
|
||||
)
|
||||
return so
|
||||
|
||||
def create_purchase_order(self, qty=1, rate=100, currency="INR", do_not_submit=False):
|
||||
"""
|
||||
Helper method
|
||||
"""
|
||||
po = create_purchase_order(
|
||||
company=self.company,
|
||||
customer=self.supplier,
|
||||
currency=currency,
|
||||
item=self.item,
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
transaction_date=today(),
|
||||
do_not_submit=do_not_submit,
|
||||
)
|
||||
return po
|
||||
|
||||
def test_so_advance_paid_and_currency_with_payment(self):
|
||||
self.create_customer("_Test USD Customer", "USD")
|
||||
|
||||
so = self.create_sales_order(currency="USD", do_not_submit=True)
|
||||
so.conversion_rate = 80
|
||||
so.submit()
|
||||
|
||||
pe_exchange_rate = 85
|
||||
pe = get_payment_entry(so.doctype, so.name, bank_account=self.cash)
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = nowdate()
|
||||
pe.paid_from = self.debtors_usd
|
||||
pe.paid_from_account_currency = "USD"
|
||||
pe.source_exchange_rate = pe_exchange_rate
|
||||
pe.paid_amount = so.grand_total
|
||||
pe.received_amount = pe_exchange_rate * pe.paid_amount
|
||||
pe.references[0].outstanding_amount = 100
|
||||
pe.references[0].total_amount = 100
|
||||
pe.references[0].allocated_amount = 100
|
||||
pe.save().submit()
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 100)
|
||||
self.assertEqual(so.party_account_currency, "USD")
|
||||
|
||||
# cancel advance payment
|
||||
pe.reload()
|
||||
pe.cancel()
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 0)
|
||||
self.assertEqual(so.party_account_currency, "USD")
|
||||
|
||||
def test_so_advance_paid_and_currency_with_journal(self):
|
||||
self.create_customer("_Test USD Customer", "USD")
|
||||
|
||||
so = self.create_sales_order(currency="USD", do_not_submit=True)
|
||||
so.conversion_rate = 80
|
||||
so.submit()
|
||||
|
||||
je_exchange_rate = 85
|
||||
je = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Journal Entry",
|
||||
"company": self.company,
|
||||
"voucher_type": "Journal Entry",
|
||||
"posting_date": so.transaction_date,
|
||||
"multi_currency": True,
|
||||
"accounts": [
|
||||
{
|
||||
"account": self.debtors_usd,
|
||||
"party_type": "Customer",
|
||||
"party": so.customer,
|
||||
"credit": 8500,
|
||||
"credit_in_account_currency": 100,
|
||||
"is_advance": "Yes",
|
||||
"reference_type": so.doctype,
|
||||
"reference_name": so.name,
|
||||
"exchange_rate": je_exchange_rate,
|
||||
},
|
||||
{
|
||||
"account": self.cash,
|
||||
"debit": 8500,
|
||||
"debit_in_account_currency": 8500,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
je.save().submit()
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 100)
|
||||
self.assertEqual(so.party_account_currency, "USD")
|
||||
|
||||
# cancel advance payment
|
||||
je.reload()
|
||||
je.cancel()
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 0)
|
||||
self.assertEqual(so.party_account_currency, "USD")
|
||||
|
||||
def test_po_advance_paid_and_currency_with_payment(self):
|
||||
self.create_supplier("_Test USD Supplier", "USD")
|
||||
|
||||
po = self.create_purchase_order(currency="USD", do_not_submit=True)
|
||||
po.conversion_rate = 80
|
||||
po.submit()
|
||||
|
||||
pe_exchange_rate = 85
|
||||
pe = get_payment_entry(po.doctype, po.name, bank_account=self.cash)
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = nowdate()
|
||||
pe.paid_to = self.creditors_usd
|
||||
pe.paid_to_account_currency = "USD"
|
||||
pe.target_exchange_rate = pe_exchange_rate
|
||||
pe.received_amount = po.grand_total
|
||||
pe.paid_amount = pe_exchange_rate * pe.received_amount
|
||||
pe.references[0].outstanding_amount = 100
|
||||
pe.references[0].total_amount = 100
|
||||
pe.references[0].allocated_amount = 100
|
||||
pe.save().submit()
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(po.advance_paid, 100)
|
||||
self.assertEqual(po.party_account_currency, "USD")
|
||||
|
||||
# cancel advance payment
|
||||
pe.reload()
|
||||
pe.cancel()
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(po.advance_paid, 0)
|
||||
self.assertEqual(po.party_account_currency, "USD")
|
||||
|
||||
def test_po_advance_paid_and_currency_with_journal(self):
|
||||
self.create_supplier("_Test USD Supplier", "USD")
|
||||
|
||||
po = self.create_purchase_order(currency="USD", do_not_submit=True)
|
||||
po.conversion_rate = 80
|
||||
po.submit()
|
||||
|
||||
je_exchange_rate = 85
|
||||
je = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Journal Entry",
|
||||
"company": self.company,
|
||||
"voucher_type": "Journal Entry",
|
||||
"posting_date": po.transaction_date,
|
||||
"multi_currency": True,
|
||||
"accounts": [
|
||||
{
|
||||
"account": self.creditors_usd,
|
||||
"party_type": "Supplier",
|
||||
"party": po.supplier,
|
||||
"debit": 8500,
|
||||
"debit_in_account_currency": 100,
|
||||
"is_advance": "Yes",
|
||||
"reference_type": po.doctype,
|
||||
"reference_name": po.name,
|
||||
"exchange_rate": je_exchange_rate,
|
||||
},
|
||||
{
|
||||
"account": self.cash,
|
||||
"credit": 8500,
|
||||
"credit_in_account_currency": 8500,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
je.save().submit()
|
||||
po.reload()
|
||||
self.assertEqual(po.advance_paid, 100)
|
||||
self.assertEqual(po.party_account_currency, "USD")
|
||||
|
||||
# cancel advance payment
|
||||
je.reload()
|
||||
je.cancel()
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(po.advance_paid, 0)
|
||||
self.assertEqual(po.party_account_currency, "USD")
|
||||
@@ -224,11 +224,6 @@
|
||||
"link_doctype": "Bank Guarantee",
|
||||
"link_fieldname": "bank_account"
|
||||
},
|
||||
{
|
||||
"group": "Transactions",
|
||||
"link_doctype": "Payroll Entry",
|
||||
"link_fieldname": "bank_account"
|
||||
},
|
||||
{
|
||||
"group": "Transactions",
|
||||
"link_doctype": "Bank Transaction",
|
||||
@@ -255,7 +250,7 @@
|
||||
"link_fieldname": "default_bank_account"
|
||||
}
|
||||
],
|
||||
"modified": "2024-09-24 06:57:41.292970",
|
||||
"modified": "2024-10-30 09:41:14.113414",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Account",
|
||||
|
||||
@@ -4,10 +4,7 @@
|
||||
frappe.ui.form.on("Fiscal Year", {
|
||||
onload: function (frm) {
|
||||
if (frm.doc.__islocal) {
|
||||
frm.set_value(
|
||||
"year_start_date",
|
||||
frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)
|
||||
);
|
||||
frm.set_value("year_start_date", frappe.datetime.year_start());
|
||||
}
|
||||
},
|
||||
year_start_date: function (frm) {
|
||||
|
||||
@@ -188,6 +188,7 @@ class JournalEntry(AccountsController):
|
||||
self.validate_cheque_info()
|
||||
self.check_credit_limit()
|
||||
self.make_gl_entries()
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid()
|
||||
self.update_asset_value()
|
||||
self.update_inter_company_jv()
|
||||
@@ -218,8 +219,10 @@ class JournalEntry(AccountsController):
|
||||
"Repost Accounting Ledger Items",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
"Advance Payment Ledger Entry",
|
||||
)
|
||||
self.make_gl_entries(1)
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid()
|
||||
self.unlink_advance_entry_reference()
|
||||
self.unlink_asset_reference()
|
||||
@@ -1668,6 +1671,8 @@ def make_reverse_journal_entry(source_name, target_doc=None):
|
||||
"debit": "credit",
|
||||
"credit_in_account_currency": "debit_in_account_currency",
|
||||
"credit": "debit",
|
||||
"reference_type": "reference_type",
|
||||
"reference_name": "reference_name",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -110,6 +110,7 @@ class PaymentEntry(AccountsController):
|
||||
self.update_outstanding_amounts()
|
||||
self.update_payment_schedule()
|
||||
self.update_payment_requests()
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid() # advance_paid_status depends on the payment request amount
|
||||
self.set_status()
|
||||
|
||||
@@ -190,6 +191,7 @@ class PaymentEntry(AccountsController):
|
||||
"Repost Accounting Ledger Items",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
"Advance Payment Ledger Entry",
|
||||
)
|
||||
super().on_cancel()
|
||||
self.make_gl_entries(cancel=1)
|
||||
@@ -197,6 +199,7 @@ class PaymentEntry(AccountsController):
|
||||
self.delink_advance_entry_references()
|
||||
self.update_payment_schedule(cancel=1)
|
||||
self.update_payment_requests(cancel=True)
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid() # advance_paid_status depends on the payment request amount
|
||||
self.set_status()
|
||||
|
||||
|
||||
@@ -1995,7 +1995,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
# Check if SO is unlinked/replaced by SI in PE & if SO advance paid is 0
|
||||
self.assertEqual(pe.references[0].reference_name, si.name)
|
||||
self.assertEqual(sales_order.advance_paid, 0.0)
|
||||
self.assertEqual(sales_order.advance_paid, 300.0)
|
||||
|
||||
# check outstanding after advance allocation
|
||||
self.assertEqual(
|
||||
|
||||
@@ -362,10 +362,14 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
# Assert 'Advance Paid'
|
||||
so.reload()
|
||||
pe.reload()
|
||||
self.assertEqual(so.advance_paid, 0)
|
||||
self.assertEqual(so.advance_paid, 100)
|
||||
self.assertEqual(len(pe.references), 0)
|
||||
self.assertEqual(pe.unallocated_amount, 100)
|
||||
|
||||
pe.cancel()
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 100)
|
||||
|
||||
def test_06_unreconcile_advance_from_payment_entry(self):
|
||||
self.enable_advance_as_liability()
|
||||
so1 = self.create_sales_order()
|
||||
@@ -411,7 +415,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
so2.reload()
|
||||
pe.reload()
|
||||
self.assertEqual(so1.advance_paid, 150)
|
||||
self.assertEqual(so2.advance_paid, 0)
|
||||
self.assertEqual(so2.advance_paid, 110)
|
||||
self.assertEqual(len(pe.references), 1)
|
||||
self.assertEqual(pe.unallocated_amount, 110)
|
||||
|
||||
@@ -459,6 +463,6 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
# Assert 'Advance Paid'
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 0)
|
||||
self.assertEqual(so.advance_paid, 1000)
|
||||
|
||||
self.disable_advance_as_liability()
|
||||
|
||||
@@ -119,6 +119,7 @@ class Asset(AccountsController):
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_precision()
|
||||
self.validate_asset_values()
|
||||
self.validate_asset_and_reference()
|
||||
self.validate_item()
|
||||
@@ -306,6 +307,15 @@ class Asset(AccountsController):
|
||||
title=_("Missing Finance Book"),
|
||||
)
|
||||
|
||||
def validate_precision(self):
|
||||
float_precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||
if self.gross_purchase_amount:
|
||||
self.gross_purchase_amount = flt(self.gross_purchase_amount, float_precision)
|
||||
if self.opening_accumulated_depreciation:
|
||||
self.opening_accumulated_depreciation = flt(
|
||||
self.opening_accumulated_depreciation, float_precision
|
||||
)
|
||||
|
||||
def validate_asset_values(self):
|
||||
if not self.asset_category:
|
||||
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
|
||||
@@ -471,6 +481,9 @@ class Asset(AccountsController):
|
||||
|
||||
def validate_expected_value_after_useful_life(self):
|
||||
for row in self.get("finance_books"):
|
||||
row.expected_value_after_useful_life = flt(
|
||||
row.expected_value_after_useful_life, self.precision("gross_purchase_amount")
|
||||
)
|
||||
depr_schedule = get_depr_schedule(self.name, "Draft", row.finance_book)
|
||||
|
||||
if not depr_schedule:
|
||||
|
||||
@@ -14,18 +14,25 @@ def get_data():
|
||||
"Material Request": ["items", "material_request"],
|
||||
"Supplier Quotation": ["items", "supplier_quotation"],
|
||||
"Project": ["items", "project"],
|
||||
"Sales Order": ["items", "sales_order"],
|
||||
"BOM": ["items", "bom"],
|
||||
"Production Plan": ["items", "production_plan"],
|
||||
"Blanket Order": ["items", "blanket_order"],
|
||||
},
|
||||
"transactions": [
|
||||
{"label": _("Related"), "items": ["Purchase Receipt", "Purchase Invoice"]},
|
||||
{"label": _("Related"), "items": ["Purchase Receipt", "Purchase Invoice", "Sales Order"]},
|
||||
{"label": _("Payment"), "items": ["Payment Entry", "Journal Entry", "Payment Request"]},
|
||||
{
|
||||
"label": _("Reference"),
|
||||
"items": ["Material Request", "Supplier Quotation", "Project", "Auto Repeat"],
|
||||
"items": ["Supplier Quotation", "Project", "Auto Repeat"],
|
||||
},
|
||||
{
|
||||
"label": _("Manufacturing"),
|
||||
"items": ["Material Request", "BOM", "Production Plan", "Blanket Order"],
|
||||
},
|
||||
{
|
||||
"label": _("Sub-contracting"),
|
||||
"items": ["Subcontracting Order", "Subcontracting Receipt", "Stock Entry"],
|
||||
},
|
||||
{"label": _("Internal"), "items": ["Sales Order"]},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -345,9 +345,21 @@ class AccountsController(TransactionBase):
|
||||
repost_doc.flags.ignore_links = True
|
||||
repost_doc.save(ignore_permissions=True)
|
||||
|
||||
def _remove_advance_payment_ledger_entries(self):
|
||||
adv = qb.DocType("Advance Payment Ledger Entry")
|
||||
qb.from_(adv).delete().where(adv.voucher_type.eq(self.doctype) & adv.voucher_no.eq(self.name)).run()
|
||||
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_doctypes")
|
||||
|
||||
if self.doctype in advance_payment_doctypes:
|
||||
qb.from_(adv).delete().where(
|
||||
adv.against_voucher_type.eq(self.doctype) & adv.against_voucher_no.eq(self.name)
|
||||
).run()
|
||||
|
||||
def on_trash(self):
|
||||
from erpnext.accounts.utils import delete_exchange_gain_loss_journal
|
||||
|
||||
self._remove_advance_payment_ledger_entries()
|
||||
self._remove_references_in_repost_doctypes()
|
||||
self._remove_references_in_unreconcile()
|
||||
self.remove_serial_and_batch_bundle()
|
||||
@@ -1935,21 +1947,23 @@ class AccountsController(TransactionBase):
|
||||
|
||||
return stock_items
|
||||
|
||||
def set_total_advance_paid(self):
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
party = self.customer if self.doctype == "Sales Order" else self.supplier
|
||||
def calculate_total_advance_from_ledger(self):
|
||||
adv = frappe.qb.DocType("Advance Payment Ledger Entry")
|
||||
advance = (
|
||||
frappe.qb.from_(ple)
|
||||
.select(ple.account_currency, Abs(Sum(ple.amount_in_account_currency)).as_("amount"))
|
||||
frappe.qb.from_(adv)
|
||||
.select(adv.currency.as_("account_currency"), Abs(Sum(adv.amount)).as_("amount"))
|
||||
.where(
|
||||
(ple.against_voucher_type == self.doctype)
|
||||
& (ple.against_voucher_no == self.name)
|
||||
& (ple.party == party)
|
||||
& (ple.delinked == 0)
|
||||
& (ple.company == self.company)
|
||||
(adv.against_voucher_type == self.doctype)
|
||||
& (adv.against_voucher_no == self.name)
|
||||
& (adv.company == self.company)
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
return advance
|
||||
|
||||
def set_total_advance_paid(self):
|
||||
advance = self.calculate_total_advance_from_ledger()
|
||||
advance_paid, order_total = None, None
|
||||
|
||||
if advance:
|
||||
advance = advance[0]
|
||||
@@ -2546,6 +2560,67 @@ class AccountsController(TransactionBase):
|
||||
repost_ledger.insert()
|
||||
repost_ledger.submit()
|
||||
|
||||
def get_advance_payment_doctypes(self) -> list:
|
||||
return frappe.get_hooks("advance_payment_doctypes")
|
||||
|
||||
def make_advance_payment_ledger_for_journal(self):
|
||||
advance_payment_doctypes = self.get_advance_payment_doctypes()
|
||||
advance_doctype_references = [
|
||||
x for x in self.accounts if x.reference_type in advance_payment_doctypes
|
||||
]
|
||||
|
||||
for x in advance_doctype_references:
|
||||
# Looking for payments
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if x.account_type == "Receivable"
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
|
||||
amount = x.get(dr_or_cr)
|
||||
if amount > 0:
|
||||
doc = frappe.new_doc("Advance Payment Ledger Entry")
|
||||
doc.company = self.company
|
||||
doc.voucher_type = self.doctype
|
||||
doc.voucher_no = self.name
|
||||
doc.against_voucher_type = x.reference_type
|
||||
doc.against_voucher_no = x.reference_name
|
||||
doc.amount = amount if self.docstatus == 1 else -1 * amount
|
||||
doc.event = "Submit" if self.docstatus == 1 else "Cancel"
|
||||
doc.currency = x.account_currency
|
||||
doc.flags.ignore_permissions = 1
|
||||
doc.save()
|
||||
|
||||
def make_advance_payment_ledger_for_payment(self):
|
||||
advance_payment_doctypes = self.get_advance_payment_doctypes()
|
||||
advance_doctype_references = [
|
||||
x for x in self.references if x.reference_doctype in advance_payment_doctypes
|
||||
]
|
||||
currency = (
|
||||
self.paid_from_account_currency
|
||||
if self.payment_type == "Receive"
|
||||
else self.paid_to_account_currency
|
||||
)
|
||||
for x in advance_doctype_references:
|
||||
doc = frappe.new_doc("Advance Payment Ledger Entry")
|
||||
doc.company = self.company
|
||||
doc.voucher_type = self.doctype
|
||||
doc.voucher_no = self.name
|
||||
doc.against_voucher_type = x.reference_doctype
|
||||
doc.against_voucher_no = x.reference_name
|
||||
doc.amount = x.allocated_amount if self.docstatus == 1 else -1 * x.allocated_amount
|
||||
doc.currency = currency
|
||||
doc.event = "Submit" if self.docstatus == 1 else "Cancel"
|
||||
doc.flags.ignore_permissions = 1
|
||||
doc.save()
|
||||
|
||||
def make_advance_payment_ledger_entries(self):
|
||||
if self.docstatus != 0:
|
||||
if self.doctype == "Journal Entry":
|
||||
self.make_advance_payment_ledger_for_journal()
|
||||
elif self.doctype == "Payment Entry":
|
||||
self.make_advance_payment_ledger_for_payment()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_tax_rate(account_head):
|
||||
|
||||
@@ -351,6 +351,9 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
doc.select_print_heading = frappe.get_cached_value("Print Heading", _("Debit Note"))
|
||||
if source.tax_withholding_category:
|
||||
doc.set_onload("supplier_tds", source.tax_withholding_category)
|
||||
elif doctype == "Delivery Note":
|
||||
# manual additions to the return should hit the return warehous, too
|
||||
doc.set_warehouse = default_warehouse_for_sales_return
|
||||
|
||||
for tax in doc.get("taxes") or []:
|
||||
if tax.charge_type == "Actual":
|
||||
@@ -597,6 +600,10 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
if default_warehouse_for_sales_return:
|
||||
target_doc.warehouse = default_warehouse_for_sales_return
|
||||
|
||||
if not source_doc.use_serial_batch_fields and source_doc.serial_and_batch_bundle:
|
||||
target_doc.serial_no = None
|
||||
target_doc.batch_no = None
|
||||
|
||||
if (
|
||||
(source_doc.serial_no or source_doc.batch_no)
|
||||
and not source_doc.serial_and_batch_bundle
|
||||
@@ -899,6 +906,7 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids):
|
||||
"`tabSerial and Batch Entry`.`serial_no`",
|
||||
"`tabSerial and Batch Entry`.`batch_no`",
|
||||
"`tabSerial and Batch Entry`.`qty`",
|
||||
"`tabSerial and Batch Entry`.`incoming_rate`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_detail_no`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_type`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_no`",
|
||||
@@ -920,15 +928,23 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids):
|
||||
|
||||
if key not in available_dict:
|
||||
available_dict[key] = frappe._dict(
|
||||
{"qty": 0.0, "serial_nos": defaultdict(float), "batches": defaultdict(float)}
|
||||
{
|
||||
"qty": 0.0,
|
||||
"serial_nos": defaultdict(float),
|
||||
"batches": defaultdict(float),
|
||||
"serial_nos_valuation": defaultdict(float),
|
||||
"batches_valuation": defaultdict(float),
|
||||
}
|
||||
)
|
||||
|
||||
available_dict[key]["qty"] += row.qty
|
||||
|
||||
if row.serial_no:
|
||||
available_dict[key]["serial_nos"][row.serial_no] += row.qty
|
||||
available_dict[key]["serial_nos_valuation"][row.serial_no] = row.incoming_rate
|
||||
elif row.batch_no:
|
||||
available_dict[key]["batches"][row.batch_no] += row.qty
|
||||
available_dict[key]["batches_valuation"][row.batch_no] = row.incoming_rate
|
||||
|
||||
return available_dict
|
||||
|
||||
@@ -964,12 +980,13 @@ def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False
|
||||
)
|
||||
)
|
||||
else:
|
||||
fields = [
|
||||
"serial_and_batch_bundle",
|
||||
]
|
||||
fields = ["serial_and_batch_bundle"]
|
||||
|
||||
if is_rejected:
|
||||
fields.extend(["rejected_serial_and_batch_bundle", "return_qty_from_rejected_warehouse"])
|
||||
fields.append("rejected_serial_and_batch_bundle")
|
||||
|
||||
if doctype == "Purchase Receipt Item":
|
||||
fields.append("return_qty_from_rejected_warehouse")
|
||||
|
||||
del filters["rejected_serial_and_batch_bundle"]
|
||||
data = frappe.get_all(
|
||||
@@ -1003,7 +1020,14 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field
|
||||
warehouse = row.get(warehouse_field)
|
||||
qty = abs(row.get(qty_field))
|
||||
|
||||
filterd_serial_batch = frappe._dict({"serial_nos": [], "batches": defaultdict(float)})
|
||||
filterd_serial_batch = frappe._dict(
|
||||
{
|
||||
"serial_nos": [],
|
||||
"batches": defaultdict(float),
|
||||
"serial_nos_valuation": data.get("serial_nos_valuation"),
|
||||
"batches_valuation": data.get("batches_valuation"),
|
||||
}
|
||||
)
|
||||
|
||||
if data.serial_nos:
|
||||
available_serial_nos = []
|
||||
@@ -1013,7 +1037,7 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field
|
||||
|
||||
if available_serial_nos:
|
||||
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
|
||||
available_serial_nos = get_available_serial_nos(available_serial_nos)
|
||||
available_serial_nos = get_available_serial_nos(available_serial_nos, warehouse)
|
||||
|
||||
if len(available_serial_nos) > qty:
|
||||
filterd_serial_batch["serial_nos"] = sorted(available_serial_nos[0 : cint(qty)])
|
||||
@@ -1098,6 +1122,8 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f
|
||||
"warehouse": warehouse,
|
||||
"serial_nos": data.get("serial_nos"),
|
||||
"batches": data.get("batches"),
|
||||
"serial_nos_valuation": data.get("serial_nos_valuation"),
|
||||
"batches_valuation": data.get("batches_valuation"),
|
||||
"posting_date": parent_doc.posting_date,
|
||||
"posting_time": parent_doc.posting_time,
|
||||
"voucher_type": parent_doc.doctype,
|
||||
|
||||
@@ -334,6 +334,11 @@ class StockController(AccountsController):
|
||||
}
|
||||
)
|
||||
|
||||
if self.doctype in ["Sales Invoice", "Delivery Note"]:
|
||||
row.db_set(
|
||||
"incoming_rate", frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate")
|
||||
)
|
||||
|
||||
def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]:
|
||||
field = {
|
||||
"Sales Invoice": "sales_invoice_item",
|
||||
|
||||
@@ -968,6 +968,13 @@ class BOM(WebsiteGenerator):
|
||||
if not d.batch_size or d.batch_size <= 0:
|
||||
d.batch_size = 1
|
||||
|
||||
if not d.workstation and not d.workstation_type:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Workstation or Workstation Type is mandatory for an operation {1}"
|
||||
).format(d.idx, d.operation)
|
||||
)
|
||||
|
||||
def get_tree_representation(self) -> BOMTree:
|
||||
"""Get a complete tree representation preserving order of child items."""
|
||||
return BOMTree(self.name)
|
||||
|
||||
@@ -173,10 +173,16 @@ class WorkOrder(Document):
|
||||
self.get_items_and_operations_from_bom()
|
||||
|
||||
def validate_workstation_type(self):
|
||||
if not self.docstatus.is_submitted():
|
||||
return
|
||||
|
||||
for row in self.operations:
|
||||
if not row.workstation and not row.workstation_type:
|
||||
msg = f"Row {row.idx}: Workstation or Workstation Type is mandatory for an operation {row.operation}"
|
||||
frappe.throw(_(msg))
|
||||
frappe.throw(
|
||||
_("Row {0}: Workstation or Workstation Type is mandatory for an operation {1}").format(
|
||||
row.idx, row.operation
|
||||
)
|
||||
)
|
||||
|
||||
def validate_sales_order(self):
|
||||
if self.sales_order:
|
||||
|
||||
@@ -359,6 +359,7 @@ erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes
|
||||
erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22
|
||||
erpnext.patches.v15_0.create_accounting_dimensions_in_payment_request
|
||||
erpnext.patches.v14_0.update_pos_return_ledger_entries #2024-08-16
|
||||
erpnext.patches.v15_0.create_advance_payment_ledger_records
|
||||
# below migration patch should always run last
|
||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
|
||||
|
||||
def get_advance_doctypes() -> list:
|
||||
return frappe.get_hooks("advance_payment_doctypes")
|
||||
|
||||
|
||||
def get_payments_with_so_po_reference() -> list:
|
||||
advance_payment_entries = []
|
||||
advance_doctypes = get_advance_doctypes()
|
||||
per = qb.DocType("Payment Entry Reference")
|
||||
payments_with_reference = (
|
||||
qb.from_(per)
|
||||
.select(per.parent)
|
||||
.distinct()
|
||||
.where(per.reference_doctype.isin(advance_doctypes) & per.docstatus.eq(1))
|
||||
.run()
|
||||
)
|
||||
if payments_with_reference:
|
||||
pe = qb.DocType("Payment Entry")
|
||||
advance_payment_entries = (
|
||||
qb.from_(pe)
|
||||
.select(ConstantColumn("Payment Entry").as_("doctype"))
|
||||
.select(pe.name)
|
||||
.where(pe.name.isin(payments_with_reference) & pe.docstatus.eq(1))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
return advance_payment_entries
|
||||
|
||||
|
||||
def get_journals_with_so_po_reference() -> list:
|
||||
advance_journal_entries = []
|
||||
advance_doctypes = get_advance_doctypes()
|
||||
jea = qb.DocType("Journal Entry Account")
|
||||
journals_with_reference = (
|
||||
qb.from_(jea)
|
||||
.select(jea.parent)
|
||||
.distinct()
|
||||
.where(jea.reference_type.isin(advance_doctypes) & jea.docstatus.eq(1))
|
||||
.run()
|
||||
)
|
||||
if journals_with_reference:
|
||||
je = qb.DocType("Journal Entry")
|
||||
advance_journal_entries = (
|
||||
qb.from_(je)
|
||||
.select(ConstantColumn("Journal Entry").as_("doctype"))
|
||||
.select(je.name)
|
||||
.where(je.name.isin(journals_with_reference) & je.docstatus.eq(1))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
return advance_journal_entries
|
||||
|
||||
|
||||
def make_advance_ledger_entries(vouchers: list):
|
||||
for x in vouchers:
|
||||
frappe.get_doc(x.doctype, x.name).make_advance_payment_ledger_entries()
|
||||
|
||||
|
||||
def execute():
|
||||
"""
|
||||
Description:
|
||||
Create Advance Payment Ledger Entry for all Payments made against Sales / Purchase Orders
|
||||
"""
|
||||
frappe.db.truncate("Advance Payment Ledger Entry")
|
||||
payment_entries = get_payments_with_so_po_reference()
|
||||
make_advance_ledger_entries(payment_entries)
|
||||
|
||||
journals = get_journals_with_so_po_reference()
|
||||
make_advance_ledger_entries(journals)
|
||||
@@ -15,6 +15,8 @@ def get_data():
|
||||
},
|
||||
"internal_links": {
|
||||
"Quotation": ["items", "prevdoc_docname"],
|
||||
"BOM": ["items", "bom_no"],
|
||||
"Blanket Order": ["items", "blanket_order"],
|
||||
},
|
||||
"transactions": [
|
||||
{
|
||||
@@ -23,7 +25,7 @@ def get_data():
|
||||
},
|
||||
{"label": _("Purchasing"), "items": ["Material Request", "Purchase Order"]},
|
||||
{"label": _("Projects"), "items": ["Project"]},
|
||||
{"label": _("Manufacturing"), "items": ["Work Order"]},
|
||||
{"label": _("Manufacturing"), "items": ["Work Order", "BOM", "Blanket Order"]},
|
||||
{"label": _("Reference"), "items": ["Quotation", "Auto Repeat", "Stock Reservation Entry"]},
|
||||
{"label": _("Payment"), "items": ["Payment Entry", "Payment Request", "Journal Entry"]},
|
||||
],
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
import json
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
@@ -2080,6 +2081,264 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
|
||||
self.assertEqual(stock_value_difference, 100.0 * 5)
|
||||
|
||||
def test_delivery_note_return_valuation_without_use_serial_batch_field(self):
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
|
||||
|
||||
batch_item = make_item(
|
||||
"_Test Delivery Note Return Valuation Batch Item",
|
||||
properties={
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"is_stock_item": 1,
|
||||
"batch_number_series": "BRTN-DNN-BI-.#####",
|
||||
},
|
||||
).name
|
||||
|
||||
serial_item = make_item(
|
||||
"_Test Delivery Note Return Valuation Serial Item",
|
||||
properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "SRTN-DNN-TP-.#####"},
|
||||
).name
|
||||
|
||||
batches = {}
|
||||
serial_nos = []
|
||||
for qty, rate in {3: 300, 2: 100}.items():
|
||||
se = make_stock_entry(
|
||||
item_code=batch_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate
|
||||
)
|
||||
batches[get_batch_from_bundle(se.items[0].serial_and_batch_bundle)] = qty
|
||||
|
||||
for qty, rate in {2: 100, 1: 50}.items():
|
||||
make_stock_entry(item_code=serial_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate)
|
||||
serial_nos.extend(get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle))
|
||||
|
||||
dn = create_delivery_note(
|
||||
item_code=batch_item,
|
||||
qty=5,
|
||||
rate=1000,
|
||||
use_serial_batch_fields=0,
|
||||
batches=batches,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
bundle_id = make_serial_batch_bundle(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": serial_item,
|
||||
"warehouse": dn.items[0].warehouse,
|
||||
"qty": 3,
|
||||
"voucher_type": "Delivery Note",
|
||||
"serial_nos": serial_nos,
|
||||
"posting_date": dn.posting_date,
|
||||
"posting_time": dn.posting_time,
|
||||
"type_of_transaction": "Outward",
|
||||
"do_not_submit": True,
|
||||
}
|
||||
)
|
||||
).name
|
||||
|
||||
dn.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": serial_item,
|
||||
"qty": 3,
|
||||
"rate": 700,
|
||||
"base_rate": 700,
|
||||
"item_name": serial_item,
|
||||
"uom": "Nos",
|
||||
"stock_uom": "Nos",
|
||||
"conversion_factor": 1,
|
||||
"warehouse": dn.items[0].warehouse,
|
||||
"use_serial_batch_fields": 0,
|
||||
"serial_and_batch_bundle": bundle_id,
|
||||
},
|
||||
)
|
||||
|
||||
dn.save()
|
||||
dn.submit()
|
||||
dn.reload()
|
||||
|
||||
batch_no_valuation = defaultdict(float)
|
||||
serial_no_valuation = defaultdict(float)
|
||||
|
||||
for row in dn.items:
|
||||
if row.serial_and_batch_bundle:
|
||||
bundle_data = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": row.serial_and_batch_bundle},
|
||||
fields=["incoming_rate", "serial_no", "batch_no"],
|
||||
)
|
||||
|
||||
for d in bundle_data:
|
||||
if d.batch_no:
|
||||
batch_no_valuation[d.batch_no] = d.incoming_rate
|
||||
elif d.serial_no:
|
||||
serial_no_valuation[d.serial_no] = d.incoming_rate
|
||||
|
||||
return_entry = make_sales_return(dn.name)
|
||||
|
||||
return_entry.save()
|
||||
return_entry.submit()
|
||||
return_entry.reload()
|
||||
|
||||
for row in return_entry.items:
|
||||
if row.item_code == batch_item:
|
||||
bundle_data = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": row.serial_and_batch_bundle},
|
||||
fields=["incoming_rate", "batch_no"],
|
||||
)
|
||||
|
||||
for d in bundle_data:
|
||||
self.assertEqual(d.incoming_rate, batch_no_valuation[d.batch_no])
|
||||
else:
|
||||
bundle_data = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": row.serial_and_batch_bundle},
|
||||
fields=["incoming_rate", "serial_no"],
|
||||
)
|
||||
|
||||
for d in bundle_data:
|
||||
self.assertEqual(d.incoming_rate, serial_no_valuation[d.serial_no])
|
||||
|
||||
def test_delivery_note_return_valuation_with_use_serial_batch_field(self):
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
|
||||
|
||||
batch_item = make_item(
|
||||
"_Test Delivery Note Return Valuation WITH Batch Item",
|
||||
properties={
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"is_stock_item": 1,
|
||||
"batch_number_series": "BRTN-DNN-BIW-.#####",
|
||||
},
|
||||
).name
|
||||
|
||||
serial_item = make_item(
|
||||
"_Test Delivery Note Return Valuation WITH Serial Item",
|
||||
properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "SRTN-DNN-TPW-.#####"},
|
||||
).name
|
||||
|
||||
batches = []
|
||||
serial_nos = []
|
||||
for qty, rate in {3: 300, 2: 100}.items():
|
||||
se = make_stock_entry(
|
||||
item_code=batch_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate
|
||||
)
|
||||
batches.append(get_batch_from_bundle(se.items[0].serial_and_batch_bundle))
|
||||
|
||||
for qty, rate in {2: 100, 1: 50}.items():
|
||||
se = make_stock_entry(
|
||||
item_code=serial_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate
|
||||
)
|
||||
serial_nos.extend(get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle))
|
||||
|
||||
dn = create_delivery_note(
|
||||
item_code=batch_item,
|
||||
qty=3,
|
||||
rate=1000,
|
||||
use_serial_batch_fields=1,
|
||||
batch_no=batches[0],
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
dn.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": batch_item,
|
||||
"qty": 2,
|
||||
"rate": 1000,
|
||||
"base_rate": 1000,
|
||||
"item_name": batch_item,
|
||||
"uom": dn.items[0].uom,
|
||||
"stock_uom": dn.items[0].uom,
|
||||
"conversion_factor": 1,
|
||||
"warehouse": dn.items[0].warehouse,
|
||||
"use_serial_batch_fields": 1,
|
||||
"batch_no": batches[1],
|
||||
},
|
||||
)
|
||||
|
||||
dn.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": serial_item,
|
||||
"qty": 2,
|
||||
"rate": 700,
|
||||
"base_rate": 700,
|
||||
"item_name": serial_item,
|
||||
"uom": "Nos",
|
||||
"stock_uom": "Nos",
|
||||
"conversion_factor": 1,
|
||||
"warehouse": dn.items[0].warehouse,
|
||||
"use_serial_batch_fields": 1,
|
||||
"serial_no": "\n".join(serial_nos[0:2]),
|
||||
},
|
||||
)
|
||||
|
||||
dn.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": serial_item,
|
||||
"qty": 1,
|
||||
"rate": 700,
|
||||
"base_rate": 700,
|
||||
"item_name": serial_item,
|
||||
"uom": "Nos",
|
||||
"stock_uom": "Nos",
|
||||
"conversion_factor": 1,
|
||||
"warehouse": dn.items[0].warehouse,
|
||||
"use_serial_batch_fields": 1,
|
||||
"serial_no": serial_nos[-1],
|
||||
},
|
||||
)
|
||||
|
||||
dn.save()
|
||||
dn.submit()
|
||||
dn.reload()
|
||||
|
||||
batch_no_valuation = defaultdict(float)
|
||||
serial_no_valuation = defaultdict(float)
|
||||
|
||||
for row in dn.items:
|
||||
if row.serial_and_batch_bundle:
|
||||
bundle_data = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": row.serial_and_batch_bundle},
|
||||
fields=["incoming_rate", "serial_no", "batch_no"],
|
||||
)
|
||||
|
||||
for d in bundle_data:
|
||||
if d.batch_no:
|
||||
batch_no_valuation[d.batch_no] = d.incoming_rate
|
||||
elif d.serial_no:
|
||||
serial_no_valuation[d.serial_no] = d.incoming_rate
|
||||
|
||||
return_entry = make_sales_return(dn.name)
|
||||
|
||||
return_entry.save()
|
||||
return_entry.submit()
|
||||
return_entry.reload()
|
||||
|
||||
for row in return_entry.items:
|
||||
if row.item_code == batch_item:
|
||||
bundle_data = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": row.serial_and_batch_bundle},
|
||||
fields=["incoming_rate", "batch_no"],
|
||||
)
|
||||
|
||||
for d in bundle_data:
|
||||
self.assertEqual(d.incoming_rate, batch_no_valuation[d.batch_no])
|
||||
else:
|
||||
bundle_data = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": row.serial_and_batch_bundle},
|
||||
fields=["incoming_rate", "serial_no"],
|
||||
)
|
||||
|
||||
for d in bundle_data:
|
||||
self.assertEqual(d.incoming_rate, serial_no_valuation[d.serial_no])
|
||||
|
||||
|
||||
def create_delivery_note(**args):
|
||||
dn = frappe.new_doc("Delivery Note")
|
||||
@@ -2107,6 +2366,9 @@ def create_delivery_note(**args):
|
||||
if args.get("batch_no"):
|
||||
batches = frappe._dict({args.batch_no: qty})
|
||||
|
||||
if args.get("batches"):
|
||||
batches = frappe._dict(args.batches)
|
||||
|
||||
bundle_id = make_serial_batch_bundle(
|
||||
frappe._dict(
|
||||
{
|
||||
|
||||
@@ -3672,6 +3672,234 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
|
||||
self.assertEqual(pr.items[0].conversion_factor, 1.0)
|
||||
|
||||
def test_purchase_receipt_return_valuation_without_use_serial_batch_field(self):
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return
|
||||
|
||||
batch_item = make_item(
|
||||
"_Test Purchase Receipt Return Valuation Batch Item",
|
||||
properties={
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"is_stock_item": 1,
|
||||
"batch_number_series": "BRTN-TPRBI-.#####",
|
||||
},
|
||||
).name
|
||||
|
||||
serial_item = make_item(
|
||||
"_Test Purchase Receipt Return Valuation Serial Item",
|
||||
properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "SRTN-TPRSI-.#####"},
|
||||
).name
|
||||
|
||||
rej_warehouse = create_warehouse("_Test Purchase Warehouse For Rejected Qty")
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
item_code=batch_item,
|
||||
received_qty=10,
|
||||
qty=8,
|
||||
rejected_qty=2,
|
||||
rejected_warehouse=rej_warehouse,
|
||||
rate=300,
|
||||
do_not_submit=1,
|
||||
use_serial_batch_fields=0,
|
||||
)
|
||||
|
||||
pr.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": serial_item,
|
||||
"qty": 2,
|
||||
"rate": 100,
|
||||
"base_rate": 100,
|
||||
"item_name": serial_item,
|
||||
"uom": "Nos",
|
||||
"stock_uom": "Nos",
|
||||
"conversion_factor": 1,
|
||||
"rejected_qty": 1,
|
||||
"warehouse": pr.items[0].warehouse,
|
||||
"use_serial_batch_fields": 0,
|
||||
"rejected_warehouse": rej_warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
pr.save()
|
||||
pr.submit()
|
||||
pr.reload()
|
||||
|
||||
batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle)
|
||||
rejected_batch_no = get_batch_from_bundle(pr.items[0].rejected_serial_and_batch_bundle)
|
||||
|
||||
self.assertEqual(batch_no, rejected_batch_no)
|
||||
|
||||
return_entry = make_purchase_return(pr.name)
|
||||
|
||||
return_entry.save()
|
||||
return_entry.submit()
|
||||
return_entry.reload()
|
||||
|
||||
for row in return_entry.items:
|
||||
if row.item_code == batch_item:
|
||||
bundle_data = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": row.serial_and_batch_bundle},
|
||||
pluck="incoming_rate",
|
||||
)
|
||||
|
||||
for incoming_rate in bundle_data:
|
||||
self.assertEqual(incoming_rate, 300.00)
|
||||
else:
|
||||
bundle_data = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": row.serial_and_batch_bundle},
|
||||
pluck="incoming_rate",
|
||||
)
|
||||
|
||||
for incoming_rate in bundle_data:
|
||||
self.assertEqual(incoming_rate, 100.00)
|
||||
|
||||
for row in return_entry.items:
|
||||
if row.item_code == batch_item:
|
||||
bundle_data = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": row.rejected_serial_and_batch_bundle},
|
||||
pluck="incoming_rate",
|
||||
)
|
||||
|
||||
for incoming_rate in bundle_data:
|
||||
self.assertEqual(incoming_rate, 0)
|
||||
else:
|
||||
bundle_data = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": row.rejected_serial_and_batch_bundle},
|
||||
pluck="incoming_rate",
|
||||
)
|
||||
|
||||
for incoming_rate in bundle_data:
|
||||
self.assertEqual(incoming_rate, 0)
|
||||
|
||||
def test_purchase_receipt_return_valuation_with_use_serial_batch_field(self):
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return
|
||||
|
||||
batch_item = make_item(
|
||||
"_Test Purchase Receipt Return Valuation With Batch Item",
|
||||
properties={"has_batch_no": 1, "create_new_batch": 1, "is_stock_item": 1},
|
||||
).name
|
||||
|
||||
serial_item = make_item(
|
||||
"_Test Purchase Receipt Return Valuation With Serial Item",
|
||||
properties={"has_serial_no": 1, "is_stock_item": 1},
|
||||
).name
|
||||
|
||||
rej_warehouse = create_warehouse("_Test Purchase Warehouse For Rejected Qty")
|
||||
|
||||
batch_no = "BATCH-RTN-BNU-TPRBI-0001"
|
||||
serial_nos = ["SNU-RTN-TPRSI-0001", "SNU-RTN-TPRSI-0002", "SNU-RTN-TPRSI-0003"]
|
||||
|
||||
if not frappe.db.exists("Batch", batch_no):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"batch_id": batch_no,
|
||||
"item": batch_item,
|
||||
}
|
||||
).insert()
|
||||
|
||||
for serial_no in serial_nos:
|
||||
if not frappe.db.exists("Serial No", serial_no):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Serial No",
|
||||
"item_code": serial_item,
|
||||
"serial_no": serial_no,
|
||||
}
|
||||
).insert()
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
item_code=batch_item,
|
||||
received_qty=10,
|
||||
qty=8,
|
||||
rejected_qty=2,
|
||||
rejected_warehouse=rej_warehouse,
|
||||
batch_no=batch_no,
|
||||
use_serial_batch_fields=1,
|
||||
rate=300,
|
||||
do_not_submit=1,
|
||||
)
|
||||
|
||||
pr.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": serial_item,
|
||||
"qty": 2,
|
||||
"rate": 100,
|
||||
"base_rate": 100,
|
||||
"item_name": serial_item,
|
||||
"uom": "Nos",
|
||||
"stock_uom": "Nos",
|
||||
"conversion_factor": 1,
|
||||
"rejected_qty": 1,
|
||||
"warehouse": pr.items[0].warehouse,
|
||||
"use_serial_batch_fields": 1,
|
||||
"rejected_warehouse": rej_warehouse,
|
||||
"serial_no": "\n".join(serial_nos[:2]),
|
||||
"rejected_serial_no": serial_nos[2],
|
||||
},
|
||||
)
|
||||
|
||||
pr.save()
|
||||
pr.submit()
|
||||
pr.reload()
|
||||
|
||||
batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle)
|
||||
rejected_batch_no = get_batch_from_bundle(pr.items[0].rejected_serial_and_batch_bundle)
|
||||
|
||||
self.assertEqual(batch_no, rejected_batch_no)
|
||||
|
||||
return_entry = make_purchase_return(pr.name)
|
||||
|
||||
return_entry.save()
|
||||
return_entry.submit()
|
||||
return_entry.reload()
|
||||
|
||||
for row in return_entry.items:
|
||||
if row.item_code == batch_item:
|
||||
bundle_data = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": row.serial_and_batch_bundle},
|
||||
pluck="incoming_rate",
|
||||
)
|
||||
|
||||
for incoming_rate in bundle_data:
|
||||
self.assertEqual(incoming_rate, 300.00)
|
||||
else:
|
||||
bundle_data = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": row.serial_and_batch_bundle},
|
||||
pluck="incoming_rate",
|
||||
)
|
||||
|
||||
for incoming_rate in bundle_data:
|
||||
self.assertEqual(incoming_rate, 100.00)
|
||||
|
||||
for row in return_entry.items:
|
||||
if row.item_code == batch_item:
|
||||
bundle_data = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": row.rejected_serial_and_batch_bundle},
|
||||
pluck="incoming_rate",
|
||||
)
|
||||
|
||||
for incoming_rate in bundle_data:
|
||||
self.assertEqual(incoming_rate, 0)
|
||||
else:
|
||||
bundle_data = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": row.rejected_serial_and_batch_bundle},
|
||||
pluck="incoming_rate",
|
||||
)
|
||||
|
||||
for incoming_rate in bundle_data:
|
||||
self.assertEqual(incoming_rate, 0)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -197,7 +197,7 @@ class SerialandBatchBundle(Document):
|
||||
def throw_error_message(self, message, exception=frappe.ValidationError):
|
||||
frappe.throw(_(message), exception, title=_("Error"))
|
||||
|
||||
def set_incoming_rate(self, row=None, save=False, allow_negative_stock=False):
|
||||
def set_incoming_rate(self, parent=None, row=None, save=False, allow_negative_stock=False):
|
||||
if self.type_of_transaction not in ["Inward", "Outward"] or self.voucher_type in [
|
||||
"Installation Note",
|
||||
"Job Card",
|
||||
@@ -206,13 +206,70 @@ class SerialandBatchBundle(Document):
|
||||
]:
|
||||
return
|
||||
|
||||
if self.type_of_transaction == "Outward":
|
||||
if return_aginst := self.get_return_aginst(parent=parent):
|
||||
self.set_valuation_rate_for_return_entry(return_aginst, save)
|
||||
elif self.type_of_transaction == "Outward":
|
||||
self.set_incoming_rate_for_outward_transaction(
|
||||
row, save, allow_negative_stock=allow_negative_stock
|
||||
)
|
||||
else:
|
||||
self.set_incoming_rate_for_inward_transaction(row, save)
|
||||
|
||||
def set_valuation_rate_for_return_entry(self, return_aginst, save=False):
|
||||
if valuation_details := self.get_valuation_rate_for_return_entry(return_aginst):
|
||||
for row in self.entries:
|
||||
if row.serial_no:
|
||||
valuation_rate = valuation_details["serial_nos"].get(row.serial_no)
|
||||
else:
|
||||
valuation_rate = valuation_details["batches"].get(row.batch_no)
|
||||
|
||||
row.incoming_rate = valuation_rate
|
||||
row.stock_value_difference = flt(row.qty) * flt(row.incoming_rate)
|
||||
|
||||
if save:
|
||||
row.db_set(
|
||||
{
|
||||
"incoming_rate": row.incoming_rate,
|
||||
"stock_value_difference": row.stock_value_difference,
|
||||
}
|
||||
)
|
||||
|
||||
def get_valuation_rate_for_return_entry(self, return_aginst):
|
||||
valuation_details = frappe._dict(
|
||||
{
|
||||
"serial_nos": defaultdict(float),
|
||||
"batches": defaultdict(float),
|
||||
}
|
||||
)
|
||||
|
||||
bundle_data = frappe.get_all(
|
||||
"Serial and Batch Bundle",
|
||||
fields=[
|
||||
"`tabSerial and Batch Entry`.`serial_no`",
|
||||
"`tabSerial and Batch Entry`.`batch_no`",
|
||||
"`tabSerial and Batch Entry`.`incoming_rate`",
|
||||
],
|
||||
filters=[
|
||||
["Serial and Batch Bundle", "voucher_no", "=", return_aginst],
|
||||
["Serial and Batch Entry", "docstatus", "=", 1],
|
||||
["Serial and Batch Bundle", "is_cancelled", "=", 0],
|
||||
["Serial and Batch Bundle", "item_code", "=", self.item_code],
|
||||
["Serial and Batch Bundle", "warehouse", "=", self.warehouse],
|
||||
],
|
||||
order_by="`tabSerial and Batch Bundle`.`creation`, `tabSerial and Batch Entry`.`idx`",
|
||||
)
|
||||
|
||||
if not bundle_data:
|
||||
return {}
|
||||
|
||||
for row in bundle_data:
|
||||
if row.serial_no:
|
||||
valuation_details["serial_nos"][row.serial_no] = row.incoming_rate
|
||||
else:
|
||||
valuation_details["batches"][row.batch_no] = row.incoming_rate
|
||||
|
||||
return valuation_details
|
||||
|
||||
def calculate_total_qty(self, save=True):
|
||||
self.total_qty = 0.0
|
||||
for d in self.entries:
|
||||
@@ -327,6 +384,33 @@ class SerialandBatchBundle(Document):
|
||||
|
||||
return sle
|
||||
|
||||
def get_return_aginst(self, parent=None):
|
||||
return_aginst = None
|
||||
|
||||
if parent and parent.get("is_return") and parent.get("return_against"):
|
||||
return parent.get("return_against")
|
||||
|
||||
if (
|
||||
self.voucher_type
|
||||
in [
|
||||
"Delivery Note",
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Purchase Receipt",
|
||||
"POS Invoice",
|
||||
"Subcontracting Receipt",
|
||||
]
|
||||
and self.voucher_type
|
||||
and self.voucher_no
|
||||
):
|
||||
voucher_details = frappe.db.get_value(
|
||||
self.voucher_type, self.voucher_no, ["is_return", "return_against"], as_dict=True
|
||||
)
|
||||
if voucher_details and voucher_details.get("is_return") and voucher_details.get("return_against"):
|
||||
return voucher_details.get("return_against")
|
||||
|
||||
return return_aginst
|
||||
|
||||
def set_incoming_rate_for_inward_transaction(self, row=None, save=False):
|
||||
valuation_field = "valuation_rate"
|
||||
if self.voucher_type in ["Sales Invoice", "Delivery Note", "Quotation"]:
|
||||
@@ -354,7 +438,9 @@ class SerialandBatchBundle(Document):
|
||||
rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field)
|
||||
|
||||
for d in self.entries:
|
||||
if (d.incoming_rate == rate) and d.qty and d.stock_value_difference:
|
||||
if self.is_rejected:
|
||||
rate = 0.0
|
||||
elif (d.incoming_rate == rate) and d.qty and d.stock_value_difference:
|
||||
continue
|
||||
|
||||
d.incoming_rate = flt(rate, precision)
|
||||
@@ -403,7 +489,7 @@ class SerialandBatchBundle(Document):
|
||||
|
||||
# If user has changed the rate in the child table
|
||||
if self.docstatus == 0:
|
||||
self.set_incoming_rate(save=True, row=row)
|
||||
self.set_incoming_rate(parent=parent, row=row, save=True)
|
||||
|
||||
if self.docstatus == 0 and parent.get("is_return") and parent.is_new():
|
||||
self.reset_qty(row, qty_field=qty_field)
|
||||
|
||||
@@ -1275,6 +1275,60 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
qty = get_batch_qty(batch_id, warehouse, batch_item_code)
|
||||
self.assertEqual(qty, 110)
|
||||
|
||||
def test_skip_reposting_for_entries_after_stock_reco(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
item_code = create_item("Test Item For Skip Reposting After Stock Reco", is_stock_item=1).name
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
make_stock_entry(
|
||||
posting_date="2024-11-01",
|
||||
posting_time="11:00",
|
||||
item_code=item_code,
|
||||
target=warehouse,
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
create_stock_reconciliation(
|
||||
posting_date="2024-11-02",
|
||||
posting_time="11:00",
|
||||
item_code=item_code,
|
||||
warehouse=warehouse,
|
||||
qty=20,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
se = make_stock_entry(
|
||||
posting_date="2024-11-03",
|
||||
posting_time="11:00",
|
||||
item_code=item_code,
|
||||
source=warehouse,
|
||||
qty=15,
|
||||
)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry", {"voucher_no": se.name, "is_cancelled": 0}, "stock_value_difference"
|
||||
)
|
||||
|
||||
self.assertEqual(stock_value_difference, 1500.00 * -1)
|
||||
|
||||
make_stock_entry(
|
||||
posting_date="2024-10-29",
|
||||
posting_time="11:00",
|
||||
item_code=item_code,
|
||||
target=warehouse,
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry", {"voucher_no": se.name, "is_cancelled": 0}, "stock_value_difference"
|
||||
)
|
||||
|
||||
self.assertEqual(stock_value_difference, 1500.00 * -1)
|
||||
|
||||
|
||||
def create_batch_item_with_batch(item_name, batch_id):
|
||||
batch_item_doc = create_item(item_name, is_stock_item=1)
|
||||
|
||||
@@ -984,7 +984,7 @@ class SerialBatchCreation:
|
||||
required_qty = flt(abs(self.actual_qty), precision)
|
||||
|
||||
if required_qty - total_qty > 0:
|
||||
msg = f"For the item {bold(doc.item_code)}, the Avaliable qty {bold(total_qty)} is less than the Required Qty {bold(required_qty)} in the warehouse {bold(doc.warehouse)}. Please add sufficient qty in the warehouse."
|
||||
msg = f"For the item {bold(doc.item_code)}, the Available qty {bold(total_qty)} is less than the Required Qty {bold(required_qty)} in the warehouse {bold(doc.warehouse)}. Please add sufficient qty in the warehouse."
|
||||
frappe.throw(msg, title=_("Insufficient Stock"))
|
||||
|
||||
def set_auto_serial_batch_entries_for_outward(self):
|
||||
@@ -1088,6 +1088,8 @@ class SerialBatchCreation:
|
||||
frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
|
||||
|
||||
def set_serial_batch_entries(self, doc):
|
||||
incoming_rate = self.get("incoming_rate")
|
||||
|
||||
if self.get("serial_nos"):
|
||||
serial_no_wise_batch = frappe._dict({})
|
||||
if self.has_batch_no:
|
||||
@@ -1095,30 +1097,54 @@ class SerialBatchCreation:
|
||||
|
||||
qty = -1 if self.type_of_transaction == "Outward" else 1
|
||||
for serial_no in self.serial_nos:
|
||||
if self.get("serial_nos_valuation"):
|
||||
incoming_rate = self.get("serial_nos_valuation").get(serial_no)
|
||||
|
||||
doc.append(
|
||||
"entries",
|
||||
{
|
||||
"serial_no": serial_no,
|
||||
"qty": qty,
|
||||
"batch_no": serial_no_wise_batch.get(serial_no) or self.get("batch_no"),
|
||||
"incoming_rate": self.get("incoming_rate"),
|
||||
"incoming_rate": incoming_rate,
|
||||
},
|
||||
)
|
||||
|
||||
elif self.get("batches"):
|
||||
for batch_no, batch_qty in self.batches.items():
|
||||
if self.get("batches_valuation"):
|
||||
incoming_rate = self.get("batches_valuation").get(batch_no)
|
||||
|
||||
doc.append(
|
||||
"entries",
|
||||
{
|
||||
"batch_no": batch_no,
|
||||
"qty": batch_qty * (-1 if self.type_of_transaction == "Outward" else 1),
|
||||
"incoming_rate": self.get("incoming_rate"),
|
||||
"incoming_rate": incoming_rate,
|
||||
},
|
||||
)
|
||||
|
||||
def create_batch(self):
|
||||
from erpnext.stock.doctype.batch.batch import make_batch
|
||||
|
||||
if self.is_rejected:
|
||||
bundle = frappe.db.get_value(
|
||||
"Serial and Batch Bundle",
|
||||
{
|
||||
"voucher_no": self.voucher_no,
|
||||
"voucher_type": self.voucher_type,
|
||||
"voucher_detail_no": self.voucher_detail_no,
|
||||
"is_rejected": 0,
|
||||
"docstatus": 1,
|
||||
"is_cancelled": 0,
|
||||
},
|
||||
"name",
|
||||
)
|
||||
|
||||
if bundle:
|
||||
if batch_no := frappe.db.get_value("Serial and Batch Entry", {"parent": bundle}, "batch_no"):
|
||||
return batch_no
|
||||
|
||||
return make_batch(
|
||||
frappe._dict(
|
||||
{
|
||||
|
||||
@@ -623,9 +623,21 @@ class update_entries_after:
|
||||
if sle.dependant_sle_voucher_detail_no:
|
||||
entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
|
||||
|
||||
if self.has_stock_reco_with_serial_batch(sle):
|
||||
break
|
||||
|
||||
if self.exceptions:
|
||||
self.raise_exceptions()
|
||||
|
||||
def has_stock_reco_with_serial_batch(self, sle):
|
||||
if (
|
||||
sle.vocher_type == "Stock Reconciliation"
|
||||
and frappe.db.get_value(sle.voucher_type, sle.voucher_no, "set_posting_time") == 1
|
||||
):
|
||||
return not (sle.batch_no or sle.serial_no or sle.serial_and_batch_bundle)
|
||||
|
||||
return False
|
||||
|
||||
def process_sle_against_current_timestamp(self):
|
||||
sl_entries = self.get_sle_against_current_voucher()
|
||||
for sle in sl_entries:
|
||||
@@ -1035,7 +1047,7 @@ class update_entries_after:
|
||||
rate = 0
|
||||
# Material Transfer, Repack, Manufacturing
|
||||
if sle.voucher_type == "Stock Entry":
|
||||
self.recalculate_amounts_in_stock_entry(sle.voucher_no)
|
||||
self.recalculate_amounts_in_stock_entry(sle.voucher_no, sle.voucher_detail_no)
|
||||
rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
|
||||
# Sales and Purchase Return
|
||||
elif sle.voucher_type in (
|
||||
@@ -1164,14 +1176,15 @@ class update_entries_after:
|
||||
|
||||
# Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
|
||||
if not sle.dependant_sle_voucher_detail_no:
|
||||
self.recalculate_amounts_in_stock_entry(sle.voucher_no)
|
||||
self.recalculate_amounts_in_stock_entry(sle.voucher_no, sle.voucher_detail_no)
|
||||
|
||||
def recalculate_amounts_in_stock_entry(self, voucher_no):
|
||||
def recalculate_amounts_in_stock_entry(self, voucher_no, voucher_detail_no):
|
||||
stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
|
||||
stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
|
||||
stock_entry.db_update()
|
||||
for d in stock_entry.items:
|
||||
d.db_update()
|
||||
if d.name == voucher_detail_no or (not d.s_warehouse and d.t_warehouse):
|
||||
d.db_update()
|
||||
|
||||
def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
|
||||
# Update item's incoming rate on transaction
|
||||
|
||||
Reference in New Issue
Block a user