mirror of
https://github.com/frappe/erpnext.git
synced 2026-02-19 09:35:03 +00:00
Merge pull request #48462 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -147,8 +147,8 @@ class AccountsSettings(Document):
|
||||
if self.add_taxes_from_item_tax_template and self.add_taxes_from_taxes_and_charges_template:
|
||||
frappe.throw(
|
||||
_("You cannot enable both the settings '{0}' and '{1}'.").format(
|
||||
frappe.bold(self.meta.get_label("add_taxes_from_item_tax_template")),
|
||||
frappe.bold(self.meta.get_label("add_taxes_from_taxes_and_charges_template")),
|
||||
frappe.bold(_(self.meta.get_label("add_taxes_from_item_tax_template"))),
|
||||
frappe.bold(_(self.meta.get_label("add_taxes_from_taxes_and_charges_template"))),
|
||||
),
|
||||
title=_("Auto Tax Settings Error"),
|
||||
)
|
||||
|
||||
@@ -6,7 +6,11 @@ import unittest
|
||||
import frappe
|
||||
from frappe.utils import now_datetime, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.budget.budget import BudgetError, get_actual_expense
|
||||
from erpnext.accounts.doctype.budget.budget import (
|
||||
BudgetError,
|
||||
get_accumulated_monthly_budget,
|
||||
get_actual_expense,
|
||||
)
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
@@ -96,6 +100,10 @@ class TestBudget(unittest.TestCase):
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
|
||||
mr = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Material Request",
|
||||
@@ -109,7 +117,7 @@ class TestBudget(unittest.TestCase):
|
||||
"uom": "_Test UOM",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"schedule_date": nowdate(),
|
||||
"rate": 100000,
|
||||
"rate": accumulated_limit + 1,
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
get_advance_payment_doctypes,
|
||||
get_balance_on,
|
||||
get_stock_accounts,
|
||||
get_stock_and_account_balance,
|
||||
@@ -146,8 +147,8 @@ class JournalEntry(AccountsController):
|
||||
|
||||
if self.docstatus == 0:
|
||||
self.apply_tax_withholding()
|
||||
|
||||
self.title = self.get_title()
|
||||
if self.is_new() or not self.title:
|
||||
self.title = self.get_title()
|
||||
|
||||
def validate_advance_accounts(self):
|
||||
journal_accounts = set([x.account for x in self.accounts])
|
||||
@@ -238,9 +239,10 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def update_advance_paid(self):
|
||||
advance_paid = frappe._dict()
|
||||
advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
for d in self.get("accounts"):
|
||||
if d.is_advance:
|
||||
if d.reference_type in frappe.get_hooks("advance_payment_doctypes"):
|
||||
if d.reference_type in advance_payment_doctypes:
|
||||
advance_paid.setdefault(d.reference_type, []).append(d.reference_name)
|
||||
|
||||
for voucher_type, order_list in advance_paid.items():
|
||||
@@ -1042,7 +1044,9 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def set_print_format_fields(self):
|
||||
bank_amount = party_amount = total_amount = 0.0
|
||||
currency = bank_account_currency = party_account_currency = pay_to_recd_from = None
|
||||
currency = (
|
||||
bank_account_currency
|
||||
) = party_account_currency = pay_to_recd_from = self.pay_to_recd_from = None
|
||||
party_type = None
|
||||
for d in self.get("accounts"):
|
||||
if d.party_type in ["Customer", "Supplier"] and d.party:
|
||||
|
||||
@@ -46,8 +46,10 @@ from erpnext.accounts.party import (
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
get_advance_payment_doctypes,
|
||||
get_balance_on,
|
||||
get_outstanding_invoices,
|
||||
get_reconciliation_effect_date,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import (
|
||||
AccountsController,
|
||||
@@ -568,7 +570,7 @@ class PaymentEntry(AccountsController):
|
||||
def validate_mandatory(self):
|
||||
for field in ("paid_amount", "received_amount", "source_exchange_rate", "target_exchange_rate"):
|
||||
if not self.get(field):
|
||||
frappe.throw(_("{0} is mandatory").format(self.meta.get_label(field)))
|
||||
frappe.throw(_("{0} is mandatory").format(_(self.meta.get_label(field))))
|
||||
|
||||
def validate_reference_documents(self):
|
||||
valid_reference_doctypes = self.get_valid_reference_doctypes()
|
||||
@@ -1028,7 +1030,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def calculate_base_allocated_amount_for_reference(self, d) -> float:
|
||||
base_allocated_amount = 0
|
||||
if d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"):
|
||||
if d.reference_doctype in get_advance_payment_doctypes():
|
||||
# When referencing Sales/Purchase Order, use the source/target exchange rate depending on payment type.
|
||||
# This is so there are no Exchange Gain/Loss generated for such doctypes
|
||||
|
||||
@@ -1308,8 +1310,7 @@ class PaymentEntry(AccountsController):
|
||||
if not self.party_account:
|
||||
return
|
||||
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_doctypes")
|
||||
|
||||
advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
if self.payment_type == "Receive":
|
||||
against_account = self.paid_to
|
||||
else:
|
||||
@@ -1492,23 +1493,7 @@ class PaymentEntry(AccountsController):
|
||||
else:
|
||||
# For backwards compatibility
|
||||
# Supporting reposting on payment entries reconciled before select field introduction
|
||||
reconciliation_takes_effect_on = frappe.get_cached_value(
|
||||
"Company", self.company, "reconciliation_takes_effect_on"
|
||||
)
|
||||
if reconciliation_takes_effect_on == "Advance Payment Date":
|
||||
posting_date = self.posting_date
|
||||
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
|
||||
date_field = "posting_date"
|
||||
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
posting_date = frappe.db.get_value(
|
||||
invoice.reference_doctype, invoice.reference_name, date_field
|
||||
)
|
||||
|
||||
if getdate(posting_date) < getdate(self.posting_date):
|
||||
posting_date = self.posting_date
|
||||
elif reconciliation_takes_effect_on == "Reconciliation Date":
|
||||
posting_date = nowdate()
|
||||
posting_date = get_reconciliation_effect_date(invoice, self.company, self.posting_date)
|
||||
frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date)
|
||||
|
||||
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
|
||||
@@ -1699,12 +1684,15 @@ class PaymentEntry(AccountsController):
|
||||
return flt(gl_dict.get(field, 0) / (conversion_rate or 1))
|
||||
|
||||
def update_advance_paid(self):
|
||||
if self.payment_type in ("Receive", "Pay") and self.party:
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount and d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"):
|
||||
frappe.get_doc(
|
||||
d.reference_doctype, d.reference_name, for_update=True
|
||||
).set_total_advance_paid()
|
||||
if self.payment_type not in ("Receive", "Pay") or not self.party:
|
||||
return
|
||||
|
||||
advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount and d.reference_doctype in advance_payment_doctypes:
|
||||
frappe.get_doc(
|
||||
d.reference_doctype, d.reference_name, for_update=True
|
||||
).set_total_advance_paid()
|
||||
|
||||
def on_recurring(self, reference_doc, auto_repeat_doc):
|
||||
self.reference_no = reference_doc.name
|
||||
|
||||
@@ -589,7 +589,7 @@ class PaymentReconciliation(Document):
|
||||
def check_mandatory_to_fetch(self):
|
||||
for fieldname in ["company", "party_type", "party", "receivable_payable_account"]:
|
||||
if not self.get(fieldname):
|
||||
frappe.throw(_("Please select {0} first").format(self.meta.get_label(fieldname)))
|
||||
frappe.throw(_("Please select {0} first").format(_(self.meta.get_label(fieldname))))
|
||||
|
||||
def validate_entries(self):
|
||||
if not self.get("invoices"):
|
||||
|
||||
@@ -829,8 +829,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
|
||||
if not references:
|
||||
return
|
||||
|
||||
precision = references[0].precision("allocated_amount")
|
||||
|
||||
precision = frappe.get_precision("Payment Entry Reference", "allocated_amount")
|
||||
referenced_payment_requests = frappe.get_all(
|
||||
"Payment Request",
|
||||
filters={"name": ["in", {row.payment_request for row in references if row.payment_request}]},
|
||||
|
||||
@@ -630,29 +630,58 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
pr = make_payment_request(dt="Sales Invoice", dn=si.name, mute_email=1)
|
||||
self.assertEqual(pr.grand_total, si.outstanding_amount)
|
||||
|
||||
def test_partial_paid_invoice_with_submitted_payment_entry(self):
|
||||
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
def test_partial_paid_invoice_with_submitted_payment_entry(self):
|
||||
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0001"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
pe.cancel()
|
||||
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0001"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
pe.cancel()
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0002"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0002"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
pi.load_from_db()
|
||||
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
|
||||
pi.load_from_db()
|
||||
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
def test_payment_request_on_unreconcile(self):
|
||||
pi = make_purchase_invoice(currency="INR", qty=1, rate=500)
|
||||
pi.submit()
|
||||
|
||||
pr = make_payment_request(
|
||||
dt=pi.doctype,
|
||||
dn=pi.name,
|
||||
mute_email=1,
|
||||
submit_doc=True,
|
||||
return_doc=True,
|
||||
)
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
|
||||
pe = pr.create_payment_entry()
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payment",
|
||||
"company": pe.company,
|
||||
"voucher_type": pe.doctype,
|
||||
"voucher_no": pe.name,
|
||||
}
|
||||
)
|
||||
unreconcile.add_references()
|
||||
unreconcile.submit()
|
||||
|
||||
pi.load_from_db()
|
||||
pr.load_from_db()
|
||||
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"merge_invoices_based_on",
|
||||
@@ -113,12 +114,22 @@
|
||||
"label": "Posting Time",
|
||||
"no_copy": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"print_hide": 1,
|
||||
"remember_last_selected_value": 1,
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-08-01 11:36:42.456429",
|
||||
"modified": "2025-07-02 17:08:04.747202",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice Merge Log",
|
||||
@@ -179,8 +190,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,11 +28,10 @@ class POSInvoiceMergeLog(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import (
|
||||
POSInvoiceReference,
|
||||
)
|
||||
from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import POSInvoiceReference
|
||||
|
||||
amended_from: DF.Link | None
|
||||
company: DF.Link
|
||||
consolidated_credit_note: DF.Link | None
|
||||
consolidated_invoice: DF.Link | None
|
||||
customer: DF.Link
|
||||
@@ -584,6 +583,7 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
|
||||
merge_log.posting_time = (
|
||||
get_time(closing_entry.get("posting_time")) if closing_entry else nowtime()
|
||||
)
|
||||
merge_log.company = closing_entry.get("company") if closing_entry else None
|
||||
merge_log.customer = customer
|
||||
merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None
|
||||
merge_log.set("pos_invoices", _invoices)
|
||||
|
||||
@@ -169,7 +169,7 @@ class PricingRule(Document):
|
||||
|
||||
tocheck = frappe.scrub(self.get("applicable_for", ""))
|
||||
if tocheck and not self.get(tocheck):
|
||||
throw(_("{0} is required").format(self.meta.get_label(tocheck)), frappe.MandatoryError)
|
||||
throw(_("{0} is required").format(_(self.meta.get_label(tocheck))), frappe.MandatoryError)
|
||||
|
||||
if self.apply_rule_on_other:
|
||||
o_field = "other_" + frappe.scrub(self.apply_rule_on_other)
|
||||
|
||||
@@ -205,6 +205,56 @@ class TestPricingRule(FrappeTestCase):
|
||||
details = get_item_details(args)
|
||||
self.assertEqual(details.get("discount_percentage"), 10)
|
||||
|
||||
def test_unset_group_condition(self):
|
||||
"""
|
||||
If args are not set for group condition, then pricing rule should not be applied.
|
||||
"""
|
||||
from erpnext.stock.get_item_details import get_item_details
|
||||
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule",
|
||||
"apply_on": "Item Code",
|
||||
"items": [{"item_code": "_Test Item"}],
|
||||
"currency": "USD",
|
||||
"selling": 1,
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"rate": 0,
|
||||
"discount_percentage": 10,
|
||||
"applicable_for": "Territory",
|
||||
"territory": "All Territories",
|
||||
"company": "_Test Company",
|
||||
}
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
args = frappe._dict(
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"company": "_Test Company",
|
||||
"price_list": "_Test Price List",
|
||||
"currency": "_Test Currency",
|
||||
"doctype": "Sales Order",
|
||||
"conversion_rate": 1,
|
||||
"price_list_currency": "_Test Currency",
|
||||
"plc_conversion_rate": 1,
|
||||
"order_type": "Sales",
|
||||
"customer": "_Test Customer",
|
||||
"name": None,
|
||||
}
|
||||
)
|
||||
|
||||
# without territory in customer
|
||||
customer = frappe.get_doc("Customer", "_Test Customer")
|
||||
territory = customer.territory
|
||||
|
||||
customer.territory = None
|
||||
customer.save()
|
||||
|
||||
details = get_item_details(args)
|
||||
self.assertEqual(details.get("discount_percentage"), 0)
|
||||
|
||||
customer.territory = territory
|
||||
customer.save()
|
||||
|
||||
def test_pricing_rule_for_variants(self):
|
||||
from erpnext.stock.get_item_details import get_item_details
|
||||
|
||||
|
||||
@@ -223,6 +223,10 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
|
||||
)
|
||||
|
||||
frappe.flags.tree_conditions[key] = condition
|
||||
|
||||
elif allow_blank:
|
||||
condition = f"ifnull({table}.{field}, '') = ''"
|
||||
|
||||
return condition
|
||||
|
||||
|
||||
|
||||
@@ -40,6 +40,13 @@ class TestProcessDeferredAccounting(unittest.TestCase):
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
original_gle = [
|
||||
["Debtors - _TC", 3000.0, 0, "2023-07-01"],
|
||||
[deferred_account, 0.0, 3000, "2023-07-01"],
|
||||
]
|
||||
|
||||
check_gl_entries(self, si.name, original_gle, "2023-07-01")
|
||||
|
||||
process_deferred_accounting = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Process Deferred Accounting",
|
||||
@@ -63,6 +70,12 @@ class TestProcessDeferredAccounting(unittest.TestCase):
|
||||
]
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, "2023-07-01")
|
||||
|
||||
# cancel the process deferred accounting document
|
||||
process_deferred_accounting.cancel()
|
||||
|
||||
# check if gl entries are cancelled
|
||||
check_gl_entries(self, si.name, original_gle, "2023-07-01")
|
||||
change_acc_settings()
|
||||
|
||||
def test_pda_submission_and_cancellation(self):
|
||||
|
||||
@@ -54,6 +54,9 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
};
|
||||
});
|
||||
frm.set_query("account", function () {
|
||||
if (!frm.doc.company) {
|
||||
frappe.throw(__("Please set Company"));
|
||||
}
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
@@ -61,6 +64,9 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
};
|
||||
});
|
||||
frm.set_query("cost_center", function () {
|
||||
if (!frm.doc.company) {
|
||||
frappe.throw(__("Please set Company"));
|
||||
}
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
@@ -68,6 +74,9 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
};
|
||||
});
|
||||
frm.set_query("project", function () {
|
||||
if (!frm.doc.company) {
|
||||
frappe.throw(__("Please set Company"));
|
||||
}
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
@@ -79,6 +88,11 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
frm.set_value("to_date", frappe.datetime.get_today());
|
||||
}
|
||||
},
|
||||
company: function (frm) {
|
||||
frm.set_value("account", "");
|
||||
frm.set_value("cost_center", "");
|
||||
frm.set_value("project", "");
|
||||
},
|
||||
report: function (frm) {
|
||||
let filters = {
|
||||
company: frm.doc.company,
|
||||
|
||||
@@ -376,7 +376,7 @@
|
||||
"default": "0",
|
||||
"fieldname": "ignore_exchange_rate_revaluation_journals",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Exchange Rate Revaluation Journals"
|
||||
"label": "Ignore Exchange Rate Revaluation and Gain / Loss Journals"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -400,7 +400,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2025-04-30 14:43:23.643006",
|
||||
"modified": "2025-07-08 16:52:12.602384",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -82,6 +82,10 @@ class ProcessStatementOfAccounts(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_account()
|
||||
self.validate_company_for_table("Cost Center")
|
||||
self.validate_company_for_table("Project")
|
||||
|
||||
if not self.subject:
|
||||
self.subject = "Statement Of Accounts for {{ customer.customer_name }}"
|
||||
if not self.body:
|
||||
@@ -104,6 +108,43 @@ class ProcessStatementOfAccounts(Document):
|
||||
self.to_date = self.start_date
|
||||
self.from_date = add_months(self.to_date, -1 * self.filter_duration)
|
||||
|
||||
def validate_account(self):
|
||||
if not self.account:
|
||||
return
|
||||
|
||||
if self.company != frappe.get_cached_value("Account", self.account, "company"):
|
||||
frappe.throw(
|
||||
_("Account {0} doesn't belong to Company {1}").format(
|
||||
frappe.bold(self.account),
|
||||
frappe.bold(self.company),
|
||||
)
|
||||
)
|
||||
|
||||
def validate_company_for_table(self, doctype):
|
||||
field = frappe.scrub(doctype)
|
||||
if not self.get(field):
|
||||
return
|
||||
|
||||
fieldname = field + "_name"
|
||||
|
||||
values = set(d.get(fieldname) for d in self.get(field))
|
||||
invalid_values = frappe.db.get_all(
|
||||
doctype, filters={"name": ["in", values], "company": ["!=", self.company]}, pluck="name"
|
||||
)
|
||||
|
||||
if invalid_values:
|
||||
msg = _("<p>Following {0}s doesn't belong to Company {1} :</p>").format(
|
||||
doctype, frappe.bold(self.company)
|
||||
)
|
||||
|
||||
msg += (
|
||||
"<ul>"
|
||||
+ "".join(_("<li>{}</li>").format(frappe.bold(row)) for row in invalid_values)
|
||||
+ "</ul>"
|
||||
)
|
||||
|
||||
frappe.throw(_(msg))
|
||||
|
||||
|
||||
def get_report_pdf(doc, consolidated=True):
|
||||
statement_dict = get_statement_dict(doc)
|
||||
|
||||
@@ -2497,6 +2497,10 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
for gle in gl_entries:
|
||||
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"book_deferred_entries_based_on": "Days", "book_deferred_entries_via_journal_entry": 0},
|
||||
)
|
||||
def test_deferred_revenue(self):
|
||||
deferred_account = create_account(
|
||||
account_name="Deferred Revenue",
|
||||
@@ -2551,6 +2555,10 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"book_deferred_entries_based_on": "Months", "book_deferred_entries_via_journal_entry": 0},
|
||||
)
|
||||
def test_fixed_deferred_revenue(self):
|
||||
deferred_account = create_account(
|
||||
account_name="Deferred Revenue",
|
||||
@@ -2558,10 +2566,6 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||
acc_settings.book_deferred_entries_based_on = "Months"
|
||||
acc_settings.save()
|
||||
|
||||
item = create_item("_Test Item for Deferred Accounting")
|
||||
item.enable_deferred_revenue = 1
|
||||
item.deferred_revenue_account = deferred_account
|
||||
@@ -2601,10 +2605,6 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
|
||||
|
||||
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||
acc_settings.book_deferred_entries_based_on = "Days"
|
||||
acc_settings.save()
|
||||
|
||||
def test_validate_inter_company_transaction_address_links(self):
|
||||
def _validate_address_link(address, link_doctype, link_name):
|
||||
return frappe.db.get_value(
|
||||
@@ -2853,7 +2853,9 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(si.items[0].rate, rate)
|
||||
self.assertEqual(target_doc.items[0].rate, rate)
|
||||
|
||||
check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1))
|
||||
check_gl_entries(
|
||||
self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1), voucher_type="Purchase Invoice"
|
||||
)
|
||||
|
||||
def test_internal_transfer_gl_precision_issues(self):
|
||||
# Make a stock queue of an item with two valuations
|
||||
@@ -4587,6 +4589,8 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date, voucher_type="
|
||||
)
|
||||
gl_entries = q.run(as_dict=True)
|
||||
|
||||
doc.assertGreater(len(gl_entries), 0)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
doc.assertEqual(expected_gle[i][0], gle.account)
|
||||
doc.assertEqual(expected_gle[i][1], gle.debit)
|
||||
|
||||
@@ -12,6 +12,7 @@ from frappe.utils.data import comma_and
|
||||
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_advance_payment_doctypes,
|
||||
unlink_ref_doc_from_payment_entries,
|
||||
update_voucher_outstanding,
|
||||
)
|
||||
@@ -84,7 +85,7 @@ class UnreconcilePayment(Document):
|
||||
update_voucher_outstanding(
|
||||
alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party
|
||||
)
|
||||
if doc.doctype in frappe.get_hooks("advance_payment_doctypes"):
|
||||
if doc.doctype in get_advance_payment_doctypes():
|
||||
doc.set_total_advance_paid()
|
||||
|
||||
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)
|
||||
|
||||
@@ -692,7 +692,18 @@ def make_reverse_gl_entries(
|
||||
query.run()
|
||||
else:
|
||||
if not immutable_ledger_enabled:
|
||||
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
||||
gle_names = [x.get("name") for x in gl_entries]
|
||||
|
||||
# if names are available, cancel only that set of entries
|
||||
if not all(gle_names):
|
||||
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
||||
else:
|
||||
frappe.db.sql(
|
||||
"""UPDATE `tabGL Entry` SET is_cancelled = 1,
|
||||
modified=%s, modified_by=%s
|
||||
where name in %s and is_cancelled = 0""",
|
||||
(now(), frappe.session.user, tuple(gle_names)),
|
||||
)
|
||||
|
||||
for entry in gl_entries:
|
||||
new_gle = copy.deepcopy(entry)
|
||||
|
||||
@@ -15,7 +15,11 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
get_dimension_with_children,
|
||||
)
|
||||
from erpnext.accounts.utils import get_currency_precision, get_party_types_from_account_type
|
||||
from erpnext.accounts.utils import (
|
||||
get_advance_payment_doctypes,
|
||||
get_currency_precision,
|
||||
get_party_types_from_account_type,
|
||||
)
|
||||
|
||||
# This report gives a summary of all Outstanding Invoices considering the following
|
||||
|
||||
@@ -62,6 +66,7 @@ class ReceivablePayableReport:
|
||||
frappe.db.get_single_value("Accounts Settings", "receivable_payable_fetch_method")
|
||||
or "Buffered Cursor"
|
||||
) # Fail Safe
|
||||
self.advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
|
||||
def run(self, args):
|
||||
self.filters.update(args)
|
||||
@@ -85,6 +90,7 @@ class ReceivablePayableReport:
|
||||
self.party_details = {}
|
||||
self.invoices = set()
|
||||
self.skip_total_row = 0
|
||||
self.advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
self.previous_party = ""
|
||||
@@ -181,7 +187,10 @@ class ReceivablePayableReport:
|
||||
if key not in self.voucher_balance:
|
||||
self.voucher_balance[key] = self.build_voucher_dict(ple)
|
||||
|
||||
if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no:
|
||||
if (ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no) or (
|
||||
ple.voucher_type in ("Payment Entry", "Journal Entry")
|
||||
and ple.against_voucher_type in self.advance_payment_doctypes
|
||||
):
|
||||
self.voucher_balance[key].cost_center = ple.cost_center
|
||||
|
||||
self.get_invoices(ple)
|
||||
|
||||
@@ -79,6 +79,14 @@ class Deferred_Item:
|
||||
return - estimated amount to post for given period
|
||||
Calculated based on already booked amount and item service period
|
||||
"""
|
||||
if self.filters.book_deferred_entries_based_on == "Months":
|
||||
# if the deferred entries are based on service period, use service start and end date
|
||||
return self.calculate_monthly_amount(start_date, end_date)
|
||||
|
||||
else:
|
||||
return self.calculate_days_amount(start_date, end_date)
|
||||
|
||||
def calculate_monthly_amount(self, start_date, end_date):
|
||||
total_months = (
|
||||
(self.service_end_date.year - self.service_start_date.year) * 12
|
||||
+ (self.service_end_date.month - self.service_start_date.month)
|
||||
@@ -105,6 +113,19 @@ class Deferred_Item:
|
||||
|
||||
return base_amount
|
||||
|
||||
def calculate_days_amount(self, start_date, end_date):
|
||||
base_amount = 0
|
||||
total_days = date_diff(self.service_end_date, self.service_start_date) + 1
|
||||
total_booking_days = date_diff(end_date, start_date) + 1
|
||||
already_booked_amount = self.get_item_total()
|
||||
|
||||
base_amount = flt(self.base_net_amount * total_booking_days / flt(total_days))
|
||||
|
||||
if base_amount + already_booked_amount > self.base_net_amount:
|
||||
base_amount = self.base_net_amount - already_booked_amount
|
||||
|
||||
return base_amount
|
||||
|
||||
def make_dummy_gle(self, name, date, amount):
|
||||
"""
|
||||
return - frappe._dict() of a dummy gle entry
|
||||
@@ -245,6 +266,10 @@ class Deferred_Revenue_and_Expense_Report:
|
||||
else:
|
||||
self.filters = frappe._dict(filters)
|
||||
|
||||
self.filters.book_deferred_entries_based_on = frappe.db.get_singles_value(
|
||||
"Accounts Settings", "book_deferred_entries_based_on"
|
||||
)
|
||||
|
||||
self.period_list = None
|
||||
self.deferred_invoices = []
|
||||
# holds period wise total for report
|
||||
@@ -289,7 +314,11 @@ class Deferred_Revenue_and_Expense_Report:
|
||||
.join(inv)
|
||||
.on(inv.name == inv_item.parent)
|
||||
.left_join(gle)
|
||||
.on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account))
|
||||
.on(
|
||||
(inv_item.name == gle.voucher_detail_no)
|
||||
& (deferred_account_field == gle.account)
|
||||
& (gle.is_cancelled == 0)
|
||||
)
|
||||
.select(
|
||||
inv.name.as_("doc"),
|
||||
inv.posting_date,
|
||||
|
||||
@@ -9,7 +9,7 @@ import re
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.query_builder.functions import Max, Min, Sum
|
||||
from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
@@ -109,14 +109,19 @@ def get_period_list(
|
||||
|
||||
|
||||
def get_fiscal_year_data(from_fiscal_year, to_fiscal_year):
|
||||
fiscal_year = frappe.db.sql(
|
||||
"""select min(year_start_date) as year_start_date,
|
||||
max(year_end_date) as year_end_date from `tabFiscal Year` where
|
||||
name between %(from_fiscal_year)s and %(to_fiscal_year)s""",
|
||||
{"from_fiscal_year": from_fiscal_year, "to_fiscal_year": to_fiscal_year},
|
||||
as_dict=1,
|
||||
from_year_start_date = frappe.get_cached_value("Fiscal Year", from_fiscal_year, "year_start_date")
|
||||
to_year_end_date = frappe.get_cached_value("Fiscal Year", to_fiscal_year, "year_end_date")
|
||||
|
||||
fy = frappe.qb.DocType("Fiscal Year")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(fy)
|
||||
.select(Min(fy.year_start_date).as_("year_start_date"), Max(fy.year_end_date).as_("year_end_date"))
|
||||
.where(fy.year_start_date >= from_year_start_date)
|
||||
.where(fy.year_end_date <= to_year_end_date)
|
||||
)
|
||||
|
||||
fiscal_year = query.run(as_dict=True)
|
||||
return fiscal_year[0] if fiscal_year else {}
|
||||
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
},
|
||||
{
|
||||
fieldname: "ignore_err",
|
||||
label: __("Ignore Exchange Rate Revaluation Journals"),
|
||||
label: __("Ignore Exchange Rate Revaluation and Gain / Loss Journals"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,12 +6,9 @@ from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import flt, today
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import execute
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||
|
||||
|
||||
class TestGeneralLedger(FrappeTestCase):
|
||||
@@ -171,90 +168,6 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
self.assertEqual(data[3]["debit"], 100)
|
||||
self.assertEqual(data[3]["credit"], 100)
|
||||
|
||||
@change_settings("Accounts Settings", {"delete_linked_ledger_entries": True})
|
||||
def test_debit_in_exchange_gain_loss_account(self):
|
||||
company = "_Test Company"
|
||||
|
||||
exchange_gain_loss_account = frappe.db.get_value("Company", "exchange_gain_loss_account")
|
||||
if not exchange_gain_loss_account:
|
||||
frappe.db.set_value(
|
||||
"Company", company, "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
|
||||
)
|
||||
|
||||
account_name = "_Test Receivable USD - _TC"
|
||||
customer_name = "_Test Customer USD"
|
||||
|
||||
sales_invoice = create_sales_invoice(
|
||||
company=company,
|
||||
customer=customer_name,
|
||||
currency="USD",
|
||||
debit_to=account_name,
|
||||
conversion_rate=85,
|
||||
posting_date=today(),
|
||||
)
|
||||
|
||||
payment_entry = create_payment_entry(
|
||||
company=company,
|
||||
party_type="Customer",
|
||||
party=customer_name,
|
||||
payment_type="Receive",
|
||||
paid_from=account_name,
|
||||
paid_from_account_currency="USD",
|
||||
paid_to="Cash - _TC",
|
||||
paid_to_account_currency="INR",
|
||||
paid_amount=10,
|
||||
do_not_submit=True,
|
||||
)
|
||||
payment_entry.base_paid_amount = 800
|
||||
payment_entry.received_amount = 800
|
||||
payment_entry.currency = "USD"
|
||||
payment_entry.source_exchange_rate = 80
|
||||
payment_entry.append(
|
||||
"references",
|
||||
frappe._dict(
|
||||
{
|
||||
"reference_doctype": "Sales Invoice",
|
||||
"reference_name": sales_invoice.name,
|
||||
"total_amount": 10,
|
||||
"outstanding_amount": 10,
|
||||
"exchange_rate": 85,
|
||||
"allocated_amount": 10,
|
||||
"exchange_gain_loss": -50,
|
||||
}
|
||||
),
|
||||
)
|
||||
payment_entry.save()
|
||||
payment_entry.submit()
|
||||
|
||||
journal_entry = frappe.get_all(
|
||||
"Journal Entry Account", filters={"reference_name": sales_invoice.name}, fields=["parent"]
|
||||
)
|
||||
|
||||
columns, data = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"company": company,
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"include_dimensions": 1,
|
||||
"include_default_book_entries": 1,
|
||||
"account": ["_Test Exchange Gain/Loss - _TC"],
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
entry = data[1]
|
||||
self.assertEqual(entry["debit"], 50)
|
||||
self.assertEqual(entry["voucher_type"], "Journal Entry")
|
||||
self.assertEqual(entry["voucher_no"], journal_entry[0]["parent"])
|
||||
|
||||
payment_entry.cancel()
|
||||
payment_entry.delete()
|
||||
sales_invoice.reload()
|
||||
sales_invoice.cancel()
|
||||
sales_invoice.delete()
|
||||
|
||||
def test_ignore_exchange_rate_journals_filter(self):
|
||||
# create a new account with USD currency
|
||||
account_name = "Test Debtors USD"
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import functions as fn
|
||||
from frappe.utils import cstr, flt
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
from frappe.utils.xlsxutils import handle_html
|
||||
from pypika import Order
|
||||
|
||||
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
|
||||
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
|
||||
from erpnext.accounts.report.utils import get_values_for_columns
|
||||
from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import (
|
||||
get_customer_details,
|
||||
)
|
||||
@@ -433,7 +434,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
si.is_internal_customer,
|
||||
si.customer,
|
||||
si.remarks,
|
||||
si.territory,
|
||||
fn.IfNull(si.territory, "Not Specified").as_("territory"),
|
||||
si.company,
|
||||
si.base_net_total,
|
||||
sii.project,
|
||||
@@ -456,7 +457,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
sii.base_net_rate,
|
||||
sii.base_net_amount,
|
||||
si.customer_name,
|
||||
si.customer_group,
|
||||
fn.IfNull(si.customer_group, "Not Specified").as_("customer_group"),
|
||||
sii.so_detail,
|
||||
si.update_stock,
|
||||
sii.uom,
|
||||
|
||||
@@ -121,7 +121,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
)
|
||||
out.append(row)
|
||||
|
||||
out.sort(key=lambda x: x["section_code"])
|
||||
out.sort(key=lambda x: (x["section_code"], x["transaction_date"]))
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@@ -67,11 +67,12 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
|
||||
mid_year = add_to_date(fiscal_year[1], months=6)
|
||||
tds_doc = frappe.get_doc("Tax Withholding Category", "TDS - 3")
|
||||
tds_doc.rates[0].to_date = mid_year
|
||||
from_date = add_to_date(mid_year, days=1)
|
||||
tds_doc.append(
|
||||
"rates",
|
||||
{
|
||||
"tax_withholding_rate": 20,
|
||||
"from_date": add_to_date(mid_year, days=1),
|
||||
"from_date": from_date,
|
||||
"to_date": fiscal_year[2],
|
||||
"single_threshold": 1,
|
||||
"cumulative_threshold": 1,
|
||||
@@ -80,18 +81,19 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
tds_doc.save()
|
||||
|
||||
inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True)
|
||||
inv_1 = make_purchase_invoice(
|
||||
rate=1000, posting_date=add_to_date(fiscal_year[1], days=1), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
inv_1.set_posting_time = 1
|
||||
inv_1.apply_tds = 1
|
||||
inv_1.tax_withholding_category = "TDS - 3"
|
||||
inv_1.tax_withholding_category = tds_doc.name
|
||||
inv_1.save()
|
||||
inv_1.submit()
|
||||
|
||||
inv_2 = make_purchase_invoice(
|
||||
rate=1000, do_not_submit=True, posting_date=add_to_date(mid_year, days=1), do_not_save=True
|
||||
)
|
||||
inv_2 = make_purchase_invoice(rate=1000, posting_date=from_date, do_not_save=True, do_not_submit=True)
|
||||
inv_2.set_posting_time = 1
|
||||
|
||||
inv_1.apply_tds = 1
|
||||
inv_2.tax_withholding_category = "TDS - 3"
|
||||
inv_2.apply_tds = 1
|
||||
inv_2.tax_withholding_category = tds_doc.name
|
||||
inv_2.save()
|
||||
inv_2.submit()
|
||||
|
||||
|
||||
@@ -107,11 +107,7 @@ def convert_to_presentation_currency(gl_entries, currency_info):
|
||||
credit_in_account_currency = flt(entry["credit_in_account_currency"])
|
||||
account_currency = entry["account_currency"]
|
||||
|
||||
if (
|
||||
len(account_currencies) == 1
|
||||
and account_currency == presentation_currency
|
||||
and (debit_in_account_currency or credit_in_account_currency)
|
||||
):
|
||||
if len(account_currencies) == 1 and account_currency == presentation_currency:
|
||||
entry["debit"] = debit_in_account_currency
|
||||
entry["credit"] = credit_in_account_currency
|
||||
else:
|
||||
|
||||
@@ -169,7 +169,7 @@ def validate_fiscal_year(date, fiscal_year, company, label="Date", doc=None):
|
||||
if doc:
|
||||
doc.fiscal_year = years[0]
|
||||
else:
|
||||
throw(_("{0} '{1}' not in Fiscal Year {2}").format(label, formatdate(date), fiscal_year))
|
||||
throw(_("{0} '{1}' not in Fiscal Year {2}").format(_(label), formatdate(date), fiscal_year))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -629,6 +629,7 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
||||
|
||||
# Update Advance Paid in SO/PO since they might be getting unlinked
|
||||
update_advance_paid = []
|
||||
|
||||
if jv_detail.get("reference_type") in ["Sales Order", "Purchase Order"]:
|
||||
update_advance_paid.append((jv_detail.reference_type, jv_detail.reference_name))
|
||||
|
||||
@@ -713,23 +714,8 @@ def update_reference_in_payment_entry(
|
||||
update_advance_paid = []
|
||||
|
||||
# Update Reconciliation effect date in reference
|
||||
reconciliation_takes_effect_on = frappe.get_cached_value(
|
||||
"Company", payment_entry.company, "reconciliation_takes_effect_on"
|
||||
)
|
||||
if payment_entry.book_advance_payments_in_separate_party_account:
|
||||
if reconciliation_takes_effect_on == "Advance Payment Date":
|
||||
reconcile_on = payment_entry.posting_date
|
||||
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
|
||||
date_field = "posting_date"
|
||||
if d.against_voucher_type in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
reconcile_on = frappe.db.get_value(d.against_voucher_type, d.against_voucher, date_field)
|
||||
|
||||
if getdate(reconcile_on) < getdate(payment_entry.posting_date):
|
||||
reconcile_on = payment_entry.posting_date
|
||||
elif reconciliation_takes_effect_on == "Reconciliation Date":
|
||||
reconcile_on = nowdate()
|
||||
|
||||
reconcile_on = get_reconciliation_effect_date(d, payment_entry.company, payment_entry.posting_date)
|
||||
reference_details.update({"reconcile_effect_on": reconcile_on})
|
||||
|
||||
if d.voucher_detail_no:
|
||||
@@ -783,6 +769,28 @@ def update_reference_in_payment_entry(
|
||||
return row, update_advance_paid
|
||||
|
||||
|
||||
def get_reconciliation_effect_date(reference, company, posting_date):
|
||||
reconciliation_takes_effect_on = frappe.get_cached_value(
|
||||
"Company", company, "reconciliation_takes_effect_on"
|
||||
)
|
||||
|
||||
if reconciliation_takes_effect_on == "Advance Payment Date":
|
||||
reconcile_on = posting_date
|
||||
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
|
||||
date_field = "posting_date"
|
||||
if reference.against_voucher_type in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
reconcile_on = frappe.db.get_value(
|
||||
reference.against_voucher_type, reference.against_voucher, date_field
|
||||
)
|
||||
if getdate(reconcile_on) < getdate(posting_date):
|
||||
reconcile_on = posting_date
|
||||
elif reconciliation_takes_effect_on == "Reconciliation Date":
|
||||
reconcile_on = nowdate()
|
||||
|
||||
return reconcile_on
|
||||
|
||||
|
||||
def cancel_exchange_gain_loss_journal(
|
||||
parent_doc: dict | object, referenced_dt: str | None = None, referenced_dn: str | None = None
|
||||
) -> None:
|
||||
@@ -997,58 +1005,79 @@ def remove_ref_doc_link_from_pe(
|
||||
per = qb.DocType("Payment Entry Reference")
|
||||
pay = qb.DocType("Payment Entry")
|
||||
|
||||
linked_pe = (
|
||||
query = (
|
||||
qb.from_(per)
|
||||
.select(per.parent)
|
||||
.where((per.reference_doctype == ref_type) & (per.reference_name == ref_no) & (per.docstatus.lt(2)))
|
||||
.run(as_list=1)
|
||||
)
|
||||
linked_pe = convert_to_list(linked_pe)
|
||||
# remove reference only from specified payment
|
||||
linked_pe = [x for x in linked_pe if x == payment_name] if payment_name else linked_pe
|
||||
|
||||
if linked_pe:
|
||||
update_query = (
|
||||
qb.update(per)
|
||||
.set(per.allocated_amount, 0)
|
||||
.set(per.modified, now())
|
||||
.set(per.modified_by, frappe.session.user)
|
||||
.where(per.docstatus.lt(2) & (per.reference_doctype == ref_type) & (per.reference_name == ref_no))
|
||||
.select("*")
|
||||
.where(
|
||||
(per.reference_doctype == ref_type)
|
||||
& (per.reference_name == ref_no)
|
||||
& (per.docstatus.lt(2))
|
||||
& (per.parenttype == "Payment Entry")
|
||||
)
|
||||
)
|
||||
|
||||
if payment_name:
|
||||
update_query = update_query.where(per.parent == payment_name)
|
||||
# update reference only from specified payment
|
||||
if payment_name:
|
||||
query = query.where(per.parent == payment_name)
|
||||
|
||||
update_query.run()
|
||||
reference_rows = query.run(as_dict=True)
|
||||
|
||||
for pe in linked_pe:
|
||||
try:
|
||||
pe_doc = frappe.get_doc("Payment Entry", pe)
|
||||
pe_doc.set_amounts()
|
||||
if not reference_rows:
|
||||
return
|
||||
|
||||
# Call cancel on only removed reference
|
||||
references = [
|
||||
x
|
||||
for x in pe_doc.references
|
||||
if x.reference_doctype == ref_type and x.reference_name == ref_no
|
||||
]
|
||||
[pe_doc.make_advance_gl_entries(x, cancel=1) for x in references]
|
||||
linked_pe = set()
|
||||
row_names = set()
|
||||
|
||||
pe_doc.clear_unallocated_reference_document_rows()
|
||||
pe_doc.validate_payment_type_with_outstanding()
|
||||
except Exception:
|
||||
msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name)
|
||||
msg += "<br>"
|
||||
msg += _("Please cancel payment entry manually first")
|
||||
frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error"))
|
||||
for row in reference_rows:
|
||||
linked_pe.add(row.parent)
|
||||
row_names.add(row.name)
|
||||
|
||||
qb.update(pay).set(pay.total_allocated_amount, pe_doc.total_allocated_amount).set(
|
||||
pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount
|
||||
).set(pay.unallocated_amount, pe_doc.unallocated_amount).set(pay.modified, now()).set(
|
||||
pay.modified_by, frappe.session.user
|
||||
).where(pay.name == pe).run()
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import (
|
||||
update_payment_requests_as_per_pe_references,
|
||||
)
|
||||
|
||||
frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe)))
|
||||
# Update payment request amount
|
||||
update_payment_requests_as_per_pe_references(reference_rows, cancel=True)
|
||||
|
||||
# Update allocated amounts and modified fields in one go
|
||||
(
|
||||
qb.update(per)
|
||||
.set(per.allocated_amount, 0)
|
||||
.set(per.modified, now())
|
||||
.set(per.modified_by, frappe.session.user)
|
||||
.where(per.name.isin(row_names))
|
||||
.where(per.parenttype == "Payment Entry")
|
||||
.run()
|
||||
)
|
||||
|
||||
for pe in linked_pe:
|
||||
try:
|
||||
pe_doc = frappe.get_doc("Payment Entry", pe)
|
||||
pe_doc.set_amounts()
|
||||
|
||||
# Call cancel on only removed reference
|
||||
references = [x for x in pe_doc.references if x.name in row_names]
|
||||
[pe_doc.make_advance_gl_entries(x, cancel=1) for x in references]
|
||||
|
||||
pe_doc.clear_unallocated_reference_document_rows()
|
||||
pe_doc.validate_payment_type_with_outstanding()
|
||||
except Exception:
|
||||
msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name)
|
||||
msg += "<br>"
|
||||
msg += _("Please cancel payment entry manually first")
|
||||
frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error"))
|
||||
|
||||
(
|
||||
qb.update(pay)
|
||||
.set(pay.total_allocated_amount, pe_doc.total_allocated_amount)
|
||||
.set(pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount)
|
||||
.set(pay.unallocated_amount, pe_doc.unallocated_amount)
|
||||
.set(pay.modified, now())
|
||||
.set(pay.modified_by, frappe.session.user)
|
||||
.where(pay.name == pe)
|
||||
.run()
|
||||
)
|
||||
frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe)))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -2235,6 +2264,15 @@ def get_party_types_from_account_type(account_type):
|
||||
return frappe.db.get_all("Party Type", {"account_type": account_type}, pluck="name")
|
||||
|
||||
|
||||
def get_advance_payment_doctypes():
|
||||
"""
|
||||
Get list of advance payment doctypes based on type.
|
||||
:param type: Optional, can be "receivable" or "payable". If not provided, returns both.
|
||||
"""
|
||||
|
||||
return frappe.get_hooks("advance_payment_doctypes")
|
||||
|
||||
|
||||
def run_ledger_health_checks():
|
||||
health_monitor_settings = frappe.get_doc("Ledger Health Monitor")
|
||||
if health_monitor_settings.enable_health_monitor:
|
||||
|
||||
@@ -807,11 +807,19 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
target.credit_to = get_party_account("Supplier", source.supplier, source.company)
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
target.amount = flt(obj.amount) - flt(obj.billed_amt)
|
||||
target.base_amount = target.amount * flt(source_parent.conversion_rate)
|
||||
target.qty = (
|
||||
target.amount / flt(obj.rate) if (flt(obj.rate) and flt(obj.billed_amt)) else flt(obj.qty)
|
||||
)
|
||||
def get_billed_qty(po_item_name):
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
table = frappe.qb.DocType("Purchase Invoice Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(Sum(table.qty).as_("qty"))
|
||||
.where((table.docstatus == 1) & (table.po_detail == po_item_name))
|
||||
)
|
||||
return query.run(pluck="qty")[0] or 0
|
||||
|
||||
billed_qty = flt(get_billed_qty(obj.name))
|
||||
target.qty = flt(obj.qty) - billed_qty
|
||||
|
||||
item = get_item_defaults(target.item_code, source_parent.company)
|
||||
item_group = get_item_group_defaults(target.item_code, source_parent.company)
|
||||
|
||||
@@ -1286,6 +1286,25 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
self.assertFalse(po.per_billed)
|
||||
self.assertEqual(po.status, "To Receive and Bill")
|
||||
|
||||
@change_settings("Buying Settings", {"maintain_same_rate": 0})
|
||||
def test_purchase_invoice_creation_with_partial_qty(self):
|
||||
po = create_purchase_order(qty=100, rate=10)
|
||||
|
||||
pi = make_pi_from_po(po.name)
|
||||
pi.items[0].qty = 42
|
||||
pi.items[0].rate = 7.5
|
||||
pi.submit()
|
||||
|
||||
pi = make_pi_from_po(po.name)
|
||||
self.assertEqual(pi.items[0].qty, 58)
|
||||
self.assertEqual(pi.items[0].rate, 10)
|
||||
pi.items[0].qty = 8
|
||||
pi.items[0].rate = 5
|
||||
pi.submit()
|
||||
|
||||
pi = make_pi_from_po(po.name)
|
||||
self.assertEqual(pi.items[0].qty, 50)
|
||||
|
||||
|
||||
def create_po_for_sc_testing():
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
|
||||
@@ -51,6 +51,9 @@ from erpnext.accounts.utils import (
|
||||
get_fiscal_years,
|
||||
validate_fiscal_year,
|
||||
)
|
||||
from erpnext.accounts.utils import (
|
||||
get_advance_payment_doctypes as _get_advance_payment_doctypes,
|
||||
)
|
||||
from erpnext.buying.utils import update_last_purchase_rate
|
||||
from erpnext.controllers.print_settings import (
|
||||
set_print_templates_for_item_table,
|
||||
@@ -386,9 +389,7 @@ class AccountsController(TransactionBase):
|
||||
adv = qb.DocType("Advance Payment Ledger Entry")
|
||||
qb.from_(adv).delete().where(adv.voucher_type.eq(self.doctype) & adv.voucher_no.eq(self.name)).run()
|
||||
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_doctypes")
|
||||
|
||||
if self.doctype in advance_payment_doctypes:
|
||||
if self.doctype in self.get_advance_payment_doctypes():
|
||||
qb.from_(adv).delete().where(
|
||||
adv.against_voucher_type.eq(self.doctype) & adv.against_voucher_no.eq(self.name)
|
||||
).run()
|
||||
@@ -2912,7 +2913,7 @@ class AccountsController(TransactionBase):
|
||||
repost_ledger.submit()
|
||||
|
||||
def get_advance_payment_doctypes(self) -> list:
|
||||
return frappe.get_hooks("advance_payment_doctypes")
|
||||
return _get_advance_payment_doctypes()
|
||||
|
||||
def make_advance_payment_ledger_for_journal(self):
|
||||
advance_payment_doctypes = self.get_advance_payment_doctypes()
|
||||
@@ -3965,6 +3966,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
).format(frappe.bold(parent.name))
|
||||
)
|
||||
else: # Sales Order
|
||||
parent.validate_selling_price()
|
||||
parent.validate_for_duplicate_items()
|
||||
parent.validate_warehouse()
|
||||
parent.update_reserved_qty()
|
||||
|
||||
@@ -1648,8 +1648,9 @@ def make_quality_inspections(doctype, docname, items):
|
||||
"sample_size": flt(item.get("sample_size")),
|
||||
"item_serial_no": item.get("serial_no").split("\n")[0] if item.get("serial_no") else None,
|
||||
"batch_no": item.get("batch_no"),
|
||||
"child_row_reference": item.get("child_row_reference"),
|
||||
}
|
||||
).insert()
|
||||
)
|
||||
quality_inspection.save()
|
||||
inspections.append(quality_inspection.name)
|
||||
|
||||
@@ -1662,14 +1663,9 @@ def is_reposting_pending():
|
||||
)
|
||||
|
||||
|
||||
def future_sle_exists(args, sl_entries=None, allow_force_reposting=True):
|
||||
def future_sle_exists(args, sl_entries=None):
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
|
||||
if allow_force_reposting and frappe.db.get_single_value(
|
||||
"Stock Reposting Settings", "do_reposting_for_each_stock_transaction"
|
||||
):
|
||||
return True
|
||||
|
||||
key = (args.voucher_type, args.voucher_no)
|
||||
if not hasattr(frappe.local, "future_sle"):
|
||||
frappe.local.future_sle = {}
|
||||
|
||||
@@ -601,12 +601,15 @@ class SubcontractingController(StockController):
|
||||
rm_obj.use_serial_batch_fields = 1
|
||||
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
|
||||
|
||||
if self.doctype == "Subcontracting Receipt" and not use_serial_batch_fields:
|
||||
rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle(
|
||||
item_row, rm_obj, rm_obj.consumed_qty
|
||||
)
|
||||
if self.doctype == "Subcontracting Receipt":
|
||||
if not use_serial_batch_fields:
|
||||
rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle(
|
||||
item_row, rm_obj, rm_obj.consumed_qty
|
||||
)
|
||||
|
||||
self.set_rate_for_supplied_items(rm_obj, item_row)
|
||||
self.set_rate_for_supplied_items(rm_obj, item_row)
|
||||
elif self.backflush_based_on == "BOM":
|
||||
self.update_rate_for_supplied_items()
|
||||
|
||||
def update_rate_for_supplied_items(self):
|
||||
if self.doctype != "Subcontracting Receipt":
|
||||
|
||||
@@ -987,7 +987,7 @@ class BOM(WebsiteGenerator):
|
||||
self.transfer_material_against = "Work Order"
|
||||
if not self.transfer_material_against and not self.is_new():
|
||||
frappe.throw(
|
||||
_("Setting {} is required").format(self.meta.get_label("transfer_material_against")),
|
||||
_("Setting {0} is required").format(_(self.meta.get_label("transfer_material_against"))),
|
||||
title=_("Missing value"),
|
||||
)
|
||||
|
||||
|
||||
@@ -51,9 +51,13 @@ frappe.ui.form.on("Job Card", {
|
||||
let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
|
||||
|
||||
if (to_request || excess_transfer_allowed) {
|
||||
frm.add_custom_button(__("Material Request"), () => {
|
||||
frm.trigger("make_material_request");
|
||||
});
|
||||
frm.add_custom_button(
|
||||
__("Material Request"),
|
||||
() => {
|
||||
frm.trigger("make_material_request");
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
|
||||
// check if any row has untransferred materials
|
||||
@@ -61,9 +65,13 @@ frappe.ui.form.on("Job Card", {
|
||||
let to_transfer = frm.doc.items.some((row) => row.transferred_qty < row.required_qty);
|
||||
|
||||
if (to_transfer || excess_transfer_allowed) {
|
||||
frm.add_custom_button(__("Material Transfer"), () => {
|
||||
frm.trigger("make_stock_entry");
|
||||
}).addClass("btn-primary");
|
||||
frm.add_custom_button(
|
||||
__("Material Transfer"),
|
||||
() => {
|
||||
frm.trigger("make_stock_entry");
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -390,7 +390,7 @@ class WorkOrder(Document):
|
||||
if qty > completed_qty:
|
||||
frappe.throw(
|
||||
_("{0} ({1}) cannot be greater than planned quantity ({2}) in Work Order {3}").format(
|
||||
self.meta.get_label(fieldname), qty, completed_qty, self.name
|
||||
_(self.meta.get_label(fieldname)), qty, completed_qty, self.name
|
||||
),
|
||||
StockOverProductionError,
|
||||
)
|
||||
@@ -1077,7 +1077,7 @@ class WorkOrder(Document):
|
||||
self.transfer_material_against = "Work Order"
|
||||
if not self.transfer_material_against:
|
||||
frappe.throw(
|
||||
_("Setting {} is required").format(self.meta.get_label("transfer_material_against")),
|
||||
_("Setting {0} is required").format(_(self.meta.get_label("transfer_material_against"))),
|
||||
title=_("Missing value"),
|
||||
)
|
||||
|
||||
|
||||
@@ -411,3 +411,4 @@ erpnext.patches.v14_0.update_full_name_in_contract
|
||||
erpnext.patches.v15_0.drop_sle_indexes
|
||||
erpnext.patches.v15_0.update_pick_list_fields
|
||||
erpnext.patches.v15_0.update_pegged_currencies
|
||||
erpnext.patches.v15_0.set_company_on_pos_inv_merge_log
|
||||
|
||||
12
erpnext/patches/v15_0/set_company_on_pos_inv_merge_log.py
Normal file
12
erpnext/patches/v15_0/set_company_on_pos_inv_merge_log.py
Normal 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)
|
||||
@@ -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) {
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"day_to_send",
|
||||
"weekly_time_to_send",
|
||||
"column_break_45",
|
||||
"subject",
|
||||
"message"
|
||||
],
|
||||
"fields": [
|
||||
@@ -447,6 +448,13 @@
|
||||
"print_hide": 1,
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "collect_progress",
|
||||
"fieldname": "subject",
|
||||
"fieldtype": "Data",
|
||||
"label": "Subject",
|
||||
"mandatory_depends_on": "collect_progress"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-puzzle-piece",
|
||||
@@ -454,7 +462,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"max_attachments": 4,
|
||||
"modified": "2024-04-24 10:56:16.001032",
|
||||
"modified": "2025-07-03 10:54:30.444139",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Project",
|
||||
@@ -501,6 +509,7 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "project_name,customer, status, priority, is_active",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
@@ -509,4 +518,4 @@
|
||||
"timeline_field": "customer",
|
||||
"title_field": "project_name",
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ class Project(Document):
|
||||
sales_order: DF.Link | None
|
||||
second_email: DF.Time | None
|
||||
status: DF.Literal["Open", "Completed", "Cancelled"]
|
||||
subject: DF.Data | None
|
||||
to_time: DF.Time | None
|
||||
total_billable_amount: DF.Currency
|
||||
total_billed_amount: DF.Currency
|
||||
@@ -606,8 +607,6 @@ def send_project_update_email_to_users(project):
|
||||
}
|
||||
).insert()
|
||||
|
||||
subject = "For project %s, update your status" % (project)
|
||||
|
||||
incoming_email_account = frappe.db.get_value(
|
||||
"Email Account", dict(enable_incoming=1, default_incoming=1), "email_id"
|
||||
)
|
||||
@@ -615,7 +614,7 @@ def send_project_update_email_to_users(project):
|
||||
frappe.sendmail(
|
||||
recipients=get_users_email(doc),
|
||||
message=doc.message,
|
||||
subject=_(subject),
|
||||
subject=doc.subject,
|
||||
reference_doctype=project_update.doctype,
|
||||
reference_name=project_update.name,
|
||||
reply_to=incoming_email_account,
|
||||
|
||||
@@ -581,7 +581,8 @@ erpnext.buying.get_items_from_product_bundle = function(frm) {
|
||||
transaction_date: frm.doc.transaction_date || frm.doc.posting_date,
|
||||
ignore_pricing_rule: frm.doc.ignore_pricing_rule,
|
||||
doctype: frm.doc.doctype
|
||||
}
|
||||
},
|
||||
price_list: frm.doc.price_list,
|
||||
},
|
||||
freeze: true,
|
||||
callback: function(r) {
|
||||
|
||||
@@ -371,6 +371,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
"inspection_type": inspection_type,
|
||||
"reference_type": me.frm.doc.doctype,
|
||||
"reference_name": me.frm.doc.name,
|
||||
"child_row_reference": row.doc.name,
|
||||
"item_code": row.doc.item_code,
|
||||
"description": row.doc.description,
|
||||
"item_serial_no": row.doc.serial_no ? row.doc.serial_no.split("\n")[0] : null,
|
||||
@@ -385,7 +386,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
docstatus: ["<", 2],
|
||||
inspection_type: inspection_type,
|
||||
reference_name: doc.name,
|
||||
item_code: d.item_code
|
||||
item_code: d.item_code,
|
||||
child_row_reference : d.name
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -2427,12 +2429,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
fields: fields,
|
||||
primary_action: function () {
|
||||
const data = dialog.get_values();
|
||||
const selected_data = data.items.filter(item => item?.__checked == 1 );
|
||||
frappe.call({
|
||||
method: "erpnext.controllers.stock_controller.make_quality_inspections",
|
||||
args: {
|
||||
doctype: me.frm.doc.doctype,
|
||||
docname: me.frm.doc.name,
|
||||
items: data.items
|
||||
items: selected_data,
|
||||
},
|
||||
freeze: true,
|
||||
callback: function (r) {
|
||||
|
||||
@@ -1063,21 +1063,31 @@
|
||||
justify-content: flex-end;
|
||||
padding-right: var(--padding-sm);
|
||||
|
||||
> .customer-name {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 700;
|
||||
}
|
||||
> .customer-section {
|
||||
margin-bottom: auto;
|
||||
|
||||
> .customer-email {
|
||||
font-size: var(--text-md);
|
||||
font-weight: 500;
|
||||
> .customer-name {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
> .customer-code {
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 500;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
> .customer-email {
|
||||
font-size: var(--text-md);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
> .cashier {
|
||||
font-size: var(--text-md);
|
||||
font-weight: 500;
|
||||
color: var(--gray-600);
|
||||
margin-top: auto;
|
||||
margin-top: var(--margin-md);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1085,7 +1095,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
|
||||
> .paid-amount {
|
||||
font-size: var(--text-2xl);
|
||||
|
||||
@@ -117,14 +117,15 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
||||
|
||||
if (doc.docstatus == 1 && !["Lost", "Ordered"].includes(doc.status)) {
|
||||
if (
|
||||
frappe.boot.sysdefaults.allow_sales_order_creation_for_expired_quotation ||
|
||||
!doc.valid_till ||
|
||||
frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0
|
||||
frappe.model.can_create("Sales Order") &&
|
||||
(frappe.boot.sysdefaults.allow_sales_order_creation_for_expired_quotation ||
|
||||
!doc.valid_till ||
|
||||
frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0)
|
||||
) {
|
||||
this.frm.add_custom_button(__("Sales Order"), () => this.make_sales_order(), __("Create"));
|
||||
}
|
||||
|
||||
if (doc.status !== "Ordered") {
|
||||
if (doc.status !== "Ordered" && this.frm.has_perm("write")) {
|
||||
this.frm.add_custom_button(__("Set as Lost"), () => {
|
||||
this.frm.trigger("set_as_lost_dialog");
|
||||
});
|
||||
@@ -133,7 +134,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
||||
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
}
|
||||
|
||||
if (this.frm.doc.docstatus === 0) {
|
||||
if (this.frm.doc.docstatus === 0 && frappe.model.can_read("Opportunity")) {
|
||||
this.frm.add_custom_button(
|
||||
__("Opportunity"),
|
||||
function () {
|
||||
|
||||
@@ -338,7 +338,7 @@ def create_opening_voucher(pos_profile, company, balance_details):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_past_order_list(search_term, status, limit=20):
|
||||
fields = ["name", "grand_total", "currency", "customer", "posting_time", "posting_date"]
|
||||
fields = ["name", "grand_total", "currency", "customer", "customer_name", "posting_time", "posting_date"]
|
||||
invoice_list = []
|
||||
|
||||
if search_term and status:
|
||||
|
||||
@@ -106,7 +106,7 @@ erpnext.PointOfSale.PastOrderList = class {
|
||||
<svg class="mr-2" width="12" height="12" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
${frappe.ellipsis(invoice.customer, 20)}
|
||||
${frappe.ellipsis(invoice.customer_name, 20)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="invoice-total-status">
|
||||
|
||||
@@ -73,14 +73,18 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
||||
get_upper_section_html(doc) {
|
||||
const { status } = doc;
|
||||
let indicator_color = "";
|
||||
const is_customer_naming_by_customer_name = frappe.sys_defaults.cust_master_name !== "Customer Name";
|
||||
|
||||
["Paid", "Consolidated"].includes(status) && (indicator_color = "green");
|
||||
status === "Draft" && (indicator_color = "red");
|
||||
status === "Return" && (indicator_color = "grey");
|
||||
|
||||
return `<div class="left-section">
|
||||
<div class="customer-name">${doc.customer}</div>
|
||||
<div class="customer-email">${this.customer_email}</div>
|
||||
<div class="customer-section">
|
||||
<div class="customer-name">${doc.customer_name}</div>
|
||||
${is_customer_naming_by_customer_name ? `<div class="customer-code">${doc.customer}</div>` : ""}
|
||||
<div class="customer-email">${this.customer_email}</div>
|
||||
</div>
|
||||
<div class="cashier">${__("Sold by")}: ${doc.owner}</div>
|
||||
</div>
|
||||
<div class="right-section">
|
||||
|
||||
@@ -395,7 +395,7 @@ class EmailDigest(Document):
|
||||
|
||||
label = get_link_to_report(
|
||||
"General Ledger",
|
||||
self.meta.get_label("income"),
|
||||
_(self.meta.get_label("income")),
|
||||
filters={
|
||||
"from_date": self.future_from_date,
|
||||
"to_date": self.future_to_date,
|
||||
@@ -427,7 +427,7 @@ class EmailDigest(Document):
|
||||
filters = {"currency": self.currency}
|
||||
label = get_link_to_report(
|
||||
"Profit and Loss Statement",
|
||||
label=self.meta.get_label(root_type + "_year_to_date"),
|
||||
label=_(self.meta.get_label(root_type + "_year_to_date")),
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
@@ -435,7 +435,7 @@ class EmailDigest(Document):
|
||||
filters = {"currency": self.currency}
|
||||
label = get_link_to_report(
|
||||
"Profit and Loss Statement",
|
||||
label=self.meta.get_label(root_type + "_year_to_date"),
|
||||
label=_(self.meta.get_label(root_type + "_year_to_date")),
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
@@ -466,7 +466,7 @@ class EmailDigest(Document):
|
||||
|
||||
label = get_link_to_report(
|
||||
"General Ledger",
|
||||
self.meta.get_label("expenses_booked"),
|
||||
_(self.meta.get_label("expenses_booked")),
|
||||
filters={
|
||||
"company": self.company,
|
||||
"from_date": self.future_from_date,
|
||||
@@ -500,7 +500,7 @@ class EmailDigest(Document):
|
||||
|
||||
label = get_link_to_report(
|
||||
"Sales Order",
|
||||
label=self.meta.get_label("sales_orders_to_bill"),
|
||||
label=_(self.meta.get_label("sales_orders_to_bill")),
|
||||
report_type="Report Builder",
|
||||
doctype="Sales Order",
|
||||
filters={
|
||||
@@ -526,7 +526,7 @@ class EmailDigest(Document):
|
||||
|
||||
label = get_link_to_report(
|
||||
"Sales Order",
|
||||
label=self.meta.get_label("sales_orders_to_deliver"),
|
||||
label=_(self.meta.get_label("sales_orders_to_deliver")),
|
||||
report_type="Report Builder",
|
||||
doctype="Sales Order",
|
||||
filters={
|
||||
@@ -552,7 +552,7 @@ class EmailDigest(Document):
|
||||
|
||||
label = get_link_to_report(
|
||||
"Purchase Order",
|
||||
label=self.meta.get_label("purchase_orders_to_receive"),
|
||||
label=_(self.meta.get_label("purchase_orders_to_receive")),
|
||||
report_type="Report Builder",
|
||||
doctype="Purchase Order",
|
||||
filters={
|
||||
@@ -578,7 +578,7 @@ class EmailDigest(Document):
|
||||
|
||||
label = get_link_to_report(
|
||||
"Purchase Order",
|
||||
label=self.meta.get_label("purchase_orders_to_bill"),
|
||||
label=_(self.meta.get_label("purchase_orders_to_bill")),
|
||||
report_type="Report Builder",
|
||||
doctype="Purchase Order",
|
||||
filters={
|
||||
@@ -630,7 +630,7 @@ class EmailDigest(Document):
|
||||
"company": self.company,
|
||||
}
|
||||
label = get_link_to_report(
|
||||
"Account Balance", label=self.meta.get_label(fieldname), filters=filters
|
||||
"Account Balance", label=_(self.meta.get_label(fieldname)), filters=filters
|
||||
)
|
||||
else:
|
||||
filters = {
|
||||
@@ -640,7 +640,7 @@ class EmailDigest(Document):
|
||||
"company": self.company,
|
||||
}
|
||||
label = get_link_to_report(
|
||||
"Account Balance", label=self.meta.get_label(fieldname), filters=filters
|
||||
"Account Balance", label=_(self.meta.get_label(fieldname)), filters=filters
|
||||
)
|
||||
|
||||
return {"label": label, "value": balance, "last_value": prev_balance}
|
||||
@@ -648,17 +648,17 @@ class EmailDigest(Document):
|
||||
if account_type == "Payable":
|
||||
label = get_link_to_report(
|
||||
"Accounts Payable",
|
||||
label=self.meta.get_label(fieldname),
|
||||
label=_(self.meta.get_label(fieldname)),
|
||||
filters={"report_date": self.future_to_date, "company": self.company},
|
||||
)
|
||||
elif account_type == "Receivable":
|
||||
label = get_link_to_report(
|
||||
"Accounts Receivable",
|
||||
label=self.meta.get_label(fieldname),
|
||||
label=_(self.meta.get_label(fieldname)),
|
||||
filters={"report_date": self.future_to_date, "company": self.company},
|
||||
)
|
||||
else:
|
||||
label = self.meta.get_label(fieldname)
|
||||
label = _(self.meta.get_label(fieldname))
|
||||
|
||||
return {"label": label, "value": balance, "last_value": prev_balance, "count": count}
|
||||
|
||||
@@ -748,7 +748,7 @@ class EmailDigest(Document):
|
||||
|
||||
label = get_link_to_report(
|
||||
"Quotation",
|
||||
label=self.meta.get_label(fieldname),
|
||||
label=_(self.meta.get_label(fieldname)),
|
||||
report_type="Report Builder",
|
||||
doctype="Quotation",
|
||||
filters={
|
||||
@@ -779,7 +779,7 @@ class EmailDigest(Document):
|
||||
|
||||
label = get_link_to_report(
|
||||
doc_type,
|
||||
label=self.meta.get_label(fieldname),
|
||||
label=_(self.meta.get_label(fieldname)),
|
||||
report_type="Report Builder",
|
||||
filters=filters,
|
||||
doctype=doc_type,
|
||||
|
||||
@@ -264,7 +264,7 @@ def update_qty(bin_name, args):
|
||||
actual_qty = bin_details.actual_qty or 0.0
|
||||
|
||||
# actual qty is not up to date in case of backdated transaction
|
||||
if future_sle_exists(args, allow_force_reposting=False):
|
||||
if future_sle_exists(args):
|
||||
actual_qty = get_actual_qty(args.get("item_code"), args.get("warehouse"))
|
||||
|
||||
ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty"))
|
||||
|
||||
@@ -1281,6 +1281,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
"doctype": target_doctype,
|
||||
"postprocess": update_details,
|
||||
"field_no_map": ["taxes_and_charges", "set_warehouse"],
|
||||
"field_map": {"shipping_address_name": "shipping_address"},
|
||||
},
|
||||
doctype + " Item": {
|
||||
"doctype": target_doctype + " Item",
|
||||
|
||||
@@ -75,7 +75,9 @@ frappe.ui.form.on("Inventory Dimension", {
|
||||
|
||||
set_parent_fields(frm) {
|
||||
if (frm.doc.apply_to_all_doctypes) {
|
||||
frm.set_df_property("fetch_from_parent", "options", frm.doc.reference_document);
|
||||
let options = ["\n", frm.doc.reference_document];
|
||||
|
||||
frm.set_df_property("fetch_from_parent", "options", options);
|
||||
} else if (frm.doc.document_type && frm.doc.istable) {
|
||||
frappe.call({
|
||||
method: "erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_parent_fields",
|
||||
@@ -85,7 +87,7 @@ frappe.ui.form.on("Inventory Dimension", {
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message && r.message.length) {
|
||||
frm.set_df_property("fetch_from_parent", "options", [""].concat(r.message));
|
||||
frm.set_df_property("fetch_from_parent", "options", ["\n"].concat(r.message));
|
||||
} else {
|
||||
frm.set_df_property("fetch_from_parent", "hidden", 1);
|
||||
}
|
||||
|
||||
@@ -143,7 +143,6 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.apply_to_all_doctypes",
|
||||
"description": "Set fieldname from which you want to fetch the data from the parent form.",
|
||||
"fieldname": "fetch_from_parent",
|
||||
"fieldtype": "Select",
|
||||
@@ -189,7 +188,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-07-08 08:58:50.228211",
|
||||
"modified": "2025-07-07 15:51:29.329064",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Inventory Dimension",
|
||||
|
||||
@@ -64,16 +64,11 @@ class InventoryDimension(Document):
|
||||
self.reset_value()
|
||||
self.set_source_and_target_fieldname()
|
||||
self.set_type_of_transaction()
|
||||
self.set_fetch_value_from()
|
||||
|
||||
def set_type_of_transaction(self):
|
||||
if self.apply_to_all_doctypes:
|
||||
self.type_of_transaction = "Both"
|
||||
|
||||
def set_fetch_value_from(self):
|
||||
if self.apply_to_all_doctypes:
|
||||
self.fetch_from_parent = self.reference_document
|
||||
|
||||
def do_not_update_document(self):
|
||||
if self.is_new() or not self.has_stock_ledger():
|
||||
return
|
||||
|
||||
@@ -155,6 +155,8 @@ class TestInventoryDimension(FrappeTestCase):
|
||||
reference_document="Rack", dimension_name="Rack", apply_to_all_doctypes=1
|
||||
)
|
||||
|
||||
inv_dimension.db_set("fetch_from_parent", "Rack")
|
||||
|
||||
self.assertEqual(inv_dimension.type_of_transaction, "Both")
|
||||
self.assertEqual(inv_dimension.fetch_from_parent, "Rack")
|
||||
|
||||
|
||||
@@ -634,7 +634,7 @@ class Item(Document):
|
||||
|
||||
if new_properties != [cstr(self.get(field)) for field in field_list]:
|
||||
msg = _("To merge, following properties must be same for both items")
|
||||
msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list])
|
||||
msg += ": \n" + ", ".join([_(self.meta.get_label(fld)) for fld in field_list])
|
||||
frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError)
|
||||
|
||||
def validate_duplicate_product_bundles_before_merge(self, old_name, new_name):
|
||||
@@ -981,7 +981,7 @@ class Item(Document):
|
||||
return
|
||||
|
||||
if linked_doc := self._get_linked_submitted_documents(changed_fields):
|
||||
changed_field_labels = [frappe.bold(self.meta.get_label(f)) for f in changed_fields]
|
||||
changed_field_labels = [frappe.bold(_(self.meta.get_label(f))) for f in changed_fields]
|
||||
msg = _(
|
||||
"As there are existing submitted transactions against item {0}, you can not change the value of {1}."
|
||||
).format(self.name, ", ".join(changed_field_labels))
|
||||
|
||||
@@ -332,5 +332,6 @@ def get_pr_items(purchase_receipt):
|
||||
(pr_item.parent == purchase_receipt.receipt_document)
|
||||
& ((item.is_stock_item == 1) | (item.is_fixed_asset == 1))
|
||||
)
|
||||
.orderby(pr_item.idx)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
@@ -42,6 +42,15 @@ frappe.ui.form.on("Material Request", {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("price_list", () => {
|
||||
return {
|
||||
filters: {
|
||||
buying: 1,
|
||||
enabled: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
@@ -70,6 +79,7 @@ frappe.ui.form.on("Material Request", {
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
frm.doc.price_list = frappe.defaults.get_default("buying_price_list");
|
||||
},
|
||||
|
||||
company: function (frm) {
|
||||
@@ -245,7 +255,9 @@ frappe.ui.form.on("Material Request", {
|
||||
from_warehouse: item.from_warehouse,
|
||||
warehouse: item.warehouse,
|
||||
doctype: frm.doc.doctype,
|
||||
buying_price_list: frappe.defaults.get_default("buying_price_list"),
|
||||
buying_price_list: frm.doc.price_list
|
||||
? frm.doc.price_list
|
||||
: frappe.defaults.get_default("buying_price_list"),
|
||||
currency: frappe.defaults.get_default("Currency"),
|
||||
name: frm.doc.name,
|
||||
qty: item.qty || 1,
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"column_break_2",
|
||||
"transaction_date",
|
||||
"schedule_date",
|
||||
"price_list",
|
||||
"amended_from",
|
||||
"warehouse_section",
|
||||
"scan_barcode",
|
||||
@@ -351,13 +352,19 @@
|
||||
{
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "price_list",
|
||||
"fieldtype": "Link",
|
||||
"label": "Price List",
|
||||
"options": "Price List"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-ticket",
|
||||
"idx": 70,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-04-21 18:36:04.827917",
|
||||
"modified": "2025-07-07 13:15:28.615984",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Material Request",
|
||||
@@ -425,10 +432,11 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "status,transaction_date",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
from frappe import _, msgprint
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder.functions import Sum
|
||||
@@ -45,6 +46,7 @@ class MaterialRequest(BuyingController):
|
||||
naming_series: DF.Literal["MAT-MR-.YYYY.-"]
|
||||
per_ordered: DF.Percent
|
||||
per_received: DF.Percent
|
||||
price_list: DF.Link | None
|
||||
scan_barcode: DF.Data | None
|
||||
schedule_date: DF.Date | None
|
||||
select_print_heading: DF.Link | None
|
||||
@@ -151,6 +153,9 @@ class MaterialRequest(BuyingController):
|
||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
|
||||
|
||||
if not self.price_list:
|
||||
self.price_list = frappe.defaults.get_defaults().buying_price_list
|
||||
|
||||
def before_update_after_submit(self):
|
||||
self.validate_schedule_date()
|
||||
|
||||
@@ -764,10 +769,11 @@ def raise_work_orders(material_request):
|
||||
"material_request_item": d.name,
|
||||
"planned_start_date": mr.transaction_date,
|
||||
"company": mr.company,
|
||||
"project": d.project,
|
||||
}
|
||||
)
|
||||
|
||||
wo_order.set_work_order_operations()
|
||||
wo_order.get_items_and_operations_from_bom()
|
||||
wo_order.flags.ignore_validate = True
|
||||
wo_order.flags.ignore_mandatory = True
|
||||
wo_order.save()
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import flt
|
||||
|
||||
@@ -339,12 +340,20 @@ def on_doctype_update():
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_items_from_product_bundle(row):
|
||||
def get_items_from_product_bundle(row, price_list):
|
||||
row, items = json.loads(row), []
|
||||
|
||||
bundled_items = get_product_bundle_items(row["item_code"])
|
||||
for item in bundled_items:
|
||||
row.update({"item_code": item.item_code, "qty": flt(row["quantity"]) * flt(item.qty)})
|
||||
row.update(
|
||||
{
|
||||
"item_code": item.item_code,
|
||||
"qty": flt(row["quantity"]) * flt(item.qty),
|
||||
"conversion_rate": 1,
|
||||
"price_list": price_list,
|
||||
"currency": frappe.defaults.get_defaults().currency,
|
||||
}
|
||||
)
|
||||
items.append(get_item_details(row))
|
||||
|
||||
return items
|
||||
|
||||
@@ -97,51 +97,25 @@ class QualityInspection(Document):
|
||||
if self.reference_type == "Stock Entry":
|
||||
doctype = "Stock Entry Detail"
|
||||
|
||||
child_row_references = frappe.get_all(
|
||||
doctype,
|
||||
filters={"parent": self.reference_name, "item_code": self.item_code},
|
||||
pluck="name",
|
||||
)
|
||||
child_doc = frappe.qb.DocType(doctype)
|
||||
qi_doc = frappe.qb.DocType("Quality Inspection")
|
||||
|
||||
if not child_row_references:
|
||||
return
|
||||
child_row_references = (
|
||||
frappe.qb.from_(child_doc)
|
||||
.left_join(qi_doc)
|
||||
.on(child_doc.name == qi_doc.child_row_reference)
|
||||
.select(child_doc.name)
|
||||
.where(
|
||||
(child_doc.item_code == self.item_code)
|
||||
& (child_doc.parent == self.reference_name)
|
||||
& (child_doc.docstatus < 2)
|
||||
& (qi_doc.name.isnull())
|
||||
)
|
||||
.orderby(child_doc.idx)
|
||||
).run(pluck=True)
|
||||
|
||||
if len(child_row_references) == 1:
|
||||
if len(child_row_references):
|
||||
self.child_row_reference = child_row_references[0]
|
||||
else:
|
||||
self.distribute_child_row_reference(child_row_references)
|
||||
|
||||
def distribute_child_row_reference(self, child_row_references):
|
||||
quality_inspections = frappe.get_all(
|
||||
"Quality Inspection",
|
||||
filters={
|
||||
"reference_name": self.reference_name,
|
||||
"item_code": self.item_code,
|
||||
"docstatus": ("<", 2),
|
||||
},
|
||||
fields=["name", "child_row_reference", "docstatus"],
|
||||
order_by="child_row_reference desc",
|
||||
)
|
||||
|
||||
for row in quality_inspections:
|
||||
if not child_row_references:
|
||||
break
|
||||
|
||||
if row.child_row_reference and row.child_row_reference in child_row_references:
|
||||
child_row_references.remove(row.child_row_reference)
|
||||
continue
|
||||
|
||||
if row.docstatus == 1:
|
||||
continue
|
||||
|
||||
if row.name == self.name:
|
||||
self.child_row_reference = child_row_references[0]
|
||||
else:
|
||||
frappe.db.set_value(
|
||||
"Quality Inspection", row.name, "child_row_reference", child_row_references[0]
|
||||
)
|
||||
|
||||
child_row_references.remove(child_row_references[0])
|
||||
|
||||
def validate_inspection_required(self):
|
||||
if frappe.db.get_single_value(
|
||||
@@ -413,7 +387,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT item_code
|
||||
SELECT distinct item_code, item_name, item_group
|
||||
FROM `tab{from_doctype}`
|
||||
WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s
|
||||
{qi_condition} {cond} {mcond}
|
||||
@@ -444,10 +418,11 @@ def quality_inspection_query(doctype, txt, searchfield, start, page_len, filters
|
||||
limit_start=start,
|
||||
limit_page_length=page_len,
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"docstatus": ("<", 2),
|
||||
"name": ("like", "%%%s%%" % txt),
|
||||
"item_code": filters.get("item_code"),
|
||||
"reference_name": ("in", [filters.get("reference_name", ""), ""]),
|
||||
"child_row_reference": ("in", [filters.get("child_row_reference", ""), ""]),
|
||||
},
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
@@ -179,6 +179,7 @@ frappe.ui.form.on("Stock Entry", {
|
||||
inspection_type: "Incoming",
|
||||
reference_type: frm.doc.doctype,
|
||||
reference_name: frm.doc.name,
|
||||
child_row_reference: row.doc.name,
|
||||
item_code: row.doc.item_code,
|
||||
description: row.doc.description,
|
||||
item_serial_no: row.doc.serial_no ? row.doc.serial_no.split("\n")[0] : null,
|
||||
@@ -194,6 +195,7 @@ frappe.ui.form.on("Stock Entry", {
|
||||
filters: {
|
||||
item_code: d.item_code,
|
||||
reference_name: doc.name,
|
||||
child_row_reference: d.name,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -192,7 +192,7 @@ class StockLedgerEntry(Document):
|
||||
mandatory = ["warehouse", "posting_date", "voucher_type", "voucher_no", "company"]
|
||||
for k in mandatory:
|
||||
if not self.get(k):
|
||||
frappe.throw(_("{0} is required").format(self.meta.get_label(k)))
|
||||
frappe.throw(_("{0} is required").format(_(self.meta.get_label(k))))
|
||||
|
||||
if self.voucher_type != "Stock Reconciliation" and not self.actual_qty:
|
||||
frappe.throw(_("Actual Qty is mandatory"))
|
||||
|
||||
@@ -898,10 +898,6 @@ class StockReconciliation(StockController):
|
||||
|
||||
self.update_inventory_dimensions(row, data)
|
||||
|
||||
if self.docstatus == 1 and has_dimensions and (not row.batch_no or not row.serial_and_batch_bundle):
|
||||
data.qty_after_transaction = data.actual_qty
|
||||
data.actual_qty = 0.0
|
||||
|
||||
return data
|
||||
|
||||
def make_sle_on_cancel(self):
|
||||
@@ -1266,12 +1262,12 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig
|
||||
itemwise_batch_data = get_itemwise_batch(warehouse, posting_date, company, item_code)
|
||||
|
||||
for d in items:
|
||||
if d.item_code in itemwise_batch_data:
|
||||
if (d.item_code, d.warehouse) in itemwise_batch_data:
|
||||
valuation_rate = get_stock_balance(
|
||||
d.item_code, d.warehouse, posting_date, posting_time, with_valuation_rate=True
|
||||
)[1]
|
||||
|
||||
for row in itemwise_batch_data.get(d.item_code):
|
||||
for row in itemwise_batch_data.get((d.item_code, d.warehouse)):
|
||||
if ignore_empty_stock and not row.qty:
|
||||
continue
|
||||
|
||||
@@ -1403,7 +1399,7 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None):
|
||||
columns, data = execute(filters)
|
||||
|
||||
for row in data:
|
||||
itemwise_batch_data.setdefault(row[0], []).append(
|
||||
itemwise_batch_data.setdefault((row[0], row[3]), []).append(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": row[0],
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"end_time",
|
||||
"limits_dont_apply_on",
|
||||
"item_based_reposting",
|
||||
"do_reposting_for_each_stock_transaction",
|
||||
"errors_notification_section",
|
||||
"notify_reposting_error_to_role"
|
||||
],
|
||||
@@ -66,18 +65,12 @@
|
||||
"fieldname": "errors_notification_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Errors Notification"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "do_reposting_for_each_stock_transaction",
|
||||
"fieldtype": "Check",
|
||||
"label": "Do reposting for each Stock Transaction"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-04-24 12:19:40.204888",
|
||||
"modified": "2025-07-08 11:27:46.659056",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Reposting Settings",
|
||||
|
||||
@@ -16,7 +16,6 @@ class StockRepostingSettings(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
do_reposting_for_each_stock_transaction: DF.Check
|
||||
end_time: DF.Time | None
|
||||
item_based_reposting: DF.Check
|
||||
limit_reposting_timeslot: DF.Check
|
||||
@@ -30,10 +29,6 @@ class StockRepostingSettings(Document):
|
||||
def validate(self):
|
||||
self.set_minimum_reposting_time_slot()
|
||||
|
||||
def before_save(self):
|
||||
if self.do_reposting_for_each_stock_transaction:
|
||||
self.item_based_reposting = 1
|
||||
|
||||
def set_minimum_reposting_time_slot(self):
|
||||
"""Ensure that timeslot for reposting is at least 12 hours."""
|
||||
if not self.limit_reposting_timeslot:
|
||||
|
||||
@@ -38,51 +38,3 @@ class TestStockRepostingSettings(unittest.TestCase):
|
||||
|
||||
users = get_recipients()
|
||||
self.assertTrue(user in users)
|
||||
|
||||
def test_do_reposting_for_each_stock_transaction(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 1)
|
||||
if frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"):
|
||||
frappe.db.set_single_value("Stock Reposting Settings", "item_based_reposting", 0)
|
||||
|
||||
item = make_item(
|
||||
"_Test item for reposting check for each transaction", properties={"is_stock_item": 1}
|
||||
).name
|
||||
|
||||
stock_entry = make_stock_entry(
|
||||
item_code=item,
|
||||
qty=1,
|
||||
rate=100,
|
||||
stock_entry_type="Material Receipt",
|
||||
target="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
riv = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name}, pluck="name")
|
||||
self.assertTrue(riv)
|
||||
|
||||
frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 0)
|
||||
|
||||
def test_do_not_reposting_for_each_stock_transaction(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 0)
|
||||
if frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"):
|
||||
frappe.db.set_single_value("Stock Reposting Settings", "item_based_reposting", 0)
|
||||
|
||||
item = make_item(
|
||||
"_Test item for do not reposting check for each transaction", properties={"is_stock_item": 1}
|
||||
).name
|
||||
|
||||
stock_entry = make_stock_entry(
|
||||
item_code=item,
|
||||
qty=1,
|
||||
rate=100,
|
||||
stock_entry_type="Material Receipt",
|
||||
target="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
riv = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name}, pluck="name")
|
||||
self.assertFalse(riv)
|
||||
|
||||
@@ -115,7 +115,7 @@ class StockReservationEntry(Document):
|
||||
]
|
||||
for d in mandatory:
|
||||
if not self.get(d):
|
||||
msg = _("{0} is required").format(self.meta.get_label(d))
|
||||
msg = _("{0} is required").format(_(self.meta.get_label(d)))
|
||||
frappe.throw(msg)
|
||||
|
||||
def validate_group_warehouse(self) -> None:
|
||||
|
||||
@@ -65,7 +65,7 @@ class TransactionBase(StatusUpdater):
|
||||
frappe.throw(_("Invalid reference {0} {1}").format(reference_doctype, reference_name))
|
||||
|
||||
for field, condition in fields:
|
||||
if prevdoc_values[field] is not None and field not in self.exclude_fields:
|
||||
if prevdoc_values[field] not in [None, ""] and field not in self.exclude_fields:
|
||||
self.validate_value(field, condition, prevdoc_values[field], doc)
|
||||
|
||||
def get_prev_doc_reference_details(self, reference_names, reference_doctype, fields):
|
||||
|
||||
Reference in New Issue
Block a user