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: if self.add_taxes_from_item_tax_template and self.add_taxes_from_taxes_and_charges_template:
frappe.throw( frappe.throw(
_("You cannot enable both the settings '{0}' and '{1}'.").format( _("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_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_taxes_and_charges_template"))),
), ),
title=_("Auto Tax Settings Error"), title=_("Auto Tax Settings Error"),
) )

View File

@@ -6,7 +6,11 @@ import unittest
import frappe import frappe
from frappe.utils import now_datetime, nowdate 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.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order 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, "action_if_accumulated_monthly_budget_exceeded", "Stop")
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year) 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( mr = frappe.get_doc(
{ {
"doctype": "Material Request", "doctype": "Material Request",
@@ -109,7 +117,7 @@ class TestBudget(unittest.TestCase):
"uom": "_Test UOM", "uom": "_Test UOM",
"warehouse": "_Test Warehouse - _TC", "warehouse": "_Test Warehouse - _TC",
"schedule_date": nowdate(), "schedule_date": nowdate(),
"rate": 100000, "rate": accumulated_limit + 1,
"expense_account": "_Test Account Cost for Goods Sold - _TC", "expense_account": "_Test Account Cost for Goods Sold - _TC",
"cost_center": "_Test Cost Center - _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 ( from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal, cancel_exchange_gain_loss_journal,
get_account_currency, get_account_currency,
get_advance_payment_doctypes,
get_balance_on, get_balance_on,
get_stock_accounts, get_stock_accounts,
get_stock_and_account_balance, get_stock_and_account_balance,
@@ -146,8 +147,8 @@ class JournalEntry(AccountsController):
if self.docstatus == 0: if self.docstatus == 0:
self.apply_tax_withholding() self.apply_tax_withholding()
if self.is_new() or not self.title:
self.title = self.get_title() self.title = self.get_title()
def validate_advance_accounts(self): def validate_advance_accounts(self):
journal_accounts = set([x.account for x in self.accounts]) journal_accounts = set([x.account for x in self.accounts])
@@ -238,9 +239,10 @@ class JournalEntry(AccountsController):
def update_advance_paid(self): def update_advance_paid(self):
advance_paid = frappe._dict() advance_paid = frappe._dict()
advance_payment_doctypes = get_advance_payment_doctypes()
for d in self.get("accounts"): for d in self.get("accounts"):
if d.is_advance: 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) advance_paid.setdefault(d.reference_type, []).append(d.reference_name)
for voucher_type, order_list in advance_paid.items(): for voucher_type, order_list in advance_paid.items():
@@ -1042,7 +1044,9 @@ class JournalEntry(AccountsController):
def set_print_format_fields(self): def set_print_format_fields(self):
bank_amount = party_amount = total_amount = 0.0 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 party_type = None
for d in self.get("accounts"): for d in self.get("accounts"):
if d.party_type in ["Customer", "Supplier"] and d.party: 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 ( from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal, cancel_exchange_gain_loss_journal,
get_account_currency, get_account_currency,
get_advance_payment_doctypes,
get_balance_on, get_balance_on,
get_outstanding_invoices, get_outstanding_invoices,
get_reconciliation_effect_date,
) )
from erpnext.controllers.accounts_controller import ( from erpnext.controllers.accounts_controller import (
AccountsController, AccountsController,
@@ -568,7 +570,7 @@ class PaymentEntry(AccountsController):
def validate_mandatory(self): def validate_mandatory(self):
for field in ("paid_amount", "received_amount", "source_exchange_rate", "target_exchange_rate"): for field in ("paid_amount", "received_amount", "source_exchange_rate", "target_exchange_rate"):
if not self.get(field): 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): def validate_reference_documents(self):
valid_reference_doctypes = self.get_valid_reference_doctypes() 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: def calculate_base_allocated_amount_for_reference(self, d) -> float:
base_allocated_amount = 0 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. # 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 # 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: if not self.party_account:
return return
advance_payment_doctypes = frappe.get_hooks("advance_payment_doctypes") advance_payment_doctypes = get_advance_payment_doctypes()
if self.payment_type == "Receive": if self.payment_type == "Receive":
against_account = self.paid_to against_account = self.paid_to
else: else:
@@ -1492,23 +1493,7 @@ class PaymentEntry(AccountsController):
else: else:
# For backwards compatibility # For backwards compatibility
# Supporting reposting on payment entries reconciled before select field introduction # Supporting reposting on payment entries reconciled before select field introduction
reconciliation_takes_effect_on = frappe.get_cached_value( posting_date = get_reconciliation_effect_date(invoice, self.company, self.posting_date)
"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()
frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", 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) 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)) return flt(gl_dict.get(field, 0) / (conversion_rate or 1))
def update_advance_paid(self): def update_advance_paid(self):
if self.payment_type in ("Receive", "Pay") and self.party: if self.payment_type not in ("Receive", "Pay") or not self.party:
for d in self.get("references"): return
if d.allocated_amount and d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"):
frappe.get_doc( advance_payment_doctypes = get_advance_payment_doctypes()
d.reference_doctype, d.reference_name, for_update=True for d in self.get("references"):
).set_total_advance_paid() 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): def on_recurring(self, reference_doc, auto_repeat_doc):
self.reference_no = reference_doc.name self.reference_no = reference_doc.name

View File

@@ -589,7 +589,7 @@ class PaymentReconciliation(Document):
def check_mandatory_to_fetch(self): def check_mandatory_to_fetch(self):
for fieldname in ["company", "party_type", "party", "receivable_payable_account"]: for fieldname in ["company", "party_type", "party", "receivable_payable_account"]:
if not self.get(fieldname): 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): def validate_entries(self):
if not self.get("invoices"): 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: if not references:
return return
precision = references[0].precision("allocated_amount") precision = frappe.get_precision("Payment Entry Reference", "allocated_amount")
referenced_payment_requests = frappe.get_all( referenced_payment_requests = frappe.get_all(
"Payment Request", "Payment Request",
filters={"name": ["in", {row.payment_request for row in references if row.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) pr = make_payment_request(dt="Sales Invoice", dn=si.name, mute_email=1)
self.assertEqual(pr.grand_total, si.outstanding_amount) 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): pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000) pe.reference_no = "PURINV0001"
pi.save() pe.reference_date = frappe.utils.nowdate()
pi.submit() 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 = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "PURINV0001" pe.reference_no = "PURINV0002"
pe.reference_date = frappe.utils.nowdate() pe.reference_date = frappe.utils.nowdate()
pe.paid_amount = 2500 pe.paid_amount = 2500
pe.references[0].allocated_amount = 2500 pe.references[0].allocated_amount = 2500
pe.save() pe.save()
pe.submit() pe.submit()
pe.cancel()
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") pi.load_from_db()
pe.reference_no = "PURINV0002" pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
pe.reference_date = frappe.utils.nowdate() self.assertEqual(pr.grand_total, pi.outstanding_amount)
pe.paid_amount = 2500
pe.references[0].allocated_amount = 2500
pe.save()
pe.submit()
pi.load_from_db() def test_payment_request_on_unreconcile(self):
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1) pi = make_purchase_invoice(currency="INR", qty=1, rate=500)
self.assertEqual(pr.grand_total, pi.outstanding_amount) 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, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"company",
"posting_date", "posting_date",
"posting_time", "posting_time",
"merge_invoices_based_on", "merge_invoices_based_on",
@@ -113,12 +114,22 @@
"label": "Posting Time", "label": "Posting Time",
"no_copy": 1, "no_copy": 1,
"reqd": 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, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-08-01 11:36:42.456429", "modified": "2025-07-02 17:08:04.747202",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice Merge Log", "name": "POS Invoice Merge Log",
@@ -179,7 +190,8 @@
"write": 1 "write": 1
} }
], ],
"sort_field": "modified", "row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1

View File

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

View File

@@ -169,7 +169,7 @@ class PricingRule(Document):
tocheck = frappe.scrub(self.get("applicable_for", "")) tocheck = frappe.scrub(self.get("applicable_for", ""))
if tocheck and not self.get(tocheck): 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: if self.apply_rule_on_other:
o_field = "other_" + frappe.scrub(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) details = get_item_details(args)
self.assertEqual(details.get("discount_percentage"), 10) 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): def test_pricing_rule_for_variants(self):
from erpnext.stock.get_item_details import get_item_details 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 frappe.flags.tree_conditions[key] = condition
elif allow_blank:
condition = f"ifnull({table}.{field}, '') = ''"
return condition return condition

View File

@@ -40,6 +40,13 @@ class TestProcessDeferredAccounting(unittest.TestCase):
si.save() si.save()
si.submit() 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( process_deferred_accounting = frappe.get_doc(
dict( dict(
doctype="Process Deferred Accounting", doctype="Process Deferred Accounting",
@@ -63,6 +70,12 @@ class TestProcessDeferredAccounting(unittest.TestCase):
] ]
check_gl_entries(self, si.name, expected_gle, "2023-07-01") 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() change_acc_settings()
def test_pda_submission_and_cancellation(self): 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 () { frm.set_query("account", function () {
if (!frm.doc.company) {
frappe.throw(__("Please set Company"));
}
return { return {
filters: { filters: {
company: frm.doc.company, company: frm.doc.company,
@@ -61,6 +64,9 @@ frappe.ui.form.on("Process Statement Of Accounts", {
}; };
}); });
frm.set_query("cost_center", function () { frm.set_query("cost_center", function () {
if (!frm.doc.company) {
frappe.throw(__("Please set Company"));
}
return { return {
filters: { filters: {
company: frm.doc.company, company: frm.doc.company,
@@ -68,6 +74,9 @@ frappe.ui.form.on("Process Statement Of Accounts", {
}; };
}); });
frm.set_query("project", function () { frm.set_query("project", function () {
if (!frm.doc.company) {
frappe.throw(__("Please set Company"));
}
return { return {
filters: { filters: {
company: frm.doc.company, 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()); 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) { report: function (frm) {
let filters = { let filters = {
company: frm.doc.company, company: frm.doc.company,

View File

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

View File

@@ -82,6 +82,10 @@ class ProcessStatementOfAccounts(Document):
# end: auto-generated types # end: auto-generated types
def validate(self): def validate(self):
self.validate_account()
self.validate_company_for_table("Cost Center")
self.validate_company_for_table("Project")
if not self.subject: if not self.subject:
self.subject = "Statement Of Accounts for {{ customer.customer_name }}" self.subject = "Statement Of Accounts for {{ customer.customer_name }}"
if not self.body: if not self.body:
@@ -104,6 +108,43 @@ class ProcessStatementOfAccounts(Document):
self.to_date = self.start_date self.to_date = self.start_date
self.from_date = add_months(self.to_date, -1 * self.filter_duration) 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): def get_report_pdf(doc, consolidated=True):
statement_dict = get_statement_dict(doc) statement_dict = get_statement_dict(doc)

View File

@@ -2497,6 +2497,10 @@ class TestSalesInvoice(FrappeTestCase):
for gle in gl_entries: for gle in gl_entries:
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) 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): def test_deferred_revenue(self):
deferred_account = create_account( deferred_account = create_account(
account_name="Deferred Revenue", account_name="Deferred Revenue",
@@ -2551,6 +2555,10 @@ class TestSalesInvoice(FrappeTestCase):
self.assertRaises(frappe.ValidationError, si.save) 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): def test_fixed_deferred_revenue(self):
deferred_account = create_account( deferred_account = create_account(
account_name="Deferred Revenue", account_name="Deferred Revenue",
@@ -2558,10 +2566,6 @@ class TestSalesInvoice(FrappeTestCase):
company="_Test Company", 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 = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_revenue = 1 item.enable_deferred_revenue = 1
item.deferred_revenue_account = deferred_account item.deferred_revenue_account = deferred_account
@@ -2601,10 +2605,6 @@ class TestSalesInvoice(FrappeTestCase):
check_gl_entries(self, si.name, expected_gle, "2019-01-30") 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 test_validate_inter_company_transaction_address_links(self):
def _validate_address_link(address, link_doctype, link_name): def _validate_address_link(address, link_doctype, link_name):
return frappe.db.get_value( return frappe.db.get_value(
@@ -2853,7 +2853,9 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(si.items[0].rate, rate) self.assertEqual(si.items[0].rate, rate)
self.assertEqual(target_doc.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): def test_internal_transfer_gl_precision_issues(self):
# Make a stock queue of an item with two valuations # 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) gl_entries = q.run(as_dict=True)
doc.assertGreater(len(gl_entries), 0)
for i, gle in enumerate(gl_entries): for i, gle in enumerate(gl_entries):
doc.assertEqual(expected_gle[i][0], gle.account) doc.assertEqual(expected_gle[i][0], gle.account)
doc.assertEqual(expected_gle[i][1], gle.debit) 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 ( from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal, cancel_exchange_gain_loss_journal,
get_advance_payment_doctypes,
unlink_ref_doc_from_payment_entries, unlink_ref_doc_from_payment_entries,
update_voucher_outstanding, update_voucher_outstanding,
) )
@@ -84,7 +85,7 @@ class UnreconcilePayment(Document):
update_voucher_outstanding( update_voucher_outstanding(
alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party 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() doc.set_total_advance_paid()
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True) frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)

View File

@@ -692,7 +692,18 @@ def make_reverse_gl_entries(
query.run() query.run()
else: else:
if not immutable_ledger_enabled: 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: for entry in gl_entries:
new_gle = copy.deepcopy(entry) new_gle = copy.deepcopy(entry)

View File

@@ -15,7 +15,11 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, get_accounting_dimensions,
get_dimension_with_children, 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 # 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") frappe.db.get_single_value("Accounts Settings", "receivable_payable_fetch_method")
or "Buffered Cursor" or "Buffered Cursor"
) # Fail Safe ) # Fail Safe
self.advance_payment_doctypes = get_advance_payment_doctypes()
def run(self, args): def run(self, args):
self.filters.update(args) self.filters.update(args)
@@ -85,6 +90,7 @@ class ReceivablePayableReport:
self.party_details = {} self.party_details = {}
self.invoices = set() self.invoices = set()
self.skip_total_row = 0 self.skip_total_row = 0
self.advance_payment_doctypes = get_advance_payment_doctypes()
if self.filters.get("group_by_party"): if self.filters.get("group_by_party"):
self.previous_party = "" self.previous_party = ""
@@ -181,7 +187,10 @@ class ReceivablePayableReport:
if key not in self.voucher_balance: if key not in self.voucher_balance:
self.voucher_balance[key] = self.build_voucher_dict(ple) 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.voucher_balance[key].cost_center = ple.cost_center
self.get_invoices(ple) self.get_invoices(ple)

View File

@@ -79,6 +79,14 @@ class Deferred_Item:
return - estimated amount to post for given period return - estimated amount to post for given period
Calculated based on already booked amount and item service 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 = ( total_months = (
(self.service_end_date.year - self.service_start_date.year) * 12 (self.service_end_date.year - self.service_start_date.year) * 12
+ (self.service_end_date.month - self.service_start_date.month) + (self.service_end_date.month - self.service_start_date.month)
@@ -105,6 +113,19 @@ class Deferred_Item:
return base_amount 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): def make_dummy_gle(self, name, date, amount):
""" """
return - frappe._dict() of a dummy gle entry return - frappe._dict() of a dummy gle entry
@@ -245,6 +266,10 @@ class Deferred_Revenue_and_Expense_Report:
else: else:
self.filters = frappe._dict(filters) 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.period_list = None
self.deferred_invoices = [] self.deferred_invoices = []
# holds period wise total for report # holds period wise total for report
@@ -289,7 +314,11 @@ class Deferred_Revenue_and_Expense_Report:
.join(inv) .join(inv)
.on(inv.name == inv_item.parent) .on(inv.name == inv_item.parent)
.left_join(gle) .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( .select(
inv.name.as_("doc"), inv.name.as_("doc"),
inv.posting_date, inv.posting_date,

View File

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

View File

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

View File

@@ -6,12 +6,9 @@ from frappe import qb
from frappe.tests.utils import FrappeTestCase, change_settings from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import flt, today 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.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.general_ledger.general_ledger import execute from erpnext.accounts.report.general_ledger.general_ledger import execute
from erpnext.controllers.sales_and_purchase_return import make_return_doc 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): class TestGeneralLedger(FrappeTestCase):
@@ -171,90 +168,6 @@ class TestGeneralLedger(FrappeTestCase):
self.assertEqual(data[3]["debit"], 100) self.assertEqual(data[3]["debit"], 100)
self.assertEqual(data[3]["credit"], 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): def test_ignore_exchange_rate_journals_filter(self):
# create a new account with USD currency # create a new account with USD currency
account_name = "Test Debtors USD" account_name = "Test Debtors USD"

View File

@@ -5,13 +5,14 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.meta import get_field_precision 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 import cstr, flt
from frappe.utils.nestedset import get_descendants_of from frappe.utils.nestedset import get_descendants_of
from frappe.utils.xlsxutils import handle_html from frappe.utils.xlsxutils import handle_html
from pypika import Order from pypika import Order
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments 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 ( from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import (
get_customer_details, get_customer_details,
) )
@@ -433,7 +434,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
si.is_internal_customer, si.is_internal_customer,
si.customer, si.customer,
si.remarks, si.remarks,
si.territory, fn.IfNull(si.territory, "Not Specified").as_("territory"),
si.company, si.company,
si.base_net_total, si.base_net_total,
sii.project, sii.project,
@@ -456,7 +457,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
sii.base_net_rate, sii.base_net_rate,
sii.base_net_amount, sii.base_net_amount,
si.customer_name, si.customer_name,
si.customer_group, fn.IfNull(si.customer_group, "Not Specified").as_("customer_group"),
sii.so_detail, sii.so_detail,
si.update_stock, si.update_stock,
sii.uom, 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.append(row)
out.sort(key=lambda x: x["section_code"]) out.sort(key=lambda x: (x["section_code"], x["transaction_date"]))
return out return out

View File

@@ -67,11 +67,12 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
mid_year = add_to_date(fiscal_year[1], months=6) mid_year = add_to_date(fiscal_year[1], months=6)
tds_doc = frappe.get_doc("Tax Withholding Category", "TDS - 3") tds_doc = frappe.get_doc("Tax Withholding Category", "TDS - 3")
tds_doc.rates[0].to_date = mid_year tds_doc.rates[0].to_date = mid_year
from_date = add_to_date(mid_year, days=1)
tds_doc.append( tds_doc.append(
"rates", "rates",
{ {
"tax_withholding_rate": 20, "tax_withholding_rate": 20,
"from_date": add_to_date(mid_year, days=1), "from_date": from_date,
"to_date": fiscal_year[2], "to_date": fiscal_year[2],
"single_threshold": 1, "single_threshold": 1,
"cumulative_threshold": 1, "cumulative_threshold": 1,
@@ -80,18 +81,19 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
tds_doc.save() 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.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_1.submit()
inv_2 = make_purchase_invoice( inv_2 = make_purchase_invoice(rate=1000, posting_date=from_date, do_not_save=True, do_not_submit=True)
rate=1000, do_not_submit=True, posting_date=add_to_date(mid_year, days=1), do_not_save=True
)
inv_2.set_posting_time = 1 inv_2.set_posting_time = 1
inv_2.apply_tds = 1
inv_1.apply_tds = 1 inv_2.tax_withholding_category = tds_doc.name
inv_2.tax_withholding_category = "TDS - 3"
inv_2.save() inv_2.save()
inv_2.submit() 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"]) credit_in_account_currency = flt(entry["credit_in_account_currency"])
account_currency = entry["account_currency"] account_currency = entry["account_currency"]
if ( if len(account_currencies) == 1 and account_currency == presentation_currency:
len(account_currencies) == 1
and account_currency == presentation_currency
and (debit_in_account_currency or credit_in_account_currency)
):
entry["debit"] = debit_in_account_currency entry["debit"] = debit_in_account_currency
entry["credit"] = credit_in_account_currency entry["credit"] = credit_in_account_currency
else: else:

View File

@@ -169,7 +169,7 @@ def validate_fiscal_year(date, fiscal_year, company, label="Date", doc=None):
if doc: if doc:
doc.fiscal_year = years[0] doc.fiscal_year = years[0]
else: 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() @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 in SO/PO since they might be getting unlinked
update_advance_paid = [] update_advance_paid = []
if jv_detail.get("reference_type") in ["Sales Order", "Purchase Order"]: if jv_detail.get("reference_type") in ["Sales Order", "Purchase Order"]:
update_advance_paid.append((jv_detail.reference_type, jv_detail.reference_name)) 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_advance_paid = []
# Update Reconciliation effect date in reference # 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 payment_entry.book_advance_payments_in_separate_party_account:
if reconciliation_takes_effect_on == "Advance Payment Date": reconcile_on = get_reconciliation_effect_date(d, payment_entry.company, payment_entry.posting_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()
reference_details.update({"reconcile_effect_on": reconcile_on}) reference_details.update({"reconcile_effect_on": reconcile_on})
if d.voucher_detail_no: if d.voucher_detail_no:
@@ -783,6 +769,28 @@ def update_reference_in_payment_entry(
return row, update_advance_paid 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( def cancel_exchange_gain_loss_journal(
parent_doc: dict | object, referenced_dt: str | None = None, referenced_dn: str | None = None parent_doc: dict | object, referenced_dt: str | None = None, referenced_dn: str | None = None
) -> None: ) -> None:
@@ -997,58 +1005,79 @@ def remove_ref_doc_link_from_pe(
per = qb.DocType("Payment Entry Reference") per = qb.DocType("Payment Entry Reference")
pay = qb.DocType("Payment Entry") pay = qb.DocType("Payment Entry")
linked_pe = ( query = (
qb.from_(per) qb.from_(per)
.select(per.parent) .select("*")
.where((per.reference_doctype == ref_type) & (per.reference_name == ref_no) & (per.docstatus.lt(2))) .where(
.run(as_list=1) (per.reference_doctype == ref_type)
) & (per.reference_name == ref_no)
linked_pe = convert_to_list(linked_pe) & (per.docstatus.lt(2))
# remove reference only from specified payment & (per.parenttype == "Payment Entry")
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))
) )
)
if payment_name: # update reference only from specified payment
update_query = update_query.where(per.parent == payment_name) 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: if not reference_rows:
try: return
pe_doc = frappe.get_doc("Payment Entry", pe)
pe_doc.set_amounts()
# Call cancel on only removed reference linked_pe = set()
references = [ row_names = set()
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]
pe_doc.clear_unallocated_reference_document_rows() for row in reference_rows:
pe_doc.validate_payment_type_with_outstanding() linked_pe.add(row.parent)
except Exception: row_names.add(row.name)
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( from erpnext.accounts.doctype.payment_request.payment_request import (
pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount update_payment_requests_as_per_pe_references,
).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))) # 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() @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") 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(): def run_ledger_health_checks():
health_monitor_settings = frappe.get_doc("Ledger Health Monitor") health_monitor_settings = frappe.get_doc("Ledger Health Monitor")
if health_monitor_settings.enable_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) target.credit_to = get_party_account("Supplier", source.supplier, source.company)
def update_item(obj, target, source_parent): def update_item(obj, target, source_parent):
target.amount = flt(obj.amount) - flt(obj.billed_amt) def get_billed_qty(po_item_name):
target.base_amount = target.amount * flt(source_parent.conversion_rate) from frappe.query_builder.functions import Sum
target.qty = (
target.amount / flt(obj.rate) if (flt(obj.rate) and flt(obj.billed_amt)) else flt(obj.qty) 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 = get_item_defaults(target.item_code, source_parent.company)
item_group = get_item_group_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.assertFalse(po.per_billed)
self.assertEqual(po.status, "To Receive and Bill") 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(): def create_po_for_sc_testing():
from erpnext.controllers.tests.test_subcontracting_controller import ( from erpnext.controllers.tests.test_subcontracting_controller import (

View File

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

View File

@@ -1648,8 +1648,9 @@ def make_quality_inspections(doctype, docname, items):
"sample_size": flt(item.get("sample_size")), "sample_size": flt(item.get("sample_size")),
"item_serial_no": item.get("serial_no").split("\n")[0] if item.get("serial_no") else None, "item_serial_no": item.get("serial_no").split("\n")[0] if item.get("serial_no") else None,
"batch_no": item.get("batch_no"), "batch_no": item.get("batch_no"),
"child_row_reference": item.get("child_row_reference"),
} }
).insert() )
quality_inspection.save() quality_inspection.save()
inspections.append(quality_inspection.name) 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 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) key = (args.voucher_type, args.voucher_no)
if not hasattr(frappe.local, "future_sle"): if not hasattr(frappe.local, "future_sle"):
frappe.local.future_sle = {} frappe.local.future_sle = {}

View File

@@ -601,12 +601,15 @@ class SubcontractingController(StockController):
rm_obj.use_serial_batch_fields = 1 rm_obj.use_serial_batch_fields = 1
self.__set_batch_nos(bom_item, item_row, rm_obj, qty) self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
if self.doctype == "Subcontracting Receipt" and not use_serial_batch_fields: if self.doctype == "Subcontracting Receipt":
rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle( if not use_serial_batch_fields:
item_row, rm_obj, rm_obj.consumed_qty 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): def update_rate_for_supplied_items(self):
if self.doctype != "Subcontracting Receipt": if self.doctype != "Subcontracting Receipt":

View File

@@ -987,7 +987,7 @@ class BOM(WebsiteGenerator):
self.transfer_material_against = "Work Order" self.transfer_material_against = "Work Order"
if not self.transfer_material_against and not self.is_new(): if not self.transfer_material_against and not self.is_new():
frappe.throw( 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"), 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; let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
if (to_request || excess_transfer_allowed) { if (to_request || excess_transfer_allowed) {
frm.add_custom_button(__("Material Request"), () => { frm.add_custom_button(
frm.trigger("make_material_request"); __("Material Request"),
}); () => {
frm.trigger("make_material_request");
},
__("Create")
);
} }
// check if any row has untransferred materials // 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); let to_transfer = frm.doc.items.some((row) => row.transferred_qty < row.required_qty);
if (to_transfer || excess_transfer_allowed) { if (to_transfer || excess_transfer_allowed) {
frm.add_custom_button(__("Material Transfer"), () => { frm.add_custom_button(
frm.trigger("make_stock_entry"); __("Material Transfer"),
}).addClass("btn-primary"); () => {
frm.trigger("make_stock_entry");
},
__("Create")
);
} }
} }

View File

@@ -390,7 +390,7 @@ class WorkOrder(Document):
if qty > completed_qty: if qty > completed_qty:
frappe.throw( frappe.throw(
_("{0} ({1}) cannot be greater than planned quantity ({2}) in Work Order {3}").format( _("{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, StockOverProductionError,
) )
@@ -1077,7 +1077,7 @@ class WorkOrder(Document):
self.transfer_material_against = "Work Order" self.transfer_material_against = "Work Order"
if not self.transfer_material_against: if not self.transfer_material_against:
frappe.throw( 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"), 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.drop_sle_indexes
erpnext.patches.v15_0.update_pick_list_fields erpnext.patches.v15_0.update_pick_list_fields
erpnext.patches.v15_0.update_pegged_currencies 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) { function open_form(frm, doctype, child_doctype, parentfield) {

View File

@@ -62,6 +62,7 @@
"day_to_send", "day_to_send",
"weekly_time_to_send", "weekly_time_to_send",
"column_break_45", "column_break_45",
"subject",
"message" "message"
], ],
"fields": [ "fields": [
@@ -447,6 +448,13 @@
"print_hide": 1, "print_hide": 1,
"reqd": 1, "reqd": 1,
"set_only_once": 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", "icon": "fa fa-puzzle-piece",
@@ -454,7 +462,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"max_attachments": 4, "max_attachments": 4,
"modified": "2024-04-24 10:56:16.001032", "modified": "2025-07-03 10:54:30.444139",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Projects", "module": "Projects",
"name": "Project", "name": "Project",
@@ -501,6 +509,7 @@
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"row_format": "Dynamic",
"search_fields": "project_name,customer, status, priority, is_active", "search_fields": "project_name,customer, status, priority, is_active",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",

View File

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

View File

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

View File

@@ -1063,21 +1063,31 @@
justify-content: flex-end; justify-content: flex-end;
padding-right: var(--padding-sm); padding-right: var(--padding-sm);
> .customer-name { > .customer-section {
font-size: var(--text-2xl); margin-bottom: auto;
font-weight: 700;
}
> .customer-email { > .customer-name {
font-size: var(--text-md); font-size: var(--text-2xl);
font-weight: 500; 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 { > .cashier {
font-size: var(--text-md); font-size: var(--text-md);
font-weight: 500; font-weight: 500;
color: var(--gray-600); color: var(--gray-600);
margin-top: auto; margin-top: var(--margin-md);
} }
} }
@@ -1085,7 +1095,6 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: flex-end;
justify-content: space-between;
> .paid-amount { > .paid-amount {
font-size: var(--text-2xl); 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 (doc.docstatus == 1 && !["Lost", "Ordered"].includes(doc.status)) {
if ( if (
frappe.boot.sysdefaults.allow_sales_order_creation_for_expired_quotation || frappe.model.can_create("Sales Order") &&
!doc.valid_till || (frappe.boot.sysdefaults.allow_sales_order_creation_for_expired_quotation ||
frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0 !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")); 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.add_custom_button(__("Set as Lost"), () => {
this.frm.trigger("set_as_lost_dialog"); 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")); 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( this.frm.add_custom_button(
__("Opportunity"), __("Opportunity"),
function () { function () {

View File

@@ -338,7 +338,7 @@ def create_opening_voucher(pos_profile, company, balance_details):
@frappe.whitelist() @frappe.whitelist()
def get_past_order_list(search_term, status, limit=20): 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 = [] invoice_list = []
if search_term and status: 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"> <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"/> <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> </svg>
${frappe.ellipsis(invoice.customer, 20)} ${frappe.ellipsis(invoice.customer_name, 20)}
</div> </div>
</div> </div>
<div class="invoice-total-status"> <div class="invoice-total-status">

View File

@@ -73,14 +73,18 @@ erpnext.PointOfSale.PastOrderSummary = class {
get_upper_section_html(doc) { get_upper_section_html(doc) {
const { status } = doc; const { status } = doc;
let indicator_color = ""; 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"); ["Paid", "Consolidated"].includes(status) && (indicator_color = "green");
status === "Draft" && (indicator_color = "red"); status === "Draft" && (indicator_color = "red");
status === "Return" && (indicator_color = "grey"); status === "Return" && (indicator_color = "grey");
return `<div class="left-section"> return `<div class="left-section">
<div class="customer-name">${doc.customer}</div> <div class="customer-section">
<div class="customer-email">${this.customer_email}</div> <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 class="cashier">${__("Sold by")}: ${doc.owner}</div>
</div> </div>
<div class="right-section"> <div class="right-section">

View File

@@ -395,7 +395,7 @@ class EmailDigest(Document):
label = get_link_to_report( label = get_link_to_report(
"General Ledger", "General Ledger",
self.meta.get_label("income"), _(self.meta.get_label("income")),
filters={ filters={
"from_date": self.future_from_date, "from_date": self.future_from_date,
"to_date": self.future_to_date, "to_date": self.future_to_date,
@@ -427,7 +427,7 @@ class EmailDigest(Document):
filters = {"currency": self.currency} filters = {"currency": self.currency}
label = get_link_to_report( label = get_link_to_report(
"Profit and Loss Statement", "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, filters=filters,
) )
@@ -435,7 +435,7 @@ class EmailDigest(Document):
filters = {"currency": self.currency} filters = {"currency": self.currency}
label = get_link_to_report( label = get_link_to_report(
"Profit and Loss Statement", "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, filters=filters,
) )
@@ -466,7 +466,7 @@ class EmailDigest(Document):
label = get_link_to_report( label = get_link_to_report(
"General Ledger", "General Ledger",
self.meta.get_label("expenses_booked"), _(self.meta.get_label("expenses_booked")),
filters={ filters={
"company": self.company, "company": self.company,
"from_date": self.future_from_date, "from_date": self.future_from_date,
@@ -500,7 +500,7 @@ class EmailDigest(Document):
label = get_link_to_report( label = get_link_to_report(
"Sales Order", "Sales Order",
label=self.meta.get_label("sales_orders_to_bill"), label=_(self.meta.get_label("sales_orders_to_bill")),
report_type="Report Builder", report_type="Report Builder",
doctype="Sales Order", doctype="Sales Order",
filters={ filters={
@@ -526,7 +526,7 @@ class EmailDigest(Document):
label = get_link_to_report( label = get_link_to_report(
"Sales Order", "Sales Order",
label=self.meta.get_label("sales_orders_to_deliver"), label=_(self.meta.get_label("sales_orders_to_deliver")),
report_type="Report Builder", report_type="Report Builder",
doctype="Sales Order", doctype="Sales Order",
filters={ filters={
@@ -552,7 +552,7 @@ class EmailDigest(Document):
label = get_link_to_report( label = get_link_to_report(
"Purchase Order", "Purchase Order",
label=self.meta.get_label("purchase_orders_to_receive"), label=_(self.meta.get_label("purchase_orders_to_receive")),
report_type="Report Builder", report_type="Report Builder",
doctype="Purchase Order", doctype="Purchase Order",
filters={ filters={
@@ -578,7 +578,7 @@ class EmailDigest(Document):
label = get_link_to_report( label = get_link_to_report(
"Purchase Order", "Purchase Order",
label=self.meta.get_label("purchase_orders_to_bill"), label=_(self.meta.get_label("purchase_orders_to_bill")),
report_type="Report Builder", report_type="Report Builder",
doctype="Purchase Order", doctype="Purchase Order",
filters={ filters={
@@ -630,7 +630,7 @@ class EmailDigest(Document):
"company": self.company, "company": self.company,
} }
label = get_link_to_report( 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: else:
filters = { filters = {
@@ -640,7 +640,7 @@ class EmailDigest(Document):
"company": self.company, "company": self.company,
} }
label = get_link_to_report( 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} return {"label": label, "value": balance, "last_value": prev_balance}
@@ -648,17 +648,17 @@ class EmailDigest(Document):
if account_type == "Payable": if account_type == "Payable":
label = get_link_to_report( label = get_link_to_report(
"Accounts Payable", "Accounts Payable",
label=self.meta.get_label(fieldname), label=_(self.meta.get_label(fieldname)),
filters={"report_date": self.future_to_date, "company": self.company}, filters={"report_date": self.future_to_date, "company": self.company},
) )
elif account_type == "Receivable": elif account_type == "Receivable":
label = get_link_to_report( label = get_link_to_report(
"Accounts Receivable", "Accounts Receivable",
label=self.meta.get_label(fieldname), label=_(self.meta.get_label(fieldname)),
filters={"report_date": self.future_to_date, "company": self.company}, filters={"report_date": self.future_to_date, "company": self.company},
) )
else: else:
label = self.meta.get_label(fieldname) label = _(self.meta.get_label(fieldname))
return {"label": label, "value": balance, "last_value": prev_balance, "count": count} return {"label": label, "value": balance, "last_value": prev_balance, "count": count}
@@ -748,7 +748,7 @@ class EmailDigest(Document):
label = get_link_to_report( label = get_link_to_report(
"Quotation", "Quotation",
label=self.meta.get_label(fieldname), label=_(self.meta.get_label(fieldname)),
report_type="Report Builder", report_type="Report Builder",
doctype="Quotation", doctype="Quotation",
filters={ filters={
@@ -779,7 +779,7 @@ class EmailDigest(Document):
label = get_link_to_report( label = get_link_to_report(
doc_type, doc_type,
label=self.meta.get_label(fieldname), label=_(self.meta.get_label(fieldname)),
report_type="Report Builder", report_type="Report Builder",
filters=filters, filters=filters,
doctype=doc_type, 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 = bin_details.actual_qty or 0.0
# actual qty is not up to date in case of backdated transaction # 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")) actual_qty = get_actual_qty(args.get("item_code"), args.get("warehouse"))
ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty")) 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, "doctype": target_doctype,
"postprocess": update_details, "postprocess": update_details,
"field_no_map": ["taxes_and_charges", "set_warehouse"], "field_no_map": ["taxes_and_charges", "set_warehouse"],
"field_map": {"shipping_address_name": "shipping_address"},
}, },
doctype + " Item": { doctype + " Item": {
"doctype": target_doctype + " Item", "doctype": target_doctype + " Item",

View File

@@ -75,7 +75,9 @@ frappe.ui.form.on("Inventory Dimension", {
set_parent_fields(frm) { set_parent_fields(frm) {
if (frm.doc.apply_to_all_doctypes) { 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) { } else if (frm.doc.document_type && frm.doc.istable) {
frappe.call({ frappe.call({
method: "erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_parent_fields", method: "erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_parent_fields",
@@ -85,7 +87,7 @@ frappe.ui.form.on("Inventory Dimension", {
}, },
callback: (r) => { callback: (r) => {
if (r.message && r.message.length) { 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 { } else {
frm.set_df_property("fetch_from_parent", "hidden", 1); frm.set_df_property("fetch_from_parent", "hidden", 1);
} }

View File

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

View File

@@ -64,16 +64,11 @@ class InventoryDimension(Document):
self.reset_value() self.reset_value()
self.set_source_and_target_fieldname() self.set_source_and_target_fieldname()
self.set_type_of_transaction() self.set_type_of_transaction()
self.set_fetch_value_from()
def set_type_of_transaction(self): def set_type_of_transaction(self):
if self.apply_to_all_doctypes: if self.apply_to_all_doctypes:
self.type_of_transaction = "Both" 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): def do_not_update_document(self):
if self.is_new() or not self.has_stock_ledger(): if self.is_new() or not self.has_stock_ledger():
return return

View File

@@ -155,6 +155,8 @@ class TestInventoryDimension(FrappeTestCase):
reference_document="Rack", dimension_name="Rack", apply_to_all_doctypes=1 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.type_of_transaction, "Both")
self.assertEqual(inv_dimension.fetch_from_parent, "Rack") 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]: if new_properties != [cstr(self.get(field)) for field in field_list]:
msg = _("To merge, following properties must be same for both items") 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) frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError)
def validate_duplicate_product_bundles_before_merge(self, old_name, new_name): def validate_duplicate_product_bundles_before_merge(self, old_name, new_name):
@@ -981,7 +981,7 @@ class Item(Document):
return return
if linked_doc := self._get_linked_submitted_documents(changed_fields): 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 = _( msg = _(
"As there are existing submitted transactions against item {0}, you can not change the value of {1}." "As there are existing submitted transactions against item {0}, you can not change the value of {1}."
).format(self.name, ", ".join(changed_field_labels)) ).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) (pr_item.parent == purchase_receipt.receipt_document)
& ((item.is_stock_item == 1) | (item.is_fixed_asset == 1)) & ((item.is_stock_item == 1) | (item.is_fixed_asset == 1))
) )
.orderby(pr_item.idx)
.run(as_dict=True) .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) { onload: function (frm) {
@@ -70,6 +79,7 @@ frappe.ui.form.on("Material Request", {
}); });
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
frm.doc.price_list = frappe.defaults.get_default("buying_price_list");
}, },
company: function (frm) { company: function (frm) {
@@ -245,7 +255,9 @@ frappe.ui.form.on("Material Request", {
from_warehouse: item.from_warehouse, from_warehouse: item.from_warehouse,
warehouse: item.warehouse, warehouse: item.warehouse,
doctype: frm.doc.doctype, 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"), currency: frappe.defaults.get_default("Currency"),
name: frm.doc.name, name: frm.doc.name,
qty: item.qty || 1, qty: item.qty || 1,

View File

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

View File

@@ -8,6 +8,7 @@
import json import json
import frappe import frappe
import frappe.defaults
from frappe import _, msgprint from frappe import _, msgprint
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum from frappe.query_builder.functions import Sum
@@ -45,6 +46,7 @@ class MaterialRequest(BuyingController):
naming_series: DF.Literal["MAT-MR-.YYYY.-"] naming_series: DF.Literal["MAT-MR-.YYYY.-"]
per_ordered: DF.Percent per_ordered: DF.Percent
per_received: DF.Percent per_received: DF.Percent
price_list: DF.Link | None
scan_barcode: DF.Data | None scan_barcode: DF.Data | None
schedule_date: DF.Date | None schedule_date: DF.Date | None
select_print_heading: DF.Link | 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_warehouse", "items", "warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_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): def before_update_after_submit(self):
self.validate_schedule_date() self.validate_schedule_date()
@@ -764,10 +769,11 @@ def raise_work_orders(material_request):
"material_request_item": d.name, "material_request_item": d.name,
"planned_start_date": mr.transaction_date, "planned_start_date": mr.transaction_date,
"company": mr.company, "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_validate = True
wo_order.flags.ignore_mandatory = True wo_order.flags.ignore_mandatory = True
wo_order.save() wo_order.save()

View File

@@ -7,6 +7,7 @@
import json import json
import frappe import frappe
import frappe.defaults
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import flt from frappe.utils import flt
@@ -339,12 +340,20 @@ def on_doctype_update():
@frappe.whitelist() @frappe.whitelist()
def get_items_from_product_bundle(row): def get_items_from_product_bundle(row, price_list):
row, items = json.loads(row), [] row, items = json.loads(row), []
bundled_items = get_product_bundle_items(row["item_code"]) bundled_items = get_product_bundle_items(row["item_code"])
for item in bundled_items: 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)) items.append(get_item_details(row))
return items return items

View File

@@ -97,51 +97,25 @@ class QualityInspection(Document):
if self.reference_type == "Stock Entry": if self.reference_type == "Stock Entry":
doctype = "Stock Entry Detail" doctype = "Stock Entry Detail"
child_row_references = frappe.get_all( child_doc = frappe.qb.DocType(doctype)
doctype, qi_doc = frappe.qb.DocType("Quality Inspection")
filters={"parent": self.reference_name, "item_code": self.item_code},
pluck="name",
)
if not child_row_references: child_row_references = (
return 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] 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): def validate_inspection_required(self):
if frappe.db.get_single_value( if frappe.db.get_single_value(
@@ -413,7 +387,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql( return frappe.db.sql(
f""" f"""
SELECT item_code SELECT distinct item_code, item_name, item_group
FROM `tab{from_doctype}` FROM `tab{from_doctype}`
WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s
{qi_condition} {cond} {mcond} {qi_condition} {cond} {mcond}
@@ -444,10 +418,11 @@ def quality_inspection_query(doctype, txt, searchfield, start, page_len, filters
limit_start=start, limit_start=start,
limit_page_length=page_len, limit_page_length=page_len,
filters={ filters={
"docstatus": 1, "docstatus": ("<", 2),
"name": ("like", "%%%s%%" % txt), "name": ("like", "%%%s%%" % txt),
"item_code": filters.get("item_code"), "item_code": filters.get("item_code"),
"reference_name": ("in", [filters.get("reference_name", ""), ""]), "reference_name": ("in", [filters.get("reference_name", ""), ""]),
"child_row_reference": ("in", [filters.get("child_row_reference", ""), ""]),
}, },
as_list=1, as_list=1,
) )

View File

@@ -179,6 +179,7 @@ frappe.ui.form.on("Stock Entry", {
inspection_type: "Incoming", inspection_type: "Incoming",
reference_type: frm.doc.doctype, reference_type: frm.doc.doctype,
reference_name: frm.doc.name, reference_name: frm.doc.name,
child_row_reference: row.doc.name,
item_code: row.doc.item_code, item_code: row.doc.item_code,
description: row.doc.description, description: row.doc.description,
item_serial_no: row.doc.serial_no ? row.doc.serial_no.split("\n")[0] : null, 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: { filters: {
item_code: d.item_code, item_code: d.item_code,
reference_name: doc.name, 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"] mandatory = ["warehouse", "posting_date", "voucher_type", "voucher_no", "company"]
for k in mandatory: for k in mandatory:
if not self.get(k): 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: if self.voucher_type != "Stock Reconciliation" and not self.actual_qty:
frappe.throw(_("Actual Qty is mandatory")) frappe.throw(_("Actual Qty is mandatory"))

View File

@@ -898,10 +898,6 @@ class StockReconciliation(StockController):
self.update_inventory_dimensions(row, data) 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 return data
def make_sle_on_cancel(self): 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) itemwise_batch_data = get_itemwise_batch(warehouse, posting_date, company, item_code)
for d in items: 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( valuation_rate = get_stock_balance(
d.item_code, d.warehouse, posting_date, posting_time, with_valuation_rate=True d.item_code, d.warehouse, posting_date, posting_time, with_valuation_rate=True
)[1] )[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: if ignore_empty_stock and not row.qty:
continue continue
@@ -1403,7 +1399,7 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None):
columns, data = execute(filters) columns, data = execute(filters)
for row in data: for row in data:
itemwise_batch_data.setdefault(row[0], []).append( itemwise_batch_data.setdefault((row[0], row[3]), []).append(
frappe._dict( frappe._dict(
{ {
"item_code": row[0], "item_code": row[0],

View File

@@ -13,7 +13,6 @@
"end_time", "end_time",
"limits_dont_apply_on", "limits_dont_apply_on",
"item_based_reposting", "item_based_reposting",
"do_reposting_for_each_stock_transaction",
"errors_notification_section", "errors_notification_section",
"notify_reposting_error_to_role" "notify_reposting_error_to_role"
], ],
@@ -66,18 +65,12 @@
"fieldname": "errors_notification_section", "fieldname": "errors_notification_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Errors Notification" "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, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2024-04-24 12:19:40.204888", "modified": "2025-07-08 11:27:46.659056",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Reposting Settings", "name": "Stock Reposting Settings",

View File

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

View File

@@ -38,51 +38,3 @@ class TestStockRepostingSettings(unittest.TestCase):
users = get_recipients() users = get_recipients()
self.assertTrue(user in users) 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: for d in mandatory:
if not self.get(d): 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) frappe.throw(msg)
def validate_group_warehouse(self) -> None: 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)) frappe.throw(_("Invalid reference {0} {1}").format(reference_doctype, reference_name))
for field, condition in fields: 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) self.validate_value(field, condition, prevdoc_values[field], doc)
def get_prev_doc_reference_details(self, reference_names, reference_doctype, fields): def get_prev_doc_reference_details(self, reference_names, reference_doctype, fields):