Compare commits

..

1 Commits

Author SHA1 Message Date
coderabbitai[bot]
741e6a7e52 📝 Add docstrings to wo-flt-issue
Docstrings generation was requested by @rtdany10.

* https://github.com/frappe/erpnext/pull/50952#issuecomment-3619564968

The following files were modified:

* `erpnext/manufacturing/doctype/work_order/work_order.py`
2025-12-06 05:39:35 +00:00
249 changed files with 52263 additions and 60090 deletions

View File

@@ -45,9 +45,3 @@ d827ed21adc7b36047e247cbb0dc6388d048a7f9
# `frappe.flags.in_test` => `frappe.in_test`
7a482a69985c952de0e8193c9d4e086aee65ee6d
# these commits actually changed something valuable
# but they have a lot of whitespace changes that make blame noisy
# PR: https://github.com/frappe/erpnext/pull/49816
3ffd50c772735877b330d010c1058f623da8721d
0e8f8677b8eb31e7834f72d1c6314d3c3f392ca6

View File

@@ -70,7 +70,6 @@ frappe.treeview_settings["Account"] = {
args: {
accounts: accounts,
company: cur_tree.args.company,
include_default_fb_balances: true,
},
});

View File

@@ -12,7 +12,6 @@
"column_break_4",
"company",
"disabled",
"exempted_role",
"section_break_7",
"closed_documents"
],
@@ -68,18 +67,10 @@
"label": "Closed Documents",
"options": "Closed Document",
"reqd": 1
},
{
"description": "Role allowed to bypass period restrictions.",
"fieldname": "exempted_role",
"fieldtype": "Link",
"label": "Exempted Role",
"link_filters": "[[\"Role\",\"disabled\",\"=\",0]]",
"options": "Role"
}
],
"links": [],
"modified": "2025-12-01 16:53:44.631299",
"modified": "2025-10-06 15:00:15.568067",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting Period",

View File

@@ -30,7 +30,6 @@ class AccountingPeriod(Document):
company: DF.Link
disabled: DF.Check
end_date: DF.Date
exempted_role: DF.Link | None
period_name: DF.Data
start_date: DF.Date
# end: auto-generated types
@@ -114,7 +113,7 @@ def validate_accounting_period_on_doc_save(doc, method=None):
accounting_period = (
frappe.qb.from_(ap)
.from_(cd)
.select(ap.name, ap.exempted_role)
.select(ap.name)
.where(
(ap.name == cd.parent)
& (ap.company == doc.company)
@@ -127,11 +126,6 @@ def validate_accounting_period_on_doc_save(doc, method=None):
).run(as_dict=1)
if accounting_period:
if (
accounting_period[0].get("exempted_role")
and accounting_period[0].get("exempted_role") in frappe.get_roles()
):
return
frappe.throw(
_("You cannot create a {0} within the closed Accounting Period {1}").format(
doc.doctype, frappe.bold(accounting_period[0]["name"])

View File

@@ -152,5 +152,6 @@ class AccountsSettings(Document):
def drop_ar_sql_procedures(self):
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
frappe.db.sql(f"drop function if exists {InitSQLProceduresForAR.genkey_function_name}")
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")

View File

@@ -38,10 +38,7 @@
"column_break_3czf",
"bank_party_name",
"bank_party_account_number",
"bank_party_iban",
"extended_bank_statement_section",
"included_fee",
"excluded_fee"
"bank_party_iban"
],
"fields": [
{
@@ -236,32 +233,12 @@
{
"fieldname": "column_break_oufv",
"fieldtype": "Column Break"
},
{
"fieldname": "extended_bank_statement_section",
"fieldtype": "Section Break",
"label": "Extended Bank Statement"
},
{
"fieldname": "included_fee",
"fieldtype": "Currency",
"label": "Included Fee",
"non_negative": 1,
"options": "currency"
},
{
"description": "On save, the Excluded Fee will be converted to an Included Fee.",
"fieldname": "excluded_fee",
"fieldtype": "Currency",
"label": "Excluded Fee",
"non_negative": 1,
"options": "currency"
}
],
"grid_page_length": 50,
"is_submittable": 1,
"links": [],
"modified": "2025-12-07 20:49:18.600757",
"modified": "2025-10-23 17:32:58.514807",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",

View File

@@ -32,8 +32,6 @@ class BankTransaction(Document):
date: DF.Date | None
deposit: DF.Currency
description: DF.SmallText | None
excluded_fee: DF.Currency
included_fee: DF.Currency
naming_series: DF.Literal["ACC-BTN-.YYYY.-"]
party: DF.DynamicLink | None
party_type: DF.Link | None
@@ -47,11 +45,9 @@ class BankTransaction(Document):
# end: auto-generated types
def before_validate(self):
self.handle_excluded_fee()
self.update_allocated_amount()
def validate(self):
self.validate_included_fee()
self.validate_duplicate_references()
self.validate_currency()
@@ -311,40 +307,6 @@ class BankTransaction(Document):
self.party_type, self.party = result
def validate_included_fee(self):
"""
The included_fee is only handled for withdrawals. An included_fee for a deposit, is not credited to the account and is
therefore outside of the deposit value and can be larger than the deposit itself.
"""
if self.included_fee and self.withdrawal:
if self.included_fee > self.withdrawal:
frappe.throw(_("Included fee is bigger than the withdrawal itself."))
def handle_excluded_fee(self):
# Include the excluded fee on validate to handle all further processing the same
excluded_fee = flt(self.excluded_fee)
if excluded_fee <= 0:
return
# Suppress a negative deposit (aka withdrawal), likely not intendend
if flt(self.deposit) > 0 and (flt(self.deposit) - excluded_fee) < 0:
frappe.throw(_("The Excluded Fee is bigger than the Deposit it is deducted from."))
# Enforce directionality
if flt(self.deposit) > 0 and flt(self.withdrawal) > 0:
frappe.throw(
_("Only one of Deposit or Withdrawal should be non-zero when applying an Excluded Fee.")
)
if flt(self.deposit) > 0:
self.deposit = flt(self.deposit) - excluded_fee
# A fee applied to deposit and withdrawal equal 0 become a withdrawal
elif flt(self.withdrawal) >= 0:
self.withdrawal = flt(self.withdrawal) + excluded_fee
self.included_fee = flt(self.included_fee) + excluded_fee
self.excluded_fee = 0
@frappe.whitelist()
def get_doctypes_for_bank_reconciliation():

View File

@@ -1,133 +0,0 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.tests import UnitTestCase
class TestBankTransactionFees(UnitTestCase):
def test_included_fee_throws(self):
"""A fee that's part of a withdrawal cannot be bigger than the
withdrawal itself."""
bt = frappe.new_doc("Bank Transaction")
bt.withdrawal = 100
bt.included_fee = 101
self.assertRaises(frappe.ValidationError, bt.validate_included_fee)
def test_included_fee_allows_equal(self):
"""A fee that's part of a withdrawal may be equal to the withdrawal
amount (only the fee was deducted from the account)."""
bt = frappe.new_doc("Bank Transaction")
bt.withdrawal = 100
bt.included_fee = 100
bt.validate_included_fee()
def test_included_fee_allows_for_deposit(self):
"""For deposits, a fee may be recorded separately without limiting the
received amount."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 10
bt.included_fee = 999
bt.validate_included_fee()
def test_excluded_fee_noop_when_zero(self):
"""When there is no excluded fee to apply, the amounts should remain
unchanged."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 100
bt.withdrawal = 0
bt.included_fee = 5
bt.excluded_fee = 0
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 100)
self.assertEqual(bt.withdrawal, 0)
self.assertEqual(bt.included_fee, 5)
self.assertEqual(bt.excluded_fee, 0)
def test_excluded_fee_throws_when_exceeds_deposit(self):
"""A fee deducted from an incoming payment must not exceed the incoming
amount (else it would be a withdrawal, a conversion we don't support)."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 10
bt.excluded_fee = 11
self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee)
def test_excluded_fee_throws_when_both_deposit_and_withdrawal_are_set(self):
"""A transaction must be either incoming or outgoing when applying a
fee, not both."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 10
bt.withdrawal = 10
bt.excluded_fee = 1
self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee)
def test_excluded_fee_deducts_from_deposit(self):
"""When a fee is deducted from an incoming payment, the net received
amount decreases and the fee is tracked as included."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 100
bt.withdrawal = 0
bt.included_fee = 2
bt.excluded_fee = 5
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 95)
self.assertEqual(bt.withdrawal, 0)
self.assertEqual(bt.included_fee, 7)
self.assertEqual(bt.excluded_fee, 0)
def test_excluded_fee_can_reduce_an_incoming_payment_to_zero(self):
"""A separately-deducted fee may reduce an incoming payment to zero,
while still tracking the fee."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 5
bt.withdrawal = 0
bt.included_fee = 0
bt.excluded_fee = 5
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 0)
self.assertEqual(bt.withdrawal, 0)
self.assertEqual(bt.included_fee, 5)
self.assertEqual(bt.excluded_fee, 0)
def test_excluded_fee_increases_outgoing_payment(self):
"""When a separately-deducted fee is provided for an outgoing payment,
the total money leaving increases and the fee is tracked."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 0
bt.withdrawal = 100
bt.included_fee = 2
bt.excluded_fee = 5
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 0)
self.assertEqual(bt.withdrawal, 105)
self.assertEqual(bt.included_fee, 7)
self.assertEqual(bt.excluded_fee, 0)
def test_excluded_fee_turns_zero_amount_into_withdrawal(self):
"""If only an excluded fee is provided, it should be treated as an
outgoing payment and the fee is then tracked as included."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 0
bt.withdrawal = 0
bt.included_fee = 0
bt.excluded_fee = 5
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 0)
self.assertEqual(bt.withdrawal, 5)
self.assertEqual(bt.included_fee, 5)
self.assertEqual(bt.excluded_fee, 0)

View File

@@ -12,15 +12,6 @@ frappe.ui.form.on("Budget", {
};
});
frm.set_query("account", function () {
return {
filters: {
is_group: 0,
company: frm.doc.company,
},
};
});
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
frappe.db.get_single_value("Accounts Settings", "use_legacy_budget_controller").then((value) => {
if (value) {
@@ -33,16 +24,24 @@ frappe.ui.form.on("Budget", {
frm.trigger("toggle_reqd_fields");
if (!frm.doc.__islocal && frm.doc.docstatus == 1) {
frm.add_custom_button(
__("Revise Budget"),
function () {
frm.events.revise_budget_action(frm);
},
__("Actions")
let exception_role = await frappe.db.get_value(
"Company",
frm.doc.company,
"exception_budget_approver_role"
);
}
toggle_distribution_fields(frm);
const role = exception_role.message.exception_budget_approver_role;
if (role && frappe.user.has_role(role)) {
frm.add_custom_button(
__("Revise Budget"),
function () {
frm.events.revise_budget_action(frm);
},
__("Actions")
);
}
}
},
budget_against: function (frm) {
@@ -55,15 +54,10 @@ frappe.ui.form.on("Budget", {
frm.doc.budget_distribution.forEach((row) => {
row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2);
});
set_total_budget_amount(frm);
frm.refresh_field("budget_distribution");
}
},
distribute_equally: function (frm) {
toggle_distribution_fields(frm);
},
set_null_value: function (frm) {
if (frm.doc.budget_against == "Cost Center") {
frm.set_value("project", null);
@@ -106,8 +100,6 @@ frappe.ui.form.on("Budget Distribution", {
let row = frappe.get_doc(cdt, cdn);
if (frm.doc.budget_amount) {
row.percent = flt((row.amount / frm.doc.budget_amount) * 100, 2);
set_total_budget_amount(frm);
frm.refresh_field("budget_distribution");
}
},
@@ -115,29 +107,7 @@ frappe.ui.form.on("Budget Distribution", {
let row = frappe.get_doc(cdt, cdn);
if (frm.doc.budget_amount) {
row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2);
set_total_budget_amount(frm);
frm.refresh_field("budget_distribution");
}
},
});
function set_total_budget_amount(frm) {
let total = 0;
(frm.doc.budget_distribution || []).forEach((row) => {
total += flt(row.amount);
});
frm.set_value("budget_distribution_total", total);
}
function toggle_distribution_fields(frm) {
const grid = frm.fields_dict.budget_distribution.grid;
["amount", "percent"].forEach((field) => {
grid.update_docfield_property(field, "read_only", frm.doc.distribute_equally);
});
grid.refresh();
}

View File

@@ -25,10 +25,6 @@
"distribute_equally",
"section_break_fpdt",
"budget_distribution",
"section_break_wkqb",
"column_break_paum",
"column_break_nwor",
"budget_distribution_total",
"section_break_6",
"applicable_on_material_request",
"action_if_annual_budget_exceeded_on_mr",
@@ -226,8 +222,7 @@
},
{
"fieldname": "section_break_fpdt",
"fieldtype": "Section Break",
"hide_border": 1
"fieldtype": "Section Break"
},
{
"fieldname": "budget_distribution",
@@ -308,32 +303,13 @@
"options": "Monthly\nQuarterly\nHalf-Yearly\nYearly",
"read_only_depends_on": "eval: doc.revision_of",
"reqd": 1
},
{
"fieldname": "section_break_wkqb",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_paum",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_nwor",
"fieldtype": "Column Break"
},
{
"fieldname": "budget_distribution_total",
"fieldtype": "Currency",
"label": "Budget Distribution Total",
"no_copy": 1,
"read_only": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-12-10 02:35:01.197613",
"modified": "2025-11-19 17:00:00.648224",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Budget",

View File

@@ -53,7 +53,6 @@ class Budget(Document):
budget_against: DF.Literal["", "Cost Center", "Project"]
budget_amount: DF.Currency
budget_distribution: DF.Table[BudgetDistribution]
budget_distribution_total: DF.Currency
budget_end_date: DF.Date | None
budget_start_date: DF.Date | None
company: DF.Link
@@ -231,49 +230,28 @@ class Budget(Document):
def before_save(self):
self.allocate_budget()
self.budget_distribution_total = sum(flt(row.amount) for row in self.budget_distribution)
def on_update(self):
self.validate_distribution_totals()
def allocate_budget(self):
if self._should_skip_allocation():
return
if self._should_recalculate_manual_distribution():
self._recalculate_manual_distribution()
if self.revision_of:
return
if not self.should_regenerate_budget_distribution():
return
self._regenerate_distribution()
self.set("budget_distribution", [])
def _should_skip_allocation(self):
return self.revision_of and not self.distribute_equally
periods = self.get_budget_periods()
total_periods = len(periods)
row_percent = 100 / total_periods if total_periods else 0
def _should_recalculate_manual_distribution(self):
return (
not self.distribute_equally
and bool(self.budget_distribution)
and self._is_only_budget_amount_changed()
)
def _is_only_budget_amount_changed(self):
old = self.get_doc_before_save()
if not old:
return False
return (
old.budget_amount != self.budget_amount
and old.distribution_frequency == self.distribution_frequency
and old.budget_start_date == self.budget_start_date
and old.budget_end_date == self.budget_end_date
)
def _recalculate_manual_distribution(self):
for row in self.budget_distribution:
row.amount = flt((row.percent / 100) * self.budget_amount, 3)
for start_date, end_date in periods:
row = self.append("budget_distribution", {})
row.start_date = start_date
row.end_date = end_date
self.add_allocated_amount(row, row_percent)
def should_regenerate_budget_distribution(self):
"""Check whether budget distribution should be recalculated."""
@@ -287,6 +265,7 @@ class Budget(Document):
"to_fiscal_year",
"budget_amount",
"distribution_frequency",
"distribute_equally",
]
for field in changed_fields:
if old_doc.get(field) != self.get(field):
@@ -294,21 +273,6 @@ class Budget(Document):
return bool(self.distribute_equally)
def _regenerate_distribution(self):
self.set("budget_distribution", [])
periods = self.get_budget_periods()
total_periods = len(periods)
row_percent = 100 / total_periods if total_periods else 0
for start_date, end_date in periods:
row = self.append("budget_distribution", {})
row.start_date = start_date
row.end_date = end_date
self.add_allocated_amount(row, row_percent)
self.budget_distribution_total = self.budget_amount
def get_budget_periods(self):
"""Return list of (start_date, end_date) tuples based on frequency."""
frequency = self.distribution_frequency
@@ -348,8 +312,12 @@ class Budget(Document):
}.get(frequency, 1)
def add_allocated_amount(self, row, row_percent):
row.amount = flt(self.budget_amount * row_percent / 100, 3)
row.percent = flt(row_percent, 3)
if not self.distribute_equally:
row.amount = 0
row.percent = 0
else:
row.amount = flt(self.budget_amount * row_percent / 100, 3)
row.percent = flt(row_percent, 3)
def validate_distribution_totals(self):
if self.should_regenerate_budget_distribution():

View File

@@ -19,7 +19,7 @@ frappe.ui.form.on("Currency Exchange Settings", {
to: "{to_currency}",
};
add_param(frm, r.message, params, result);
} else if (["frankfurter.app", "frankfurter.dev"].includes(frm.doc.service_provider)) {
} else if (frm.doc.service_provider == "frankfurter.dev") {
let result = ["rates", "{to_currency}"];
let params = {
base: "{from_currency}",

View File

@@ -60,7 +60,7 @@ class CurrencyExchangeSettings(Document):
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
self.append("req_params", {"key": "from", "value": "{from_currency}"})
self.append("req_params", {"key": "to", "value": "{to_currency}"})
elif self.service_provider in ("frankfurter.dev", "frankfurter.app"):
elif self.service_provider == "frankfurter.dev":
self.set("result_key", [])
self.set("req_params", [])
@@ -105,11 +105,9 @@ class CurrencyExchangeSettings(Document):
@frappe.whitelist()
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev", "frankfurter.app"]:
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev"]:
if service_provider == "exchangerate.host":
api = "api.exchangerate.host/convert"
elif service_provider == "frankfurter.app":
api = "api.frankfurter.app/{transaction_date}"
elif service_provider == "frankfurter.dev":
api = "api.frankfurter.dev/v1/{transaction_date}"

View File

@@ -33,7 +33,6 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
get_depr_schedule,
)
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.setup.utils import get_exchange_rate as _get_exchange_rate
class StockAccountInvalidTransaction(frappe.ValidationError):
@@ -354,7 +353,93 @@ class JournalEntry(AccountsController):
)
def apply_tax_withholding(self):
JournalEntryTaxWithholding(self).apply()
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
if not self.apply_tds or self.voucher_type not in ("Debit Note", "Credit Note"):
return
parties = [d.party for d in self.get("accounts") if d.party]
parties = list(set(parties))
if len(parties) > 1:
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
account_type_map = get_account_type_map(self.company)
party_type = "supplier" if self.voucher_type == "Credit Note" else "customer"
doctype = "Purchase Invoice" if self.voucher_type == "Credit Note" else "Sales Invoice"
debit_or_credit = (
"debit_in_account_currency"
if self.voucher_type == "Credit Note"
else "credit_in_account_currency"
)
rev_debit_or_credit = (
"credit_in_account_currency"
if debit_or_credit == "debit_in_account_currency"
else "debit_in_account_currency"
)
party_account = get_party_account(party_type.title(), parties[0], self.company)
net_total = sum(
d.get(debit_or_credit)
for d in self.get("accounts")
if account_type_map.get(d.account) not in ("Tax", "Chargeable")
)
party_amount = sum(
d.get(rev_debit_or_credit) for d in self.get("accounts") if d.account == party_account
)
inv = frappe._dict(
{
party_type: parties[0],
"doctype": doctype,
"company": self.company,
"posting_date": self.posting_date,
"net_total": net_total,
}
)
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
inv, self.tax_withholding_category
)
if not tax_withholding_details:
return
accounts = []
for d in self.get("accounts"):
if d.get("account") == tax_withholding_details.get("account_head"):
d.update(
{
"account": tax_withholding_details.get("account_head"),
debit_or_credit: tax_withholding_details.get("tax_amount"),
}
)
accounts.append(d.get("account"))
if d.get("account") == party_account:
d.update({rev_debit_or_credit: party_amount - tax_withholding_details.get("tax_amount")})
if not accounts or tax_withholding_details.get("account_head") not in accounts:
self.append(
"accounts",
{
"account": tax_withholding_details.get("account_head"),
rev_debit_or_credit: tax_withholding_details.get("tax_amount"),
"against_account": parties[0],
},
)
to_remove = [
d
for d in self.get("accounts")
if not d.get(rev_debit_or_credit) and d.account == tax_withholding_details.get("account_head")
]
for d in to_remove:
self.remove(d)
def update_asset_value(self):
self.update_asset_on_depreciation()
@@ -1297,230 +1382,6 @@ class JournalEntry(AccountsController):
frappe.throw(_("Accounts table cannot be blank."))
class JournalEntryTaxWithholding:
def __init__(self, journal_entry):
self.doc: JournalEntry = journal_entry
self.party = None
self.party_type = None
self.party_account = None
self.party_row = None
self.existing_tds_rows = []
self.precision = None
self.has_multiple_parties = False
# Direction fields based on party type
self.party_field = None # "credit" for Supplier, "debit" for Customer
self.reverse_field = None # opposite of party_field
def apply(self):
if not self._set_party_info():
return
self._setup_direction_fields()
self._reset_existing_tds()
if not self._should_apply_tds():
self._cleanup_duplicate_tds_rows(None)
return
if self.has_multiple_parties:
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
net_total = self._calculate_net_total()
if net_total <= 0:
return
tds_details = self._get_tds_details(net_total)
if not tds_details or not tds_details.get("tax_amount"):
return
self._create_or_update_tds_row(tds_details)
self._update_party_amount(tds_details.get("tax_amount"), is_reversal=False)
self._recalculate_totals()
def _should_apply_tds(self):
return self.doc.apply_tds and self.doc.voucher_type in ("Debit Note", "Credit Note")
def _set_party_info(self):
for row in self.doc.get("accounts"):
if row.party_type in ("Customer", "Supplier") and row.party:
if self.party and row.party != self.party:
self.has_multiple_parties = True
if not self.party:
self.party = row.party
self.party_type = row.party_type
self.party_account = row.account
self.party_row = row
if row.get("is_tax_withholding_account"):
self.existing_tds_rows.append(row)
return bool(self.party)
def _setup_direction_fields(self):
"""
For Supplier (TDS): party has credit, TDS reduces credit
For Customer (TCS): party has debit, TCS increases debit
"""
if self.party_type == "Supplier":
self.party_field = "credit"
self.reverse_field = "debit"
else: # Customer
self.party_field = "debit"
self.reverse_field = "credit"
self.precision = self.doc.precision(self.party_field, self.party_row)
def _reset_existing_tds(self):
for row in self.existing_tds_rows:
# TDS amount is always in credit (liability to government)
tds_amount = flt(row.get("credit") - row.get("debit"), self.precision)
if not tds_amount:
continue
self._update_party_amount(tds_amount, is_reversal=True)
# zero_out_tds_row
row.update(
{
"credit": 0,
"credit_in_account_currency": 0,
"debit": 0,
"debit_in_account_currency": 0,
}
)
def _update_party_amount(self, amount, is_reversal=False):
amount = flt(amount, self.precision)
amount_in_party_currency = flt(amount / self.party_row.get("exchange_rate", 1), self.precision)
# Determine which field the party amount is in
active_field = self.party_field if self.party_row.get(self.party_field) else self.reverse_field
# If amount is in reverse field, flip the signs
if active_field == self.reverse_field:
amount = -amount
amount_in_party_currency = -amount_in_party_currency
# Direction multiplier based on party type:
# Customer (TCS): +1 (add to debit)
# Supplier (TDS): -1 (subtract from credit)
direction = 1 if self.party_type == "Customer" else -1
# Reversal inverts the direction
if is_reversal:
direction = -direction
adjustment = amount * direction
adjustment_in_party_currency = amount_in_party_currency * direction
active_field_account_currency = f"{active_field}_in_account_currency"
self.party_row.update(
{
active_field: flt(self.party_row.get(active_field) + adjustment, self.precision),
active_field_account_currency: flt(
self.party_row.get(active_field_account_currency) + adjustment_in_party_currency,
self.precision,
),
}
)
def _calculate_net_total(self):
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
account_type_map = get_account_type_map(self.doc.company)
return flt(
sum(
d.get(self.reverse_field) - d.get(self.party_field)
for d in self.doc.get("accounts")
if account_type_map.get(d.account) not in ("Tax", "Chargeable")
and d.account != self.party_account
and not d.get("is_tax_withholding_account")
),
self.precision,
)
def _get_tds_details(self, net_total):
return get_party_tax_withholding_details(
frappe._dict(
{
"party_type": self.party_type,
"party": self.party,
"doctype": self.doc.doctype,
"company": self.doc.company,
"posting_date": self.doc.posting_date,
"tax_withholding_net_total": net_total,
"base_tax_withholding_net_total": net_total,
"grand_total": net_total,
}
),
self.doc.tax_withholding_category,
)
def _create_or_update_tds_row(self, tds_details):
tax_account = tds_details.get("account_head")
account_currency = get_account_currency(tax_account)
company_currency = frappe.get_cached_value("Company", self.doc.company, "default_currency")
exchange_rate = _get_exchange_rate(account_currency, company_currency, self.doc.posting_date)
tax_amount = flt(tds_details.get("tax_amount"), self.precision)
tax_amount_in_account_currency = flt(tax_amount / exchange_rate, self.precision)
# Find existing TDS row for this account
tax_row = None
for row in self.doc.get("accounts"):
if row.account == tax_account and row.get("is_tax_withholding_account"):
tax_row = row
break
if not tax_row:
tax_row = self.doc.append(
"accounts",
{
"account": tax_account,
"account_currency": account_currency,
"exchange_rate": exchange_rate,
"cost_center": tds_details.get("cost_center"),
"credit": 0,
"credit_in_account_currency": 0,
"debit": 0,
"debit_in_account_currency": 0,
"is_tax_withholding_account": 1,
},
)
# TDS/TCS is always credited (liability to government)
tax_row.update(
{
"credit": tax_amount,
"credit_in_account_currency": tax_amount_in_account_currency,
"debit": 0,
"debit_in_account_currency": 0,
}
)
self._cleanup_duplicate_tds_rows(tax_row)
def _cleanup_duplicate_tds_rows(self, current_tax_row):
rows_to_remove = [
row
for row in self.doc.get("accounts")
if row.get("is_tax_withholding_account") and row != current_tax_row
]
for row in rows_to_remove:
self.doc.remove(row)
def _recalculate_totals(self):
self.doc.set_amounts_in_company_currency()
self.doc.set_total_debit_credit()
self.doc.set_against_account()
@frappe.whitelist()
def get_default_bank_cash_account(
company, account_type=None, mode_of_payment=None, account=None, *, fetch_balance=True
@@ -1893,6 +1754,8 @@ def get_exchange_rate(
credit=None,
exchange_rate=None,
):
from erpnext.setup.utils import get_exchange_rate
account_details = frappe.get_cached_value(
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
)
@@ -1915,7 +1778,7 @@ def get_exchange_rate(
# The date used to retreive the exchange rate here is the date passed
# in as an argument to this function.
elif (not flt(exchange_rate) or flt(exchange_rate) == 1) and account_currency and posting_date:
exchange_rate = _get_exchange_rate(account_currency, company_currency, posting_date)
exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
else:
exchange_rate = 1

View File

@@ -34,7 +34,6 @@
"reference_detail_no",
"advance_voucher_type",
"advance_voucher_no",
"is_tax_withholding_account",
"col_break3",
"is_advance",
"user_remark",
@@ -282,19 +281,12 @@
"options": "advance_voucher_type",
"read_only": 1,
"search_index": 1
},
{
"default": "0",
"fieldname": "is_tax_withholding_account",
"fieldtype": "Check",
"label": "Is Tax Withholding Account",
"read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-11-27 12:23:33.157655",
"modified": "2025-10-27 13:48:32.805100",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",

View File

@@ -28,7 +28,6 @@ class JournalEntryAccount(Document):
debit_in_account_currency: DF.Currency
exchange_rate: DF.Float
is_advance: DF.Literal["No", "Yes"]
is_tax_withholding_account: DF.Check
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data

View File

@@ -214,9 +214,6 @@ class OpeningInvoiceCreationTool(Document):
}
)
if self.invoice_type == "Purchase" and row.supplier_invoice_date:
invoice.update({"bill_date": row.supplier_invoice_date})
accounting_dimension = get_accounting_dimensions()
for dimension in accounting_dimension:
invoice.update({dimension: self.get(dimension) or item.get(dimension)})

View File

@@ -12,7 +12,6 @@
"column_break_3",
"posting_date",
"due_date",
"supplier_invoice_date",
"section_break_5",
"item_name",
"outstanding_amount",
@@ -112,26 +111,19 @@
"fieldname": "invoice_number",
"fieldtype": "Data",
"label": "Invoice Number"
},
{
"depends_on": "eval: parent.invoice_type == \"Purchase\"",
"fieldname": "supplier_invoice_date",
"fieldtype": "Date",
"label": "Supplier Invoice Date"
}
],
"istable": 1,
"links": [],
"modified": "2025-12-01 16:18:07.997594",
"modified": "2024-03-27 13:10:06.703006",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Opening Invoice Creation Tool Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -26,7 +26,6 @@ class OpeningInvoiceCreationToolItem(Document):
party_type: DF.Link | None
posting_date: DF.Date | None
qty: DF.Data | None
supplier_invoice_date: DF.Date | None
temporary_opening_account: DF.Link | None
# end: auto-generated types

View File

@@ -1277,14 +1277,15 @@ frappe.ui.form.on("Payment Entry", {
let row = (frm.doc.deductions || []).find((t) => t.is_exchange_gain_loss);
if (!row) {
const company_defaults = frappe.get_doc(":Company", frm.doc.company);
const response = await get_company_defaults(frm.doc.company);
const account =
company_defaults?.[account_fieldname] ||
response.message?.[account_fieldname] ||
(await prompt_for_missing_account(frm, account_fieldname));
row = frm.add_child("deductions");
row.account = account;
row.cost_center = company_defaults?.cost_center;
row.cost_center = response.message?.cost_center;
row.is_exchange_gain_loss = 1;
}
@@ -1494,14 +1495,18 @@ frappe.ui.form.on("Payment Entry", {
"Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'"
);
d.row_id = "";
} else if (d.charge_type == "On Previous Row Amount" || d.charge_type == "On Previous Row Total") {
} else if (
(d.charge_type == "On Previous Row Amount" || d.charge_type == "On Previous Row Total") &&
d.row_id
) {
if (d.idx == 1) {
msg = __(
"Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"
);
d.charge_type = "";
} else if (!d.row_id) {
d.row_id = d.idx - 1;
msg = __("Please specify a valid Row ID for row {0} in table {1}", [d.idx, __(d.doctype)]);
d.row_id = "";
} else if (d.row_id && d.row_id >= d.idx) {
msg = __(
"Cannot refer row number greater than or equal to current row number for this Charge type"

View File

@@ -593,9 +593,9 @@
"label": "Apply Tax Withholding Amount"
},
{
"collapsible": 1,
"fieldname": "taxes_and_charges_section",
"fieldtype": "Section Break",
"hide_border": 1,
"label": "Taxes and Charges"
},
{
@@ -695,7 +695,8 @@
},
{
"fieldname": "section_break_60",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"hide_border": 1
},
{
"depends_on": "eval:doc.docstatus==0",
@@ -766,7 +767,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2025-12-18 13:56:40.206038",
"modified": "2025-05-08 11:18:10.238085",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

@@ -38,7 +38,7 @@
"search_index": 1
},
{
"columns": 4,
"columns": 2,
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"in_global_search": 1,
@@ -49,10 +49,8 @@
"search_index": 1
},
{
"columns": 2,
"fieldname": "due_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Due Date",
"read_only": 1
},
@@ -176,7 +174,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-12-08 13:57:30.098239",
"modified": "2025-07-25 04:32:11.040025",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",

View File

@@ -427,7 +427,6 @@ class PaymentRequest(Document):
context = {
"doc": frappe.get_doc(self.reference_doctype, self.reference_name),
"payment_url": self.payment_url,
"payment_request": self,
}
if self.message:
@@ -540,9 +539,6 @@ def make_payment_request(**args):
if args.dt not in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST:
frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt)))
if args.dn and not isinstance(args.dn, str):
frappe.throw(_("Invalid parameter. 'dn' should be of type str"))
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
if not args.get("company"):
args.company = ref_doc.company
@@ -847,7 +843,6 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
)
referenced_payment_requests = {pr.name: pr for pr in referenced_payment_requests}
doc_updates = {}
for ref in references:
if not ref.payment_request:
@@ -873,7 +868,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
title=_("Invalid Allocated Amount"),
)
# determine status
# update status
if new_outstanding_amount == payment_request["grand_total"]:
status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested"
elif new_outstanding_amount == 0:
@@ -881,37 +876,31 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
elif new_outstanding_amount > 0:
status = "Partially Paid"
# prepare bulk update data
doc_updates[ref.payment_request] = {
"outstanding_amount": new_outstanding_amount,
"status": status,
}
# bulk update all payment requests
if doc_updates:
frappe.db.bulk_update("Payment Request", doc_updates)
# update database
frappe.db.set_value(
"Payment Request",
ref.payment_request,
{"outstanding_amount": new_outstanding_amount, "status": status},
)
def get_dummy_message(doc):
return """
{% if doc.contact_person -%}
<p>Dear {{ doc.contact_person }},</p>
{%- else %}<p>Hello,</p>{% endif %}
return frappe.render_template(
"""{% if doc.contact_person -%}
<p>Dear {{ doc.contact_person }},</p>
{%- else %}<p>Hello,</p>{% endif %}
<p>
{{ _("Requesting payment against {0} {1} for amount {2}").format(
doc.doctype,
doc.name,
payment_request.get_formatted("grand_total")
) }}
</p>
<p>{{ _("Requesting payment against {0} {1} for amount {2}").format(doc.doctype,
doc.name, doc.get_formatted("grand_total")) }}</p>
<a href="{{ payment_url }}">{{ _("Make Payment") }}</a>
<a href="{{ payment_url }}">{{ _("Make Payment") }}</a>
<p>{{ _("If you have any questions, please get back to us.") }}</p>
<p>{{ _("If you have any questions, please get back to us.") }}</p>
<p>{{ _("Thank you for your business!") }}</p>
"""
<p>{{ _("Thank you for your business!") }}</p>
""",
dict(doc=doc, payment_url="{{ payment_url }}"),
)
@frappe.whitelist()

View File

@@ -130,7 +130,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
}
if (doc.docstatus == 1 && doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
if (doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
@@ -579,6 +579,17 @@ cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function
};
};
cur_frm.cscript.cost_center = function (doc, cdt, cdn) {
var d = locals[cdt][cdn];
if (d.cost_center) {
var cl = doc.items || [];
for (var i = 0; i < cl.length; i++) {
if (!cl[i].cost_center) cl[i].cost_center = d.cost_center;
}
}
refresh_field("items");
};
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
return {
filters: [["Project", "status", "not in", "Completed, Cancelled"]],

View File

@@ -2027,7 +2027,6 @@ def get_list_context(context=None):
"show_search": True,
"no_breadcrumbs": True,
"title": _("Purchase Invoices"),
"list_template": "templates/includes/list/list.html",
}
)
return list_context
@@ -2098,26 +2097,10 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
if isinstance(args, str):
args = json.loads(args)
def post_parent_process(source_parent, target_parent):
for row in target_parent.get("items"):
if row.get("qty") == 0:
target_parent.remove(row)
def update_item(obj, target, source_parent):
from erpnext.controllers.sales_and_purchase_return import get_returned_qty_map_for_row
returned_qty_map = (
get_returned_qty_map_for_row(
source_parent.name, source_parent.supplier, obj.name, "Purchase Invoice"
)
or {}
)
target.qty = flt(obj.qty) - flt(obj.received_qty) - flt(returned_qty_map.get("qty"))
target.qty = flt(obj.qty) - flt(obj.received_qty)
target.received_qty = flt(obj.qty) - flt(obj.received_qty)
target.stock_qty = (flt(obj.qty) - flt(obj.received_qty) - flt(returned_qty_map.get("qty"))) * flt(
obj.conversion_factor
)
target.stock_qty = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.conversion_factor)
target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate)
target.base_amount = (
(flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate)
@@ -2156,7 +2139,6 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"},
},
target_doc,
post_parent_process,
)
return doc

View File

@@ -2931,29 +2931,6 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
pi.save()
self.assertEqual(pi.discount_amount, discount_amount)
def test_returned_item_purchase_receipt(self):
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import (
make_purchase_receipt as make_purchase_receipt_from_pi,
)
item = create_item("_Test Returned Item Purchase Receipt", is_stock_item=1)
pi = make_purchase_invoice(item_code=item.name, qty=5, rate=100)
return_pi = make_purchase_invoice(
item_code=item.name,
is_return=1,
return_against=pi.name,
qty=-5,
do_not_submit=True,
)
return_pi.items[0].purchase_invoice_item = pi.items[0].name
return_pi.submit()
pr = make_purchase_receipt_from_pi(pi.name)
self.assertFalse(pr.items)
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(

View File

@@ -228,6 +228,7 @@
"reqd": 1
},
{
"default": "1",
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "conversion_factor",
"fieldtype": "Float",
@@ -984,7 +985,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-12-13 14:10:02.379392",
"modified": "2025-10-14 13:00:54.441511",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@@ -618,6 +618,10 @@ cur_frm.cscript.expense_account = function (doc, cdt, cdn) {
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "expense_account");
};
cur_frm.cscript.cost_center = function (doc, cdt, cdn) {
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "cost_center");
};
frappe.ui.form.on("Sales Invoice", {
setup: function (frm) {
frm.add_fetch("customer", "tax_id", "tax_id");

View File

@@ -2326,7 +2326,6 @@ def get_list_context(context=None):
"show_search": True,
"no_breadcrumbs": True,
"title": _("Invoices"),
"list_template": "templates/includes/list/list.html",
}
)
return list_context

View File

@@ -35,7 +35,7 @@
},
{
"fieldname": "rate",
"fieldtype": "Currency",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Rate",
"read_only": 1,
@@ -62,7 +62,7 @@
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Amount",
"read_only": 1,
@@ -91,16 +91,15 @@
],
"istable": 1,
"links": [],
"modified": "2025-12-10 08:06:40.611761",
"modified": "2024-03-27 13:10:39.866399",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Share Balance",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -14,7 +14,7 @@ class ShareBalance(Document):
if TYPE_CHECKING:
from frappe.types import DF
amount: DF.Currency
amount: DF.Int
current_state: DF.Literal["", "Issued", "Purchased"]
from_no: DF.Int
is_company: DF.Check
@@ -22,7 +22,7 @@ class ShareBalance(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
rate: DF.Currency
rate: DF.Int
share_type: DF.Link
to_no: DF.Int
# end: auto-generated types

View File

@@ -85,9 +85,6 @@ def get_party_details(inv):
if inv.doctype == "Sales Invoice":
party_type = "Customer"
party = inv.customer
elif inv.doctype == "Journal Entry":
party_type = inv.party_type
party = inv.party
else:
party_type = "Supplier"
party = inv.supplier
@@ -158,7 +155,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
party_type, parties, inv, tax_details, posting_date, pan_no
)
if party_type == "Supplier" or inv.doctype == "Journal Entry":
if party_type == "Supplier":
tax_row = get_tax_row_for_tds(tax_details, tax_amount)
else:
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
@@ -349,10 +346,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
elif party_type == "Customer":
if tax_deducted:
# if already TCS is charged, then amount will be calculated based on 'Previous Row Total'
if inv.doctype == "Sales Invoice":
tax_amount = 0
else:
tax_amount = inv.base_tax_withholding_net_total * tax_details.rate / 100
tax_amount = 0
else:
# if no TCS has been charged in FY,
# then chargeable value is "prev invoices + advances - advance_adjusted" value which cross the threshold
@@ -724,7 +718,7 @@ def get_advance_adjusted_in_invoice(inv):
def get_invoice_total_without_tcs(inv, tax_details):
tcs_tax_row = [d for d in inv.get("taxes") or [] if d.account_head == tax_details.account_head]
tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head]
tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0
return inv.grand_total - tcs_tax_row_amount

View File

@@ -849,90 +849,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
self.assertEqual(payment.taxes[0].tax_amount, 6000)
self.assertEqual(payment.taxes[0].allocated_amount, 6000)
def test_tds_on_journal_entry_for_supplier(self):
"""Test TDS deduction for Supplier in Debit Note"""
frappe.db.set_value(
"Supplier", "Test TDS Supplier", "tax_withholding_category", "Cumulative Threshold TDS"
)
jv = make_journal_entry_with_tax_withholding(
party_type="Supplier",
party="Test TDS Supplier",
voucher_type="Debit Note",
amount=50000,
save=False,
)
jv.apply_tds = 1
jv.tax_withholding_category = "Cumulative Threshold TDS"
jv.save()
# Again saving should not change tds amount
jv.user_remark = "Test TDS on Journal Entry for Supplier"
jv.save()
jv.submit()
# TDS = 50000 * 10% = 5000
self.assertEqual(len(jv.accounts), 3)
# Find TDS account row
tds_row = None
supplier_row = None
for row in jv.accounts:
if row.account == "TDS - _TC":
tds_row = row
elif row.party == "Test TDS Supplier":
supplier_row = row
self.assertEqual(tds_row.credit, 5000)
self.assertEqual(tds_row.debit, 0)
# Supplier amount should be reduced by TDS
self.assertEqual(supplier_row.credit, 45000)
jv.cancel()
def test_tcs_on_journal_entry_for_customer(self):
"""Test TCS collection for Customer in Credit Note"""
frappe.db.set_value(
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
)
# Create Credit Note with amount exceeding threshold
jv = make_journal_entry_with_tax_withholding(
party_type="Customer",
party="Test TCS Customer",
voucher_type="Credit Note",
amount=50000,
save=False,
)
jv.apply_tds = 1
jv.tax_withholding_category = "Cumulative Threshold TCS"
jv.save()
# Again saving should not change tds amount
jv.user_remark = "Test TCS on Journal Entry for Customer"
jv.save()
jv.submit()
# Assert TCS calculation (10% on amount above threshold of 30000)
self.assertEqual(len(jv.accounts), 3)
# Find TCS account row
tcs_row = None
customer_row = None
for row in jv.accounts:
if row.account == "TCS - _TC":
tcs_row = row
elif row.party == "Test TCS Customer":
customer_row = row
# TCS should be credited (liability to government)
self.assertEqual(tcs_row.credit, 2000) # above threshold 20000*10%
self.assertEqual(tcs_row.debit, 0)
# Customer amount should be increased by TCS
self.assertEqual(customer_row.debit, 52000)
jv.cancel()
def cancel_invoices():
purchase_invoices = frappe.get_all(
@@ -1081,88 +997,6 @@ def create_payment_entry(**args):
return pe
def make_journal_entry_with_tax_withholding(
party_type,
party,
voucher_type,
amount,
cost_center=None,
posting_date=None,
save=True,
submit=False,
):
"""Helper function to create Journal Entry for tax withholding"""
if not cost_center:
cost_center = "_Test Cost Center - _TC"
jv = frappe.new_doc("Journal Entry")
jv.posting_date = posting_date or today()
jv.company = "_Test Company"
jv.voucher_type = voucher_type
jv.multi_currency = 0
if party_type == "Supplier":
# Debit Note: Expense Dr, Supplier Cr
expense_account = "Stock Received But Not Billed - _TC"
party_account = "Creditors - _TC"
jv.append(
"accounts",
{
"account": expense_account,
"cost_center": cost_center,
"debit_in_account_currency": amount,
"exchange_rate": 1,
},
)
jv.append(
"accounts",
{
"account": party_account,
"party_type": party_type,
"party": party,
"cost_center": cost_center,
"credit_in_account_currency": amount,
"exchange_rate": 1,
},
)
else: # Customer
# Credit Note: Customer Dr, Income Cr
party_account = "Debtors - _TC"
income_account = "Sales - _TC"
jv.append(
"accounts",
{
"account": party_account,
"party_type": party_type,
"party": party,
"cost_center": cost_center,
"debit_in_account_currency": amount,
"exchange_rate": 1,
},
)
jv.append(
"accounts",
{
"account": income_account,
"cost_center": cost_center,
"credit_in_account_currency": amount,
"exchange_rate": 1,
},
)
if save or submit:
jv.insert()
if submit:
jv.submit()
return jv
def create_records():
# create a new suppliers
for name in [

View File

@@ -210,20 +210,19 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None, from_r
for d in gl_map:
cost_center = d.get("cost_center")
cost_center_allocation = get_cost_center_allocation_data(
gl_map[0]["company"], gl_map[0]["posting_date"], cost_center
)
if not cost_center_allocation:
new_gl_map.append(d)
continue
# Validate budget against main cost center
if not from_repost:
validate_expense_against_budget(
d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision)
)
cost_center_allocation = get_cost_center_allocation_data(
gl_map[0]["company"], gl_map[0]["posting_date"], cost_center
)
if not cost_center_allocation:
new_gl_map.append(d)
continue
if d.account == round_off_account:
d.cost_center = cost_center_allocation[0][0]
new_gl_map.append(d)
@@ -427,11 +426,7 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
gle.flags.notify_update = False
gle.submit()
if (
not from_repost
and gle.voucher_type != "Period Closing Voucher"
and (gle.is_cancelled == 0 or gle.voucher_type == "Journal Entry")
):
if not from_repost and gle.voucher_type != "Period Closing Voucher":
validate_expense_against_budget(args)

View File

@@ -1384,14 +1384,27 @@ class InitSQLProceduresForAR:
amount_in_account_currency {_currency_type}) engine=memory;
"""
# Function
genkey_function_name = "ar_genkey"
genkey_function_sql = f"""
create function `{genkey_function_name}`(rec row type of `{_row_def_table_name}`, allocate bool) returns char(40)
begin
if allocate then
return sha1(concat_ws(',', rec.account, rec.against_voucher_type, rec.against_voucher_no, rec.party));
else
return sha1(concat_ws(',', rec.account, rec.voucher_type, rec.voucher_no, rec.party));
end if;
end
"""
# Procedures
init_procedure_name = "ar_init_tmp_table"
init_procedure_sql = f"""
create procedure ar_init_tmp_table(in ple row type of `{_row_def_table_name}`)
begin
if not exists (select name from `{_voucher_balance_name}` where name = sha1(concat_ws(',', ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)))
if not exists (select name from `{_voucher_balance_name}` where name = `{genkey_function_name}`(ple, false))
then
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0);
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0);
end if;
end;
"""
@@ -1433,13 +1446,16 @@ class InitSQLProceduresForAR:
end if;
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.voucher_type, ple.voucher_no, ple.party)), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
end;
"""
def __init__(self):
existing_procedures = frappe.db.get_routines()
if self.genkey_function_name not in existing_procedures:
frappe.db.sql(self.genkey_function_sql)
if self.init_procedure_name not in existing_procedures:
frappe.db.sql(self.init_procedure_sql)

View File

@@ -500,7 +500,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
immutable_ledger = frappe.get_single_value("Accounts Settings", "enable_immutable_ledger")
def update_value_in_dict(data, key, gle, show_net_values=False):
def update_value_in_dict(data, key, gle):
data[key].debit += gle.debit
data[key].credit += gle.credit
@@ -511,14 +511,10 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
data[key].debit_in_transaction_currency += gle.debit_in_transaction_currency
data[key].credit_in_transaction_currency += gle.credit_in_transaction_currency
if (
filters.get("show_net_values_in_party_account")
and account_type_map.get(data[key].account)
in (
"Receivable",
"Payable",
)
) or show_net_values:
if filters.get("show_net_values_in_party_account") and account_type_map.get(data[key].account) in (
"Receivable",
"Payable",
):
net_value = data[key].debit - data[key].credit
net_value_in_account_currency = (
data[key].debit_in_account_currency - data[key].credit_in_account_currency
@@ -552,11 +548,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
if gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries):
if not group_by_voucher_consolidated:
update_value_in_dict(gle_map[group_by_value].totals, "opening", gle, True)
update_value_in_dict(gle_map[group_by_value].totals, "closing", gle, True)
update_value_in_dict(gle_map[group_by_value].totals, "opening", gle)
update_value_in_dict(gle_map[group_by_value].totals, "closing", gle)
update_value_in_dict(totals, "opening", gle, True)
update_value_in_dict(totals, "closing", gle, True)
update_value_in_dict(totals, "opening", gle)
update_value_in_dict(totals, "closing", gle)
elif gle.posting_date <= to_date or (cstr(gle.is_opening) == "Yes" and show_opening_entries):
if not group_by_voucher_consolidated:

View File

@@ -5,6 +5,7 @@
import frappe
from frappe import _
from frappe.utils import flt
from pypika import Order
import erpnext
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
@@ -40,6 +41,16 @@ def _execute(filters=None, additional_table_columns=None):
tax_doctype="Purchase Taxes and Charges",
)
scrubbed_tax_fields = {}
for tax in tax_columns:
scrubbed_tax_fields.update(
{
tax + " Rate": frappe.scrub(tax + " Rate"),
tax + " Amount": frappe.scrub(tax + " Amount"),
}
)
po_pr_map = get_purchase_receipts_against_purchase_order(item_list)
data = []
@@ -89,8 +100,8 @@ def _execute(filters=None, additional_table_columns=None):
for tax, details in itemised_tax.get(d.name, {}).items():
row.update(
{
f"{tax}_rate": details.get("tax_rate", 0),
f"{tax}_amount": details.get("tax_amount", 0),
scrubbed_tax_fields[tax + " Rate"]: details.get("tax_rate", 0),
scrubbed_tax_fields[tax + " Amount"]: details.get("tax_amount", 0),
}
)
if details.get("is_other_charges"):

View File

@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.query_builder import functions as fn
from frappe.utils import flt
from frappe.utils import cstr, flt
from frappe.utils.nestedset import get_descendants_of
from frappe.utils.xlsxutils import handle_html
@@ -32,6 +32,15 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
return columns, [], None, None, None, 0
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
scrubbed_tax_fields = {}
for tax in tax_columns:
scrubbed_tax_fields.update(
{
tax + " Rate": frappe.scrub(tax + " Rate"),
tax + " Amount": frappe.scrub(tax + " Amount"),
}
)
mode_of_payments = get_mode_of_payments(set(d.parent for d in item_list))
so_dn_map = get_delivery_notes_against_sales_order(item_list)
@@ -92,8 +101,8 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
for tax, details in itemised_tax.get(d.name, {}).items():
row.update(
{
f"{tax}_rate": details.get("tax_rate", 0),
f"{tax}_amount": details.get("tax_amount", 0),
scrubbed_tax_fields[tax + " Rate"]: details.get("tax_rate", 0),
scrubbed_tax_fields[tax + " Amount"]: details.get("tax_amount", 0),
}
)
if details.get("is_other_charges"):
@@ -558,24 +567,15 @@ def get_tax_accounts(
tax_details = query.run(as_dict=True)
precision = frappe.get_precision(tax_doctype, "tax_amount", currency=company_currency) or 2
tax_columns = {}
tax_columns = set()
itemised_tax = {}
scrubbed_description_map = {}
for row in tax_details:
description = handle_html(row.description) or row.account_head
scrubbed_description = scrubbed_description_map.get(description)
if not scrubbed_description:
scrubbed_description = frappe.scrub(description)
scrubbed_description_map[description] = scrubbed_description
if scrubbed_description not in tax_columns and row.amount:
# as description is text editor earlier and markup can break the column convention in reports
tax_columns[scrubbed_description] = description
rate = "NA" if row.rate == 0 else row.rate
tax_columns.add(description)
itemised_tax.setdefault(row.item_row, {}).setdefault(
scrubbed_description,
description,
frappe._dict(
{
"tax_rate": rate,
@@ -585,16 +585,14 @@ def get_tax_accounts(
),
)
itemised_tax[row.item_row][scrubbed_description].tax_amount += flt(row.amount, precision)
itemised_tax[row.item_row][description].tax_amount += flt(row.amount, precision)
tax_columns_list = list(tax_columns.keys())
tax_columns_list.sort()
for scrubbed_desc in tax_columns_list:
desc = tax_columns[scrubbed_desc]
tax_columns = sorted(tax_columns)
for desc in tax_columns:
columns.append(
{
"label": _(desc + " Rate"),
"fieldname": f"{scrubbed_desc}_rate",
"fieldname": frappe.scrub(desc + " Rate"),
"fieldtype": "Float",
"width": 100,
}
@@ -603,7 +601,7 @@ def get_tax_accounts(
columns.append(
{
"label": _(desc + " Amount"),
"fieldname": f"{scrubbed_desc}_amount",
"fieldname": frappe.scrub(desc + " Amount"),
"fieldtype": "Currency",
"options": "currency",
"width": 100,
@@ -641,7 +639,7 @@ def get_tax_accounts(
},
]
return itemised_tax, tax_columns_list
return itemised_tax, tax_columns
def get_tax_details_query(doctype, tax_doctype):
@@ -758,5 +756,5 @@ def add_sub_total_row(item, total_row_map, group_by_value, tax_columns):
total_row["percent_gt"] += item["percent_gt"]
for tax in tax_columns:
total_row.setdefault(f"{tax}_amount", 0.0)
total_row[f"{tax}_amount"] += flt(item[f"{tax}_amount"])
total_row.setdefault(frappe.scrub(tax + " Amount"), 0.0)
total_row[frappe.scrub(tax + " Amount")] += flt(item[frappe.scrub(tax + " Amount")])

View File

@@ -447,7 +447,7 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
}
for key in value_fields:
row[key] = flt(d.get(key, 0.0))
row[key] = flt(d.get(key, 0.0), 3)
if abs(row[key]) >= get_zero_cutoff(company_currency):
# ignore zero values

View File

@@ -199,8 +199,6 @@ def get_balance_on(
ignore_account_permission=False,
account_type=None,
start_date=None,
finance_book=None,
include_default_fb_balances=False,
):
if not account and frappe.form_dict.get("account"):
account = frappe.form_dict.get("account")
@@ -295,31 +293,8 @@ def get_balance_on(
f"""gle.party_type = {frappe.db.escape(party_type)} and gle.party = {frappe.db.escape(party)} """
)
default_finance_book = None
if company:
cond.append("""gle.company = %s """ % (frappe.db.escape(company)))
default_finance_book = frappe.get_cached_value("Company", company, "default_finance_book")
if finance_book:
if default_finance_book and include_default_fb_balances:
cond.append(
f"""(gle.finance_book IN (
{frappe.db.escape(finance_book)},
{frappe.db.escape(default_finance_book)}
) OR gle.finance_book IS NULL
)"""
)
else:
cond.append(f"(gle.finance_book = {frappe.db.escape(finance_book)} OR gle.finance_book IS NULL)")
elif default_finance_book and include_default_fb_balances:
# No finance book passed → fall back to default
cond.append(
f"""(
gle.finance_book = {frappe.db.escape(default_finance_book)}
OR gle.finance_book IS NULL
)"""
)
if account or (party_type and party) or account_type:
precision = get_currency_precision()
@@ -1358,7 +1333,7 @@ def get_children(doctype, parent, company, is_root=False, include_disabled=False
@frappe.whitelist()
def get_account_balances(accounts, company, finance_book=None, include_default_fb_balances=False):
def get_account_balances(accounts, company):
if isinstance(accounts, str):
accounts = loads(accounts)
@@ -1369,25 +1344,9 @@ def get_account_balances(accounts, company, finance_book=None, include_default_f
for account in accounts:
account["company_currency"] = company_currency
account["balance"] = flt(
get_balance_on(
account=account["value"],
in_account_currency=False,
company=company,
finance_book=finance_book,
include_default_fb_balances=include_default_fb_balances,
)
)
account["balance"] = flt(get_balance_on(account["value"], in_account_currency=False, company=company))
if account["account_currency"] and account["account_currency"] != company_currency:
account["balance_in_account_currency"] = flt(
get_balance_on(
account=account["value"],
company=company,
finance_book=finance_book,
include_default_fb_balances=include_default_fb_balances,
)
)
account["balance_in_account_currency"] = flt(get_balance_on(account["value"], company=company))
return accounts

View File

@@ -80,12 +80,6 @@ frappe.ui.form.on("Asset", {
}
},
before_submit: function (frm) {
if (frm.doc.is_composite_asset && !frm.has_active_capitalization) {
frappe.throw(__("Please capitalize this asset before submitting."));
}
},
refresh: function (frm) {
frappe.ui.form.trigger("Asset", "is_existing_asset");
frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1);
@@ -202,10 +196,9 @@ frappe.ui.form.on("Asset", {
asset: frm.doc.name,
},
callback: function (r) {
frm.has_active_capitalization = r.message;
if (!r.message) {
$(".form-message").text(__("Capitalize this asset before submitting."));
$(".primary-action").prop("hidden", true);
$(".form-message").text(__("Capitalize this asset to confirm"));
frm.add_custom_button(__("Capitalize Asset"), function () {
frm.trigger("create_asset_capitalization");
@@ -277,14 +270,8 @@ frappe.ui.form.on("Asset", {
const row = [
sch["idx"],
frappe.format(sch["schedule_date"], { fieldtype: "Date" }),
frappe.format(sch["depreciation_amount"], {
fieldtype: "Currency",
options: "Company:company:default_currency",
}),
frappe.format(sch["accumulated_depreciation_amount"], {
fieldtype: "Currency",
options: "Company:company:default_currency",
}),
frappe.format(sch["depreciation_amount"], { fieldtype: "Currency" }),
frappe.format(sch["accumulated_depreciation_amount"], { fieldtype: "Currency" }),
sch["journal_entry"] || "",
];
@@ -477,6 +464,7 @@ frappe.ui.form.on("Asset", {
is_composite_asset: function (frm) {
if (frm.doc.is_composite_asset) {
frm.set_value("net_purchase_amount", 0);
frm.set_df_property("net_purchase_amount", "read_only", 1);
} else {
frm.set_df_property("net_purchase_amount", "read_only", 0);
}
@@ -544,6 +532,7 @@ frappe.ui.form.on("Asset", {
callback: function (r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
$(".primary-action").prop("hidden", false);
},
});
},

View File

@@ -556,8 +556,7 @@
"fieldtype": "Currency",
"label": "Net Purchase Amount",
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
"options": "Company:company:default_currency",
"read_only_depends_on": "eval: doc.is_composite_asset"
"options": "Company:company:default_currency"
}
],
"idx": 72,
@@ -601,7 +600,7 @@
"link_fieldname": "target_asset"
}
],
"modified": "2025-12-18 16:36:40.904246",
"modified": "2025-11-04 22:39:00.817405",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@@ -142,75 +142,21 @@ class Asset(AccountsController):
if self.split_from or not self.calculate_depreciation:
return
created_schedules = []
for fb_row in self.get("finance_books"):
if not fb_row.rate_of_depreciation:
fb_row.rate_of_depreciation = self.get_depreciation_rate(fb_row, on_validate=True)
schedules = []
for row in self.get("finance_books"):
self.validate_asset_finance_books(row)
if not row.rate_of_depreciation:
row.rate_of_depreciation = self.get_depreciation_rate(row, on_validate=True)
existing_schedule = get_asset_depr_schedule_doc(self.name, "Draft", fb_row.finance_book)
schedule_doc = get_asset_depr_schedule_doc(self.name, "Draft", row.finance_book)
if not schedule_doc:
schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
schedule_doc.asset = self.name
schedule_doc.create_depreciation_schedule(row)
schedule_doc.save()
schedules.append(schedule_doc.name)
if not existing_schedule:
new_schedule = frappe.new_doc("Asset Depreciation Schedule")
new_schedule.asset = self.name
new_schedule.create_depreciation_schedule(fb_row)
new_schedule.save()
created_schedules.append(new_schedule.name)
continue
self.evaluate_and_recreate_depreciation_schedule(existing_schedule, fb_row)
created_schedules.append(existing_schedule.name)
self.show_schedule_creation_message(created_schedules)
def evaluate_and_recreate_depreciation_schedule(self, existing_doc, fb_row):
"""Determine if depreciation schedule needs to be regenerated and recreate if necessary"""
asset_details_changed = self.has_asset_details_changed(existing_doc)
depreciation_settings_changed = self.has_depreciation_settings_changed(existing_doc, fb_row)
if self.should_regenerate_depreciation_schedule(
existing_doc, asset_details_changed, depreciation_settings_changed
):
existing_doc.create_depreciation_schedule(fb_row)
existing_doc.save()
def has_asset_details_changed(self, existing_doc):
"""Check if core asset details that affect depreciation have changed"""
return (
self.net_purchase_amount != existing_doc.net_purchase_amount
or self.opening_accumulated_depreciation != existing_doc.opening_accumulated_depreciation
or self.opening_number_of_booked_depreciations
!= existing_doc.opening_number_of_booked_depreciations
)
def has_depreciation_settings_changed(self, existing_doc, fb_row):
"""Check if depreciation calculation settings have changed"""
if not existing_doc.get("depreciation_schedule") or fb_row.depreciation_method != "Manual":
return True
return (
fb_row.depreciation_method != existing_doc.depreciation_method
or fb_row.total_number_of_depreciations != existing_doc.total_number_of_depreciations
or fb_row.frequency_of_depreciation != existing_doc.frequency_of_depreciation
or getdate(fb_row.depreciation_start_date)
!= existing_doc.get("depreciation_schedule")[0].schedule_date
or fb_row.expected_value_after_useful_life != existing_doc.expected_value_after_useful_life
)
def should_regenerate_depreciation_schedule(
self, existing_doc, asset_details_changed, depreciation_settings_changed
):
"""Check all conditions to determine if schedule regeneration is required"""
# Schedule doesn't exist yet
if not existing_doc.get("depreciation_schedule"):
return True
# Either asset details or depreciation settings have changed
if asset_details_changed or depreciation_settings_changed:
return True
return False
self.show_schedule_creation_message(schedules)
def set_depr_rate_and_value_after_depreciation(self):
if self.split_from:
@@ -242,10 +188,6 @@ class Asset(AccountsController):
self.validate_expected_value_after_useful_life()
self.set_total_booked_depreciations()
def before_submit(self):
if self.is_composite_asset and not has_active_capitalization(self.name):
frappe.throw(_("Please capitalize this asset before submitting."))
def on_submit(self):
self.validate_in_use_date()
self.make_asset_movement()
@@ -588,7 +530,6 @@ class Asset(AccountsController):
def set_depreciation_rate(self):
for d in self.get("finance_books"):
self.validate_asset_finance_books(d)
d.rate_of_depreciation = flt(
self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation")
)
@@ -597,9 +538,6 @@ class Asset(AccountsController):
row.expected_value_after_useful_life = flt(
row.expected_value_after_useful_life, self.precision("net_purchase_amount")
)
if flt(row.expected_value_after_useful_life) < 0:
frappe.throw(_("Row {0}: Expected Value After Useful Life cannot be negative").format(row.idx))
if flt(row.expected_value_after_useful_life) >= flt(self.net_purchase_amount):
frappe.throw(
_("Row {0}: Expected Value After Useful Life must be less than Net Purchase Amount").format(
@@ -610,7 +548,6 @@ class Asset(AccountsController):
if not row.depreciation_start_date:
row.depreciation_start_date = get_last_day(self.available_for_use_date)
self.validate_depreciation_start_date(row)
self.validate_total_number_of_depreciations_and_frequency(row)
if not self.is_existing_asset:
self.opening_accumulated_depreciation = 0
@@ -647,15 +584,6 @@ class Asset(AccountsController):
title=_("Invalid Schedule"),
)
def validate_total_number_of_depreciations_and_frequency(self, row):
if row.total_number_of_depreciations <= 0:
frappe.throw(
_("Row #{0}: Total Number of Depreciations must be greater than zero").format(row.idx)
)
if row.frequency_of_depreciation <= 0:
frappe.throw(_("Row #{0}: Frequency of Depreciation must be greater than zero").format(row.idx))
def validate_depreciation_start_date(self, row):
if row.depreciation_start_date:
if getdate(row.depreciation_start_date) < getdate(self.purchase_date):

View File

@@ -194,13 +194,6 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
}
}
serial_and_batch_bundle(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
if (cdt === "Asset Capitalization Stock Item") {
this.get_warehouse_details(row);
}
}
asset(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
if (cdt === "Asset Capitalization Asset Item") {
@@ -414,7 +407,6 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
voucher_type: me.frm.doc.doctype,
voucher_no: me.frm.doc.name,
allow_zero_valuation: 1,
serial_and_batch_bundle: item.serial_and_batch_bundle,
},
},
callback: function (r) {

View File

@@ -352,7 +352,6 @@ class AssetCapitalization(StockController):
"voucher_no": self.name,
"company": self.company,
"allow_zero_valuation": cint(item.get("allow_zero_valuation_rate")),
"serial_and_batch_bundle": item.serial_and_batch_bundle,
}
)
@@ -724,7 +723,6 @@ def get_consumed_stock_item_details(ctx: ItemDetailsCtx):
"company": ctx.company,
"serial_no": ctx.serial_no,
"batch_no": ctx.batch_no,
"serial_and_batch_bundle": ctx.serial_and_batch_bundle,
}
)
out.update(get_warehouse_details(incoming_rate_args))

View File

@@ -117,52 +117,25 @@ frappe.ui.form.on("Asset Repair", {
frm.refresh_field("stock_items");
}
},
});
frappe.ui.form.on("Asset Repair Purchase Invoice", {
purchase_invoice: function (frm, cdt, cdn) {
frappe.model.set_value(cdt, cdn, {
expense_account: "",
repair_cost: 0,
});
},
expense_account: function (frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (!row.purchase_invoice || !row.expense_account) {
frappe.model.set_value(cdt, cdn, "repair_cost", 0);
return;
}
frappe.call({
method: "erpnext.assets.doctype.asset_repair.asset_repair.get_unallocated_repair_cost",
args: {
purchase_invoice: row.purchase_invoice,
expense_account: row.expense_account,
},
callback: function (r) {
if (r.message !== undefined) {
if (r.message > 0) {
frappe.model.set_value(cdt, cdn, "repair_cost", r.message);
} else {
frappe.model.set_value(cdt, cdn, "repair_cost", 0);
let pi_link = frappe.utils.get_form_link(
"Purchase Invoice",
row.purchase_invoice,
true
);
frappe.msgprint({
message: __(
"Row {0}: The entire expense amount for account {1} in {2} has already been allocated.",
[row.idx, row.expense_account.bold(), pi_link]
),
indicator: "orange",
});
purchase_invoice: function (frm) {
if (frm.doc.purchase_invoice) {
frappe.call({
method: "frappe.client.get_value",
args: {
doctype: "Purchase Invoice",
fieldname: "base_net_total",
filters: { name: frm.doc.purchase_invoice },
},
callback: function (r) {
if (r.message) {
frm.set_value("repair_cost", r.message.base_net_total);
}
}
},
});
},
});
} else {
frm.set_value("repair_cost", 0);
}
},
show_general_ledger: (frm) => {

View File

@@ -4,7 +4,6 @@
import frappe
from frappe import _
from frappe.query_builder import DocType
from frappe.query_builder.functions import Sum
from frappe.utils import cint, flt, get_link_to_form, getdate, time_diff_in_hours
import erpnext
@@ -15,6 +14,7 @@ from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.assets.doctype.asset.asset import get_asset_account
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_depr_schedule,
reschedule_depreciation,
)
from erpnext.controllers.accounts_controller import AccountsController
@@ -59,7 +59,7 @@ class AssetRepair(AccountsController):
# end: auto-generated types
def validate(self):
self.asset_doc = frappe.get_lazy_doc("Asset", self.asset)
self.asset_doc = frappe.get_doc("Asset", self.asset)
self.validate_asset()
self.validate_dates()
self.validate_purchase_invoices()
@@ -84,91 +84,60 @@ class AssetRepair(AccountsController):
)
def validate_purchase_invoices(self):
self.validate_duplicate_purchase_invoices()
self.validate_purchase_invoice_status()
for d in self.invoices:
self.validate_expense_account(d)
self.validate_purchase_invoice_repair_cost(d)
self.validate_purchase_invoice_status(d.purchase_invoice)
invoice_items = self.get_invoice_items(d.purchase_invoice)
self.validate_service_purchase_invoice(d.purchase_invoice, invoice_items)
self.validate_expense_account(d, invoice_items)
self.validate_purchase_invoice_repair_cost(d, invoice_items)
def validate_duplicate_purchase_invoices(self):
# account wise duplicate check
purchase_invoices = set()
duplicates = []
for row in self.invoices:
key = (row.purchase_invoice, row.expense_account)
if key in purchase_invoices:
duplicates.append((row.idx, row.purchase_invoice, row.expense_account))
else:
purchase_invoices.add(key)
if duplicates:
duplicate_links = "".join(
[
f"<li>{_('Row #{0}:').format(idx)} {get_link_to_form('Purchase Invoice', pi)} - {frappe.bold(account)}</li>"
for idx, pi, account in duplicates
]
)
msg = _("The following rows are duplicates:") + f"<br><ul>{duplicate_links}</ul>"
frappe.throw(msg)
def validate_purchase_invoice_status(self):
pi_names = [row.purchase_invoice for row in self.invoices]
docstatus = frappe._dict(
frappe.db.get_all(
"Purchase Invoice",
filters={"name": ["in", pi_names]},
fields=["name", "docstatus"],
as_list=True,
)
)
invalid_invoice = []
for row in self.invoices:
if docstatus.get(row.purchase_invoice) != 1:
invalid_invoice.append((row.idx, row.purchase_invoice))
if invalid_invoice:
invoice_links = "".join(
[
f"<li>{_('Row #{0}:').format(idx)} {get_link_to_form('Purchase Invoice', pi)}</li>"
for idx, pi in invalid_invoice
]
)
msg = _("The following Purchase Invoices are not submitted:") + f"<br><ul>{invoice_links}</ul>"
frappe.throw(msg)
def validate_expense_account(self, row):
"""Validate that the expense account exists in the purchase invoice for non-stock items."""
valid_accounts = _get_expense_accounts_for_purchase_invoice(row.purchase_invoice)
if row.expense_account not in valid_accounts:
def validate_purchase_invoice_status(self, purchase_invoice):
docstatus = frappe.db.get_value("Purchase Invoice", purchase_invoice, "docstatus")
if docstatus == 0:
frappe.throw(
_(
"Row #{0}: Expense account {1} is not valid for Purchase Invoice {2}. "
"Only expense accounts from non-stock items are allowed."
).format(
row.idx,
frappe.bold(row.expense_account),
get_link_to_form("Purchase Invoice", row.purchase_invoice),
_("{0} is still in Draft. Please submit it before saving the Asset Repair.").format(
get_link_to_form("Purchase Invoice", purchase_invoice)
)
)
def validate_purchase_invoice_repair_cost(self, row):
"""Validate that repair cost doesn't exceed available amount."""
available_amount = get_unallocated_repair_cost(
row.purchase_invoice, row.expense_account, exclude_asset_repair=self.name
def get_invoice_items(self, pi):
invoice_items = frappe.get_all(
"Purchase Invoice Item",
filters={"parent": pi},
fields=["item_code", "expense_account", "base_net_amount"],
)
if flt(row.repair_cost) > available_amount:
return invoice_items
def validate_service_purchase_invoice(self, purchase_invoice, invoice_items):
service_item_exists = False
for item in invoice_items:
if frappe.db.get_value("Item", item.item_code, "is_stock_item") == 0:
service_item_exists = True
break
if not service_item_exists:
frappe.throw(
_(
"Row #{0}: Repair cost {1} exceeds available amount {2} for Purchase Invoice {3} and Account {4}"
).format(
row.idx,
frappe.bold(frappe.format_value(row.repair_cost, {"fieldtype": "Currency"})),
frappe.bold(frappe.format_value(available_amount, {"fieldtype": "Currency"})),
get_link_to_form("Purchase Invoice", row.purchase_invoice),
frappe.bold(row.expense_account),
_("Service item not present in Purchase Invoice {0}").format(
get_link_to_form("Purchase Invoice", purchase_invoice)
)
)
def validate_expense_account(self, row, invoice_items):
pi_expense_accounts = set([item.expense_account for item in invoice_items])
if row.expense_account not in pi_expense_accounts:
frappe.throw(
_("Expense account {0} not present in Purchase Invoice {1}").format(
row.expense_account, get_link_to_form("Purchase Invoice", row.purchase_invoice)
)
)
def validate_purchase_invoice_repair_cost(self, row, invoice_items):
pi_net_total = sum([flt(item.base_net_amount) for item in invoice_items])
if flt(row.repair_cost) > pi_net_total:
frappe.throw(
_("Repair cost cannot be greater than purchase invoice base net total {0}").format(
pi_net_total
)
)
@@ -230,7 +199,7 @@ class AssetRepair(AccountsController):
self.cancel_sabb()
def after_delete(self):
frappe.get_lazy_doc("Asset", self.asset).set_status()
frappe.get_doc("Asset", self.asset).set_status()
def check_repair_status(self):
if self.repair_status == "Pending" and self.docstatus == 1:
@@ -372,10 +341,7 @@ class AssetRepair(AccountsController):
return
# creating GL Entries for each row in Stock Items based on the Stock Entry created for it
stock_entry_name = frappe.db.get_value("Stock Entry", {"asset_repair": self.name}, "name")
stock_entry_items = frappe.get_all(
"Stock Entry Detail", filters={"parent": stock_entry_name}, fields=["expense_account", "amount"]
)
stock_entry = frappe.get_doc("Stock Entry", {"asset_repair": self.name})
default_expense_account = None
if not erpnext.is_perpetual_inventory_enabled(self.company):
@@ -385,7 +351,7 @@ class AssetRepair(AccountsController):
if not default_expense_account:
frappe.throw(_("Please set default Expense Account in Company {0}").format(self.company))
for item in stock_entry_items:
for item in stock_entry.items:
if flt(item.amount) > 0:
gl_entries.append(
self.get_gl_dict(
@@ -416,7 +382,7 @@ class AssetRepair(AccountsController):
"cost_center": self.cost_center,
"posting_date": self.completion_date,
"against_voucher_type": "Stock Entry",
"against_voucher": stock_entry_name,
"against_voucher": stock_entry.name,
"company": self.company,
},
item=self,
@@ -454,152 +420,33 @@ def get_downtime(failure_date, completion_date):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_purchase_invoice(doctype, txt, searchfield, start, page_len, filters):
"""
Get Purchase Invoices that have expense accounts for non-stock items.
Only returns invoices with at least one non-stock, non-fixed-asset item with an expense account.
"""
pi = DocType("Purchase Invoice")
pi_item = DocType("Purchase Invoice Item")
item = DocType("Item")
PurchaseInvoice = DocType("Purchase Invoice")
PurchaseInvoiceItem = DocType("Purchase Invoice Item")
Item = DocType("Item")
query = (
frappe.qb.from_(pi)
.join(pi_item)
.on(pi_item.parent == pi.name)
.left_join(item)
.on(item.name == pi_item.item_code)
.select(pi.name)
.distinct()
return (
frappe.qb.from_(PurchaseInvoice)
.join(PurchaseInvoiceItem)
.on(PurchaseInvoiceItem.parent == PurchaseInvoice.name)
.join(Item)
.on(Item.name == PurchaseInvoiceItem.item_code)
.select(PurchaseInvoice.name)
.where(
(pi.company == filters.get("company"))
& (pi.docstatus == 1)
& (pi_item.is_fixed_asset == 0)
& (pi_item.expense_account.isnotnull())
& (pi_item.expense_account != "")
& ((pi_item.item_code.isnull()) | (item.is_stock_item == 0))
(Item.is_stock_item == 0)
& (Item.is_fixed_asset == 0)
& (PurchaseInvoice.company == filters.get("company"))
& (PurchaseInvoice.docstatus == 1)
)
)
if txt:
query = query.where(pi.name.like(f"%{txt}%"))
return query.run(as_list=1)
).run(as_list=1)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_expense_accounts(doctype, txt, searchfield, start, page_len, filters):
"""
Get expense accounts for non-stock (service) items from the purchase invoice.
Used as a query function for link fields.
"""
purchase_invoice = filters.get("purchase_invoice")
if not purchase_invoice:
return []
expense_accounts = _get_expense_accounts_for_purchase_invoice(purchase_invoice)
return [[account] for account in expense_accounts]
def _get_expense_accounts_for_purchase_invoice(purchase_invoice: str) -> list[str]:
"""
Get expense accounts for non-stock items from the purchase invoice.
"""
pi_items = frappe.db.get_all(
"Purchase Invoice Item",
filters={"parent": purchase_invoice},
fields=["item_code", "expense_account", "is_fixed_asset"],
)
if not pi_items:
return []
# Get list of stock item codes from the invoice
item_codes = {item.item_code for item in pi_items if item.item_code}
stock_items = set()
if item_codes:
stock_items = set(
frappe.db.get_all(
"Item", filters={"name": ["in", list(item_codes)], "is_stock_item": 1}, pluck="name"
)
)
expense_accounts = set()
for item in pi_items:
# Skip stock items - they use warehouse accounts
if item.item_code and item.item_code in stock_items:
continue
# Skip fixed assets - they use asset accounts
if item.is_fixed_asset:
continue
# Use expense account from Purchase Invoice Item
if item.expense_account:
expense_accounts.add(item.expense_account)
return list(expense_accounts)
@frappe.whitelist()
def get_unallocated_repair_cost(
purchase_invoice: str, expense_account: str, exclude_asset_repair: str | None = None
) -> float:
"""
Calculate the unused repair cost for a purchase invoice and expense account.
"""
if not purchase_invoice or not expense_account:
return 0.0
frappe.has_permission("Purchase Invoice", "read", purchase_invoice, throw=True)
used_amount = get_allocated_repair_cost(purchase_invoice, expense_account, exclude_asset_repair)
total_amount = get_total_expense_amount(purchase_invoice, expense_account)
return flt(total_amount - used_amount)
def get_allocated_repair_cost(
purchase_invoice: str, expense_account: str, exclude_asset_repair: str | None = None
) -> float:
"""Get the total repair cost already allocated from submitted Asset Repairs."""
asset_repair_pi = DocType("Asset Repair Purchase Invoice")
query = (
frappe.qb.from_(asset_repair_pi)
.select(Sum(asset_repair_pi.repair_cost).as_("total"))
.where(
(asset_repair_pi.purchase_invoice == purchase_invoice)
& (asset_repair_pi.expense_account == expense_account)
& (asset_repair_pi.docstatus == 1)
)
)
if exclude_asset_repair:
query = query.where(asset_repair_pi.parent != exclude_asset_repair)
result = query.run(as_dict=True)
return flt(result[0].total) if result else 0.0
def get_total_expense_amount(purchase_invoice: str, expense_account: str) -> float:
"""Get the total expense amount from GL entries for a purchase invoice and account."""
gl_entry = DocType("GL Entry")
result = (
frappe.qb.from_(gl_entry)
.select((Sum(gl_entry.debit) - Sum(gl_entry.credit)).as_("total"))
.where(
(gl_entry.voucher_type == "Purchase Invoice")
& (gl_entry.voucher_no == purchase_invoice)
& (gl_entry.account == expense_account)
& (gl_entry.is_cancelled == 0)
)
).run(as_dict=True)
return flt(result[0].total) if result else 0.0
PurchaseInvoiceItem = DocType("Purchase Invoice Item")
return (
frappe.qb.from_(PurchaseInvoiceItem)
.select(PurchaseInvoiceItem.expense_account)
.distinct()
.where(PurchaseInvoiceItem.parent == filters.get("purchase_invoice"))
).run(as_list=1)

View File

@@ -175,39 +175,6 @@ class TestAssetRepair(IntegrationTestCase):
)
self.assertTrue(asset_repair.invoices)
def test_repair_cost_exceeds_available_amount(self):
"""Test that repair cost cannot exceed available amount from Purchase Invoice."""
asset_repair1 = create_asset_repair(
capitalize_repair_cost=1,
item="_Test Non Stock Item",
submit=1,
)
pi_name = asset_repair1.invoices[0].purchase_invoice
expense_account = asset_repair1.invoices[0].expense_account
asset_repair2 = frappe.new_doc("Asset Repair")
asset_repair2.update(
{
"asset": asset_repair1.asset,
"asset_name": asset_repair1.asset_name,
"failure_date": nowdate(),
"description": "Second Repair",
"company": asset_repair1.company,
"capitalize_repair_cost": 1,
}
)
asset_repair2.append(
"invoices",
{
"purchase_invoice": pi_name,
"expense_account": expense_account,
"repair_cost": 10, # PI already fully used, so this should fail
},
)
self.assertRaises(frappe.ValidationError, asset_repair2.save)
def test_gl_entries_with_perpetual_inventory(self):
set_depreciation_settings_in_company(company="_Test Company with perpetual inventory")

View File

@@ -929,7 +929,6 @@ def get_list_context(context=None):
"show_search": True,
"no_breadcrumbs": True,
"title": _("Purchase Orders"),
"list_template": "templates/includes/list/list.html",
}
)
return list_context

View File

@@ -51,11 +51,11 @@ frappe.listview_settings["Purchase Order"] = {
onload: function (listview) {
var method = "erpnext.buying.doctype.purchase_order.purchase_order.close_or_unclose_purchase_orders";
listview.page.add_action_item(__("Close"), function () {
listview.page.add_menu_item(__("Close"), function () {
listview.call_for_selected_items(method, { status: "Closed" });
});
listview.page.add_action_item(__("Reopen"), function () {
listview.page.add_menu_item(__("Reopen"), function () {
listview.call_for_selected_items(method, { status: "Submitted" });
});

View File

@@ -551,10 +551,7 @@ erpnext.buying.RequestforQuotationController = class RequestforQuotationControll
} else if (args.supplier_group) {
frappe.db
.get_list("Supplier", {
filters: {
supplier_group: args.supplier_group,
disabled: 0,
},
filters: { supplier_group: args.supplier_group },
limit: 100,
order_by: "name",
})

View File

@@ -386,7 +386,6 @@ def get_list_context(context=None):
"show_search": True,
"no_breadcrumbs": True,
"title": _("Request for Quotation"),
"list_template": "templates/includes/list/list.html",
}
)
return list_context

View File

@@ -32,13 +32,6 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
this.make_purchase_order.bind(this),
__("Create")
);
this.frm.add_custom_button(__("Update Items"), () => {
erpnext.utils.update_child_items({
frm: this.frm,
child_docname: "items",
cannot_add_row: false,
});
});
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
this.frm.add_custom_button(__("Quotation"), this.make_quotation.bind(this), __("Create"));
} else if (this.frm.doc.docstatus === 0) {

View File

@@ -232,7 +232,6 @@ def get_list_context(context=None):
"show_search": True,
"no_breadcrumbs": True,
"title": _("Supplier Quotation"),
"list_template": "templates/includes/list/list.html",
}
)
@@ -348,15 +347,3 @@ def set_expired_status():
""",
(nowdate()),
)
def get_purchased_items(supplier_quotation: str):
return frappe._dict(
frappe.get_all(
"Purchase Order Item",
filters={"supplier_quotation": supplier_quotation, "docstatus": 1},
fields=["supplier_quotation_item", {"SUM": "qty"}],
group_by="supplier_quotation_item",
as_list=1,
)
)

View File

@@ -2,115 +2,15 @@
# License: GNU General Public License v3. See license.txt
import json
import frappe
from frappe.tests import IntegrationTestCase, change_settings
from frappe.utils import add_days, today
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order
from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate
from erpnext.controllers.accounts_controller import InvalidQtyError
class TestPurchaseOrder(IntegrationTestCase):
def test_update_child_supplier_quotation_add_item(self):
sq = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0])
sq.submit()
trans_item = json.dumps(
[
{
"item_code": sq.items[0].item_code,
"rate": sq.items[0].rate,
"qty": 5,
"docname": sq.items[0].name,
},
{
"item_code": "_Test Item 2",
"rate": 300,
"qty": 3,
},
]
)
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
sq.reload()
self.assertEqual(sq.get("items")[0].qty, 5)
self.assertEqual(sq.get("items")[1].rate, 300)
def test_update_supplier_quotation_child_rate_disallow(self):
sq = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0])
sq.submit()
trans_item = json.dumps(
[
{
"item_code": sq.items[0].item_code,
"rate": 300,
"qty": sq.items[0].qty,
"docname": sq.items[0].name,
},
]
)
self.assertRaises(
frappe.ValidationError, update_child_qty_rate, "Supplier Quotation", trans_item, sq.name
)
def test_update_supplier_quotation_child_remove_item(self):
sq = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0])
sq.submit()
po = make_purchase_order(sq.name)
trans_item = json.dumps(
[
{
"item_code": sq.items[0].item_code,
"rate": sq.items[0].rate,
"qty": sq.items[0].qty,
"docname": sq.items[0].name,
},
{
"item_code": "_Test Item 2",
"rate": 300,
"qty": 3,
},
]
)
po.get("items")[0].schedule_date = add_days(today(), 1)
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
po.submit()
sq.reload()
trans_item = json.dumps(
[
{
"item_code": "_Test Item 2",
"rate": 300,
"qty": 3,
}
]
)
frappe.db.savepoint("before_cancel")
# check if item having purchase order can be removed
self.assertRaises(
frappe.LinkExistsError, update_child_qty_rate, "Supplier Quotation", trans_item, sq.name
)
frappe.db.rollback(save_point="before_cancel")
trans_item = json.dumps(
[
{
"item_code": sq.items[0].item_code,
"rate": sq.items[0].rate,
"qty": sq.items[0].qty,
"docname": sq.items[0].name,
}
]
)
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
sq.reload()
self.assertEqual(len(sq.get("items")), 1)
def test_supplier_quotation_qty(self):
sq = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0])
sq.items[0].qty = 0

View File

@@ -1,26 +0,0 @@
{
"aggregate_function_based_on": "base_rounded_total",
"creation": "2025-12-19 16:10:00.369804",
"currency": "",
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Purchase Order",
"dynamic_filters_json": "[[\"Purchase Order\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Purchase Order\",\"transaction_date\",\"Timespan\",\"this quarter\"],[\"Purchase Order\",\"docstatus\",\"=\",\"1\"],[\"Purchase Order\",\"is_subcontracted\",\"=\",0]]",
"function": "Average",
"idx": 0,
"is_public": 1,
"is_standard": 1,
"label": "Average Order Values",
"modified": "2025-12-19 16:57:02.546572",
"modified_by": "Administrator",
"module": "Buying",
"name": "Average Order Values",
"owner": "Administrator",
"parent_document_type": "",
"report_function": "Sum",
"show_full_number": 0,
"show_percentage_stats": 1,
"stats_time_interval": "Weekly",
"type": "Document Type"
}

View File

@@ -1,26 +0,0 @@
{
"aggregate_function_based_on": "",
"creation": "2025-12-19 16:08:48.262514",
"currency": "",
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Purchase Order",
"dynamic_filters_json": "[[\"Purchase Order\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Purchase Order\",\"transaction_date\",\"Timespan\",\"this quarter\"],[\"Purchase Order\",\"docstatus\",\"=\",\"1\"],[\"Purchase Order\",\"is_subcontracted\",\"=\",0]]",
"function": "Count",
"idx": 1,
"is_public": 1,
"is_standard": 1,
"label": "Purchase Orders Count",
"modified": "2025-12-19 16:57:12.960310",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Orders Count",
"owner": "Administrator",
"parent_document_type": "",
"report_function": "Sum",
"show_full_number": 0,
"show_percentage_stats": 1,
"stats_time_interval": "Weekly",
"type": "Document Type"
}

View File

@@ -1,26 +0,0 @@
{
"aggregate_function_based_on": "base_rounded_total",
"creation": "2025-12-19 16:07:19.293268",
"currency": "",
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Purchase Order",
"dynamic_filters_json": "[[\"Purchase Order\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Purchase Order\",\"transaction_date\",\"Timespan\",\"this quarter\"],[\"Purchase Order\",\"docstatus\",\"=\",\"1\"],[\"Purchase Order\",\"is_subcontracted\",\"=\",0]]",
"function": "Sum",
"idx": 0,
"is_public": 1,
"is_standard": 1,
"label": "Total Purchase Amount",
"modified": "2025-12-19 16:57:24.263495",
"modified_by": "Administrator",
"module": "Buying",
"name": "Total Purchase Amount",
"owner": "Administrator",
"parent_document_type": "",
"report_function": "Sum",
"show_full_number": 0,
"show_percentage_stats": 0,
"stats_time_interval": "Weekly",
"type": "Document Type"
}

View File

@@ -36,7 +36,7 @@ def get_chart_data(data, conditions, filters):
# fetch only periodic columns as labels
columns = conditions.get("columns")[start:-2][2::2]
labels = [column.split(":")[0].replace(" (Amt)", "") for column in columns]
labels = [column.split(":")[0] for column in columns]
datapoints = [0] * len(labels)
for row in data:

View File

@@ -1,12 +1,11 @@
{
"app": "erpnext",
"charts": [
{
"chart_name": "Purchase Order Trends",
"label": "Purchase Order Trends"
}
],
"content": "[{\"id\":\"j3dJGo8Ok6\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Purchase Order Trends\",\"col\":12}},{\"id\":\"k75jSq2D6Z\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Purchase Orders Count\",\"col\":4}},{\"id\":\"UPXys0lQLj\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Purchase Amount\",\"col\":4}},{\"id\":\"yQGK3eb2hg\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Average Order Values\",\"col\":4}},{\"id\":\"oN7lXSwQji\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"Ivw1PI_wEJ\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"RrWFEi4kCf\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"id\":\"RFIakryyJP\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Material Request\",\"col\":3}},{\"id\":\"bM10abFmf6\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Order\",\"col\":3}},{\"id\":\"lR0Hw_37Pu\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Analytics\",\"col\":3}},{\"id\":\"_HN0Ljw1lX\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Order Analysis\",\"col\":3}},{\"id\":\"kuLuiMRdnX\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"tQFeiKptW2\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Procurement\",\"col\":3}},{\"id\":\"0NiuFE_EGS\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"Xe2GVLOq8J\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"QwqyG6XuUt\",\"type\":\"card\",\"data\":{\"card_name\":\"Buying\",\"col\":4}},{\"id\":\"bTPjOxC_N_\",\"type\":\"card\",\"data\":{\"card_name\":\"Items & Pricing\",\"col\":4}},{\"id\":\"87ht0HIneb\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"EDOsBOmwgw\",\"type\":\"card\",\"data\":{\"card_name\":\"Supplier\",\"col\":4}},{\"id\":\"oWNNIiNb2i\",\"type\":\"card\",\"data\":{\"card_name\":\"Supplier Scorecard\",\"col\":4}},{\"id\":\"7F_13-ihHB\",\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"id\":\"pfwiLvionl\",\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}},{\"id\":\"8ySDy6s4qn\",\"type\":\"card\",\"data\":{\"card_name\":\"Regional\",\"col\":4}}]",
"content": "[{\"id\":\"I3JijHOxil\",\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Buying\",\"col\":12}},{\"id\":\"j3dJGo8Ok6\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Purchase Order Trends\",\"col\":12}},{\"id\":\"oN7lXSwQji\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"Ivw1PI_wEJ\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"RrWFEi4kCf\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"id\":\"RFIakryyJP\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Material Request\",\"col\":3}},{\"id\":\"bM10abFmf6\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Order\",\"col\":3}},{\"id\":\"lR0Hw_37Pu\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Analytics\",\"col\":3}},{\"id\":\"_HN0Ljw1lX\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Order Analysis\",\"col\":3}},{\"id\":\"kuLuiMRdnX\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"tQFeiKptW2\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Procurement\",\"col\":3}},{\"id\":\"0NiuFE_EGS\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"Xe2GVLOq8J\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"QwqyG6XuUt\",\"type\":\"card\",\"data\":{\"card_name\":\"Buying\",\"col\":4}},{\"id\":\"bTPjOxC_N_\",\"type\":\"card\",\"data\":{\"card_name\":\"Items & Pricing\",\"col\":4}},{\"id\":\"87ht0HIneb\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"EDOsBOmwgw\",\"type\":\"card\",\"data\":{\"card_name\":\"Supplier\",\"col\":4}},{\"id\":\"oWNNIiNb2i\",\"type\":\"card\",\"data\":{\"card_name\":\"Supplier Scorecard\",\"col\":4}},{\"id\":\"7F_13-ihHB\",\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"id\":\"pfwiLvionl\",\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}},{\"id\":\"8ySDy6s4qn\",\"type\":\"card\",\"data\":{\"card_name\":\"Regional\",\"col\":4}}]",
"creation": "2020-01-28 11:50:26.195467",
"custom_blocks": [],
"docstatus": 0,
@@ -512,24 +511,11 @@
"type": "Link"
}
],
"modified": "2025-12-19 16:12:02.461082",
"modified": "2023-07-04 14:43:30.387683",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying",
"number_cards": [
{
"label": "Purchase Orders Count",
"number_card_name": "Purchase Orders Count"
},
{
"label": "Total Purchase Amount",
"number_card_name": "Total Purchase Amount"
},
{
"label": "Average Order Values",
"number_card_name": "Average Order Values"
}
],
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,
@@ -572,13 +558,11 @@
{
"label": "Purchase Analytics",
"link_to": "Purchase Analytics",
"report_ref_doctype": "Purchase Order",
"type": "Report"
},
{
"label": "Purchase Order Analysis",
"link_to": "Purchase Order Analysis",
"report_ref_doctype": "Purchase Order",
"type": "Report"
},
{
@@ -587,6 +571,5 @@
"type": "Dashboard"
}
],
"title": "Buying",
"type": "Workspace"
"title": "Buying"
}

View File

@@ -315,30 +315,13 @@ class AccountsController(TransactionBase):
def validate_company_linked_addresses(self):
address_fields = []
sales_doctypes = ("Quotation", "Sales Order", "Delivery Note", "Sales Invoice")
purchase_doctypes = ("Purchase Order", "Purchase Receipt", "Purchase Invoice", "Supplier Quotation")
if self.doctype in sales_doctypes:
if self.doctype in ("Quotation", "Sales Order", "Delivery Note", "Sales Invoice"):
address_fields = ["dispatch_address_name", "company_address"]
elif self.doctype in purchase_doctypes:
elif self.doctype in ("Purchase Order", "Purchase Receipt", "Purchase Invoice", "Supplier Quotation"):
address_fields = ["billing_address", "shipping_address"]
if not address_fields:
return
# Determine if drop ship applies
is_drop_ship = self.doctype in {
"Purchase Order",
"Sales Order",
"Sales Invoice",
} and self.is_drop_ship(self.items)
for field in address_fields:
address = self.get(field)
if (field in ["dispatch_address_name", "shipping_address"]) and is_drop_ship:
continue
if address and not frappe.db.exists(
"Dynamic Link",
{
@@ -349,15 +332,11 @@ class AccountsController(TransactionBase):
},
):
frappe.throw(
_("{0} does not belong to the Company {1}.").format(
_("{0} does not belong to the {1}.").format(
_(self.meta.get_label(field)), bold(self.company)
)
)
@staticmethod
def is_drop_ship(items):
return any(item.delivered_by_supplier for item in items)
def set_default_letter_head(self):
if hasattr(self, "letter_head") and not self.letter_head:
self.letter_head = frappe.db.get_value("Company", self.company, "default_letter_head")
@@ -2707,9 +2686,7 @@ class AccountsController(TransactionBase):
for d in self.get("payment_schedule"):
d.validate_from_to_dates("discount_date", "due_date")
if self.doctype in ["Sales Order", "Quotation"] and getdate(d.due_date) < getdate(
self.transaction_date
):
if self.doctype == "Sales Order" and getdate(d.due_date) < getdate(self.transaction_date):
frappe.throw(
_("Row {0}: Due Date in the Payment Terms table cannot be before Posting Date").format(
d.idx
@@ -3709,7 +3686,7 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
child_item.conversion_factor = flt(trans_item.get("conversion_factor")) or conversion_factor
if child_doctype in ["Purchase Order Item", "Supplier Quotation Item"]:
if child_doctype == "Purchase Order Item":
# Initialized value will update in parent validation
child_item.base_rate = 1
child_item.base_amount = 1
@@ -3727,7 +3704,7 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
return child_item
def validate_child_on_delete(row, parent, ordered_item=None):
def validate_child_on_delete(row, parent):
"""Check if partially transacted item (row) is being deleted."""
if parent.doctype == "Sales Order":
if flt(row.delivered_qty):
@@ -3755,17 +3732,13 @@ def validate_child_on_delete(row, parent, ordered_item=None):
row.idx, row.item_code
)
)
if parent.doctype in ["Purchase Order", "Sales Order"]:
if flt(row.billed_amt):
frappe.throw(
_("Row #{0}: Cannot delete item {1} which has already been billed.").format(
row.idx, row.item_code
)
)
if parent.doctype == "Quotation":
if ordered_item.get(row.name):
frappe.throw(_("Cannot delete an item which has been ordered"))
if flt(row.billed_amt):
frappe.throw(
_("Row #{0}: Cannot delete item {1} which has already been billed.").format(
row.idx, row.item_code
)
)
def update_bin_on_delete(row, doctype):
@@ -3791,7 +3764,7 @@ def update_bin_on_delete(row, doctype):
update_bin_qty(row.item_code, row.warehouse, qty_dict)
def validate_and_delete_children(parent, data, ordered_item=None) -> bool:
def validate_and_delete_children(parent, data) -> bool:
deleted_children = []
updated_item_names = [d.get("docname") for d in data]
for item in parent.items:
@@ -3799,7 +3772,7 @@ def validate_and_delete_children(parent, data, ordered_item=None) -> bool:
deleted_children.append(item)
for d in deleted_children:
validate_child_on_delete(d, parent, ordered_item)
validate_child_on_delete(d, parent)
d.cancel()
d.delete()
@@ -3808,19 +3781,16 @@ def validate_and_delete_children(parent, data, ordered_item=None) -> bool:
# need to update ordered qty in Material Request first
# bin uses Material Request Items to recalculate & update
if parent.doctype not in ["Quotation", "Supplier Quotation"]:
parent.update_prevdoc_status()
for d in deleted_children:
update_bin_on_delete(d, parent.doctype)
parent.update_prevdoc_status()
for d in deleted_children:
update_bin_on_delete(d, parent.doctype)
return bool(deleted_children)
@frappe.whitelist()
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import get_purchased_items
from erpnext.selling.doctype.quotation.quotation import get_ordered_items
def check_doc_permissions(doc, perm_type="create"):
try:
doc.check_permission(perm_type)
@@ -3859,7 +3829,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
)
def get_new_child_item(item_row):
child_doctype = parent_doctype + " Item"
child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item"
return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row)
def is_allowed_zero_qty():
@@ -3884,21 +3854,6 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt(child_item.received_qty):
frappe.throw(_("Cannot set quantity less than received quantity"))
if parent_doctype in ["Quotation", "Supplier Quotation"]:
if (parent_doctype == "Quotation" and not ordered_items) or (
parent_doctype == "Supplier Quotation" and not purchased_items
):
return
qty_to_check = (
ordered_items.get(child_item.name)
if parent_doctype == "Quotation"
else purchased_items.get(child_item.name)
)
if qty_to_check:
if flt(new_data.get("qty")) < qty_to_check:
frappe.throw(_("Cannot reduce quantity than ordered or purchased quantity"))
def should_update_supplied_items(doc) -> bool:
"""Subcontracted PO can allow following changes *after submit*:
@@ -3941,6 +3896,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
frappe.throw(_("Finished Good Item {0} Qty can not be zero").format(new_data["fg_item"]))
data = json.loads(trans_items)
any_qty_changed = False # updated to true if any item's qty changes
items_added_or_removed = False # updated to true if any new item is added or removed
any_conversion_factor_changed = False
@@ -3948,16 +3904,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
check_doc_permissions(parent, "write")
if parent_doctype == "Quotation":
ordered_items = get_ordered_items(parent.name)
_removed_items = validate_and_delete_children(parent, data, ordered_items)
elif parent_doctype == "Supplier Quotation":
purchased_items = get_purchased_items(parent.name)
_removed_items = validate_and_delete_children(parent, data, purchased_items)
else:
_removed_items = validate_and_delete_children(parent, data)
_removed_items = validate_and_delete_children(parent, data)
items_added_or_removed |= _removed_items
for d in data:
@@ -3997,9 +3944,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
conversion_factor_unchanged = prev_con_fac == new_con_fac
any_conversion_factor_changed |= not conversion_factor_unchanged
date_unchanged = (
(prev_date == getdate(new_date) if prev_date and new_date else False)
if parent_doctype not in ["Quotation", "Supplier Quotation"]
else None
prev_date == getdate(new_date) if prev_date and new_date else False
) # in case of delivery note etc
if (
rate_unchanged
@@ -4012,10 +3957,6 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
continue
validate_quantity(child_item, d)
if parent_doctype in ["Quotation", "Supplier Quotation"]:
if not rate_unchanged:
frappe.throw(_("Rates cannot be modified for quoted items"))
if flt(child_item.get("qty")) != flt(d.get("qty")):
any_qty_changed = True
@@ -4039,21 +3980,18 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
rate_unchanged = prev_rate == new_rate
if not rate_unchanged and not child_item.get("qty") and is_allowed_zero_qty():
frappe.throw(_("Rate of '{}' items cannot be changed").format(frappe.bold(_("Unit Price"))))
# Amount cannot be lesser than billed amount, except for negative amounts
row_rate = flt(d.get("rate"), rate_precision)
if parent_doctype in ["Purchase Order", "Sales Order"]:
amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt(
row_rate * flt(d.get("qty"), qty_precision), rate_precision
amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt(
row_rate * flt(d.get("qty"), qty_precision), rate_precision
)
if amount_below_billed_amt and row_rate > 0.0:
frappe.throw(
_(
"Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}."
).format(child_item.idx, child_item.item_code)
)
if amount_below_billed_amt and row_rate > 0.0:
frappe.throw(
_(
"Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}."
).format(child_item.idx, child_item.item_code)
)
else:
child_item.rate = row_rate
else:
child_item.rate = row_rate
@@ -4081,27 +4019,26 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
if d.get("bom_no") and parent_doctype == "Sales Order":
child_item.bom_no = d.get("bom_no")
if parent_doctype in ["Sales Order", "Purchase Order"]:
if flt(child_item.price_list_rate):
if flt(child_item.rate) > flt(child_item.price_list_rate):
# if rate is greater than price_list_rate, set margin
# or set discount
child_item.discount_percentage = 0
child_item.margin_type = "Amount"
child_item.margin_rate_or_amount = flt(
child_item.rate - child_item.price_list_rate,
child_item.precision("margin_rate_or_amount"),
)
child_item.rate_with_margin = child_item.rate
else:
child_item.discount_percentage = flt(
(1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0,
child_item.precision("discount_percentage"),
)
child_item.discount_amount = flt(child_item.price_list_rate) - flt(child_item.rate)
child_item.margin_type = ""
child_item.margin_rate_or_amount = 0
child_item.rate_with_margin = 0
if flt(child_item.price_list_rate):
if flt(child_item.rate) > flt(child_item.price_list_rate):
# if rate is greater than price_list_rate, set margin
# or set discount
child_item.discount_percentage = 0
child_item.margin_type = "Amount"
child_item.margin_rate_or_amount = flt(
child_item.rate - child_item.price_list_rate,
child_item.precision("margin_rate_or_amount"),
)
child_item.rate_with_margin = child_item.rate
else:
child_item.discount_percentage = flt(
(1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0,
child_item.precision("discount_percentage"),
)
child_item.discount_amount = flt(child_item.price_list_rate) - flt(child_item.rate)
child_item.margin_type = ""
child_item.margin_rate_or_amount = 0
child_item.rate_with_margin = 0
child_item.flags.ignore_validate_update_after_submit = True
if new_child_flag:
@@ -4123,14 +4060,13 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.doctype, parent.company, parent.base_grand_total
)
if parent_doctype != "Supplier Quotation":
parent.set_payment_schedule()
parent.set_payment_schedule()
if parent_doctype == "Purchase Order":
parent.validate_minimum_order_qty()
parent.validate_budget()
if parent.is_against_so():
parent.update_status_updater()
elif parent_doctype == "Sales Order":
else:
parent.check_credit_limit()
# reset index of child table
@@ -4163,7 +4099,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
"Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}."
).format(frappe.bold(parent.name))
)
elif parent_doctype == "Sales Order": # Sales Order
else: # Sales Order
if parent.is_subcontracted and not parent.can_update_items():
frappe.throw(
_(
@@ -4181,10 +4117,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.reload()
validate_workflow_conditions(parent)
if parent_doctype in ["Purchase Order", "Sales Order"]:
parent.update_blanket_order()
parent.update_billing_percentage()
parent.set_status()
parent.update_blanket_order()
parent.update_billing_percentage()
parent.set_status()
parent.validate_uom_is_integer("uom", "qty")
parent.validate_uom_is_integer("stock_uom", "stock_qty")

View File

@@ -81,19 +81,6 @@ class BuyingController(SubcontractingController):
),
)
if (
self.get("company")
and (
default_buying_terms := frappe.get_value(
"Company", self.get("company"), "default_buying_terms"
)
)
and not self.get("tc_name")
and not self.get("terms")
):
self.tc_name = default_buying_terms
self.terms = frappe.get_value("Terms and Conditions", self.get("tc_name"), "terms")
def validate_posting_date_with_po(self):
po_list = {x.purchase_order for x in self.items if x.purchase_order}

View File

@@ -43,19 +43,6 @@ class SellingController(StockController):
),
)
if (
self.get("company")
and (
default_selling_terms := frappe.get_value(
"Company", self.get("company"), "default_selling_terms"
)
)
and not self.get("tc_name")
and not self.get("terms")
):
self.tc_name = default_selling_terms
self.terms = frappe.get_value("Terms and Conditions", self.get("tc_name"), "terms")
def validate(self):
super().validate()
self.validate_items()

View File

@@ -94,7 +94,6 @@ status_map = {
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
["Return", "eval:self.is_return == 1 and self.per_billed == 0 and self.docstatus == 1"],
["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
],
@@ -130,7 +129,7 @@ status_map = {
],
[
"Received",
"eval:self.status != 'Stopped' and self.docstatus == 1 and ((self.per_received == 100 and self.material_request_type == 'Purchase') or (self.per_ordered == 100 and self.material_request_type == 'Customer Provided'))",
"eval:self.status != 'Stopped' and self.per_received == 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'",
],
[
"Partially Received",
@@ -138,11 +137,11 @@ status_map = {
],
[
"Partially Received",
"eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1 and self.material_request_type in ['Material Transfer', 'Customer Provided']",
"eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1 and self.material_request_type == 'Material Transfer'",
],
[
"Partially Ordered",
"eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1 and self.material_request_type not in ['Material Transfer', 'Customer Provided']",
"eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1 and self.material_request_type != 'Material Transfer'",
],
],
"POS Opening Entry": [
@@ -393,16 +392,12 @@ class StatusUpdater(Document):
self.item_allowance,
self.global_qty_allowance,
self.global_amount_allowance,
) = (
get_allowance_for(
item["item_code"],
self.item_allowance,
self.global_qty_allowance,
self.global_amount_allowance,
qty_or_amount,
)
if args["source_dt"] != "Pick List Item"
else (0, {}, None, None)
) = get_allowance_for(
item["item_code"],
self.item_allowance,
self.global_qty_allowance,
self.global_amount_allowance,
qty_or_amount,
)
role_allowed_to_over_deliver_receive = frappe.get_single_value(
@@ -440,17 +435,14 @@ class StatusUpdater(Document):
):
return
if args["source_dt"] != "Pick List Item":
if qty_or_amount == "qty":
action_msg = _(
'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.'
)
else:
action_msg = _(
'To allow over billing, update "Over Billing Allowance" in Accounts Settings or the Item.'
)
if qty_or_amount == "qty":
action_msg = _(
'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.'
)
else:
action_msg = None
action_msg = _(
'To allow over billing, update "Over Billing Allowance" in Accounts Settings or the Item.'
)
frappe.throw(
_(
@@ -462,7 +454,8 @@ class StatusUpdater(Document):
frappe.bold(_(self.doctype)),
frappe.bold(item.get("item_code")),
)
+ ("<br><br>" + action_msg if action_msg else ""),
+ "<br><br>"
+ action_msg,
OverAllowanceError,
title=_("Limit Crossed"),
)

View File

@@ -221,7 +221,7 @@ class SubcontractingController(StockController):
and self._doc_before_save
):
for row in self._doc_before_save.get("items"):
item_dict[row.name] = (row.item_code, row.qty + (row.get("rejected_qty") or 0))
item_dict[row.name] = (row.item_code, row.qty)
return item_dict
@@ -245,10 +245,7 @@ class SubcontractingController(StockController):
for row in self.items:
self.__reference_name.append(row.name)
if (row.name not in item_dict) or (
row.item_code,
row.qty + (row.get("rejected_qty") or 0),
) != item_dict[row.name]:
if (row.name not in item_dict) or (row.item_code, row.qty) != item_dict[row.name]:
self.__changed_name.append(row.name)
if item_dict.get(row.name):
@@ -937,11 +934,7 @@ class SubcontractingController(StockController):
for bom_item in self._get_materials_from_bom(
row.item_code, row.bom, row.get("include_exploded_items")
):
qty = (
flt(bom_item.qty_consumed_per_unit)
* flt(row.qty + (row.get("rejected_qty") or 0))
* row.conversion_factor
)
qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor
bom_item.main_item_code = row.item_code
self.__update_reserve_warehouse(bom_item, row)
self.__set_alternative_item(bom_item)

View File

@@ -7,7 +7,6 @@ import json
import frappe
from frappe import _, scrub
from frappe.model.document import Document, bulk_insert
from frappe.query_builder import functions
from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction
import erpnext
@@ -398,9 +397,6 @@ class calculate_taxes_and_totals:
self._calculate()
def calculate_taxes(self):
# reset value from earlier calculations
self.grand_total_diff = 0
doc = self.doc
if not doc.get("taxes"):
return
@@ -687,7 +683,7 @@ class calculate_taxes_and_totals:
self.grand_total_diff = 0
def calculate_totals(self):
grand_total_diff = self.grand_total_diff
grand_total_diff = getattr(self, "grand_total_diff", 0)
if self.doc.get("taxes"):
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + grand_total_diff
@@ -780,22 +776,6 @@ class calculate_taxes_and_totals:
discount_amount = self.doc.discount_amount or 0
grand_total = self.doc.grand_total
if self.doc.get("is_return") and self.doc.get("return_against"):
doctype = frappe.qb.DocType(self.doc.doctype)
result = (
frappe.qb.from_(doctype)
.select(functions.Sum(doctype.discount_amount).as_("total_return_discount"))
.where(
(doctype.return_against == self.doc.return_against)
& (doctype.is_return == 1)
& (doctype.docstatus == 1)
)
).run(as_dict=True)
total_return_discount = abs(result[0].get("total_return_discount") or 0)
discount_amount += total_return_discount
# validate that discount amount cannot exceed the total before discount
if (
(grand_total >= 0 and discount_amount > grand_total)
@@ -944,11 +924,12 @@ class calculate_taxes_and_totals:
)
)
if self.doc.get("write_off_outstanding_amount_automatically"):
self.doc.write_off_amount = 0
if self.doc.docstatus.is_draft():
if self.doc.get("write_off_outstanding_amount_automatically"):
self.doc.write_off_amount = 0
self.calculate_outstanding_amount()
self.calculate_write_off_amount()
self.calculate_outstanding_amount()
self.calculate_write_off_amount()
def is_internal_invoice(self):
"""

View File

@@ -2438,7 +2438,6 @@ class TestAccountsController(IntegrationTestCase):
def test_company_linked_address(self):
from erpnext.crm.doctype.prospect.test_prospect import make_address
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
company_address = make_address(
address_title="Company", address_type="Shipping", address_line1="100", city="Mumbai"
@@ -2467,16 +2466,3 @@ class TestAccountsController(IntegrationTestCase):
po.billing_address = company_address.name
po.reload()
po.save()
si = make_sales_order(do_not_save=1, do_not_submit=1)
si.dispatch_address_name = supplier_billing.name
self.assertRaises(frappe.ValidationError, si.save)
si.items[0].delivered_by_supplier = 1
si.items[0].supplier = "_Test Supplier"
si.save()
po = create_purchase_order(do_not_save=True)
po.shipping_address = customer_shipping.name
self.assertRaises(frappe.ValidationError, po.save)
po.items[0].delivered_by_supplier = 1
po.save()

View File

@@ -1,6 +1,5 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:market_segment",
"creation": "2018-10-01 09:59:14.479509",
"doctype": "DocType",
@@ -18,11 +17,10 @@
}
],
"links": [],
"modified": "2025-12-17 12:09:34.687368",
"modified": "2024-08-16 19:24:55.811760",
"modified_by": "Administrator",
"module": "CRM",
"name": "Market Segment",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -39,10 +37,9 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1,
"translated_doctype": 1
}
}

View File

@@ -154,14 +154,6 @@ website_route_rules = [
"parents": [{"label": "Purchase Order", "route": "purchase-orders"}],
},
},
{
"from_route": "/purchase-orders/<path:name>",
"to_route": "order",
"defaults": {
"doctype": "Purchase Order",
"parents": [{"label": "Purchase Order", "route": "purchase-orders"}],
},
},
{"from_route": "/purchase-invoices", "to_route": "Purchase Invoice"},
{
"from_route": "/purchase-invoices/<path:name>",
@@ -419,9 +411,7 @@ scheduler_events = {
"0/15 * * * *": [
"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
],
"0/30 * * * *": [
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.run_parallel_reposting",
],
"0/30 * * * *": [],
# Hourly but offset by 30 minutes
"30 * * * *": [
"erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More