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

chore: release v15
This commit is contained in:
ruthra kumar
2025-07-08 18:29:42 +05:30
committed by GitHub
71 changed files with 660 additions and 460 deletions

View File

@@ -147,8 +147,8 @@ class AccountsSettings(Document):
if self.add_taxes_from_item_tax_template and self.add_taxes_from_taxes_and_charges_template:
frappe.throw(
_("You cannot enable both the settings '{0}' and '{1}'.").format(
frappe.bold(self.meta.get_label("add_taxes_from_item_tax_template")),
frappe.bold(self.meta.get_label("add_taxes_from_taxes_and_charges_template")),
frappe.bold(_(self.meta.get_label("add_taxes_from_item_tax_template"))),
frappe.bold(_(self.meta.get_label("add_taxes_from_taxes_and_charges_template"))),
),
title=_("Auto Tax Settings Error"),
)

View File

@@ -6,7 +6,11 @@ import unittest
import frappe
from frappe.utils import now_datetime, nowdate
from erpnext.accounts.doctype.budget.budget import BudgetError, get_actual_expense
from erpnext.accounts.doctype.budget.budget import (
BudgetError,
get_accumulated_monthly_budget,
get_actual_expense,
)
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.utils import get_fiscal_year
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
@@ -96,6 +100,10 @@ class TestBudget(unittest.TestCase):
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
accumulated_limit = get_accumulated_monthly_budget(
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
)
mr = frappe.get_doc(
{
"doctype": "Material Request",
@@ -109,7 +117,7 @@ class TestBudget(unittest.TestCase):
"uom": "_Test UOM",
"warehouse": "_Test Warehouse - _TC",
"schedule_date": nowdate(),
"rate": 100000,
"rate": accumulated_limit + 1,
"expense_account": "_Test Account Cost for Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
}

View File

@@ -24,6 +24,7 @@ from erpnext.accounts.party import get_party_account
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
get_account_currency,
get_advance_payment_doctypes,
get_balance_on,
get_stock_accounts,
get_stock_and_account_balance,
@@ -146,8 +147,8 @@ class JournalEntry(AccountsController):
if self.docstatus == 0:
self.apply_tax_withholding()
self.title = self.get_title()
if self.is_new() or not self.title:
self.title = self.get_title()
def validate_advance_accounts(self):
journal_accounts = set([x.account for x in self.accounts])
@@ -238,9 +239,10 @@ class JournalEntry(AccountsController):
def update_advance_paid(self):
advance_paid = frappe._dict()
advance_payment_doctypes = get_advance_payment_doctypes()
for d in self.get("accounts"):
if d.is_advance:
if d.reference_type in frappe.get_hooks("advance_payment_doctypes"):
if d.reference_type in advance_payment_doctypes:
advance_paid.setdefault(d.reference_type, []).append(d.reference_name)
for voucher_type, order_list in advance_paid.items():
@@ -1042,7 +1044,9 @@ class JournalEntry(AccountsController):
def set_print_format_fields(self):
bank_amount = party_amount = total_amount = 0.0
currency = bank_account_currency = party_account_currency = pay_to_recd_from = None
currency = (
bank_account_currency
) = party_account_currency = pay_to_recd_from = self.pay_to_recd_from = None
party_type = None
for d in self.get("accounts"):
if d.party_type in ["Customer", "Supplier"] and d.party:

View File

@@ -46,8 +46,10 @@ from erpnext.accounts.party import (
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
get_account_currency,
get_advance_payment_doctypes,
get_balance_on,
get_outstanding_invoices,
get_reconciliation_effect_date,
)
from erpnext.controllers.accounts_controller import (
AccountsController,
@@ -568,7 +570,7 @@ class PaymentEntry(AccountsController):
def validate_mandatory(self):
for field in ("paid_amount", "received_amount", "source_exchange_rate", "target_exchange_rate"):
if not self.get(field):
frappe.throw(_("{0} is mandatory").format(self.meta.get_label(field)))
frappe.throw(_("{0} is mandatory").format(_(self.meta.get_label(field))))
def validate_reference_documents(self):
valid_reference_doctypes = self.get_valid_reference_doctypes()
@@ -1028,7 +1030,7 @@ class PaymentEntry(AccountsController):
def calculate_base_allocated_amount_for_reference(self, d) -> float:
base_allocated_amount = 0
if d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"):
if d.reference_doctype in get_advance_payment_doctypes():
# When referencing Sales/Purchase Order, use the source/target exchange rate depending on payment type.
# This is so there are no Exchange Gain/Loss generated for such doctypes
@@ -1308,8 +1310,7 @@ class PaymentEntry(AccountsController):
if not self.party_account:
return
advance_payment_doctypes = frappe.get_hooks("advance_payment_doctypes")
advance_payment_doctypes = get_advance_payment_doctypes()
if self.payment_type == "Receive":
against_account = self.paid_to
else:
@@ -1492,23 +1493,7 @@ class PaymentEntry(AccountsController):
else:
# For backwards compatibility
# Supporting reposting on payment entries reconciled before select field introduction
reconciliation_takes_effect_on = frappe.get_cached_value(
"Company", self.company, "reconciliation_takes_effect_on"
)
if reconciliation_takes_effect_on == "Advance Payment Date":
posting_date = self.posting_date
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
date_field = "posting_date"
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
date_field = "transaction_date"
posting_date = frappe.db.get_value(
invoice.reference_doctype, invoice.reference_name, date_field
)
if getdate(posting_date) < getdate(self.posting_date):
posting_date = self.posting_date
elif reconciliation_takes_effect_on == "Reconciliation Date":
posting_date = nowdate()
posting_date = get_reconciliation_effect_date(invoice, self.company, self.posting_date)
frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date)
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
@@ -1699,12 +1684,15 @@ class PaymentEntry(AccountsController):
return flt(gl_dict.get(field, 0) / (conversion_rate or 1))
def update_advance_paid(self):
if self.payment_type in ("Receive", "Pay") and self.party:
for d in self.get("references"):
if d.allocated_amount and d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"):
frappe.get_doc(
d.reference_doctype, d.reference_name, for_update=True
).set_total_advance_paid()
if self.payment_type not in ("Receive", "Pay") or not self.party:
return
advance_payment_doctypes = get_advance_payment_doctypes()
for d in self.get("references"):
if d.allocated_amount and d.reference_doctype in advance_payment_doctypes:
frappe.get_doc(
d.reference_doctype, d.reference_name, for_update=True
).set_total_advance_paid()
def on_recurring(self, reference_doc, auto_repeat_doc):
self.reference_no = reference_doc.name

View File

@@ -589,7 +589,7 @@ class PaymentReconciliation(Document):
def check_mandatory_to_fetch(self):
for fieldname in ["company", "party_type", "party", "receivable_payable_account"]:
if not self.get(fieldname):
frappe.throw(_("Please select {0} first").format(self.meta.get_label(fieldname)))
frappe.throw(_("Please select {0} first").format(_(self.meta.get_label(fieldname))))
def validate_entries(self):
if not self.get("invoices"):

View File

@@ -829,8 +829,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
if not references:
return
precision = references[0].precision("allocated_amount")
precision = frappe.get_precision("Payment Entry Reference", "allocated_amount")
referenced_payment_requests = frappe.get_all(
"Payment Request",
filters={"name": ["in", {row.payment_request for row in references if row.payment_request}]},

View File

@@ -630,29 +630,58 @@ class TestPaymentRequest(FrappeTestCase):
pr = make_payment_request(dt="Sales Invoice", dn=si.name, mute_email=1)
self.assertEqual(pr.grand_total, si.outstanding_amount)
def test_partial_paid_invoice_with_submitted_payment_entry(self):
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)
pi.save()
pi.submit()
def test_partial_paid_invoice_with_submitted_payment_entry(self):
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)
pi.save()
pi.submit()
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "PURINV0001"
pe.reference_date = frappe.utils.nowdate()
pe.paid_amount = 2500
pe.references[0].allocated_amount = 2500
pe.save()
pe.submit()
pe.cancel()
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "PURINV0001"
pe.reference_date = frappe.utils.nowdate()
pe.paid_amount = 2500
pe.references[0].allocated_amount = 2500
pe.save()
pe.submit()
pe.cancel()
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "PURINV0002"
pe.reference_date = frappe.utils.nowdate()
pe.paid_amount = 2500
pe.references[0].allocated_amount = 2500
pe.save()
pe.submit()
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "PURINV0002"
pe.reference_date = frappe.utils.nowdate()
pe.paid_amount = 2500
pe.references[0].allocated_amount = 2500
pe.save()
pe.submit()
pi.load_from_db()
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
self.assertEqual(pr.grand_total, pi.outstanding_amount)
pi.load_from_db()
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
self.assertEqual(pr.grand_total, pi.outstanding_amount)
def test_payment_request_on_unreconcile(self):
pi = make_purchase_invoice(currency="INR", qty=1, rate=500)
pi.submit()
pr = make_payment_request(
dt=pi.doctype,
dn=pi.name,
mute_email=1,
submit_doc=True,
return_doc=True,
)
self.assertEqual(pr.grand_total, pi.outstanding_amount)
pe = pr.create_payment_entry()
unreconcile = frappe.get_doc(
{
"doctype": "Unreconcile Payment",
"company": pe.company,
"voucher_type": pe.doctype,
"voucher_no": pe.name,
}
)
unreconcile.add_references()
unreconcile.submit()
pi.load_from_db()
pr.load_from_db()
self.assertEqual(pr.grand_total, pi.outstanding_amount)

View File

@@ -5,6 +5,7 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"posting_date",
"posting_time",
"merge_invoices_based_on",
@@ -113,12 +114,22 @@
"label": "Posting Time",
"no_copy": 1,
"reqd": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Company",
"options": "Company",
"print_hide": 1,
"remember_last_selected_value": 1,
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-08-01 11:36:42.456429",
"modified": "2025-07-02 17:08:04.747202",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Merge Log",
@@ -179,8 +190,9 @@
"write": 1
}
],
"sort_field": "modified",
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -28,11 +28,10 @@ class POSInvoiceMergeLog(Document):
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import (
POSInvoiceReference,
)
from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import POSInvoiceReference
amended_from: DF.Link | None
company: DF.Link
consolidated_credit_note: DF.Link | None
consolidated_invoice: DF.Link | None
customer: DF.Link
@@ -584,6 +583,7 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
merge_log.posting_time = (
get_time(closing_entry.get("posting_time")) if closing_entry else nowtime()
)
merge_log.company = closing_entry.get("company") if closing_entry else None
merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None
merge_log.set("pos_invoices", _invoices)

View File

@@ -169,7 +169,7 @@ class PricingRule(Document):
tocheck = frappe.scrub(self.get("applicable_for", ""))
if tocheck and not self.get(tocheck):
throw(_("{0} is required").format(self.meta.get_label(tocheck)), frappe.MandatoryError)
throw(_("{0} is required").format(_(self.meta.get_label(tocheck))), frappe.MandatoryError)
if self.apply_rule_on_other:
o_field = "other_" + frappe.scrub(self.apply_rule_on_other)

View File

@@ -205,6 +205,56 @@ class TestPricingRule(FrappeTestCase):
details = get_item_details(args)
self.assertEqual(details.get("discount_percentage"), 10)
def test_unset_group_condition(self):
"""
If args are not set for group condition, then pricing rule should not be applied.
"""
from erpnext.stock.get_item_details import get_item_details
test_record = {
"doctype": "Pricing Rule",
"title": "_Test Pricing Rule",
"apply_on": "Item Code",
"items": [{"item_code": "_Test Item"}],
"currency": "USD",
"selling": 1,
"rate_or_discount": "Discount Percentage",
"rate": 0,
"discount_percentage": 10,
"applicable_for": "Territory",
"territory": "All Territories",
"company": "_Test Company",
}
frappe.get_doc(test_record.copy()).insert()
args = frappe._dict(
{
"item_code": "_Test Item",
"company": "_Test Company",
"price_list": "_Test Price List",
"currency": "_Test Currency",
"doctype": "Sales Order",
"conversion_rate": 1,
"price_list_currency": "_Test Currency",
"plc_conversion_rate": 1,
"order_type": "Sales",
"customer": "_Test Customer",
"name": None,
}
)
# without territory in customer
customer = frappe.get_doc("Customer", "_Test Customer")
territory = customer.territory
customer.territory = None
customer.save()
details = get_item_details(args)
self.assertEqual(details.get("discount_percentage"), 0)
customer.territory = territory
customer.save()
def test_pricing_rule_for_variants(self):
from erpnext.stock.get_item_details import get_item_details

View File

@@ -223,6 +223,10 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
)
frappe.flags.tree_conditions[key] = condition
elif allow_blank:
condition = f"ifnull({table}.{field}, '') = ''"
return condition

View File

@@ -40,6 +40,13 @@ class TestProcessDeferredAccounting(unittest.TestCase):
si.save()
si.submit()
original_gle = [
["Debtors - _TC", 3000.0, 0, "2023-07-01"],
[deferred_account, 0.0, 3000, "2023-07-01"],
]
check_gl_entries(self, si.name, original_gle, "2023-07-01")
process_deferred_accounting = frappe.get_doc(
dict(
doctype="Process Deferred Accounting",
@@ -63,6 +70,12 @@ class TestProcessDeferredAccounting(unittest.TestCase):
]
check_gl_entries(self, si.name, expected_gle, "2023-07-01")
# cancel the process deferred accounting document
process_deferred_accounting.cancel()
# check if gl entries are cancelled
check_gl_entries(self, si.name, original_gle, "2023-07-01")
change_acc_settings()
def test_pda_submission_and_cancellation(self):

View File

@@ -54,6 +54,9 @@ frappe.ui.form.on("Process Statement Of Accounts", {
};
});
frm.set_query("account", function () {
if (!frm.doc.company) {
frappe.throw(__("Please set Company"));
}
return {
filters: {
company: frm.doc.company,
@@ -61,6 +64,9 @@ frappe.ui.form.on("Process Statement Of Accounts", {
};
});
frm.set_query("cost_center", function () {
if (!frm.doc.company) {
frappe.throw(__("Please set Company"));
}
return {
filters: {
company: frm.doc.company,
@@ -68,6 +74,9 @@ frappe.ui.form.on("Process Statement Of Accounts", {
};
});
frm.set_query("project", function () {
if (!frm.doc.company) {
frappe.throw(__("Please set Company"));
}
return {
filters: {
company: frm.doc.company,
@@ -79,6 +88,11 @@ frappe.ui.form.on("Process Statement Of Accounts", {
frm.set_value("to_date", frappe.datetime.get_today());
}
},
company: function (frm) {
frm.set_value("account", "");
frm.set_value("cost_center", "");
frm.set_value("project", "");
},
report: function (frm) {
let filters = {
company: frm.doc.company,

View File

@@ -376,7 +376,7 @@
"default": "0",
"fieldname": "ignore_exchange_rate_revaluation_journals",
"fieldtype": "Check",
"label": "Ignore Exchange Rate Revaluation Journals"
"label": "Ignore Exchange Rate Revaluation and Gain / Loss Journals"
},
{
"default": "0",
@@ -400,7 +400,7 @@
}
],
"links": [],
"modified": "2025-04-30 14:43:23.643006",
"modified": "2025-07-08 16:52:12.602384",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",

View File

@@ -82,6 +82,10 @@ class ProcessStatementOfAccounts(Document):
# end: auto-generated types
def validate(self):
self.validate_account()
self.validate_company_for_table("Cost Center")
self.validate_company_for_table("Project")
if not self.subject:
self.subject = "Statement Of Accounts for {{ customer.customer_name }}"
if not self.body:
@@ -104,6 +108,43 @@ class ProcessStatementOfAccounts(Document):
self.to_date = self.start_date
self.from_date = add_months(self.to_date, -1 * self.filter_duration)
def validate_account(self):
if not self.account:
return
if self.company != frappe.get_cached_value("Account", self.account, "company"):
frappe.throw(
_("Account {0} doesn't belong to Company {1}").format(
frappe.bold(self.account),
frappe.bold(self.company),
)
)
def validate_company_for_table(self, doctype):
field = frappe.scrub(doctype)
if not self.get(field):
return
fieldname = field + "_name"
values = set(d.get(fieldname) for d in self.get(field))
invalid_values = frappe.db.get_all(
doctype, filters={"name": ["in", values], "company": ["!=", self.company]}, pluck="name"
)
if invalid_values:
msg = _("<p>Following {0}s doesn't belong to Company {1} :</p>").format(
doctype, frappe.bold(self.company)
)
msg += (
"<ul>"
+ "".join(_("<li>{}</li>").format(frappe.bold(row)) for row in invalid_values)
+ "</ul>"
)
frappe.throw(_(msg))
def get_report_pdf(doc, consolidated=True):
statement_dict = get_statement_dict(doc)

View File

@@ -2497,6 +2497,10 @@ class TestSalesInvoice(FrappeTestCase):
for gle in gl_entries:
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
@change_settings(
"Accounts Settings",
{"book_deferred_entries_based_on": "Days", "book_deferred_entries_via_journal_entry": 0},
)
def test_deferred_revenue(self):
deferred_account = create_account(
account_name="Deferred Revenue",
@@ -2551,6 +2555,10 @@ class TestSalesInvoice(FrappeTestCase):
self.assertRaises(frappe.ValidationError, si.save)
@change_settings(
"Accounts Settings",
{"book_deferred_entries_based_on": "Months", "book_deferred_entries_via_journal_entry": 0},
)
def test_fixed_deferred_revenue(self):
deferred_account = create_account(
account_name="Deferred Revenue",
@@ -2558,10 +2566,6 @@ class TestSalesInvoice(FrappeTestCase):
company="_Test Company",
)
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
acc_settings.book_deferred_entries_based_on = "Months"
acc_settings.save()
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_revenue = 1
item.deferred_revenue_account = deferred_account
@@ -2601,10 +2605,6 @@ class TestSalesInvoice(FrappeTestCase):
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
acc_settings.book_deferred_entries_based_on = "Days"
acc_settings.save()
def test_validate_inter_company_transaction_address_links(self):
def _validate_address_link(address, link_doctype, link_name):
return frappe.db.get_value(
@@ -2853,7 +2853,9 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(si.items[0].rate, rate)
self.assertEqual(target_doc.items[0].rate, rate)
check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1))
check_gl_entries(
self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1), voucher_type="Purchase Invoice"
)
def test_internal_transfer_gl_precision_issues(self):
# Make a stock queue of an item with two valuations
@@ -4587,6 +4589,8 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date, voucher_type="
)
gl_entries = q.run(as_dict=True)
doc.assertGreater(len(gl_entries), 0)
for i, gle in enumerate(gl_entries):
doc.assertEqual(expected_gle[i][0], gle.account)
doc.assertEqual(expected_gle[i][1], gle.debit)

View File

@@ -12,6 +12,7 @@ from frappe.utils.data import comma_and
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
get_advance_payment_doctypes,
unlink_ref_doc_from_payment_entries,
update_voucher_outstanding,
)
@@ -84,7 +85,7 @@ class UnreconcilePayment(Document):
update_voucher_outstanding(
alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party
)
if doc.doctype in frappe.get_hooks("advance_payment_doctypes"):
if doc.doctype in get_advance_payment_doctypes():
doc.set_total_advance_paid()
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)

View File

@@ -692,7 +692,18 @@ def make_reverse_gl_entries(
query.run()
else:
if not immutable_ledger_enabled:
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
gle_names = [x.get("name") for x in gl_entries]
# if names are available, cancel only that set of entries
if not all(gle_names):
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
else:
frappe.db.sql(
"""UPDATE `tabGL Entry` SET is_cancelled = 1,
modified=%s, modified_by=%s
where name in %s and is_cancelled = 0""",
(now(), frappe.session.user, tuple(gle_names)),
)
for entry in gl_entries:
new_gle = copy.deepcopy(entry)

View File

@@ -15,7 +15,11 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
get_dimension_with_children,
)
from erpnext.accounts.utils import get_currency_precision, get_party_types_from_account_type
from erpnext.accounts.utils import (
get_advance_payment_doctypes,
get_currency_precision,
get_party_types_from_account_type,
)
# This report gives a summary of all Outstanding Invoices considering the following
@@ -62,6 +66,7 @@ class ReceivablePayableReport:
frappe.db.get_single_value("Accounts Settings", "receivable_payable_fetch_method")
or "Buffered Cursor"
) # Fail Safe
self.advance_payment_doctypes = get_advance_payment_doctypes()
def run(self, args):
self.filters.update(args)
@@ -85,6 +90,7 @@ class ReceivablePayableReport:
self.party_details = {}
self.invoices = set()
self.skip_total_row = 0
self.advance_payment_doctypes = get_advance_payment_doctypes()
if self.filters.get("group_by_party"):
self.previous_party = ""
@@ -181,7 +187,10 @@ class ReceivablePayableReport:
if key not in self.voucher_balance:
self.voucher_balance[key] = self.build_voucher_dict(ple)
if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no:
if (ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no) or (
ple.voucher_type in ("Payment Entry", "Journal Entry")
and ple.against_voucher_type in self.advance_payment_doctypes
):
self.voucher_balance[key].cost_center = ple.cost_center
self.get_invoices(ple)

View File

@@ -79,6 +79,14 @@ class Deferred_Item:
return - estimated amount to post for given period
Calculated based on already booked amount and item service period
"""
if self.filters.book_deferred_entries_based_on == "Months":
# if the deferred entries are based on service period, use service start and end date
return self.calculate_monthly_amount(start_date, end_date)
else:
return self.calculate_days_amount(start_date, end_date)
def calculate_monthly_amount(self, start_date, end_date):
total_months = (
(self.service_end_date.year - self.service_start_date.year) * 12
+ (self.service_end_date.month - self.service_start_date.month)
@@ -105,6 +113,19 @@ class Deferred_Item:
return base_amount
def calculate_days_amount(self, start_date, end_date):
base_amount = 0
total_days = date_diff(self.service_end_date, self.service_start_date) + 1
total_booking_days = date_diff(end_date, start_date) + 1
already_booked_amount = self.get_item_total()
base_amount = flt(self.base_net_amount * total_booking_days / flt(total_days))
if base_amount + already_booked_amount > self.base_net_amount:
base_amount = self.base_net_amount - already_booked_amount
return base_amount
def make_dummy_gle(self, name, date, amount):
"""
return - frappe._dict() of a dummy gle entry
@@ -245,6 +266,10 @@ class Deferred_Revenue_and_Expense_Report:
else:
self.filters = frappe._dict(filters)
self.filters.book_deferred_entries_based_on = frappe.db.get_singles_value(
"Accounts Settings", "book_deferred_entries_based_on"
)
self.period_list = None
self.deferred_invoices = []
# holds period wise total for report
@@ -289,7 +314,11 @@ class Deferred_Revenue_and_Expense_Report:
.join(inv)
.on(inv.name == inv_item.parent)
.left_join(gle)
.on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account))
.on(
(inv_item.name == gle.voucher_detail_no)
& (deferred_account_field == gle.account)
& (gle.is_cancelled == 0)
)
.select(
inv.name.as_("doc"),
inv.posting_date,

View File

@@ -9,7 +9,7 @@ import re
import frappe
from frappe import _
from frappe.query_builder.functions import Sum
from frappe.query_builder.functions import Max, Min, Sum
from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate
from pypika.terms import ExistsCriterion
@@ -109,14 +109,19 @@ def get_period_list(
def get_fiscal_year_data(from_fiscal_year, to_fiscal_year):
fiscal_year = frappe.db.sql(
"""select min(year_start_date) as year_start_date,
max(year_end_date) as year_end_date from `tabFiscal Year` where
name between %(from_fiscal_year)s and %(to_fiscal_year)s""",
{"from_fiscal_year": from_fiscal_year, "to_fiscal_year": to_fiscal_year},
as_dict=1,
from_year_start_date = frappe.get_cached_value("Fiscal Year", from_fiscal_year, "year_start_date")
to_year_end_date = frappe.get_cached_value("Fiscal Year", to_fiscal_year, "year_end_date")
fy = frappe.qb.DocType("Fiscal Year")
query = (
frappe.qb.from_(fy)
.select(Min(fy.year_start_date).as_("year_start_date"), Max(fy.year_end_date).as_("year_end_date"))
.where(fy.year_start_date >= from_year_start_date)
.where(fy.year_end_date <= to_year_end_date)
)
fiscal_year = query.run(as_dict=True)
return fiscal_year[0] if fiscal_year else {}

View File

@@ -209,7 +209,7 @@ frappe.query_reports["General Ledger"] = {
},
{
fieldname: "ignore_err",
label: __("Ignore Exchange Rate Revaluation Journals"),
label: __("Ignore Exchange Rate Revaluation and Gain / Loss Journals"),
fieldtype: "Check",
},
{

View File

@@ -6,12 +6,9 @@ from frappe import qb
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import flt, today
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.general_ledger.general_ledger import execute
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
class TestGeneralLedger(FrappeTestCase):
@@ -171,90 +168,6 @@ class TestGeneralLedger(FrappeTestCase):
self.assertEqual(data[3]["debit"], 100)
self.assertEqual(data[3]["credit"], 100)
@change_settings("Accounts Settings", {"delete_linked_ledger_entries": True})
def test_debit_in_exchange_gain_loss_account(self):
company = "_Test Company"
exchange_gain_loss_account = frappe.db.get_value("Company", "exchange_gain_loss_account")
if not exchange_gain_loss_account:
frappe.db.set_value(
"Company", company, "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
)
account_name = "_Test Receivable USD - _TC"
customer_name = "_Test Customer USD"
sales_invoice = create_sales_invoice(
company=company,
customer=customer_name,
currency="USD",
debit_to=account_name,
conversion_rate=85,
posting_date=today(),
)
payment_entry = create_payment_entry(
company=company,
party_type="Customer",
party=customer_name,
payment_type="Receive",
paid_from=account_name,
paid_from_account_currency="USD",
paid_to="Cash - _TC",
paid_to_account_currency="INR",
paid_amount=10,
do_not_submit=True,
)
payment_entry.base_paid_amount = 800
payment_entry.received_amount = 800
payment_entry.currency = "USD"
payment_entry.source_exchange_rate = 80
payment_entry.append(
"references",
frappe._dict(
{
"reference_doctype": "Sales Invoice",
"reference_name": sales_invoice.name,
"total_amount": 10,
"outstanding_amount": 10,
"exchange_rate": 85,
"allocated_amount": 10,
"exchange_gain_loss": -50,
}
),
)
payment_entry.save()
payment_entry.submit()
journal_entry = frappe.get_all(
"Journal Entry Account", filters={"reference_name": sales_invoice.name}, fields=["parent"]
)
columns, data = execute(
frappe._dict(
{
"company": company,
"from_date": today(),
"to_date": today(),
"include_dimensions": 1,
"include_default_book_entries": 1,
"account": ["_Test Exchange Gain/Loss - _TC"],
"categorize_by": "Categorize by Voucher (Consolidated)",
}
)
)
entry = data[1]
self.assertEqual(entry["debit"], 50)
self.assertEqual(entry["voucher_type"], "Journal Entry")
self.assertEqual(entry["voucher_no"], journal_entry[0]["parent"])
payment_entry.cancel()
payment_entry.delete()
sales_invoice.reload()
sales_invoice.cancel()
sales_invoice.delete()
def test_ignore_exchange_rate_journals_filter(self):
# create a new account with USD currency
account_name = "Test Debtors USD"

View File

@@ -5,13 +5,14 @@
import frappe
from frappe import _
from frappe.model.meta import get_field_precision
from frappe.query_builder import functions as fn
from frappe.utils import cstr, flt
from frappe.utils.nestedset import get_descendants_of
from frappe.utils.xlsxutils import handle_html
from pypika import Order
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
from erpnext.accounts.report.utils import get_values_for_columns
from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import (
get_customer_details,
)
@@ -433,7 +434,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
si.is_internal_customer,
si.customer,
si.remarks,
si.territory,
fn.IfNull(si.territory, "Not Specified").as_("territory"),
si.company,
si.base_net_total,
sii.project,
@@ -456,7 +457,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
sii.base_net_rate,
sii.base_net_amount,
si.customer_name,
si.customer_group,
fn.IfNull(si.customer_group, "Not Specified").as_("customer_group"),
sii.so_detail,
si.update_stock,
sii.uom,

View File

@@ -121,7 +121,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
)
out.append(row)
out.sort(key=lambda x: x["section_code"])
out.sort(key=lambda x: (x["section_code"], x["transaction_date"]))
return out

View File

@@ -67,11 +67,12 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
mid_year = add_to_date(fiscal_year[1], months=6)
tds_doc = frappe.get_doc("Tax Withholding Category", "TDS - 3")
tds_doc.rates[0].to_date = mid_year
from_date = add_to_date(mid_year, days=1)
tds_doc.append(
"rates",
{
"tax_withholding_rate": 20,
"from_date": add_to_date(mid_year, days=1),
"from_date": from_date,
"to_date": fiscal_year[2],
"single_threshold": 1,
"cumulative_threshold": 1,
@@ -80,18 +81,19 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
tds_doc.save()
inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True)
inv_1 = make_purchase_invoice(
rate=1000, posting_date=add_to_date(fiscal_year[1], days=1), do_not_save=True, do_not_submit=True
)
inv_1.set_posting_time = 1
inv_1.apply_tds = 1
inv_1.tax_withholding_category = "TDS - 3"
inv_1.tax_withholding_category = tds_doc.name
inv_1.save()
inv_1.submit()
inv_2 = make_purchase_invoice(
rate=1000, do_not_submit=True, posting_date=add_to_date(mid_year, days=1), do_not_save=True
)
inv_2 = make_purchase_invoice(rate=1000, posting_date=from_date, do_not_save=True, do_not_submit=True)
inv_2.set_posting_time = 1
inv_1.apply_tds = 1
inv_2.tax_withholding_category = "TDS - 3"
inv_2.apply_tds = 1
inv_2.tax_withholding_category = tds_doc.name
inv_2.save()
inv_2.submit()

View File

@@ -107,11 +107,7 @@ def convert_to_presentation_currency(gl_entries, currency_info):
credit_in_account_currency = flt(entry["credit_in_account_currency"])
account_currency = entry["account_currency"]
if (
len(account_currencies) == 1
and account_currency == presentation_currency
and (debit_in_account_currency or credit_in_account_currency)
):
if len(account_currencies) == 1 and account_currency == presentation_currency:
entry["debit"] = debit_in_account_currency
entry["credit"] = credit_in_account_currency
else:

View File

@@ -169,7 +169,7 @@ def validate_fiscal_year(date, fiscal_year, company, label="Date", doc=None):
if doc:
doc.fiscal_year = years[0]
else:
throw(_("{0} '{1}' not in Fiscal Year {2}").format(label, formatdate(date), fiscal_year))
throw(_("{0} '{1}' not in Fiscal Year {2}").format(_(label), formatdate(date), fiscal_year))
@frappe.whitelist()
@@ -629,6 +629,7 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
# Update Advance Paid in SO/PO since they might be getting unlinked
update_advance_paid = []
if jv_detail.get("reference_type") in ["Sales Order", "Purchase Order"]:
update_advance_paid.append((jv_detail.reference_type, jv_detail.reference_name))
@@ -713,23 +714,8 @@ def update_reference_in_payment_entry(
update_advance_paid = []
# Update Reconciliation effect date in reference
reconciliation_takes_effect_on = frappe.get_cached_value(
"Company", payment_entry.company, "reconciliation_takes_effect_on"
)
if payment_entry.book_advance_payments_in_separate_party_account:
if reconciliation_takes_effect_on == "Advance Payment Date":
reconcile_on = payment_entry.posting_date
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
date_field = "posting_date"
if d.against_voucher_type in ["Sales Order", "Purchase Order"]:
date_field = "transaction_date"
reconcile_on = frappe.db.get_value(d.against_voucher_type, d.against_voucher, date_field)
if getdate(reconcile_on) < getdate(payment_entry.posting_date):
reconcile_on = payment_entry.posting_date
elif reconciliation_takes_effect_on == "Reconciliation Date":
reconcile_on = nowdate()
reconcile_on = get_reconciliation_effect_date(d, payment_entry.company, payment_entry.posting_date)
reference_details.update({"reconcile_effect_on": reconcile_on})
if d.voucher_detail_no:
@@ -783,6 +769,28 @@ def update_reference_in_payment_entry(
return row, update_advance_paid
def get_reconciliation_effect_date(reference, company, posting_date):
reconciliation_takes_effect_on = frappe.get_cached_value(
"Company", company, "reconciliation_takes_effect_on"
)
if reconciliation_takes_effect_on == "Advance Payment Date":
reconcile_on = posting_date
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
date_field = "posting_date"
if reference.against_voucher_type in ["Sales Order", "Purchase Order"]:
date_field = "transaction_date"
reconcile_on = frappe.db.get_value(
reference.against_voucher_type, reference.against_voucher, date_field
)
if getdate(reconcile_on) < getdate(posting_date):
reconcile_on = posting_date
elif reconciliation_takes_effect_on == "Reconciliation Date":
reconcile_on = nowdate()
return reconcile_on
def cancel_exchange_gain_loss_journal(
parent_doc: dict | object, referenced_dt: str | None = None, referenced_dn: str | None = None
) -> None:
@@ -997,58 +1005,79 @@ def remove_ref_doc_link_from_pe(
per = qb.DocType("Payment Entry Reference")
pay = qb.DocType("Payment Entry")
linked_pe = (
query = (
qb.from_(per)
.select(per.parent)
.where((per.reference_doctype == ref_type) & (per.reference_name == ref_no) & (per.docstatus.lt(2)))
.run(as_list=1)
)
linked_pe = convert_to_list(linked_pe)
# remove reference only from specified payment
linked_pe = [x for x in linked_pe if x == payment_name] if payment_name else linked_pe
if linked_pe:
update_query = (
qb.update(per)
.set(per.allocated_amount, 0)
.set(per.modified, now())
.set(per.modified_by, frappe.session.user)
.where(per.docstatus.lt(2) & (per.reference_doctype == ref_type) & (per.reference_name == ref_no))
.select("*")
.where(
(per.reference_doctype == ref_type)
& (per.reference_name == ref_no)
& (per.docstatus.lt(2))
& (per.parenttype == "Payment Entry")
)
)
if payment_name:
update_query = update_query.where(per.parent == payment_name)
# update reference only from specified payment
if payment_name:
query = query.where(per.parent == payment_name)
update_query.run()
reference_rows = query.run(as_dict=True)
for pe in linked_pe:
try:
pe_doc = frappe.get_doc("Payment Entry", pe)
pe_doc.set_amounts()
if not reference_rows:
return
# Call cancel on only removed reference
references = [
x
for x in pe_doc.references
if x.reference_doctype == ref_type and x.reference_name == ref_no
]
[pe_doc.make_advance_gl_entries(x, cancel=1) for x in references]
linked_pe = set()
row_names = set()
pe_doc.clear_unallocated_reference_document_rows()
pe_doc.validate_payment_type_with_outstanding()
except Exception:
msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name)
msg += "<br>"
msg += _("Please cancel payment entry manually first")
frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error"))
for row in reference_rows:
linked_pe.add(row.parent)
row_names.add(row.name)
qb.update(pay).set(pay.total_allocated_amount, pe_doc.total_allocated_amount).set(
pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount
).set(pay.unallocated_amount, pe_doc.unallocated_amount).set(pay.modified, now()).set(
pay.modified_by, frappe.session.user
).where(pay.name == pe).run()
from erpnext.accounts.doctype.payment_request.payment_request import (
update_payment_requests_as_per_pe_references,
)
frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe)))
# Update payment request amount
update_payment_requests_as_per_pe_references(reference_rows, cancel=True)
# Update allocated amounts and modified fields in one go
(
qb.update(per)
.set(per.allocated_amount, 0)
.set(per.modified, now())
.set(per.modified_by, frappe.session.user)
.where(per.name.isin(row_names))
.where(per.parenttype == "Payment Entry")
.run()
)
for pe in linked_pe:
try:
pe_doc = frappe.get_doc("Payment Entry", pe)
pe_doc.set_amounts()
# Call cancel on only removed reference
references = [x for x in pe_doc.references if x.name in row_names]
[pe_doc.make_advance_gl_entries(x, cancel=1) for x in references]
pe_doc.clear_unallocated_reference_document_rows()
pe_doc.validate_payment_type_with_outstanding()
except Exception:
msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name)
msg += "<br>"
msg += _("Please cancel payment entry manually first")
frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error"))
(
qb.update(pay)
.set(pay.total_allocated_amount, pe_doc.total_allocated_amount)
.set(pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount)
.set(pay.unallocated_amount, pe_doc.unallocated_amount)
.set(pay.modified, now())
.set(pay.modified_by, frappe.session.user)
.where(pay.name == pe)
.run()
)
frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe)))
@frappe.whitelist()
@@ -2235,6 +2264,15 @@ def get_party_types_from_account_type(account_type):
return frappe.db.get_all("Party Type", {"account_type": account_type}, pluck="name")
def get_advance_payment_doctypes():
"""
Get list of advance payment doctypes based on type.
:param type: Optional, can be "receivable" or "payable". If not provided, returns both.
"""
return frappe.get_hooks("advance_payment_doctypes")
def run_ledger_health_checks():
health_monitor_settings = frappe.get_doc("Ledger Health Monitor")
if health_monitor_settings.enable_health_monitor:

View File

@@ -807,11 +807,19 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
target.credit_to = get_party_account("Supplier", source.supplier, source.company)
def update_item(obj, target, source_parent):
target.amount = flt(obj.amount) - flt(obj.billed_amt)
target.base_amount = target.amount * flt(source_parent.conversion_rate)
target.qty = (
target.amount / flt(obj.rate) if (flt(obj.rate) and flt(obj.billed_amt)) else flt(obj.qty)
)
def get_billed_qty(po_item_name):
from frappe.query_builder.functions import Sum
table = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(table)
.select(Sum(table.qty).as_("qty"))
.where((table.docstatus == 1) & (table.po_detail == po_item_name))
)
return query.run(pluck="qty")[0] or 0
billed_qty = flt(get_billed_qty(obj.name))
target.qty = flt(obj.qty) - billed_qty
item = get_item_defaults(target.item_code, source_parent.company)
item_group = get_item_group_defaults(target.item_code, source_parent.company)

View File

@@ -1286,6 +1286,25 @@ class TestPurchaseOrder(FrappeTestCase):
self.assertFalse(po.per_billed)
self.assertEqual(po.status, "To Receive and Bill")
@change_settings("Buying Settings", {"maintain_same_rate": 0})
def test_purchase_invoice_creation_with_partial_qty(self):
po = create_purchase_order(qty=100, rate=10)
pi = make_pi_from_po(po.name)
pi.items[0].qty = 42
pi.items[0].rate = 7.5
pi.submit()
pi = make_pi_from_po(po.name)
self.assertEqual(pi.items[0].qty, 58)
self.assertEqual(pi.items[0].rate, 10)
pi.items[0].qty = 8
pi.items[0].rate = 5
pi.submit()
pi = make_pi_from_po(po.name)
self.assertEqual(pi.items[0].qty, 50)
def create_po_for_sc_testing():
from erpnext.controllers.tests.test_subcontracting_controller import (

View File

@@ -51,6 +51,9 @@ from erpnext.accounts.utils import (
get_fiscal_years,
validate_fiscal_year,
)
from erpnext.accounts.utils import (
get_advance_payment_doctypes as _get_advance_payment_doctypes,
)
from erpnext.buying.utils import update_last_purchase_rate
from erpnext.controllers.print_settings import (
set_print_templates_for_item_table,
@@ -386,9 +389,7 @@ class AccountsController(TransactionBase):
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:
if self.doctype in self.get_advance_payment_doctypes():
qb.from_(adv).delete().where(
adv.against_voucher_type.eq(self.doctype) & adv.against_voucher_no.eq(self.name)
).run()
@@ -2912,7 +2913,7 @@ class AccountsController(TransactionBase):
repost_ledger.submit()
def get_advance_payment_doctypes(self) -> list:
return frappe.get_hooks("advance_payment_doctypes")
return _get_advance_payment_doctypes()
def make_advance_payment_ledger_for_journal(self):
advance_payment_doctypes = self.get_advance_payment_doctypes()
@@ -3965,6 +3966,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
).format(frappe.bold(parent.name))
)
else: # Sales Order
parent.validate_selling_price()
parent.validate_for_duplicate_items()
parent.validate_warehouse()
parent.update_reserved_qty()

View File

@@ -1648,8 +1648,9 @@ def make_quality_inspections(doctype, docname, items):
"sample_size": flt(item.get("sample_size")),
"item_serial_no": item.get("serial_no").split("\n")[0] if item.get("serial_no") else None,
"batch_no": item.get("batch_no"),
"child_row_reference": item.get("child_row_reference"),
}
).insert()
)
quality_inspection.save()
inspections.append(quality_inspection.name)
@@ -1662,14 +1663,9 @@ def is_reposting_pending():
)
def future_sle_exists(args, sl_entries=None, allow_force_reposting=True):
def future_sle_exists(args, sl_entries=None):
from erpnext.stock.utils import get_combine_datetime
if allow_force_reposting and frappe.db.get_single_value(
"Stock Reposting Settings", "do_reposting_for_each_stock_transaction"
):
return True
key = (args.voucher_type, args.voucher_no)
if not hasattr(frappe.local, "future_sle"):
frappe.local.future_sle = {}

View File

@@ -601,12 +601,15 @@ class SubcontractingController(StockController):
rm_obj.use_serial_batch_fields = 1
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
if self.doctype == "Subcontracting Receipt" and not use_serial_batch_fields:
rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle(
item_row, rm_obj, rm_obj.consumed_qty
)
if self.doctype == "Subcontracting Receipt":
if not use_serial_batch_fields:
rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle(
item_row, rm_obj, rm_obj.consumed_qty
)
self.set_rate_for_supplied_items(rm_obj, item_row)
self.set_rate_for_supplied_items(rm_obj, item_row)
elif self.backflush_based_on == "BOM":
self.update_rate_for_supplied_items()
def update_rate_for_supplied_items(self):
if self.doctype != "Subcontracting Receipt":

View File

@@ -987,7 +987,7 @@ class BOM(WebsiteGenerator):
self.transfer_material_against = "Work Order"
if not self.transfer_material_against and not self.is_new():
frappe.throw(
_("Setting {} is required").format(self.meta.get_label("transfer_material_against")),
_("Setting {0} is required").format(_(self.meta.get_label("transfer_material_against"))),
title=_("Missing value"),
)

View File

@@ -51,9 +51,13 @@ frappe.ui.form.on("Job Card", {
let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
if (to_request || excess_transfer_allowed) {
frm.add_custom_button(__("Material Request"), () => {
frm.trigger("make_material_request");
});
frm.add_custom_button(
__("Material Request"),
() => {
frm.trigger("make_material_request");
},
__("Create")
);
}
// check if any row has untransferred materials
@@ -61,9 +65,13 @@ frappe.ui.form.on("Job Card", {
let to_transfer = frm.doc.items.some((row) => row.transferred_qty < row.required_qty);
if (to_transfer || excess_transfer_allowed) {
frm.add_custom_button(__("Material Transfer"), () => {
frm.trigger("make_stock_entry");
}).addClass("btn-primary");
frm.add_custom_button(
__("Material Transfer"),
() => {
frm.trigger("make_stock_entry");
},
__("Create")
);
}
}

View File

@@ -390,7 +390,7 @@ class WorkOrder(Document):
if qty > completed_qty:
frappe.throw(
_("{0} ({1}) cannot be greater than planned quantity ({2}) in Work Order {3}").format(
self.meta.get_label(fieldname), qty, completed_qty, self.name
_(self.meta.get_label(fieldname)), qty, completed_qty, self.name
),
StockOverProductionError,
)
@@ -1077,7 +1077,7 @@ class WorkOrder(Document):
self.transfer_material_against = "Work Order"
if not self.transfer_material_against:
frappe.throw(
_("Setting {} is required").format(self.meta.get_label("transfer_material_against")),
_("Setting {0} is required").format(_(self.meta.get_label("transfer_material_against"))),
title=_("Missing value"),
)

View File

@@ -411,3 +411,4 @@ erpnext.patches.v14_0.update_full_name_in_contract
erpnext.patches.v15_0.drop_sle_indexes
erpnext.patches.v15_0.update_pick_list_fields
erpnext.patches.v15_0.update_pegged_currencies
erpnext.patches.v15_0.set_company_on_pos_inv_merge_log

View File

@@ -0,0 +1,12 @@
import frappe
def execute():
pos_invoice_merge_logs = frappe.db.get_all(
"POS Invoice Merge Log", {"docstatus": 1}, ["name", "pos_closing_entry"]
)
for log in pos_invoice_merge_logs:
if log.pos_closing_entry and frappe.db.exists("POS Closing Entry", log.pos_closing_entry):
company = frappe.db.get_value("POS Closing Entry", log.pos_closing_entry, "company")
frappe.db.set_value("POS Invoice Merge Log", log.name, "company", company)

View File

@@ -202,6 +202,12 @@ frappe.ui.form.on("Project", {
});
});
},
collect_progress: function (frm) {
if (frm.doc.collect_progress && !frm.doc.subject) {
frm.set_value("subject", __("For project {0}, update your status", [frm.doc.name]));
}
},
});
function open_form(frm, doctype, child_doctype, parentfield) {

View File

@@ -62,6 +62,7 @@
"day_to_send",
"weekly_time_to_send",
"column_break_45",
"subject",
"message"
],
"fields": [
@@ -447,6 +448,13 @@
"print_hide": 1,
"reqd": 1,
"set_only_once": 1
},
{
"depends_on": "collect_progress",
"fieldname": "subject",
"fieldtype": "Data",
"label": "Subject",
"mandatory_depends_on": "collect_progress"
}
],
"icon": "fa fa-puzzle-piece",
@@ -454,7 +462,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 4,
"modified": "2024-04-24 10:56:16.001032",
"modified": "2025-07-03 10:54:30.444139",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project",
@@ -501,6 +509,7 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"search_fields": "project_name,customer, status, priority, is_active",
"show_name_in_global_search": 1,
"sort_field": "modified",
@@ -509,4 +518,4 @@
"timeline_field": "customer",
"title_field": "project_name",
"track_seen": 1
}
}

View File

@@ -62,6 +62,7 @@ class Project(Document):
sales_order: DF.Link | None
second_email: DF.Time | None
status: DF.Literal["Open", "Completed", "Cancelled"]
subject: DF.Data | None
to_time: DF.Time | None
total_billable_amount: DF.Currency
total_billed_amount: DF.Currency
@@ -606,8 +607,6 @@ def send_project_update_email_to_users(project):
}
).insert()
subject = "For project %s, update your status" % (project)
incoming_email_account = frappe.db.get_value(
"Email Account", dict(enable_incoming=1, default_incoming=1), "email_id"
)
@@ -615,7 +614,7 @@ def send_project_update_email_to_users(project):
frappe.sendmail(
recipients=get_users_email(doc),
message=doc.message,
subject=_(subject),
subject=doc.subject,
reference_doctype=project_update.doctype,
reference_name=project_update.name,
reply_to=incoming_email_account,

View File

@@ -581,7 +581,8 @@ erpnext.buying.get_items_from_product_bundle = function(frm) {
transaction_date: frm.doc.transaction_date || frm.doc.posting_date,
ignore_pricing_rule: frm.doc.ignore_pricing_rule,
doctype: frm.doc.doctype
}
},
price_list: frm.doc.price_list,
},
freeze: true,
callback: function(r) {

View File

@@ -371,6 +371,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
"inspection_type": inspection_type,
"reference_type": me.frm.doc.doctype,
"reference_name": me.frm.doc.name,
"child_row_reference": row.doc.name,
"item_code": row.doc.item_code,
"description": row.doc.description,
"item_serial_no": row.doc.serial_no ? row.doc.serial_no.split("\n")[0] : null,
@@ -385,7 +386,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
docstatus: ["<", 2],
inspection_type: inspection_type,
reference_name: doc.name,
item_code: d.item_code
item_code: d.item_code,
child_row_reference : d.name
}
}
});
@@ -2427,12 +2429,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
fields: fields,
primary_action: function () {
const data = dialog.get_values();
const selected_data = data.items.filter(item => item?.__checked == 1 );
frappe.call({
method: "erpnext.controllers.stock_controller.make_quality_inspections",
args: {
doctype: me.frm.doc.doctype,
docname: me.frm.doc.name,
items: data.items
items: selected_data,
},
freeze: true,
callback: function (r) {

View File

@@ -1063,21 +1063,31 @@
justify-content: flex-end;
padding-right: var(--padding-sm);
> .customer-name {
font-size: var(--text-2xl);
font-weight: 700;
}
> .customer-section {
margin-bottom: auto;
> .customer-email {
font-size: var(--text-md);
font-weight: 500;
> .customer-name {
font-size: var(--text-2xl);
font-weight: 700;
}
> .customer-code {
font-size: var(--text-xs);
font-weight: 500;
color: var(--text-light);
}
> .customer-email {
font-size: var(--text-md);
font-weight: 500;
}
}
> .cashier {
font-size: var(--text-md);
font-weight: 500;
color: var(--gray-600);
margin-top: auto;
margin-top: var(--margin-md);
}
}
@@ -1085,7 +1095,6 @@
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: space-between;
> .paid-amount {
font-size: var(--text-2xl);

View File

@@ -117,14 +117,15 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
if (doc.docstatus == 1 && !["Lost", "Ordered"].includes(doc.status)) {
if (
frappe.boot.sysdefaults.allow_sales_order_creation_for_expired_quotation ||
!doc.valid_till ||
frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0
frappe.model.can_create("Sales Order") &&
(frappe.boot.sysdefaults.allow_sales_order_creation_for_expired_quotation ||
!doc.valid_till ||
frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0)
) {
this.frm.add_custom_button(__("Sales Order"), () => this.make_sales_order(), __("Create"));
}
if (doc.status !== "Ordered") {
if (doc.status !== "Ordered" && this.frm.has_perm("write")) {
this.frm.add_custom_button(__("Set as Lost"), () => {
this.frm.trigger("set_as_lost_dialog");
});
@@ -133,7 +134,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
}
if (this.frm.doc.docstatus === 0) {
if (this.frm.doc.docstatus === 0 && frappe.model.can_read("Opportunity")) {
this.frm.add_custom_button(
__("Opportunity"),
function () {

View File

@@ -338,7 +338,7 @@ def create_opening_voucher(pos_profile, company, balance_details):
@frappe.whitelist()
def get_past_order_list(search_term, status, limit=20):
fields = ["name", "grand_total", "currency", "customer", "posting_time", "posting_date"]
fields = ["name", "grand_total", "currency", "customer", "customer_name", "posting_time", "posting_date"]
invoice_list = []
if search_term and status:

View File

@@ -106,7 +106,7 @@ erpnext.PointOfSale.PastOrderList = class {
<svg class="mr-2" width="12" height="12" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
</svg>
${frappe.ellipsis(invoice.customer, 20)}
${frappe.ellipsis(invoice.customer_name, 20)}
</div>
</div>
<div class="invoice-total-status">

View File

@@ -73,14 +73,18 @@ erpnext.PointOfSale.PastOrderSummary = class {
get_upper_section_html(doc) {
const { status } = doc;
let indicator_color = "";
const is_customer_naming_by_customer_name = frappe.sys_defaults.cust_master_name !== "Customer Name";
["Paid", "Consolidated"].includes(status) && (indicator_color = "green");
status === "Draft" && (indicator_color = "red");
status === "Return" && (indicator_color = "grey");
return `<div class="left-section">
<div class="customer-name">${doc.customer}</div>
<div class="customer-email">${this.customer_email}</div>
<div class="customer-section">
<div class="customer-name">${doc.customer_name}</div>
${is_customer_naming_by_customer_name ? `<div class="customer-code">${doc.customer}</div>` : ""}
<div class="customer-email">${this.customer_email}</div>
</div>
<div class="cashier">${__("Sold by")}: ${doc.owner}</div>
</div>
<div class="right-section">

View File

@@ -395,7 +395,7 @@ class EmailDigest(Document):
label = get_link_to_report(
"General Ledger",
self.meta.get_label("income"),
_(self.meta.get_label("income")),
filters={
"from_date": self.future_from_date,
"to_date": self.future_to_date,
@@ -427,7 +427,7 @@ class EmailDigest(Document):
filters = {"currency": self.currency}
label = get_link_to_report(
"Profit and Loss Statement",
label=self.meta.get_label(root_type + "_year_to_date"),
label=_(self.meta.get_label(root_type + "_year_to_date")),
filters=filters,
)
@@ -435,7 +435,7 @@ class EmailDigest(Document):
filters = {"currency": self.currency}
label = get_link_to_report(
"Profit and Loss Statement",
label=self.meta.get_label(root_type + "_year_to_date"),
label=_(self.meta.get_label(root_type + "_year_to_date")),
filters=filters,
)
@@ -466,7 +466,7 @@ class EmailDigest(Document):
label = get_link_to_report(
"General Ledger",
self.meta.get_label("expenses_booked"),
_(self.meta.get_label("expenses_booked")),
filters={
"company": self.company,
"from_date": self.future_from_date,
@@ -500,7 +500,7 @@ class EmailDigest(Document):
label = get_link_to_report(
"Sales Order",
label=self.meta.get_label("sales_orders_to_bill"),
label=_(self.meta.get_label("sales_orders_to_bill")),
report_type="Report Builder",
doctype="Sales Order",
filters={
@@ -526,7 +526,7 @@ class EmailDigest(Document):
label = get_link_to_report(
"Sales Order",
label=self.meta.get_label("sales_orders_to_deliver"),
label=_(self.meta.get_label("sales_orders_to_deliver")),
report_type="Report Builder",
doctype="Sales Order",
filters={
@@ -552,7 +552,7 @@ class EmailDigest(Document):
label = get_link_to_report(
"Purchase Order",
label=self.meta.get_label("purchase_orders_to_receive"),
label=_(self.meta.get_label("purchase_orders_to_receive")),
report_type="Report Builder",
doctype="Purchase Order",
filters={
@@ -578,7 +578,7 @@ class EmailDigest(Document):
label = get_link_to_report(
"Purchase Order",
label=self.meta.get_label("purchase_orders_to_bill"),
label=_(self.meta.get_label("purchase_orders_to_bill")),
report_type="Report Builder",
doctype="Purchase Order",
filters={
@@ -630,7 +630,7 @@ class EmailDigest(Document):
"company": self.company,
}
label = get_link_to_report(
"Account Balance", label=self.meta.get_label(fieldname), filters=filters
"Account Balance", label=_(self.meta.get_label(fieldname)), filters=filters
)
else:
filters = {
@@ -640,7 +640,7 @@ class EmailDigest(Document):
"company": self.company,
}
label = get_link_to_report(
"Account Balance", label=self.meta.get_label(fieldname), filters=filters
"Account Balance", label=_(self.meta.get_label(fieldname)), filters=filters
)
return {"label": label, "value": balance, "last_value": prev_balance}
@@ -648,17 +648,17 @@ class EmailDigest(Document):
if account_type == "Payable":
label = get_link_to_report(
"Accounts Payable",
label=self.meta.get_label(fieldname),
label=_(self.meta.get_label(fieldname)),
filters={"report_date": self.future_to_date, "company": self.company},
)
elif account_type == "Receivable":
label = get_link_to_report(
"Accounts Receivable",
label=self.meta.get_label(fieldname),
label=_(self.meta.get_label(fieldname)),
filters={"report_date": self.future_to_date, "company": self.company},
)
else:
label = self.meta.get_label(fieldname)
label = _(self.meta.get_label(fieldname))
return {"label": label, "value": balance, "last_value": prev_balance, "count": count}
@@ -748,7 +748,7 @@ class EmailDigest(Document):
label = get_link_to_report(
"Quotation",
label=self.meta.get_label(fieldname),
label=_(self.meta.get_label(fieldname)),
report_type="Report Builder",
doctype="Quotation",
filters={
@@ -779,7 +779,7 @@ class EmailDigest(Document):
label = get_link_to_report(
doc_type,
label=self.meta.get_label(fieldname),
label=_(self.meta.get_label(fieldname)),
report_type="Report Builder",
filters=filters,
doctype=doc_type,

View File

@@ -264,7 +264,7 @@ def update_qty(bin_name, args):
actual_qty = bin_details.actual_qty or 0.0
# actual qty is not up to date in case of backdated transaction
if future_sle_exists(args, allow_force_reposting=False):
if future_sle_exists(args):
actual_qty = get_actual_qty(args.get("item_code"), args.get("warehouse"))
ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty"))

View File

@@ -1281,6 +1281,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"doctype": target_doctype,
"postprocess": update_details,
"field_no_map": ["taxes_and_charges", "set_warehouse"],
"field_map": {"shipping_address_name": "shipping_address"},
},
doctype + " Item": {
"doctype": target_doctype + " Item",

View File

@@ -75,7 +75,9 @@ frappe.ui.form.on("Inventory Dimension", {
set_parent_fields(frm) {
if (frm.doc.apply_to_all_doctypes) {
frm.set_df_property("fetch_from_parent", "options", frm.doc.reference_document);
let options = ["\n", frm.doc.reference_document];
frm.set_df_property("fetch_from_parent", "options", options);
} else if (frm.doc.document_type && frm.doc.istable) {
frappe.call({
method: "erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_parent_fields",
@@ -85,7 +87,7 @@ frappe.ui.form.on("Inventory Dimension", {
},
callback: (r) => {
if (r.message && r.message.length) {
frm.set_df_property("fetch_from_parent", "options", [""].concat(r.message));
frm.set_df_property("fetch_from_parent", "options", ["\n"].concat(r.message));
} else {
frm.set_df_property("fetch_from_parent", "hidden", 1);
}

View File

@@ -143,7 +143,6 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:!doc.apply_to_all_doctypes",
"description": "Set fieldname from which you want to fetch the data from the parent form.",
"fieldname": "fetch_from_parent",
"fieldtype": "Select",
@@ -189,7 +188,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-07-08 08:58:50.228211",
"modified": "2025-07-07 15:51:29.329064",
"modified_by": "Administrator",
"module": "Stock",
"name": "Inventory Dimension",

View File

@@ -64,16 +64,11 @@ class InventoryDimension(Document):
self.reset_value()
self.set_source_and_target_fieldname()
self.set_type_of_transaction()
self.set_fetch_value_from()
def set_type_of_transaction(self):
if self.apply_to_all_doctypes:
self.type_of_transaction = "Both"
def set_fetch_value_from(self):
if self.apply_to_all_doctypes:
self.fetch_from_parent = self.reference_document
def do_not_update_document(self):
if self.is_new() or not self.has_stock_ledger():
return

View File

@@ -155,6 +155,8 @@ class TestInventoryDimension(FrappeTestCase):
reference_document="Rack", dimension_name="Rack", apply_to_all_doctypes=1
)
inv_dimension.db_set("fetch_from_parent", "Rack")
self.assertEqual(inv_dimension.type_of_transaction, "Both")
self.assertEqual(inv_dimension.fetch_from_parent, "Rack")

View File

@@ -634,7 +634,7 @@ class Item(Document):
if new_properties != [cstr(self.get(field)) for field in field_list]:
msg = _("To merge, following properties must be same for both items")
msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list])
msg += ": \n" + ", ".join([_(self.meta.get_label(fld)) for fld in field_list])
frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError)
def validate_duplicate_product_bundles_before_merge(self, old_name, new_name):
@@ -981,7 +981,7 @@ class Item(Document):
return
if linked_doc := self._get_linked_submitted_documents(changed_fields):
changed_field_labels = [frappe.bold(self.meta.get_label(f)) for f in changed_fields]
changed_field_labels = [frappe.bold(_(self.meta.get_label(f))) for f in changed_fields]
msg = _(
"As there are existing submitted transactions against item {0}, you can not change the value of {1}."
).format(self.name, ", ".join(changed_field_labels))

View File

@@ -332,5 +332,6 @@ def get_pr_items(purchase_receipt):
(pr_item.parent == purchase_receipt.receipt_document)
& ((item.is_stock_item == 1) | (item.is_fixed_asset == 1))
)
.orderby(pr_item.idx)
.run(as_dict=True)
)

View File

@@ -42,6 +42,15 @@ frappe.ui.form.on("Material Request", {
},
};
});
frm.set_query("price_list", () => {
return {
filters: {
buying: 1,
enabled: 1,
},
};
});
},
onload: function (frm) {
@@ -70,6 +79,7 @@ frappe.ui.form.on("Material Request", {
});
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
frm.doc.price_list = frappe.defaults.get_default("buying_price_list");
},
company: function (frm) {
@@ -245,7 +255,9 @@ frappe.ui.form.on("Material Request", {
from_warehouse: item.from_warehouse,
warehouse: item.warehouse,
doctype: frm.doc.doctype,
buying_price_list: frappe.defaults.get_default("buying_price_list"),
buying_price_list: frm.doc.price_list
? frm.doc.price_list
: frappe.defaults.get_default("buying_price_list"),
currency: frappe.defaults.get_default("Currency"),
name: frm.doc.name,
qty: item.qty || 1,

View File

@@ -16,6 +16,7 @@
"column_break_2",
"transaction_date",
"schedule_date",
"price_list",
"amended_from",
"warehouse_section",
"scan_barcode",
@@ -351,13 +352,19 @@
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
},
{
"fieldname": "price_list",
"fieldtype": "Link",
"label": "Price List",
"options": "Price List"
}
],
"icon": "fa fa-ticket",
"idx": 70,
"is_submittable": 1,
"links": [],
"modified": "2025-04-21 18:36:04.827917",
"modified": "2025-07-07 13:15:28.615984",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request",
@@ -425,10 +432,11 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"search_fields": "status,transaction_date",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "title"
}
}

View File

@@ -8,6 +8,7 @@
import json
import frappe
import frappe.defaults
from frappe import _, msgprint
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum
@@ -45,6 +46,7 @@ class MaterialRequest(BuyingController):
naming_series: DF.Literal["MAT-MR-.YYYY.-"]
per_ordered: DF.Percent
per_received: DF.Percent
price_list: DF.Link | None
scan_barcode: DF.Data | None
schedule_date: DF.Date | None
select_print_heading: DF.Link | None
@@ -151,6 +153,9 @@ class MaterialRequest(BuyingController):
self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
if not self.price_list:
self.price_list = frappe.defaults.get_defaults().buying_price_list
def before_update_after_submit(self):
self.validate_schedule_date()
@@ -764,10 +769,11 @@ def raise_work_orders(material_request):
"material_request_item": d.name,
"planned_start_date": mr.transaction_date,
"company": mr.company,
"project": d.project,
}
)
wo_order.set_work_order_operations()
wo_order.get_items_and_operations_from_bom()
wo_order.flags.ignore_validate = True
wo_order.flags.ignore_mandatory = True
wo_order.save()

View File

@@ -7,6 +7,7 @@
import json
import frappe
import frappe.defaults
from frappe.model.document import Document
from frappe.utils import flt
@@ -339,12 +340,20 @@ def on_doctype_update():
@frappe.whitelist()
def get_items_from_product_bundle(row):
def get_items_from_product_bundle(row, price_list):
row, items = json.loads(row), []
bundled_items = get_product_bundle_items(row["item_code"])
for item in bundled_items:
row.update({"item_code": item.item_code, "qty": flt(row["quantity"]) * flt(item.qty)})
row.update(
{
"item_code": item.item_code,
"qty": flt(row["quantity"]) * flt(item.qty),
"conversion_rate": 1,
"price_list": price_list,
"currency": frappe.defaults.get_defaults().currency,
}
)
items.append(get_item_details(row))
return items

View File

@@ -97,51 +97,25 @@ class QualityInspection(Document):
if self.reference_type == "Stock Entry":
doctype = "Stock Entry Detail"
child_row_references = frappe.get_all(
doctype,
filters={"parent": self.reference_name, "item_code": self.item_code},
pluck="name",
)
child_doc = frappe.qb.DocType(doctype)
qi_doc = frappe.qb.DocType("Quality Inspection")
if not child_row_references:
return
child_row_references = (
frappe.qb.from_(child_doc)
.left_join(qi_doc)
.on(child_doc.name == qi_doc.child_row_reference)
.select(child_doc.name)
.where(
(child_doc.item_code == self.item_code)
& (child_doc.parent == self.reference_name)
& (child_doc.docstatus < 2)
& (qi_doc.name.isnull())
)
.orderby(child_doc.idx)
).run(pluck=True)
if len(child_row_references) == 1:
if len(child_row_references):
self.child_row_reference = child_row_references[0]
else:
self.distribute_child_row_reference(child_row_references)
def distribute_child_row_reference(self, child_row_references):
quality_inspections = frappe.get_all(
"Quality Inspection",
filters={
"reference_name": self.reference_name,
"item_code": self.item_code,
"docstatus": ("<", 2),
},
fields=["name", "child_row_reference", "docstatus"],
order_by="child_row_reference desc",
)
for row in quality_inspections:
if not child_row_references:
break
if row.child_row_reference and row.child_row_reference in child_row_references:
child_row_references.remove(row.child_row_reference)
continue
if row.docstatus == 1:
continue
if row.name == self.name:
self.child_row_reference = child_row_references[0]
else:
frappe.db.set_value(
"Quality Inspection", row.name, "child_row_reference", child_row_references[0]
)
child_row_references.remove(child_row_references[0])
def validate_inspection_required(self):
if frappe.db.get_single_value(
@@ -413,7 +387,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(
f"""
SELECT item_code
SELECT distinct item_code, item_name, item_group
FROM `tab{from_doctype}`
WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s
{qi_condition} {cond} {mcond}
@@ -444,10 +418,11 @@ def quality_inspection_query(doctype, txt, searchfield, start, page_len, filters
limit_start=start,
limit_page_length=page_len,
filters={
"docstatus": 1,
"docstatus": ("<", 2),
"name": ("like", "%%%s%%" % txt),
"item_code": filters.get("item_code"),
"reference_name": ("in", [filters.get("reference_name", ""), ""]),
"child_row_reference": ("in", [filters.get("child_row_reference", ""), ""]),
},
as_list=1,
)

View File

@@ -179,6 +179,7 @@ frappe.ui.form.on("Stock Entry", {
inspection_type: "Incoming",
reference_type: frm.doc.doctype,
reference_name: frm.doc.name,
child_row_reference: row.doc.name,
item_code: row.doc.item_code,
description: row.doc.description,
item_serial_no: row.doc.serial_no ? row.doc.serial_no.split("\n")[0] : null,
@@ -194,6 +195,7 @@ frappe.ui.form.on("Stock Entry", {
filters: {
item_code: d.item_code,
reference_name: doc.name,
child_row_reference: d.name,
},
};
});

View File

@@ -192,7 +192,7 @@ class StockLedgerEntry(Document):
mandatory = ["warehouse", "posting_date", "voucher_type", "voucher_no", "company"]
for k in mandatory:
if not self.get(k):
frappe.throw(_("{0} is required").format(self.meta.get_label(k)))
frappe.throw(_("{0} is required").format(_(self.meta.get_label(k))))
if self.voucher_type != "Stock Reconciliation" and not self.actual_qty:
frappe.throw(_("Actual Qty is mandatory"))

View File

@@ -898,10 +898,6 @@ class StockReconciliation(StockController):
self.update_inventory_dimensions(row, data)
if self.docstatus == 1 and has_dimensions and (not row.batch_no or not row.serial_and_batch_bundle):
data.qty_after_transaction = data.actual_qty
data.actual_qty = 0.0
return data
def make_sle_on_cancel(self):
@@ -1266,12 +1262,12 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig
itemwise_batch_data = get_itemwise_batch(warehouse, posting_date, company, item_code)
for d in items:
if d.item_code in itemwise_batch_data:
if (d.item_code, d.warehouse) in itemwise_batch_data:
valuation_rate = get_stock_balance(
d.item_code, d.warehouse, posting_date, posting_time, with_valuation_rate=True
)[1]
for row in itemwise_batch_data.get(d.item_code):
for row in itemwise_batch_data.get((d.item_code, d.warehouse)):
if ignore_empty_stock and not row.qty:
continue
@@ -1403,7 +1399,7 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None):
columns, data = execute(filters)
for row in data:
itemwise_batch_data.setdefault(row[0], []).append(
itemwise_batch_data.setdefault((row[0], row[3]), []).append(
frappe._dict(
{
"item_code": row[0],

View File

@@ -13,7 +13,6 @@
"end_time",
"limits_dont_apply_on",
"item_based_reposting",
"do_reposting_for_each_stock_transaction",
"errors_notification_section",
"notify_reposting_error_to_role"
],
@@ -66,18 +65,12 @@
"fieldname": "errors_notification_section",
"fieldtype": "Section Break",
"label": "Errors Notification"
},
{
"default": "0",
"fieldname": "do_reposting_for_each_stock_transaction",
"fieldtype": "Check",
"label": "Do reposting for each Stock Transaction"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2024-04-24 12:19:40.204888",
"modified": "2025-07-08 11:27:46.659056",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reposting Settings",

View File

@@ -16,7 +16,6 @@ class StockRepostingSettings(Document):
if TYPE_CHECKING:
from frappe.types import DF
do_reposting_for_each_stock_transaction: DF.Check
end_time: DF.Time | None
item_based_reposting: DF.Check
limit_reposting_timeslot: DF.Check
@@ -30,10 +29,6 @@ class StockRepostingSettings(Document):
def validate(self):
self.set_minimum_reposting_time_slot()
def before_save(self):
if self.do_reposting_for_each_stock_transaction:
self.item_based_reposting = 1
def set_minimum_reposting_time_slot(self):
"""Ensure that timeslot for reposting is at least 12 hours."""
if not self.limit_reposting_timeslot:

View File

@@ -38,51 +38,3 @@ class TestStockRepostingSettings(unittest.TestCase):
users = get_recipients()
self.assertTrue(user in users)
def test_do_reposting_for_each_stock_transaction(self):
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 1)
if frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"):
frappe.db.set_single_value("Stock Reposting Settings", "item_based_reposting", 0)
item = make_item(
"_Test item for reposting check for each transaction", properties={"is_stock_item": 1}
).name
stock_entry = make_stock_entry(
item_code=item,
qty=1,
rate=100,
stock_entry_type="Material Receipt",
target="_Test Warehouse - _TC",
)
riv = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name}, pluck="name")
self.assertTrue(riv)
frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 0)
def test_do_not_reposting_for_each_stock_transaction(self):
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 0)
if frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"):
frappe.db.set_single_value("Stock Reposting Settings", "item_based_reposting", 0)
item = make_item(
"_Test item for do not reposting check for each transaction", properties={"is_stock_item": 1}
).name
stock_entry = make_stock_entry(
item_code=item,
qty=1,
rate=100,
stock_entry_type="Material Receipt",
target="_Test Warehouse - _TC",
)
riv = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name}, pluck="name")
self.assertFalse(riv)

View File

@@ -115,7 +115,7 @@ class StockReservationEntry(Document):
]
for d in mandatory:
if not self.get(d):
msg = _("{0} is required").format(self.meta.get_label(d))
msg = _("{0} is required").format(_(self.meta.get_label(d)))
frappe.throw(msg)
def validate_group_warehouse(self) -> None:

View File

@@ -65,7 +65,7 @@ class TransactionBase(StatusUpdater):
frappe.throw(_("Invalid reference {0} {1}").format(reference_doctype, reference_name))
for field, condition in fields:
if prevdoc_values[field] is not None and field not in self.exclude_fields:
if prevdoc_values[field] not in [None, ""] and field not in self.exclude_fields:
self.validate_value(field, condition, prevdoc_values[field], doc)
def get_prev_doc_reference_details(self, reference_names, reference_doctype, fields):