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

chore: release v15
This commit is contained in:
ruthra kumar
2024-11-06 10:50:24 +05:30
committed by GitHub
27 changed files with 1306 additions and 48 deletions

View File

@@ -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) {
// },
// });

View File

@@ -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": []
}

View File

@@ -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

View File

@@ -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")

View File

@@ -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",

View File

@@ -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) {

View File

@@ -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",
},
},
},

View File

@@ -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()

View File

@@ -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(

View File

@@ -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()

View File

@@ -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:

View File

@@ -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"]},
],
}

View File

@@ -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):

View File

@@ -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,

View File

@@ -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",

View File

@@ -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)

View File

@@ -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:

View File

@@ -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

View File

@@ -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)

View File

@@ -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"]},
],

View File

@@ -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(
{

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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(
{

View File

@@ -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