mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-27 02:28:30 +00:00
Merge pull request #51711 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -187,7 +187,6 @@ class GLEntry(Document):
|
||||
account_type == "Profit and Loss"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_pl
|
||||
and not dimension.disabled
|
||||
and not self.is_cancelled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
@@ -201,7 +200,6 @@ class GLEntry(Document):
|
||||
account_type == "Balance Sheet"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_bs
|
||||
and not dimension.disabled
|
||||
and not self.is_cancelled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
|
||||
@@ -177,6 +177,9 @@ class JournalEntry(AccountsController):
|
||||
else:
|
||||
return self._submit()
|
||||
|
||||
def before_cancel(self):
|
||||
self.has_asset_adjustment_entry()
|
||||
|
||||
def cancel(self):
|
||||
if len(self.accounts) > 100:
|
||||
queue_submission(self, "_cancel")
|
||||
@@ -447,12 +450,27 @@ class JournalEntry(AccountsController):
|
||||
)
|
||||
frappe.db.set_value("Journal Entry", self.name, "inter_company_journal_entry_reference", "")
|
||||
|
||||
def unlink_asset_adjustment_entry(self):
|
||||
frappe.db.sql(
|
||||
""" update `tabAsset Value Adjustment`
|
||||
set journal_entry = null where journal_entry = %s""",
|
||||
self.name,
|
||||
def has_asset_adjustment_entry(self):
|
||||
if self.flags.get("via_asset_value_adjustment"):
|
||||
return
|
||||
|
||||
asset_value_adjustment = frappe.db.get_value(
|
||||
"Asset Value Adjustment", {"docstatus": 1, "journal_entry": self.name}, "name"
|
||||
)
|
||||
if asset_value_adjustment:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot cancel this document as it is linked with the submitted Asset Value Adjustment <b>{0}</b>. Please cancel the Asset Value Adjustment to continue."
|
||||
).format(frappe.utils.get_link_to_form("Asset Value Adjustment", asset_value_adjustment))
|
||||
)
|
||||
|
||||
def unlink_asset_adjustment_entry(self):
|
||||
AssetValueAdjustment = frappe.qb.DocType("Asset Value Adjustment")
|
||||
(
|
||||
frappe.qb.update(AssetValueAdjustment)
|
||||
.set(AssetValueAdjustment.journal_entry, None)
|
||||
.where(AssetValueAdjustment.journal_entry == self.name)
|
||||
).run()
|
||||
|
||||
def validate_party(self):
|
||||
for d in self.get("accounts"):
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "total_amount",
|
||||
"fieldtype": "Float",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Grand Total",
|
||||
"print_hide": 1,
|
||||
@@ -77,7 +77,7 @@
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "outstanding_amount",
|
||||
"fieldtype": "Float",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Outstanding",
|
||||
"read_only": 1
|
||||
@@ -85,7 +85,7 @@
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Float",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Allocated"
|
||||
},
|
||||
@@ -174,7 +174,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-25 04:32:11.040025",
|
||||
"modified": "2026-01-05 14:18:03.286224",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Reference",
|
||||
|
||||
@@ -18,12 +18,12 @@ class PaymentEntryReference(Document):
|
||||
account_type: DF.Data | None
|
||||
advance_voucher_no: DF.DynamicLink | None
|
||||
advance_voucher_type: DF.Link | None
|
||||
allocated_amount: DF.Float
|
||||
allocated_amount: DF.Currency
|
||||
bill_no: DF.Data | None
|
||||
due_date: DF.Date | None
|
||||
exchange_gain_loss: DF.Currency
|
||||
exchange_rate: DF.Float
|
||||
outstanding_amount: DF.Float
|
||||
outstanding_amount: DF.Currency
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
@@ -34,7 +34,7 @@ class PaymentEntryReference(Document):
|
||||
reconcile_effect_on: DF.Date | None
|
||||
reference_doctype: DF.Link
|
||||
reference_name: DF.DynamicLink
|
||||
total_amount: DF.Float
|
||||
total_amount: DF.Currency
|
||||
# end: auto-generated types
|
||||
|
||||
@property
|
||||
|
||||
@@ -133,7 +133,6 @@ class PaymentLedgerEntry(Document):
|
||||
account_type == "Profit and Loss"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_pl
|
||||
and not dimension.disabled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(
|
||||
@@ -146,7 +145,6 @@ class PaymentLedgerEntry(Document):
|
||||
account_type == "Balance Sheet"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_bs
|
||||
and not dimension.disabled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(
|
||||
|
||||
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe import _, msgprint, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder import Case, Criterion
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
|
||||
|
||||
@@ -393,6 +393,9 @@ class PaymentReconciliation(Document):
|
||||
inv.outstanding_amount = flt(entry.get("outstanding_amount"))
|
||||
|
||||
def get_difference_amount(self, payment_entry, invoice, allocated_amount):
|
||||
party_account_defaults = frappe.get_cached_value(
|
||||
"Account", self.receivable_payable_account, ["account_type", "account_currency"], as_dict=True
|
||||
)
|
||||
allocated_amount_precision = get_field_precision(
|
||||
frappe.get_meta("Payment Reconciliation Allocation").get_field("allocated_amount")
|
||||
)
|
||||
@@ -400,9 +403,9 @@ class PaymentReconciliation(Document):
|
||||
frappe.get_meta("Payment Reconciliation Allocation").get_field("difference_amount")
|
||||
)
|
||||
difference_amount = 0
|
||||
if frappe.get_cached_value(
|
||||
"Account", self.receivable_payable_account, "account_currency"
|
||||
) != frappe.get_cached_value("Company", self.company, "default_currency"):
|
||||
if party_account_defaults.get("account_currency") != frappe.get_cached_value(
|
||||
"Company", self.company, "default_currency"
|
||||
):
|
||||
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
|
||||
"exchange_rate", 1
|
||||
):
|
||||
@@ -414,7 +417,14 @@ class PaymentReconciliation(Document):
|
||||
invoice.get("exchange_rate", 1) * flt(allocated_amount, allocated_amount_precision),
|
||||
difference_amount_precision,
|
||||
)
|
||||
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
|
||||
|
||||
# Added If clause to handle return Adhoc payments for account type holders ("Payable")
|
||||
if party_account_defaults.get("account_type") in ("Payable") and invoice.get(
|
||||
"invoice_type"
|
||||
) in ["Payment Entry", "Journal Entry"]:
|
||||
difference_amount = allocated_amount_in_inv_rate - allocated_amount_in_ref_rate
|
||||
else:
|
||||
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
|
||||
|
||||
return difference_amount
|
||||
|
||||
@@ -677,6 +687,28 @@ class PaymentReconciliation(Document):
|
||||
)
|
||||
invoice_exchange_map.update(journals_map)
|
||||
|
||||
payment_entries = [
|
||||
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Payment Entry"
|
||||
]
|
||||
payment_entries.extend(
|
||||
[d.get("reference_name") for d in payments if d.get("reference_type") == "Payment Entry"]
|
||||
)
|
||||
if payment_entries:
|
||||
pe = frappe.qb.DocType("Payment Entry")
|
||||
query = (
|
||||
frappe.qb.from_(pe)
|
||||
.select(
|
||||
pe.name,
|
||||
Case()
|
||||
.when(pe.payment_type == "Receive", pe.source_exchange_rate)
|
||||
.else_(pe.target_exchange_rate)
|
||||
.as_("exchange_rate"),
|
||||
)
|
||||
.where(pe.name.isin(payment_entries))
|
||||
)
|
||||
payment_entries = query.run(as_list=1)
|
||||
invoice_exchange_map.update(payment_entries)
|
||||
|
||||
return invoice_exchange_map
|
||||
|
||||
def validate_allocation(self):
|
||||
|
||||
@@ -2336,6 +2336,210 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
|
||||
frappe.db.set_value("Company", self.company, default_settings)
|
||||
|
||||
def test_foreign_currency_reverse_payment_entry_against_payment_entry_for_customer(self):
|
||||
transaction_date = nowdate()
|
||||
customer = self.customer3
|
||||
amount = 1000
|
||||
exchange_rate_at_payment = 100
|
||||
exchange_rate_at_reverse_payment = 95
|
||||
|
||||
# Receive amount from customer - 1,00,000
|
||||
pe = self.create_payment_entry(amount=amount, posting_date=transaction_date, customer=customer)
|
||||
pe.payment_type = "Receive"
|
||||
pe.paid_from = self.debtors_eur
|
||||
pe.paid_from_account_currency = "EUR"
|
||||
pe.source_exchange_rate = exchange_rate_at_payment
|
||||
pe.paid_amount = amount
|
||||
pe.received_amount = exchange_rate_at_payment * amount
|
||||
pe.paid_to = self.cash
|
||||
pe.paid_to_account_currency = "INR"
|
||||
pe = pe.save().submit()
|
||||
|
||||
# Pay amount to customer - 95,000
|
||||
reverse_pe = self.create_payment_entry(
|
||||
amount=amount, posting_date=transaction_date, customer=customer
|
||||
)
|
||||
reverse_pe.payment_type = "Pay"
|
||||
reverse_pe.paid_from = self.cash
|
||||
reverse_pe.paid_from_account_currency = "INR"
|
||||
reverse_pe.target_exchange_rate = exchange_rate_at_reverse_payment
|
||||
reverse_pe.paid_amount = exchange_rate_at_reverse_payment * amount
|
||||
reverse_pe.received_amount = amount
|
||||
reverse_pe.paid_to = self.debtors_eur
|
||||
reverse_pe.paid_to_account_currency = "EUR"
|
||||
reverse_pe.save().submit()
|
||||
|
||||
# Reconcile payments
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.party = customer
|
||||
pr.receivable_payable_account = self.debtors_eur
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||
payments = [payment.as_dict() for payment in pr.payments]
|
||||
self.assertEqual(len(pr.get("invoices")), 1)
|
||||
self.assertEqual(len(pr.get("payments")), 1)
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Check the difference_amount is a gain of 5000
|
||||
self.assertEqual(flt(pr.allocation[0].get("difference_amount")), 5000.0)
|
||||
pr.reconcile()
|
||||
|
||||
def test_foreign_currency_reverse_payment_entry_against_payment_entry_for_supplier(self):
|
||||
transaction_date = nowdate()
|
||||
self.supplier = "_Test Supplier USD"
|
||||
amount = 1000
|
||||
exchange_rate_at_payment = 100
|
||||
exchange_rate_at_reverse_payment = 95
|
||||
|
||||
# Pay amount to supplier - 1,00,000
|
||||
pe = self.create_payment_entry(amount=amount, posting_date=transaction_date)
|
||||
pe.payment_type = "Pay"
|
||||
pe.party_type = "Supplier"
|
||||
pe.party = self.supplier
|
||||
pe.paid_from = self.cash
|
||||
pe.paid_from_account_currency = "INR"
|
||||
pe.target_exchange_rate = exchange_rate_at_payment
|
||||
pe.paid_amount = exchange_rate_at_payment * amount
|
||||
pe.received_amount = amount
|
||||
pe.paid_to = self.creditors_usd
|
||||
pe.paid_to_account_currency = "USD"
|
||||
pe.save().submit()
|
||||
|
||||
# Receive amount from supplier - 95,000
|
||||
reverse_pe = self.create_payment_entry(amount=amount, posting_date=transaction_date)
|
||||
reverse_pe.payment_type = "Receive"
|
||||
reverse_pe.party_type = "Supplier"
|
||||
reverse_pe.party = self.supplier
|
||||
reverse_pe.paid_from = self.creditors_usd
|
||||
reverse_pe.paid_from_account_currency = "USD"
|
||||
reverse_pe.source_exchange_rate = exchange_rate_at_reverse_payment
|
||||
reverse_pe.paid_amount = amount
|
||||
reverse_pe.received_amount = exchange_rate_at_reverse_payment * amount
|
||||
reverse_pe.paid_to = self.cash
|
||||
reverse_pe.paid_to_account_currency = "INR"
|
||||
reverse_pe = reverse_pe.save().submit()
|
||||
|
||||
# Reconcile payments
|
||||
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||
pr.party = self.supplier
|
||||
pr.receivable_payable_account = self.creditors_usd
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||
payments = [payment.as_dict() for payment in pr.payments]
|
||||
|
||||
self.assertEqual(len(pr.get("invoices")), 1)
|
||||
self.assertEqual(len(pr.get("payments")), 1)
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Check the difference_amount is a loss of 5000
|
||||
self.assertEqual(flt(pr.allocation[0].get("difference_amount")), -5000.0)
|
||||
pr.reconcile()
|
||||
|
||||
def test_foreign_currency_reverse_journal_entry_against_journal_entry_for_customer(self):
|
||||
transaction_date = nowdate()
|
||||
customer = self.customer3
|
||||
amount = 1000
|
||||
exchange_rate_at_payment = 95
|
||||
exchange_rate_at_reverse_payment = 100
|
||||
|
||||
# Receive amount from customer - 95,000
|
||||
je1 = self.create_journal_entry(self.cash, self.debtors_eur, amount, transaction_date)
|
||||
je1.multi_currency = 1
|
||||
je1.accounts[0].exchange_rate = 1
|
||||
je1.accounts[0].debit_in_account_currency = exchange_rate_at_payment * amount
|
||||
je1.accounts[0].debit = exchange_rate_at_payment * amount
|
||||
je1.accounts[1].party_type = "Customer"
|
||||
je1.accounts[1].party = customer
|
||||
je1.accounts[1].exchange_rate = exchange_rate_at_payment
|
||||
je1.accounts[1].credit_in_account_currency = amount
|
||||
je1.accounts[1].credit = exchange_rate_at_payment * amount
|
||||
je1.save()
|
||||
je1.submit()
|
||||
|
||||
# Pay amount to customer - 1,00,000
|
||||
je2 = self.create_journal_entry(self.debtors_eur, self.cash, amount, transaction_date)
|
||||
je2.multi_currency = 1
|
||||
je2.accounts[0].party_type = "Customer"
|
||||
je2.accounts[0].party = customer
|
||||
je2.accounts[0].exchange_rate = exchange_rate_at_reverse_payment
|
||||
je2.accounts[0].debit_in_account_currency = amount
|
||||
je2.accounts[0].debit = exchange_rate_at_reverse_payment * amount
|
||||
je2.accounts[1].exchange_rate = 1
|
||||
je2.accounts[1].credit_in_account_currency = exchange_rate_at_reverse_payment * amount
|
||||
je2.accounts[1].credit = exchange_rate_at_reverse_payment * amount
|
||||
je2.save()
|
||||
je2.submit()
|
||||
|
||||
# Reconcile payments
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.party = customer
|
||||
pr.receivable_payable_account = self.debtors_eur
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||
payments = [payment.as_dict() for payment in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Check the difference_amount is a loss of 5000
|
||||
self.assertEqual(flt(pr.allocation[0].difference_amount), -5000.0)
|
||||
pr.reconcile()
|
||||
|
||||
def test_foreign_currency_reverse_journal_entry_against_journal_entry_for_supplier(self):
|
||||
transaction_date = nowdate()
|
||||
self.supplier = "_Test Supplier USD"
|
||||
amount = 1000
|
||||
exchange_rate_at_payment = 95
|
||||
exchange_rate_at_reverse_payment = 100
|
||||
|
||||
# Pay amount to supplier - 95,000
|
||||
je1 = self.create_journal_entry(self.creditors_usd, self.cash, amount, transaction_date)
|
||||
je1.multi_currency = 1
|
||||
je1.accounts[0].party_type = "Supplier"
|
||||
je1.accounts[0].party = self.supplier
|
||||
je1.accounts[0].exchange_rate = exchange_rate_at_payment
|
||||
je1.accounts[0].debit_in_account_currency = amount
|
||||
je1.accounts[0].debit = exchange_rate_at_payment * amount
|
||||
je1.accounts[1].exchange_rate = 1
|
||||
je1.accounts[1].credit = exchange_rate_at_payment * amount
|
||||
je1.accounts[1].credit_in_account_currency = exchange_rate_at_payment * amount
|
||||
je1.save()
|
||||
je1.submit()
|
||||
|
||||
# Receive amount from supplier - 1,00,000
|
||||
je2 = self.create_journal_entry(self.cash, self.creditors_usd, amount, transaction_date)
|
||||
je2.multi_currency = 1
|
||||
je2.accounts[0].exchange_rate = 1
|
||||
je2.accounts[0].debit = exchange_rate_at_reverse_payment * amount
|
||||
je2.accounts[0].debit_in_account_currency = exchange_rate_at_reverse_payment * amount
|
||||
je2.accounts[1].party_type = "Supplier"
|
||||
je2.accounts[1].party = self.supplier
|
||||
je2.accounts[1].exchange_rate = exchange_rate_at_reverse_payment
|
||||
je2.accounts[1].credit_in_account_currency = amount
|
||||
je2.accounts[1].credit = exchange_rate_at_reverse_payment * amount
|
||||
je2.save()
|
||||
je2.submit()
|
||||
|
||||
# Reconcile payments
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.party_type = "Supplier"
|
||||
pr.party = self.supplier
|
||||
pr.receivable_payable_account = self.creditors_usd
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||
payments = [payment.as_dict() for payment in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Check the difference_amount is a gain of 5000
|
||||
self.assertEqual(flt(pr.allocation[0].difference_amount), 5000.0)
|
||||
pr.reconcile()
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
||||
@@ -481,6 +481,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
rate=1000,
|
||||
serial_no=[serial_nos[0]],
|
||||
do_not_save=1,
|
||||
ignore_sabb_validation=True,
|
||||
)
|
||||
|
||||
pos2.append("payments", {"mode_of_payment": "Bank Draft", "amount": 1000})
|
||||
@@ -956,6 +957,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
qty=1,
|
||||
rate=100,
|
||||
do_not_submit=True,
|
||||
ignore_sabb_validation=True,
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pos_inv.submit)
|
||||
@@ -1097,6 +1099,7 @@ def create_pos_invoice(**args):
|
||||
"posting_time": pos_inv.posting_time,
|
||||
"type_of_transaction": type_of_transaction,
|
||||
"do_not_submit": True,
|
||||
"ignore_sabb_validation": args.ignore_sabb_validation,
|
||||
}
|
||||
)
|
||||
).name
|
||||
|
||||
@@ -12,6 +12,7 @@ from frappe.utils import cint, flt, formatdate, get_link_to_form, getdate, now
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
)
|
||||
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
|
||||
get_dimension_filter_map,
|
||||
@@ -612,6 +613,18 @@ def update_accounting_dimensions(round_off_gle):
|
||||
|
||||
for dimension in dimensions:
|
||||
round_off_gle[dimension] = dimension_values.get(dimension)
|
||||
else:
|
||||
report_type = frappe.get_cached_value("Account", round_off_gle.account, "report_type")
|
||||
for dimension in get_checks_for_pl_and_bs_accounts():
|
||||
if (
|
||||
round_off_gle.company == dimension.company
|
||||
and (
|
||||
(report_type == "Profit and Loss" and dimension.mandatory_for_pl)
|
||||
or (report_type == "Balance Sheet" and dimension.mandatory_for_bs)
|
||||
)
|
||||
and dimension.default_dimension
|
||||
):
|
||||
round_off_gle[dimension.fieldname] = dimension.default_dimension
|
||||
|
||||
|
||||
def get_round_off_account_and_cost_center(company, voucher_type, voucher_no, use_company_default=False):
|
||||
|
||||
@@ -235,26 +235,64 @@ frappe.ui.form.on("Asset", {
|
||||
},
|
||||
|
||||
toggle_reference_doc: function (frm) {
|
||||
if (frm.doc.purchase_receipt && frm.doc.purchase_invoice && frm.doc.docstatus === 1) {
|
||||
frm.set_df_property("purchase_invoice", "read_only", 1);
|
||||
frm.set_df_property("purchase_receipt", "read_only", 1);
|
||||
} else if (frm.doc.is_existing_asset || frm.doc.is_composite_asset) {
|
||||
frm.toggle_reqd("purchase_receipt", 0);
|
||||
frm.toggle_reqd("purchase_invoice", 0);
|
||||
} else if (frm.doc.purchase_receipt) {
|
||||
// if purchase receipt link is set then set PI disabled
|
||||
frm.toggle_reqd("purchase_invoice", 0);
|
||||
frm.set_df_property("purchase_invoice", "read_only", 1);
|
||||
} else if (frm.doc.purchase_invoice) {
|
||||
// if purchase invoice link is set then set PR disabled
|
||||
frm.toggle_reqd("purchase_receipt", 0);
|
||||
frm.set_df_property("purchase_receipt", "read_only", 1);
|
||||
} else {
|
||||
frm.toggle_reqd("purchase_receipt", 1);
|
||||
frm.set_df_property("purchase_receipt", "read_only", 0);
|
||||
frm.toggle_reqd("purchase_invoice", 1);
|
||||
frm.set_df_property("purchase_invoice", "read_only", 0);
|
||||
const is_submitted = frm.doc.docstatus === 1;
|
||||
const is_special_asset = frm.doc.is_existing_asset || frm.doc.is_composite_asset;
|
||||
|
||||
const clear_field = (field) => {
|
||||
if (frm.doc[field]) {
|
||||
frm.set_value(field, "");
|
||||
}
|
||||
};
|
||||
|
||||
["purchase_receipt", "purchase_receipt_item", "purchase_invoice", "purchase_invoice_item"].forEach(
|
||||
(field) => {
|
||||
frm.toggle_reqd(field, 0);
|
||||
frm.set_df_property(field, "read_only", 0);
|
||||
}
|
||||
);
|
||||
|
||||
if (is_submitted) {
|
||||
[
|
||||
"purchase_receipt",
|
||||
"purchase_receipt_item",
|
||||
"purchase_invoice",
|
||||
"purchase_invoice_item",
|
||||
].forEach((field) => {
|
||||
frm.set_df_property(field, "read_only", 1);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_special_asset) {
|
||||
clear_field("purchase_receipt");
|
||||
clear_field("purchase_receipt_item");
|
||||
clear_field("purchase_invoice");
|
||||
clear_field("purchase_invoice_item");
|
||||
return;
|
||||
}
|
||||
|
||||
if (frm.doc.purchase_receipt) {
|
||||
frm.toggle_reqd("purchase_receipt_item", 1);
|
||||
|
||||
["purchase_invoice", "purchase_invoice_item"].forEach((field) => {
|
||||
clear_field(field);
|
||||
frm.set_df_property(field, "read_only", 1);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (frm.doc.purchase_invoice) {
|
||||
frm.toggle_reqd("purchase_invoice_item", 1);
|
||||
|
||||
["purchase_receipt", "purchase_receipt_item"].forEach((field) => {
|
||||
clear_field(field);
|
||||
frm.set_df_property(field, "read_only", 1);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
frm.toggle_reqd("purchase_receipt", 1);
|
||||
frm.toggle_reqd("purchase_invoice", 1);
|
||||
},
|
||||
|
||||
make_journal_entry: function (frm) {
|
||||
@@ -484,7 +522,6 @@ frappe.ui.form.on("Asset", {
|
||||
} else {
|
||||
frm.set_df_property("gross_purchase_amount", "read_only", 0);
|
||||
}
|
||||
|
||||
frm.trigger("toggle_reference_doc");
|
||||
},
|
||||
|
||||
|
||||
@@ -177,6 +177,7 @@
|
||||
"default": "1",
|
||||
"fieldname": "target_qty",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 1,
|
||||
"label": "Target Qty"
|
||||
},
|
||||
{
|
||||
@@ -290,10 +291,10 @@
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
@@ -324,7 +325,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-08 13:14:33.008458",
|
||||
"modified": "2026-01-13 17:25:01.352568",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Capitalization",
|
||||
@@ -362,10 +363,11 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,7 @@ class AssetCapitalization(StockController):
|
||||
naming_series: DF.Literal["ACC-ASC-.YYYY.-"]
|
||||
posting_date: DF.Date
|
||||
posting_time: DF.Time
|
||||
project: DF.Link | None
|
||||
service_items: DF.Table[AssetCapitalizationServiceItem]
|
||||
service_items_total: DF.Currency
|
||||
set_posting_time: DF.Check
|
||||
|
||||
@@ -57,7 +57,7 @@ class AssetValueAdjustment(Document):
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
frappe.get_doc("Journal Entry", self.journal_entry).cancel()
|
||||
self.cancel_asset_revaluation_entry()
|
||||
self.update_asset()
|
||||
add_asset_activity(
|
||||
self.asset,
|
||||
@@ -159,6 +159,16 @@ class AssetValueAdjustment(Document):
|
||||
|
||||
self.db_set("journal_entry", je.name)
|
||||
|
||||
def cancel_asset_revaluation_entry(self):
|
||||
if not self.journal_entry:
|
||||
return
|
||||
|
||||
revaluation_entry = frappe.get_doc("Journal Entry", self.journal_entry)
|
||||
if revaluation_entry.docstatus == 1:
|
||||
revaluation_entry.flags.ignore_permissions = True
|
||||
revaluation_entry.flags.via_asset_value_adjustment = True
|
||||
revaluation_entry.cancel()
|
||||
|
||||
def update_asset(self, asset_value=None):
|
||||
difference_amount = self.difference_amount if self.docstatus == 1 else -1 * self.difference_amount
|
||||
asset = self.update_asset_value_after_depreciation(difference_amount)
|
||||
|
||||
@@ -794,7 +794,7 @@ def make_purchase_invoice(source_name, target_doc=None, args=None):
|
||||
@frappe.whitelist()
|
||||
def make_purchase_invoice_from_portal(purchase_order_name):
|
||||
doc = get_mapped_purchase_invoice(purchase_order_name, ignore_permissions=True)
|
||||
if doc.contact_email != frappe.session.user:
|
||||
if frappe.session.user not in frappe.get_all("Portal User", {"parent": doc.supplier}, pluck="user"):
|
||||
frappe.throw(_("Not Permitted"), frappe.PermissionError)
|
||||
doc.save()
|
||||
frappe.db.commit()
|
||||
|
||||
9
erpnext/change_log/v15/v15_94_2.md
Normal file
9
erpnext/change_log/v15/v15_94_2.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Version 16 Released!
|
||||
|
||||
ERPNext version 16 has been released!
|
||||
|
||||
Since it's the latest version of ERPNext, we recommend that you update to it to get the latest features, bug fixes and other improvements.
|
||||
|
||||
[Click here to know more about v16](https://frappe.io/erpnext/version-16)
|
||||
|
||||
If you're on [Frappe Cloud](https://frappe.io/cloud), [click here to learn how to update to v16](https://docs.frappe.io/cloud/sites/version-upgrade)
|
||||
@@ -3714,9 +3714,9 @@ def validate_child_on_delete(row, parent):
|
||||
)
|
||||
if flt(row.ordered_qty):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Cannot delete item {1} which is assigned to customer's purchase order.").format(
|
||||
row.idx, row.item_code
|
||||
)
|
||||
_(
|
||||
"Row #{0}: Cannot delete item {1} which is already ordered against this Sales Order."
|
||||
).format(row.idx, row.item_code)
|
||||
)
|
||||
|
||||
if parent.doctype == "Purchase Order" and flt(row.received_qty):
|
||||
|
||||
@@ -1004,10 +1004,19 @@ class SellingController(StockController):
|
||||
|
||||
|
||||
def set_default_income_account_for_item(obj):
|
||||
for d in obj.get("items"):
|
||||
if d.item_code:
|
||||
if getattr(d, "income_account", None):
|
||||
set_item_default(d.item_code, obj.company, "income_account", d.income_account)
|
||||
"""Set income account as default for items in the transaction.
|
||||
|
||||
Updates the item default income account for each item in the transaction
|
||||
if it differs from the company's default income account.
|
||||
|
||||
Args:
|
||||
obj: Transaction document containing items table with income_account field
|
||||
"""
|
||||
company_default = frappe.get_cached_value("Company", obj.company, "default_income_account")
|
||||
for d in obj.get("items", default=[]):
|
||||
income_account = getattr(d, "income_account", None)
|
||||
if d.item_code and income_account and income_account != company_default:
|
||||
set_item_default(d.item_code, obj.company, "income_account", income_account)
|
||||
|
||||
|
||||
def get_serial_and_batch_bundle(child, parent, delivery_note_child=None):
|
||||
|
||||
@@ -778,9 +778,8 @@ class TestSubcontractingController(FrappeTestCase):
|
||||
row.serial_no = "ABC"
|
||||
break
|
||||
|
||||
bundle.save()
|
||||
self.assertRaises(frappe.ValidationError, bundle.save)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, scr1.save)
|
||||
bundle.load_from_db()
|
||||
for row in bundle.entries:
|
||||
if row.idx == 1:
|
||||
|
||||
@@ -316,7 +316,7 @@ class WorkOrder(Document):
|
||||
# already ordered qty
|
||||
ordered_qty_against_so = frappe.db.sql(
|
||||
"""select sum(qty) from `tabWork Order`
|
||||
where production_item = %s and sales_order = %s and docstatus < 2 and name != %s""",
|
||||
where production_item = %s and sales_order = %s and docstatus < 2 and status != 'Closed' and name != %s""",
|
||||
(self.production_item, self.sales_order, self.name),
|
||||
)[0][0]
|
||||
|
||||
@@ -351,15 +351,16 @@ class WorkOrder(Document):
|
||||
|
||||
def update_status(self, status=None):
|
||||
"""Update status of work order if unknown"""
|
||||
if status != "Stopped" and status != "Closed":
|
||||
status = self.get_status(status)
|
||||
if self.status != "Closed":
|
||||
if status not in ["Stopped", "Closed"]:
|
||||
status = self.get_status(status)
|
||||
|
||||
if status != self.status:
|
||||
self.db_set("status", status)
|
||||
if status != self.status:
|
||||
self.db_set("status", status)
|
||||
|
||||
self.update_required_items()
|
||||
self.update_required_items()
|
||||
|
||||
return status
|
||||
return status or self.status
|
||||
|
||||
def get_status(self, status=None):
|
||||
"""Return the status based on stock entries against this work order"""
|
||||
@@ -515,6 +516,9 @@ class WorkOrder(Document):
|
||||
self.validate_cancel()
|
||||
self.db_set("status", "Cancelled")
|
||||
|
||||
self.on_close_or_cancel()
|
||||
|
||||
def on_close_or_cancel(self):
|
||||
if self.production_plan and frappe.db.exists(
|
||||
"Production Plan Item Reference", {"parent": self.production_plan}
|
||||
):
|
||||
@@ -842,7 +846,7 @@ class WorkOrder(Document):
|
||||
|
||||
qty = frappe.db.sql(
|
||||
f""" select sum(qty) from
|
||||
`tabWork Order` where sales_order = %s and docstatus = 1 and {cond}
|
||||
`tabWork Order` where sales_order = %s and docstatus = 1 and status <> 'Closed' and {cond}
|
||||
""",
|
||||
(self.sales_order, (self.product_bundle_item or self.production_item)),
|
||||
as_list=1,
|
||||
@@ -1603,8 +1607,8 @@ def close_work_order(work_order, status):
|
||||
)
|
||||
)
|
||||
|
||||
work_order.on_close_or_cancel()
|
||||
work_order.update_status(status)
|
||||
work_order.update_planned_qty()
|
||||
frappe.msgprint(_("Work Order has been {0}").format(status))
|
||||
work_order.notify_update()
|
||||
return work_order.status
|
||||
|
||||
@@ -555,10 +555,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
var item = frappe.get_doc(cdt, cdn);
|
||||
var update_stock = 0, show_batch_dialog = 0;
|
||||
|
||||
item.weight_per_unit = 0;
|
||||
item.weight_uom = '';
|
||||
item.uom = null // make UOM blank to update the existing UOM when item changes
|
||||
if(!item.barcode){
|
||||
item.uom = null // make UOM blank to update the existing UOM when item changes
|
||||
}
|
||||
item.conversion_factor = 0;
|
||||
|
||||
if(['Sales Invoice', 'Purchase Invoice'].includes(this.frm.doc.doctype)) {
|
||||
@@ -574,6 +575,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
show_batch_dialog = 0;
|
||||
}
|
||||
|
||||
|
||||
item.barcode = null;
|
||||
|
||||
|
||||
|
||||
@@ -404,6 +404,8 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
async set_barcode(row, barcode) {
|
||||
if (barcode && frappe.meta.has_field(row.doctype, this.barcode_field)) {
|
||||
await frappe.model.set_value(row.doctype, row.name, this.barcode_field, barcode);
|
||||
} else {
|
||||
row.barcode = barcode;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1875,6 +1875,7 @@ def get_work_order_items(sales_order, for_raw_material_request=0):
|
||||
& (wo.sales_order == so.name)
|
||||
& (wo.sales_order_item == i.name)
|
||||
& (wo.docstatus.lt(2))
|
||||
& (wo.status != "Closed")
|
||||
)
|
||||
.run()[0][0]
|
||||
)
|
||||
|
||||
@@ -2790,6 +2790,23 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
self.assertEqual(sre_details[0].reserved_qty, so.items[0].qty)
|
||||
self.assertEqual(sre_details[0].delivered_qty, dn.items[0].qty)
|
||||
|
||||
def test_negative_stock_with_higher_precision(self):
|
||||
original_flt_precision = frappe.db.get_default("float_precision")
|
||||
frappe.db.set_single_value("System Settings", "float_precision", 7)
|
||||
|
||||
item_code = make_item(
|
||||
"Test Negative Stock High Precision Item", properties={"is_stock_item": 1, "valuation_rate": 1}
|
||||
).name
|
||||
dn = create_delivery_note(
|
||||
item_code=item_code,
|
||||
qty=0.0000010,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, dn.submit)
|
||||
|
||||
frappe.db.set_single_value("System Settings", "float_precision", original_flt_precision)
|
||||
|
||||
|
||||
def create_delivery_note(**args):
|
||||
dn = frappe.new_doc("Delivery Note")
|
||||
|
||||
@@ -273,6 +273,9 @@ class MaterialRequest(BuyingController):
|
||||
.groupby(doctype.material_request_item)
|
||||
)
|
||||
|
||||
if self.material_request_type == "Manufacture":
|
||||
query = query.where(doctype.status != "Closed")
|
||||
|
||||
mr_items_ordered_qty = frappe._dict(query.run())
|
||||
|
||||
return mr_items_ordered_qty
|
||||
|
||||
@@ -383,7 +383,7 @@ class PickList(TransactionBase):
|
||||
picked_items = get_picked_items_qty(packed_items, contains_packed_items=True)
|
||||
self.validate_picked_qty(picked_items)
|
||||
|
||||
doc_updates = {}
|
||||
doc_updates = {item: {"picked_qty": 0} for item in set(packed_items)}
|
||||
for d in picked_items:
|
||||
doc_updates[d.product_bundle_item] = {"picked_qty": flt(d.picked_qty)}
|
||||
|
||||
@@ -394,7 +394,7 @@ class PickList(TransactionBase):
|
||||
picked_items = get_picked_items_qty(so_items)
|
||||
self.validate_picked_qty(picked_items)
|
||||
|
||||
doc_updates = {}
|
||||
doc_updates = {item: {"picked_qty": 0} for item in set(so_items)}
|
||||
for d in picked_items:
|
||||
doc_updates[d.sales_order_item] = {"picked_qty": flt(d.picked_qty)}
|
||||
|
||||
|
||||
@@ -116,10 +116,20 @@ class SerialandBatchBundle(Document):
|
||||
return
|
||||
|
||||
self.allow_existing_serial_nos()
|
||||
if not self.flags.ignore_validate_serial_batch or frappe.flags.in_test:
|
||||
self.validate_serial_nos_duplicate()
|
||||
if self.docstatus == 1:
|
||||
if not self.flags.ignore_validate_serial_batch or frappe.flags.in_test:
|
||||
self.validate_serial_nos_duplicate()
|
||||
|
||||
self.check_future_entries_exists()
|
||||
|
||||
elif (
|
||||
self.has_serial_no
|
||||
and self.type_of_transaction == "Outward"
|
||||
and self.voucher_type != "Stock Reconciliation"
|
||||
and self.voucher_no
|
||||
):
|
||||
self.validate_serial_no_status()
|
||||
|
||||
self.check_future_entries_exists()
|
||||
self.set_is_outward()
|
||||
self.calculate_total_qty()
|
||||
self.set_warehouse()
|
||||
@@ -129,6 +139,25 @@ class SerialandBatchBundle(Document):
|
||||
|
||||
self.calculate_qty_and_amount()
|
||||
|
||||
def validate_serial_no_status(self):
|
||||
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
|
||||
invalid_serial_nos = frappe.get_all(
|
||||
"Serial No",
|
||||
filters={
|
||||
"name": ("in", serial_nos),
|
||||
"warehouse": ("!=", self.warehouse),
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
if invalid_serial_nos:
|
||||
msg = _(
|
||||
"You cannot outward following {0} as either they are Delivered, Inactive or located in a different warehouse."
|
||||
).format(_("Serial Nos") if len(invalid_serial_nos) > 1 else _("Serial No"))
|
||||
msg += "<hr>"
|
||||
msg += ", ".join(sn for sn in invalid_serial_nos)
|
||||
frappe.throw(msg)
|
||||
|
||||
def validate_voucher_detail_no(self):
|
||||
if self.type_of_transaction not in ["Inward", "Outward"] or self.voucher_type in [
|
||||
"Installation Note",
|
||||
@@ -702,10 +731,16 @@ class SerialandBatchBundle(Document):
|
||||
"Buying Settings", "set_valuation_rate_for_rejected_materials"
|
||||
)
|
||||
|
||||
precision = frappe.get_precision("Serial and Batch Entry", "incoming_rate")
|
||||
for d in self.entries:
|
||||
if self.is_rejected and not set_valuation_rate_for_rejected_materials:
|
||||
rate = 0.0
|
||||
elif (d.incoming_rate == rate) and not stock_queue and d.qty and d.stock_value_difference:
|
||||
elif (
|
||||
(flt(d.incoming_rate, precision) == flt(rate, precision))
|
||||
and not stock_queue
|
||||
and d.qty
|
||||
and d.stock_value_difference
|
||||
):
|
||||
continue
|
||||
|
||||
if is_packed_item and d.incoming_rate:
|
||||
@@ -766,7 +801,7 @@ class SerialandBatchBundle(Document):
|
||||
self.calculate_total_qty(save=True)
|
||||
|
||||
# If user has changed the rate in the child table
|
||||
if self.docstatus == 0:
|
||||
if self.docstatus == 0 and self.type_of_transaction == "Inward":
|
||||
self.set_incoming_rate(parent=parent, row=row, save=True)
|
||||
|
||||
if self.docstatus == 0 and parent.get("is_return") and parent.is_new():
|
||||
|
||||
@@ -982,6 +982,7 @@ def make_serial_batch_bundle(kwargs):
|
||||
"type_of_transaction": type_of_transaction,
|
||||
"company": kwargs.company or "_Test Company",
|
||||
"do_not_submit": kwargs.do_not_submit,
|
||||
"ignore_sabb_validation": kwargs.ignore_sabb_validation or False,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -382,6 +382,7 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "tracking_status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Tracking Status",
|
||||
@@ -440,7 +441,7 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-02-20 16:55:20.076418",
|
||||
"modified": "2026-01-07 19:24:23.566312",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Shipment",
|
||||
|
||||
@@ -20,9 +20,7 @@ class Shipment(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.stock.doctype.shipment_delivery_note.shipment_delivery_note import (
|
||||
ShipmentDeliveryNote,
|
||||
)
|
||||
from erpnext.stock.doctype.shipment_delivery_note.shipment_delivery_note import ShipmentDeliveryNote
|
||||
from erpnext.stock.doctype.shipment_parcel.shipment_parcel import ShipmentParcel
|
||||
|
||||
amended_from: DF.Link | None
|
||||
|
||||
@@ -4,7 +4,7 @@ import frappe
|
||||
from frappe import _, bold
|
||||
from frappe.model.naming import NamingSeries, make_autoname, parse_naming_series
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.functions import CombineDatetime, Sum, Timestamp
|
||||
from frappe.query_builder.functions import CombineDatetime, Max, Sum, Timestamp
|
||||
from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, now, nowtime, today
|
||||
from pypika import Order
|
||||
from pypika.terms import ExistsCriterion
|
||||
@@ -616,8 +616,9 @@ class SerialNoValuation(DeprecatedSerialNoValuation):
|
||||
self.old_serial_nos = []
|
||||
|
||||
serial_nos = self.get_serial_nos()
|
||||
result = self.get_serial_no_wise_incoming_rate(serial_nos)
|
||||
for serial_no in serial_nos:
|
||||
incoming_rate = self.get_incoming_rate_from_bundle(serial_no)
|
||||
incoming_rate = result.get(serial_no)
|
||||
if incoming_rate is None:
|
||||
self.old_serial_nos.append(serial_no)
|
||||
continue
|
||||
@@ -627,44 +628,103 @@ class SerialNoValuation(DeprecatedSerialNoValuation):
|
||||
|
||||
self.calculate_stock_value_from_deprecarated_ledgers()
|
||||
|
||||
def get_incoming_rate_from_bundle(self, serial_no) -> float:
|
||||
def get_serial_no_wise_incoming_rate(self, serial_nos):
|
||||
bundle = frappe.qb.DocType("Serial and Batch Bundle")
|
||||
bundle_child = frappe.qb.DocType("Serial and Batch Entry")
|
||||
|
||||
def get_latest_based_on_posting_datetime():
|
||||
# Get latest inward record based on posting datetime for each serial no
|
||||
|
||||
latest_posting = (
|
||||
frappe.qb.from_(bundle)
|
||||
.inner_join(bundle_child)
|
||||
.on(bundle.name == bundle_child.parent)
|
||||
.select(
|
||||
bundle_child.serial_no,
|
||||
Max(CombineDatetime(bundle.posting_date, bundle.posting_time)).as_("max_posting_dt"),
|
||||
)
|
||||
.where(
|
||||
(bundle.is_cancelled == 0)
|
||||
& (bundle.docstatus == 1)
|
||||
& (bundle.type_of_transaction == "Inward")
|
||||
& (bundle_child.qty > 0)
|
||||
& (bundle.item_code == self.sle.item_code)
|
||||
& (bundle_child.warehouse == self.sle.warehouse)
|
||||
& (bundle_child.serial_no.isin(serial_nos))
|
||||
)
|
||||
.groupby(bundle_child.serial_no)
|
||||
)
|
||||
|
||||
# Important to exclude the current voucher to calculate correct the stock value difference
|
||||
if self.sle.voucher_no:
|
||||
latest_posting = latest_posting.where(bundle.voucher_no != self.sle.voucher_no)
|
||||
|
||||
if self.sle.posting_date:
|
||||
if self.sle.posting_time is None:
|
||||
self.sle.posting_time = nowtime()
|
||||
|
||||
timestamp_condition = CombineDatetime(
|
||||
bundle.posting_date, bundle.posting_time
|
||||
) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time)
|
||||
|
||||
latest_posting = latest_posting.where(timestamp_condition)
|
||||
|
||||
latest_posting = latest_posting.as_("latest_posting")
|
||||
|
||||
return latest_posting
|
||||
|
||||
def get_latest_based_on_creation(latest_posting):
|
||||
# Get latest inward record based on creation for each serial no
|
||||
latest_creation = (
|
||||
frappe.qb.from_(bundle)
|
||||
.join(bundle_child)
|
||||
.on(bundle.name == bundle_child.parent)
|
||||
.join(latest_posting)
|
||||
.on(
|
||||
(latest_posting.serial_no == bundle_child.serial_no)
|
||||
& (
|
||||
latest_posting.max_posting_dt
|
||||
== CombineDatetime(bundle.posting_date, bundle.posting_time)
|
||||
)
|
||||
)
|
||||
.select(
|
||||
bundle_child.serial_no,
|
||||
Max(bundle.creation).as_("max_creation"),
|
||||
)
|
||||
.where(
|
||||
(bundle.is_cancelled == 0)
|
||||
& (bundle.docstatus == 1)
|
||||
& (bundle.type_of_transaction == "Inward")
|
||||
& (bundle_child.qty > 0)
|
||||
& (bundle.item_code == self.sle.item_code)
|
||||
& (bundle_child.warehouse == self.sle.warehouse)
|
||||
)
|
||||
.groupby(bundle_child.serial_no)
|
||||
).as_("latest_creation")
|
||||
|
||||
return latest_creation
|
||||
|
||||
latest_posting = get_latest_based_on_posting_datetime()
|
||||
latest_creation = get_latest_based_on_creation(latest_posting)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(bundle)
|
||||
.inner_join(bundle_child)
|
||||
.join(bundle_child)
|
||||
.on(bundle.name == bundle_child.parent)
|
||||
.select((bundle_child.incoming_rate * bundle_child.qty).as_("incoming_rate"))
|
||||
.where(
|
||||
(bundle.is_cancelled == 0)
|
||||
& (bundle.docstatus == 1)
|
||||
& (bundle_child.serial_no == serial_no)
|
||||
& (bundle.type_of_transaction == "Inward")
|
||||
& (bundle_child.qty > 0)
|
||||
& (bundle.item_code == self.sle.item_code)
|
||||
& (bundle_child.warehouse == self.sle.warehouse)
|
||||
.join(latest_creation)
|
||||
.on(
|
||||
(latest_creation.serial_no == bundle_child.serial_no)
|
||||
& (latest_creation.max_creation == bundle.creation)
|
||||
)
|
||||
.select(
|
||||
bundle_child.serial_no,
|
||||
bundle_child.incoming_rate,
|
||||
)
|
||||
.orderby(Timestamp(bundle.posting_date, bundle.posting_time), order=Order.desc)
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
# Important to exclude the current voucher to calculate correct the stock value difference
|
||||
if self.sle.voucher_no:
|
||||
query = query.where(bundle.voucher_no != self.sle.voucher_no)
|
||||
result = query.run(as_list=1)
|
||||
|
||||
if self.sle.posting_date:
|
||||
if self.sle.posting_time is None:
|
||||
self.sle.posting_time = nowtime()
|
||||
|
||||
timestamp_condition = CombineDatetime(
|
||||
bundle.posting_date, bundle.posting_time
|
||||
) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time)
|
||||
|
||||
query = query.where(timestamp_condition)
|
||||
|
||||
incoming_rate = query.run()
|
||||
return flt(incoming_rate[0][0]) if incoming_rate else None
|
||||
return frappe._dict(result) if result else frappe._dict({})
|
||||
|
||||
def get_serial_nos(self):
|
||||
if self.sle.get("serial_nos"):
|
||||
@@ -1131,6 +1191,9 @@ class SerialBatchCreation:
|
||||
|
||||
doc.submit()
|
||||
else:
|
||||
if self.get("ignore_sabb_validation"):
|
||||
doc.flags.ignore_validate = True
|
||||
|
||||
doc.save()
|
||||
|
||||
self.validate_qty(doc)
|
||||
|
||||
@@ -1170,7 +1170,11 @@ class update_entries_after:
|
||||
diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) - flt(self.reserved_stock)
|
||||
diff = flt(diff, self.flt_precision) # respect system precision
|
||||
|
||||
if diff < 0 and abs(diff) > 0.0001:
|
||||
diff_threshold = 0.0001
|
||||
if self.flt_precision > 4:
|
||||
diff_threshold = 10 ** (-1 * self.flt_precision)
|
||||
|
||||
if diff < 0 and abs(diff) > diff_threshold:
|
||||
# negative stock!
|
||||
exc = sle.copy().update({"diff": diff})
|
||||
self.exceptions.setdefault(sle.warehouse, []).append(exc)
|
||||
|
||||
Reference in New Issue
Block a user