Compare commits

..

1 Commits

Author SHA1 Message Date
Ankush Menat
7ab241fab4 perf: Skip link checking on repost's remove_attached_file (#45061)
This is internal detail, doesn't need to do horrible link checks in
framework.

(cherry picked from commit 4f690affc9)

# Conflicts:
#	erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
2025-05-07 06:24:00 +00:00
129 changed files with 716 additions and 1722 deletions

View File

@@ -3,7 +3,7 @@ import inspect
import frappe
__version__ = "14.92.12"
__version__ = "14.67.1"
def get_default_company(user=None):

View File

@@ -3,14 +3,4 @@
frappe.ui.form.on("Accounts Settings", {
refresh: function (frm) {},
drop_ar_procedures: function (frm) {
frm.call({
doc: frm.doc,
method: "drop_ar_sql_procedures",
callback: function (r) {
frappe.show_alert(__("Procedures dropped"), 5);
},
});
},
});

View File

@@ -77,8 +77,6 @@
"receivable_payable_remarks_length",
"accounts_receivable_payable_tuning_section",
"receivable_payable_fetch_method",
"column_break_ntmi",
"drop_ar_procedures",
"legacy_section",
"ignore_is_opening_check_for_reporting"
],
@@ -490,7 +488,7 @@
"fieldname": "receivable_payable_fetch_method",
"fieldtype": "Select",
"label": "Data Fetch Method",
"options": "Buffered Cursor\nUnBuffered Cursor\nRaw SQL"
"options": "Buffered Cursor\nUnBuffered Cursor"
},
{
"fieldname": "accounts_receivable_payable_tuning_section",
@@ -501,17 +499,6 @@
"fieldname": "legacy_section",
"fieldtype": "Section Break",
"label": "Legacy Fields"
},
{
"fieldname": "column_break_ntmi",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.receivable_payable_fetch_method == \"Raw SQL\"",
"description": "Drops existing SQL Procedures and Function setup by Accounts Receivable report",
"fieldname": "drop_ar_procedures",
"fieldtype": "Button",
"label": "Drop Procedures"
}
],
"icon": "icon-cog",

View File

@@ -65,11 +65,3 @@ class AccountsSettings(Document):
def validate_pending_reposts(self):
if self.acc_frozen_upto:
check_pending_reposting(self.acc_frozen_upto)
@frappe.whitelist()
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

@@ -9,6 +9,13 @@ cur_frm.add_fetch("bank", "swift_number", "swift_number");
frappe.ui.form.on("Bank Guarantee", {
setup: function (frm) {
frm.set_query("bank", function () {
return {
filters: {
company: frm.doc.company,
},
};
});
frm.set_query("bank_account", function () {
return {
filters: {

View File

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

View File

@@ -448,8 +448,9 @@ def unset_existing_data(company):
"Sales Taxes and Charges Template",
"Purchase Taxes and Charges Template",
]:
dt = frappe.qb.DocType(doctype)
frappe.qb.from_(dt).where(dt.company == company).delete().run()
frappe.db.sql(
f'''delete from `tab{doctype}` where `company`="%s"''' % (company) # nosec
)
def set_default_accounts(company):

View File

@@ -4,8 +4,6 @@
import unittest
import frappe
from frappe.query_builder.functions import Sum
from frappe.tests.utils import change_settings
from frappe.utils import add_days, today
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
@@ -192,31 +190,6 @@ class TestCostCenterAllocation(unittest.TestCase):
coa2.cancel()
jv.cancel()
@change_settings("System Settings", {"rounding_method": "Commercial Rounding"})
def test_debit_credit_on_cost_center_allocation_for_commercial_rounding(self):
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
cca = create_cost_center_allocation(
"_Test Company",
"Main Cost Center 1 - _TC",
{"Sub Cost Center 2 - _TC": 50, "Sub Cost Center 3 - _TC": 50},
)
si = create_sales_invoice(rate=145.65, cost_center="Main Cost Center 1 - _TC")
gl_entry = frappe.qb.DocType("GL Entry")
gl_entries = (
frappe.qb.from_(gl_entry)
.select(Sum(gl_entry.credit).as_("cr"), Sum(gl_entry.debit).as_("dr"))
.where(gl_entry.voucher_type == "Sales Invoice")
.where(gl_entry.voucher_no == si.name)
).run(as_dict=1)
self.assertEqual(gl_entries[0].cr, gl_entries[0].dr)
si.cancel()
cca.cancel()
def create_cost_center_allocation(
company,

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.app") {
let result = ["rates", "{to_currency}"];
let params = {
base: "{from_currency}",

View File

@@ -78,7 +78,7 @@
"fieldname": "service_provider",
"fieldtype": "Select",
"label": "Service Provider",
"options": "frankfurter.dev\nexchangerate.host\nCustom",
"options": "frankfurter.app\nexchangerate.host\nCustom",
"reqd": 1
},
{
@@ -104,7 +104,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-11-25 13:03:41.896424",
"modified": "2024-03-18 08:32:26.895076",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Currency Exchange Settings",
@@ -141,9 +141,8 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -35,7 +35,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.app":
self.set("result_key", [])
self.set("req_params", [])
@@ -80,13 +80,11 @@ 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.app"]:
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}"
protocol = "https://"
if use_http:

View File

@@ -55,46 +55,46 @@ class Dunning(AccountsController):
"conversion_rate",
"cost_center",
]
inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1)
accounting_dimensions = get_accounting_dimensions()
invoice_fields.extend(accounting_dimensions)
inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1)
dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate)
default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
debit = {
"account": inv.debit_to,
"party_type": "Customer",
"party": self.customer,
"due_date": self.due_date,
"against": self.income_account,
"debit": dunning_in_company_currency,
"debit_in_account_currency": self.dunning_amount,
"against_voucher": self.name,
"against_voucher_type": "Dunning",
"cost_center": inv.cost_center or default_cost_center,
"project": inv.project,
}
credit = {
"account": self.income_account,
"against": self.customer,
"credit": dunning_in_company_currency,
"credit_in_account_currency": self.dunning_amount,
"cost_center": inv.cost_center or default_cost_center,
"project": inv.project,
}
for dimension in accounting_dimensions:
if val := inv.get(dimension):
debit[dimension] = credit[dimension] = val
gl_entries = [
self.get_gl_dict(debit, inv.party_account_currency, item=inv),
self.get_gl_dict(credit, item=inv),
]
gl_entries.append(
self.get_gl_dict(
{
"account": inv.debit_to,
"party_type": "Customer",
"party": self.customer,
"due_date": self.due_date,
"against": self.income_account,
"debit": dunning_in_company_currency,
"debit_in_account_currency": self.dunning_amount,
"against_voucher": self.name,
"against_voucher_type": "Dunning",
"cost_center": inv.cost_center or default_cost_center,
"project": inv.project,
},
inv.party_account_currency,
item=inv,
)
)
gl_entries.append(
self.get_gl_dict(
{
"account": self.income_account,
"against": self.customer,
"credit": dunning_in_company_currency,
"cost_center": inv.cost_center or default_cost_center,
"credit_in_account_currency": self.dunning_amount,
"project": inv.project,
},
item=inv,
)
)
make_gl_entries(
gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False
)

View File

@@ -261,7 +261,7 @@ def validate_balance_type(account, adv_adj=False):
if balance_must_be:
balance = frappe.db.sql(
"""select sum(debit) - sum(credit)
from `tabGL Entry` where is_cancelled = 0 and account = %s""",
from `tabGL Entry` where account = %s""",
account,
)[0][0]

View File

@@ -5,7 +5,6 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.utils import flt, today
@@ -19,29 +18,21 @@ def get_loyalty_details(
if not expiry_date:
expiry_date = today()
LoyaltyPointEntry = frappe.qb.DocType("Loyalty Point Entry")
query = (
frappe.qb.from_(LoyaltyPointEntry)
.select(
Sum(LoyaltyPointEntry.loyalty_points).as_("loyalty_points"),
Sum(LoyaltyPointEntry.purchase_amount).as_("total_spent"),
)
.where(
(LoyaltyPointEntry.customer == customer)
& (LoyaltyPointEntry.loyalty_program == loyalty_program)
& (LoyaltyPointEntry.posting_date <= expiry_date)
)
.groupby(LoyaltyPointEntry.customer)
)
condition = ""
if company:
query = query.where(LoyaltyPointEntry.company == company)
condition = " and company=%s " % frappe.db.escape(company)
if not include_expired_entry:
query = query.where(LoyaltyPointEntry.expiry_date >= expiry_date)
condition += " and expiry_date>='%s' " % expiry_date
loyalty_point_details = query.run(as_dict=True)
loyalty_point_details = frappe.db.sql(
f"""select sum(loyalty_points) as loyalty_points,
sum(purchase_amount) as total_spent from `tabLoyalty Point Entry`
where customer=%s and loyalty_program=%s and posting_date <= %s
{condition}
group by customer""",
(customer, loyalty_program, expiry_date),
as_dict=1,
)
if loyalty_point_details:
return loyalty_point_details[0]

View File

@@ -60,6 +60,6 @@ def create_party_link(primary_role, primary_party, secondary_party):
party_link.secondary_role = "Customer" if primary_role == "Supplier" else "Supplier"
party_link.secondary_party = secondary_party
party_link.save()
party_link.save(ignore_permissions=True)
return party_link

View File

@@ -330,7 +330,7 @@ frappe.ui.form.on('Payment Entry', {
payment_type: function(frm) {
set_default_party_type(frm);
if(frm.doc.payment_type == "Internal Transfer") {
$.each(["party", "party_type", "party_balance", "paid_from", "paid_to",
$.each(["party", "party_balance", "paid_from", "paid_to",
"references", "total_allocated_amount"], function(i, field) {
frm.set_value(field, null);
});

View File

@@ -10,19 +10,14 @@
"description",
"section_break_4",
"due_date",
"invoice_portion",
"mode_of_payment",
"column_break_5",
"due_date_based_on",
"credit_days",
"credit_months",
"invoice_portion",
"section_break_6",
"discount_date",
"discount",
"discount_type",
"discount_date",
"column_break_9",
"discount_validity_based_on",
"discount_validity",
"discount",
"section_break_9",
"payment_amount",
"outstanding",
@@ -160,50 +155,12 @@
"fieldtype": "Currency",
"label": "Payment Amount (Company Currency)",
"options": "Company:company:default_currency"
},
{
"fieldname": "due_date_based_on",
"fieldtype": "Select",
"label": "Due Date Based On",
"options": "\nDay(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
"read_only": 1
},
{
"depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
"fieldname": "credit_days",
"fieldtype": "Int",
"label": "Credit Days",
"non_negative": 1,
"read_only": 1
},
{
"depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
"fieldname": "credit_months",
"fieldtype": "Int",
"label": "Credit Months",
"non_negative": 1,
"read_only": 1
},
{
"depends_on": "discount",
"fieldname": "discount_validity_based_on",
"fieldtype": "Select",
"label": "Discount Validity Based On",
"options": "\nDay(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
"read_only": 1
},
{
"depends_on": "discount_validity_based_on",
"fieldname": "discount_validity",
"fieldtype": "Int",
"label": "Discount Validity",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-07-31 08:38:25.820701",
"modified": "2022-09-16 13:57:06.382859",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Schedule",
@@ -214,4 +171,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -161,4 +161,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@@ -11,9 +11,9 @@ frappe.ui.form.on("Period Closing Voucher", {
return {
filters: [
["Account", "company", "=", frm.doc.company],
["Account", "is_group", "=", 0],
["Account", "is_group", "=", "0"],
["Account", "freeze_account", "=", "No"],
["Account", "root_type", "in", ["Liability", "Equity"]],
["Account", "root_type", "in", "Liability, Equity"],
],
};
});

View File

@@ -32,13 +32,8 @@ class PeriodClosingVoucher(AccountsController):
def on_cancel(self):
self.validate_future_closing_vouchers()
self.ignore_linked_doctypes = (
"GL Entry",
"Stock Ledger Entry",
"Payment Ledger Entry",
"Account Closing Balance",
)
self.db_set("gle_processing_status", "In Progress")
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
gle_count = frappe.db.count(
"GL Entry",
{"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0},
@@ -206,9 +201,6 @@ class PeriodClosingVoucher(AccountsController):
return gl_entry
def get_gle_for_closing_account(self, acc):
debit = abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0
credit = abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0
gl_entry = self.get_gl_dict(
{
"company": self.company,
@@ -217,10 +209,16 @@ class PeriodClosingVoucher(AccountsController):
"cost_center": acc.cost_center,
"finance_book": acc.finance_book,
"account_currency": acc.account_currency,
"debit_in_account_currency": debit,
"debit": debit,
"credit_in_account_currency": credit,
"credit": credit,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) > 0
else 0,
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) < 0
else 0,
"credit": abs(flt(acc.bal_in_company_currency))
if flt(acc.bal_in_company_currency) < 0
else 0,
"is_period_closing_voucher_entry": 1,
},
item=acc,

View File

@@ -560,13 +560,7 @@ class POSInvoice(SalesInvoice):
"Account", self.debit_to, "account_currency", cache=True
)
if not self.due_date and self.customer:
self.due_date = get_due_date(
self.posting_date,
"Customer",
self.customer,
self.company,
template_name=self.payment_terms_template,
)
self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company)
super(SalesInvoice, self).set_missing_values(for_validate)

View File

@@ -449,7 +449,7 @@ def send_auto_email():
selected = frappe.get_list(
"Process Statement Of Accounts",
filters={"enable_auto_email": 1},
or_filters={"to_date": today(), "posting_date": today()},
or_filters={"to_date": format_date(today()), "posting_date": format_date(today())},
)
for entry in selected:
send_emails(entry.name, from_scheduler=True)

View File

@@ -29,10 +29,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
this.frm.set_query("expense_account", "items", function () {
return {
query: "erpnext.controllers.queries.get_expense_account",
filters: {
company: doc.company,
disabled: 0,
},
filters: { company: doc.company },
};
});
}

View File

@@ -173,12 +173,7 @@ class PurchaseInvoice(BuyingController):
)
if not self.due_date:
self.due_date = get_due_date(
self.posting_date,
"Supplier",
self.supplier,
self.company,
self.bill_date,
template_name=self.payment_terms_template,
self.posting_date, "Supplier", self.supplier, self.company, self.bill_date
)
tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
@@ -786,7 +781,7 @@ class PurchaseInvoice(BuyingController):
self.get_provisional_accounts()
for item in self.get("items"):
if flt(item.base_net_amount) or (self.get("update_stock") and item.valuation_rate):
if flt(item.base_net_amount):
account_currency = get_account_currency(item.expense_account)
if item.item_code:
frappe.get_cached_value("Item", item.item_code, "asset_category")
@@ -1021,9 +1016,6 @@ class PurchaseInvoice(BuyingController):
def get_provisional_accounts(self):
self.provisional_accounts = frappe._dict()
linked_purchase_receipts = set([d.purchase_receipt for d in self.items if d.purchase_receipt])
if not linked_purchase_receipts:
return
pr_items = frappe.get_all(
"Purchase Receipt Item",
filters={"parent": ("in", linked_purchase_receipts)},
@@ -1132,30 +1124,6 @@ class PurchaseInvoice(BuyingController):
warehouse_debit_amount = stock_amount
elif self.is_return and self.update_stock and (self.is_internal_supplier or not self.return_against):
net_rate = item.base_net_amount
stock_amount = net_rate + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision):
cost_of_goods_sold_account = self.get_company_default("default_expense_account")
stock_adjustment_amt = stock_amount - warehouse_debit_amount
gl_entries.append(
self.get_gl_dict(
{
"account": cost_of_goods_sold_account,
"against": item.expense_account,
"debit": stock_adjustment_amt,
"debit_in_transaction_currency": stock_adjustment_amt / self.conversion_rate,
"remarks": self.get("remarks") or _("Stock Adjustment"),
"cost_center": item.cost_center,
"project": item.project or self.project,
},
account_currency,
item=item,
)
)
return warehouse_debit_amount
def make_tax_gl_entries(self, gl_entries):

View File

@@ -1816,16 +1816,19 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
self.assertAlmostEqual(rate, 500)
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
def test_payment_allocation_for_payment_terms(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
create_pr_against_po,
create_purchase_order,
)
from erpnext.selling.doctype.sales_order.test_sales_order import (
automatically_fetch_payment_terms,
)
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as make_pi_from_pr,
)
automatically_fetch_payment_terms()
frappe.db.set_value(
"Payment Terms Template",
"_Test Payment Term Template",
@@ -1851,6 +1854,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
pi = make_pi_from_pr(pr.name)
self.assertEqual(pi.payment_schedule[0].payment_amount, 1000)
automatically_fetch_payment_terms(enable=0)
frappe.db.set_value(
"Payment Terms Template",
"_Test Payment Term Template",

View File

@@ -151,7 +151,7 @@ def start_repost(account_repost_doc=str) -> None:
if doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
if not repost_doc.delete_cancelled_entries:
doc.docstatus = 2
doc.make_gl_entries_on_cancel(from_repost=True)
doc.make_gl_entries_on_cancel()
doc.docstatus = 1
if doc.doctype == "Sales Invoice":
@@ -163,7 +163,7 @@ def start_repost(account_repost_doc=str) -> None:
elif doc.doctype == "Purchase Receipt":
if not repost_doc.delete_cancelled_entries:
doc.docstatus = 2
doc.make_gl_entries_on_cancel(from_repost=True)
doc.make_gl_entries_on_cancel()
doc.docstatus = 1
doc.make_gl_entries(from_repost=True)

View File

@@ -498,13 +498,7 @@ class SalesInvoice(SellingController):
"Account", self.debit_to, "account_currency", cache=True
)
if not self.due_date and self.customer:
self.due_date = get_due_date(
self.posting_date,
"Customer",
self.customer,
self.company,
template_name=self.payment_terms_template,
)
self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company)
super().set_missing_values(for_validate)
@@ -1025,6 +1019,7 @@ class SalesInvoice(SellingController):
self.make_exchange_gain_loss_journal()
elif self.docstatus == 2:
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
if update_outstanding == "No":

View File

@@ -2828,60 +2828,6 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
self.assertEqual(sales_invoice.items[0].item_tax_rate, item_tax_map)
def test_item_tax_template_change_with_grand_total_discount(self):
"""
Test that when item tax template changes due to discount on Grand Total,
the tax calculations are consistent.
"""
item = create_item("Test Item With Multiple Tax Templates")
item.set("taxes", [])
item.append(
"taxes",
{
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
"minimum_net_rate": 0,
"maximum_net_rate": 500,
},
)
item.append(
"taxes",
{
"item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
"minimum_net_rate": 501,
"maximum_net_rate": 1000,
},
)
item.save()
si = create_sales_invoice(item=item.name, rate=700, do_not_save=True)
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Excise Duty - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Excise Duty",
"rate": 0,
},
)
si.insert()
self.assertEqual(si.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
si.apply_discount_on = "Grand Total"
si.discount_amount = 300
si.save()
# Verify template changed to 10%
self.assertEqual(si.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
self.assertEqual(si.taxes[0].tax_amount, 70) # 10% of 700
self.assertEqual(si.grand_total, 470) # 700 + 70 - 300
si.submit()
@change_settings("Selling Settings", {"enable_discount_accounting": 1})
def test_sales_invoice_with_discount_accounting_enabled(self):
discount_account = create_account(
@@ -3341,7 +3287,6 @@ class TestSalesInvoice(FrappeTestCase):
si.posting_date = getdate()
si.submit()
@change_settings("Accounts Settings", {"over_billing_allowance": 0})
def test_over_billing_case_against_delivery_note(self):
"""
Test a case where duplicating the item with qty = 1 in the invoice
@@ -3349,23 +3294,24 @@ class TestSalesInvoice(FrappeTestCase):
"""
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", 0)
dn = create_delivery_note()
dn.submit()
si = make_sales_invoice(dn.name)
# make a copy of first item and add it to invoice
item_copy = frappe.copy_doc(si.items[0])
si.save()
si.items = [] # Clear existing items
si.append("items", item_copy)
si.save()
si.append("items", item_copy)
with self.assertRaises(frappe.ValidationError) as err:
si.save()
si.submit()
self.assertTrue("cannot overbill" in str(err.exception).lower())
dn.cancel()
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", over_billing_allowance)
@change_settings(
"Accounts Settings",
@@ -3943,60 +3889,6 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(invoice.outstanding_amount, 0)
def test_system_generated_exchange_gain_or_loss_je_after_repost(self):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.repost_accounting_ledger.test_repost_accounting_ledger import (
update_repost_settings,
)
update_repost_settings()
si = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=80,
)
pe = get_payment_entry("Sales Invoice", si.name)
pe.reference_no = "10"
pe.reference_date = nowdate()
pe.paid_from_account_currency = si.currency
pe.paid_to_account_currency = "INR"
pe.source_exchange_rate = 85
pe.target_exchange_rate = 1
pe.paid_amount = si.outstanding_amount
pe.received_amount = 8500
pe.insert()
pe.submit()
ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = si.company
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
ral.save()
ral.submit()
je = frappe.qb.DocType("Journal Entry")
jea = frappe.qb.DocType("Journal Entry Account")
q = (
(
frappe.qb.from_(je)
.join(jea)
.on(je.name == jea.parent)
.select(je.docstatus)
.where(
(je.voucher_type == "Exchange Gain Or Loss")
& (jea.reference_name == si.name)
& (jea.reference_type == "Sales Invoice")
& (je.is_system_generated == 1)
)
)
.limit(1)
.run()
)
self.assertEqual(q[0][0], 1)
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql(

View File

@@ -80,7 +80,7 @@
"collapsible": 0,
"columns": 0,
"fieldname": "rate",
"fieldtype": "Currency",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@@ -102,7 +102,7 @@
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@@ -199,7 +199,7 @@
"collapsible": 0,
"columns": 0,
"fieldname": "amount",
"fieldtype": "Currency",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@@ -221,7 +221,7 @@
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@@ -324,7 +324,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2025-12-10 08:06:40.611761",
"modified": "2018-01-10 18:32:36.201124",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Share Balance",
@@ -339,4 +339,4 @@
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}
}

View File

@@ -75,7 +75,7 @@
},
{
"default": "0",
"description": "Only payment entries with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
"description": "Even invoices with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
"fieldname": "consider_party_ledger_amount",
"fieldtype": "Check",
"label": "Consider Entire Party Ledger Amount",
@@ -102,11 +102,10 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-07-30 07:13:51.785735",
"modified": "2021-07-27 21:47:34.396071",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Category",
"naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
@@ -149,4 +148,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@@ -622,7 +622,6 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
conditions.append(ple.party.isin(parties))
conditions.append(ple.voucher_no == ple.against_voucher_no)
conditions.append(ple.company == inv.company)
conditions.append(ple.posting_date[tax_details.from_date : tax_details.to_date])
advance_amt = (
qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run()[0][0] or 0.0

View File

@@ -243,18 +243,17 @@ class TestTaxWithholdingCategory(FrappeTestCase):
frappe.db.set_value(
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
)
fiscal_year = get_fiscal_year(today(), company="_Test Company")
vouchers = []
# create advance payment
pe1 = create_payment_entry(
pe = create_payment_entry(
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=20000
)
pe1.paid_from = "Debtors - _TC"
pe1.paid_to = "Cash - _TC"
pe1.submit()
vouchers.append(pe1)
pe.paid_from = "Debtors - _TC"
pe.paid_to = "Cash - _TC"
pe.submit()
vouchers.append(pe)
# create invoice
si1 = create_sales_invoice(customer="Test TCS Customer", rate=5000)
@@ -276,17 +275,6 @@ class TestTaxWithholdingCategory(FrappeTestCase):
# make another invoice
# sum of unallocated amount from payment entry and this sales invoice will breach cumulative threashold
# TDS should be calculated
# this payment should not be considered for TCS calculation as it is outside of fiscal year
pe2 = create_payment_entry(
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=10000
)
pe2.paid_from = "Debtors - _TC"
pe2.paid_to = "Cash - _TC"
pe2.posting_date = add_days(fiscal_year[1], -10)
pe2.submit()
vouchers.append(pe2)
si2 = create_sales_invoice(customer="Test TCS Customer", rate=15000)
si2.submit()
vouchers.append(si2)

View File

@@ -179,15 +179,6 @@ def process_gl_map(gl_map, merge_entries=True, precision=None):
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
round_off_account, default_currency = frappe.get_cached_value(
"Company", gl_map[0].company, ["round_off_account", "default_currency"]
)
if not precision:
precision = get_field_precision(
frappe.get_meta("GL Entry").get_field("debit"),
currency=default_currency,
)
new_gl_map = []
for d in gl_map:
cost_center = d.get("cost_center")
@@ -201,11 +192,6 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
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)
continue
for sub_cost_center, percentage in cost_center_allocation:
gle = copy.deepcopy(d)
gle.cost_center = sub_cost_center

View File

@@ -552,13 +552,12 @@ def validate_party_accounts(doc):
@frappe.whitelist()
def get_due_date(posting_date, party_type, party, company=None, bill_date=None, template_name=None):
def get_due_date(posting_date, party_type, party, company=None, bill_date=None):
"""Get due date from `Payment Terms Template`"""
due_date = None
if (bill_date or posting_date) and party:
due_date = bill_date or posting_date
if not template_name:
template_name = get_payment_terms_template(party, party_type, company)
template_name = get_payment_terms_template(party, party_type, company)
if template_name:
due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d")

View File

@@ -6,7 +6,6 @@ from collections import OrderedDict
import frappe
from frappe import _, qb, query_builder, scrub
from frappe.database.schema import get_definition
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Date, Substring, Sum
from frappe.utils import cint, cstr, flt, getdate, nowdate
@@ -15,10 +14,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
get_dimension_with_children,
)
from erpnext.accounts.utils import (
build_qb_match_conditions,
get_currency_precision,
)
from erpnext.accounts.utils import get_currency_precision
# This report gives a summary of all Outstanding Invoices considering the following
@@ -95,6 +91,9 @@ class ReceivablePayableReport:
def get_data(self):
self.get_sales_invoices_or_customers_based_on_sales_person()
# Build delivery note map against all sales invoices
self.build_delivery_note_map()
# Get invoice details like bill_no, due_date etc for all invoices
self.get_invoice_details()
@@ -115,16 +114,11 @@ class ReceivablePayableReport:
self.fetch_ple_in_buffered_cursor()
elif self.ple_fetch_method == "UnBuffered Cursor":
self.fetch_ple_in_unbuffered_cursor()
elif self.ple_fetch_method == "Raw SQL":
self.fetch_ple_in_sql_procedures()
# Build delivery note map against all sales invoices
self.build_delivery_note_map()
self.build_data()
def fetch_ple_in_buffered_cursor(self):
self.ple_entries = self.ple_query.run(as_dict=True)
self.ple_entries = frappe.db.sql(self.ple_query.get_sql(), as_dict=True)
for ple in self.ple_entries:
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
@@ -138,7 +132,7 @@ class ReceivablePayableReport:
def fetch_ple_in_unbuffered_cursor(self):
self.ple_entries = []
with frappe.db.unbuffered_cursor():
for ple in self.ple_query.run(as_dict=True, as_iterator=True):
for ple in frappe.db.sql(self.ple_query.get_sql(), as_dict=True, as_iterator=True):
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
self.ple_entries.append(ple)
@@ -303,79 +297,6 @@ class ReceivablePayableReport:
row.paid -= amount
row.paid_in_account_currency -= amount_in_account_currency
def fetch_ple_in_sql_procedures(self):
self.proc = InitSQLProceduresForAR()
build_balance = f"""
begin not atomic
declare done boolean default false;
declare rec1 row type of `{self.proc._row_def_table_name}`;
declare ple cursor for {self.ple_query.get_sql()};
declare continue handler for not found set done = true;
open ple;
fetch ple into rec1;
while not done do
call {self.proc.init_procedure_name}(rec1);
fetch ple into rec1;
end while;
close ple;
set done = false;
open ple;
fetch ple into rec1;
while not done do
call {self.proc.allocate_procedure_name}(rec1);
fetch ple into rec1;
end while;
close ple;
end;
"""
frappe.db.sql(build_balance)
balances = frappe.db.sql(
f"""select
name,
voucher_type,
voucher_no,
party,
party_account `account`,
posting_date,
account_currency,
cost_center,
sum(invoiced) `invoiced`,
sum(paid) `paid`,
sum(credit_note) `credit_note`,
sum(invoiced) - sum(paid) - sum(credit_note) `outstanding`,
sum(invoiced_in_account_currency) `invoiced_in_account_currency`,
sum(paid_in_account_currency) `paid_in_account_currency`,
sum(credit_note_in_account_currency) `credit_note_in_account_currency`,
sum(invoiced_in_account_currency) - sum(paid_in_account_currency) - sum(credit_note_in_account_currency) `outstanding_in_account_currency`
from `{self.proc._voucher_balance_name}` group by name order by posting_date;""",
as_dict=True,
)
for x in balances:
if self.filters.get("ignore_accounts"):
key = (x.voucher_type, x.voucher_no, x.party)
else:
key = (x.account, x.voucher_type, x.voucher_no, x.party)
_d = self.build_voucher_dict(x)
for field in [
"invoiced",
"paid",
"credit_note",
"outstanding",
"invoiced_in_account_currency",
"paid_in_account_currency",
"credit_note_in_account_currency",
"outstanding_in_account_currency",
"cost_center",
]:
_d[field] = x.get(field)
self.voucher_balance[key] = _d
def update_sub_total_row(self, row, party):
total_row = self.total_row_map.get(party)
@@ -916,9 +837,6 @@ class ReceivablePayableReport:
else:
query = query.select(ple.remarks)
if match_conditions := build_qb_match_conditions("Payment Ledger Entry"):
query = query.where(Criterion.all(match_conditions))
if self.filters.get("group_by_party"):
query = query.orderby(self.ple.party, self.ple.posting_date)
else:
@@ -1311,134 +1229,3 @@ def get_customer_group_with_children(customer_groups):
frappe.throw(_("Customer Group: {0} does not exist").format(d))
return list(set(all_customer_groups))
class InitSQLProceduresForAR:
"""
Initialize SQL Procedures, Functions and Temporary tables to build Receivable / Payable report
"""
_varchar_type = get_definition("Data")
_currency_type = get_definition("Currency")
# Temporary Tables
_voucher_balance_name = "_ar_voucher_balance"
_voucher_balance_definition = f"""
create temporary table `{_voucher_balance_name}`(
name {_varchar_type},
voucher_type {_varchar_type},
voucher_no {_varchar_type},
party {_varchar_type},
party_account {_varchar_type},
posting_date date,
account_currency {_varchar_type},
cost_center {_varchar_type},
invoiced {_currency_type},
paid {_currency_type},
credit_note {_currency_type},
invoiced_in_account_currency {_currency_type},
paid_in_account_currency {_currency_type},
credit_note_in_account_currency {_currency_type}) engine=memory;
"""
_row_def_table_name = "_ar_ple_row"
_row_def_table_definition = f"""
create temporary table `{_row_def_table_name}`(
name {_varchar_type},
account {_varchar_type},
voucher_type {_varchar_type},
voucher_no {_varchar_type},
against_voucher_type {_varchar_type},
against_voucher_no {_varchar_type},
party_type {_varchar_type},
cost_center {_varchar_type},
party {_varchar_type},
posting_date date,
due_date date,
account_currency {_varchar_type},
amount {_currency_type},
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 = `{genkey_function_name}`(ple, false))
then
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;
"""
allocate_procedure_name = "ar_allocate_to_tmp_table"
allocate_procedure_sql = f"""
create procedure ar_allocate_to_tmp_table(in ple row type of `{_row_def_table_name}`)
begin
declare invoiced {_currency_type} default 0;
declare invoiced_in_account_currency {_currency_type} default 0;
declare paid {_currency_type} default 0;
declare paid_in_account_currency {_currency_type} default 0;
declare credit_note {_currency_type} default 0;
declare credit_note_in_account_currency {_currency_type} default 0;
if ple.amount > 0 then
if (ple.voucher_type in ("Journal Entry", "Payment Entry") and (ple.voucher_no != ple.against_voucher_no)) then
set paid = -1 * ple.amount;
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
else
set invoiced = ple.amount;
set invoiced_in_account_currency = ple.amount_in_account_currency;
end if;
else
if ple.voucher_type in ("Sales Invoice", "Purchase Invoice") then
if (ple.voucher_no = ple.against_voucher_no) then
set paid = -1 * ple.amount;
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
else
set credit_note = -1 * ple.amount;
set credit_note_in_account_currency = -1 * ple.amount_in_account_currency;
end if;
else
set paid = -1 * ple.amount;
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
end if;
end if;
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)
if self.allocate_procedure_name not in existing_procedures:
frappe.db.sql(self.allocate_procedure_sql)
frappe.db.sql(f"drop table if exists `{self._voucher_balance_name}`")
frappe.db.sql(self._voucher_balance_definition)
frappe.db.sql(f"drop table if exists `{self._row_def_table_name}`")
frappe.db.sql(self._row_def_table_definition)

View File

@@ -197,11 +197,6 @@ frappe.query_reports["General Ledger"] = {
label: __("Show Net Values in Party Account"),
fieldtype: "Check",
},
{
fieldname: "show_amount_in_company_currency",
label: __("Show Credit / Debit in Company Currency"),
fieldtype: "Check",
},
{
fieldname: "show_remarks",
label: __("Show Remarks"),

View File

@@ -205,7 +205,7 @@ def get_gl_entries(filters, accounting_dimensions):
)
if filters.get("presentation_currency"):
return convert_to_presentation_currency(gl_entries, currency_map, filters)
return convert_to_presentation_currency(gl_entries, currency_map)
else:
return gl_entries
@@ -538,20 +538,17 @@ def get_account_type_map(company):
def get_result_as_list(data, filters):
balance = 0
balance, _balance_in_account_currency = 0, 0
for d in data:
if not d.get("posting_date"):
balance = 0
balance, _balance_in_account_currency = 0, 0
balance = get_balance(d, balance, "debit", "credit")
d["balance"] = balance
d["account_currency"] = filters.account_currency
d["presentation_currency"] = filters.presentation_currency
return data
@@ -577,21 +574,11 @@ def get_columns(filters):
if filters.get("presentation_currency"):
currency = filters["presentation_currency"]
else:
company = filters.get("company") or get_default_company()
filters["presentation_currency"] = currency = get_company_currency(company)
company_currency = get_company_currency(filters.get("company") or get_default_company())
if (
filters.get("show_amount_in_company_currency")
and filters["presentation_currency"] != company_currency
):
frappe.throw(
_("Presentation Currency cannot be {0} , When {1} is enabled.").format(
frappe.bold(filters["presentation_currency"]),
frappe.bold("Show Credit / Debit in Company Currency"),
)
)
if filters.get("company"):
currency = get_company_currency(filters["company"])
else:
company = get_default_company()
currency = get_company_currency(company)
columns = [
{
@@ -612,22 +599,19 @@ def get_columns(filters):
{
"label": _("Debit ({0})").format(currency),
"fieldname": "debit",
"fieldtype": "Currency",
"options": "presentation_currency",
"fieldtype": "Float",
"width": 130,
},
{
"label": _("Credit ({0})").format(currency),
"fieldname": "credit",
"fieldtype": "Currency",
"options": "presentation_currency",
"fieldtype": "Float",
"width": 130,
},
{
"label": _("Balance ({0})").format(currency),
"fieldname": "balance",
"fieldtype": "Currency",
"options": "presentation_currency",
"fieldtype": "Float",
"width": 130,
},
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 120},

View File

@@ -46,7 +46,6 @@ class PaymentLedger:
against_voucher_no=ple.against_voucher_no,
amount=ple.amount,
currency=ple.account_currency,
company=ple.company,
)
if self.filters.include_account_currency:
@@ -78,7 +77,6 @@ class PaymentLedger:
against_voucher_no="Outstanding:",
amount=total,
currency=voucher_data[0].currency,
company=voucher_data[0].company,
)
if self.filters.include_account_currency:
@@ -87,12 +85,7 @@ class PaymentLedger:
voucher_data.append(entry)
# empty row
voucher_data.append(
frappe._dict(
currency=voucher_data[0].currency,
company=voucher_data[0].company,
)
)
voucher_data.append(frappe._dict())
self.data.extend(voucher_data)
def build_conditions(self):

View File

@@ -45,7 +45,6 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
gle_map = get_gle_map(tds_docs)
out = []
entries = {}
for name, details in gle_map.items():
for entry in details:
tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0
@@ -120,14 +119,9 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
"supplier_invoice_date": bill_date,
}
)
out.append(row)
key = entry.voucher_no
if key in entries:
entries[key]["tax_amount"] += tax_amount
else:
entries[key] = row
out = list(entries.values())
out.sort(key=lambda x: (x["section_code"], x["transaction_date"]))
out.sort(key=lambda x: x["section_code"])
return out

View File

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

View File

@@ -79,14 +79,8 @@ frappe.require("assets/erpnext/js/financial_statements.js", function () {
options: erpnext.get_presentation_currency_list(),
},
{
fieldname: "with_period_closing_entry_for_opening",
label: __("With Period Closing Entry For Opening Balances"),
fieldtype: "Check",
default: 1,
},
{
fieldname: "with_period_closing_entry_for_current_period",
label: __("Period Closing Entry For Current Period"),
fieldname: "with_period_closing_entry",
label: __("Period Closing Entry"),
fieldtype: "Check",
default: 1,
},

View File

@@ -120,7 +120,7 @@ def get_data(filters):
max_rgt,
filters,
gl_entries_by_account,
ignore_closing_entries=not flt(filters.with_period_closing_entry_for_current_period),
ignore_closing_entries=not flt(filters.with_period_closing_entry),
ignore_opening_entries=True,
)
@@ -274,7 +274,7 @@ def get_opening_balance(
):
opening_balance = opening_balance.where(closing_balance.posting_date >= filters.year_start_date)
if not flt(filters.with_period_closing_entry_for_opening):
if not flt(filters.with_period_closing_entry):
if doctype == "Account Closing Balance":
opening_balance = opening_balance.where(closing_balance.is_period_closing_voucher_entry == 0)
else:

View File

@@ -4,25 +4,15 @@
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"mandatory": 1,
"options": "Company",
"wildcard_filter": 0
}
],
"idx": 0,
"is_standard": "Yes",
"modified": "2025-08-28 19:06:54.273322",
"modified": "2019-01-17 17:20:42.374958",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Trial Balance (Simple)",
"owner": "Administrator",
"prepared_report": 0,
"query": "select fiscal_year as \"Fiscal Year:Data:80\",\n\tcompany as \"Company:Data:220\",\n\tposting_date as \"Posting Date:Date:100\",\n\taccount as \"Account:Data:380\",\n\tsum(debit) as \"Debit:Currency:140\",\n\tsum(credit) as \"Credit:Currency:140\",\n\tfinance_book as \"Finance Book:Link/Finance Book:140\"\nfrom `tabGL Entry`\nwhere is_cancelled = 0 and company = %(company)s\ngroup by fiscal_year, company, posting_date, account\norder by fiscal_year, company, posting_date, account",
"query": "select fiscal_year as \"Fiscal Year:Data:80\",\n\tcompany as \"Company:Data:220\",\n\tposting_date as \"Posting Date:Date:100\",\n\taccount as \"Account:Data:380\",\n\tsum(debit) as \"Debit:Currency:140\",\n\tsum(credit) as \"Credit:Currency:140\",\n\tfinance_book as \"Finance Book:Link/Finance Book:140\"\nfrom `tabGL Entry`\ngroup by fiscal_year, company, posting_date, account\norder by fiscal_year, company, posting_date, account",
"ref_doctype": "GL Entry",
"report_name": "Trial Balance (Simple)",
"report_type": "Query Report",

View File

@@ -86,7 +86,7 @@ def get_rate_as_at(date, from_currency, to_currency):
return rate
def convert_to_presentation_currency(gl_entries, currency_info, filters=None):
def convert_to_presentation_currency(gl_entries, currency_info):
"""
Take a list of GL Entries and change the 'debit' and 'credit' values to currencies
in `currency_info`.
@@ -99,13 +99,6 @@ def convert_to_presentation_currency(gl_entries, currency_info, filters=None):
company_currency = currency_info["company_currency"]
account_currencies = list(set(entry["account_currency"] for entry in gl_entries))
exchange_gain_or_loss = False
if filters and isinstance(filters.get("account"), list):
account_filter = filters.get("account")
gain_loss_account = frappe.db.get_value("Company", filters.company, "exchange_gain_loss_account")
exchange_gain_or_loss = len(account_filter) == 1 and account_filter[0] == gain_loss_account
for entry in gl_entries:
debit = flt(entry["debit"])
@@ -114,11 +107,7 @@ def convert_to_presentation_currency(gl_entries, currency_info, filters=None):
credit_in_account_currency = flt(entry["credit_in_account_currency"])
account_currency = entry["account_currency"]
if (
len(account_currencies) == 1
and account_currency == presentation_currency
and not exchange_gain_or_loss
) and not (filters and filters.get("show_amount_in_company_currency")):
if len(account_currencies) == 1 and account_currency == presentation_currency:
entry["debit"] = debit_in_account_currency
entry["credit"] = credit_in_account_currency
else:

View File

@@ -2,14 +2,12 @@
# License: GNU General Public License v3. See license.txt
from collections import defaultdict
from json import loads
from typing import TYPE_CHECKING, Optional
import frappe
import frappe.defaults
from frappe import _, qb, throw
from frappe.desk.reportview import build_match_conditions
from frappe.model.meta import get_field_precision
from frappe.query_builder import AliasedQuery, Criterion, Table
from frappe.query_builder.functions import Round, Sum
@@ -28,7 +26,6 @@ from frappe.utils import (
nowdate,
)
from pypika import Order
from pypika.functions import Coalesce
from pypika.terms import ExistsCriterion
import erpnext
@@ -2194,44 +2191,3 @@ def run_ledger_health_checks():
doc.general_and_payment_ledger_mismatch = True
doc.checked_on = run_date
doc.save()
def get_link_fields_grouped_by_option(doctype):
meta = frappe.get_meta(doctype)
link_fields_map = defaultdict(list)
for df in meta.fields:
if df.fieldtype == "Link" and df.options and not df.ignore_user_permissions:
link_fields_map[df.options].append(df.fieldname)
return link_fields_map
def build_qb_match_conditions(doctype, user=None) -> list:
match_filters = build_match_conditions(doctype, user, False)
link_fields_map = get_link_fields_grouped_by_option(doctype)
criterion = []
apply_strict_user_permissions = frappe.get_system_settings("apply_strict_user_permissions")
if match_filters:
_dt = qb.DocType(doctype)
for filter in match_filters:
for link_option, allowed_values in filter.items():
fieldnames = link_fields_map.get(link_option, [])
cond = None
if link_option == doctype:
cond = _dt["name"].isin(allowed_values)
for fieldname in fieldnames:
field = _dt[fieldname]
cond = field.isin(allowed_values)
if not apply_strict_user_permissions:
cond = (Coalesce(field, "") == "") | cond
if cond:
criterion.append(cond)
return criterion

View File

@@ -152,7 +152,6 @@
{
"allow_on_submit": 1,
"fetch_from": "item_code.image",
"fetch_if_empty": 1,
"fieldname": "image",
"fieldtype": "Attach Image",
"hidden": 1,
@@ -583,7 +582,7 @@
"link_fieldname": "target_asset"
}
],
"modified": "2025-05-20 00:44:06.229177",
"modified": "2024-01-15 17:35:49.226603",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@@ -313,7 +313,6 @@ class Asset(AccountsController):
"asset_name": self.asset_name,
"target_location": self.location,
"to_employee": self.custodian,
"company": self.company,
}
]
asset_movement = frappe.get_doc(
@@ -1532,7 +1531,11 @@ def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx, number_of_
# if the Depreciation Schedule is being prepared for the first time
else:
if row.daily_prorata_based:
amount = flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
amount = (
flt(asset.gross_purchase_amount)
- flt(asset.opening_accumulated_depreciation)
- flt(row.expected_value_after_useful_life)
)
total_days = (
date_diff(
get_last_day(
@@ -1544,11 +1547,7 @@ def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx, number_of_
),
add_days(
get_last_day(
add_months(
row.depreciation_start_date,
(row.frequency_of_depreciation * (asset.number_of_depreciations_booked + 1))
* -1,
),
add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)
),
1,
),
@@ -1571,9 +1570,11 @@ def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx, number_of_
return daily_depr_amount * (date_diff(to_date, from_date) + 1)
else:
return (flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)) / flt(
row.total_number_of_depreciations
)
return (
flt(asset.gross_purchase_amount)
- flt(asset.opening_accumulated_depreciation)
- flt(row.expected_value_after_useful_life)
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
def get_shift_depr_amount(asset, row, schedule_idx):

View File

@@ -660,7 +660,7 @@ class TestDepreciationMethods(AssetSetup):
available_for_use_date="2030-06-06",
is_existing_asset=1,
number_of_depreciations_booked=2,
opening_accumulated_depreciation=47178.08,
opening_accumulated_depreciation=47095.89,
expected_value_after_useful_life=10000,
depreciation_start_date="2032-12-31",
total_number_of_depreciations=3,
@@ -668,7 +668,7 @@ class TestDepreciationMethods(AssetSetup):
)
self.assertEqual(asset.status, "Draft")
expected_schedules = [["2032-12-31", 30000.0, 77178.08], ["2033-06-06", 12821.92, 90000.0]]
expected_schedules = [["2032-12-31", 42904.11, 90000.0]]
schedules = [
[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
for d in asset.get("schedules")

View File

@@ -380,7 +380,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
args: {
item_code: item.item_code,
warehouse: cstr(item.warehouse),
qty: -1 * flt(item.stock_qty),
qty: flt(item.stock_qty),
serial_no: item.serial_no,
posting_date: me.frm.doc.posting_date,
posting_time: me.frm.doc.posting_time,

View File

@@ -76,7 +76,6 @@
"fieldname": "completion_date",
"fieldtype": "Datetime",
"label": "Completion Date",
"mandatory_depends_on": "eval:doc.repair_status==\"Completed\"",
"no_copy": 1
},
{
@@ -265,7 +264,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-07-29 15:14:34.044564",
"modified": "2022-08-16 15:55:25.023471",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair",
@@ -303,11 +302,10 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "asset_name",
"track_changes": 1,
"track_seen": 1
}
}

View File

@@ -3,7 +3,7 @@
import frappe
from frappe import _
from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_diff_in_hours
from frappe.utils import add_months, cint, flt, getdate, time_diff_in_hours
import erpnext
from erpnext.accounts.general_ledger import make_gl_entries
@@ -20,20 +20,6 @@ class AssetRepair(AccountsController):
self.set_stock_items_cost()
self.calculate_total_repair_cost()
def validate_asset(self):
if self.asset_doc.status in ("Sold", "Fully Depreciated", "Scrapped"):
frappe.throw(
_("Asset {0} is in {1} status and cannot be repaired.").format(
get_link_to_form("Asset", self.asset), self.asset_doc.status
)
)
def validate_dates(self):
if self.completion_date and (getdate(self.failure_date) > getdate(self.completion_date)):
frappe.throw(
_("Completion Date can not be before Failure Date. Please adjust the dates accordingly.")
)
def update_status(self):
if self.repair_status == "Pending":
frappe.db.set_value("Asset", self.asset, "status", "Out of Order")
@@ -209,7 +195,7 @@ class AssetRepair(AccountsController):
"voucher_type": self.doctype,
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": self.completion_date,
"posting_date": getdate(),
"against_voucher_type": "Purchase Invoice",
"against_voucher": self.purchase_invoice,
"company": self.company,
@@ -228,7 +214,7 @@ class AssetRepair(AccountsController):
"voucher_type": self.doctype,
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": self.completion_date,
"posting_date": getdate(),
"company": self.company,
},
item=self,
@@ -262,7 +248,7 @@ class AssetRepair(AccountsController):
"voucher_type": self.doctype,
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": self.completion_date,
"posting_date": getdate(),
"company": self.company,
},
item=self,
@@ -279,7 +265,7 @@ class AssetRepair(AccountsController):
"voucher_type": self.doctype,
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": self.completion_date,
"posting_date": getdate(),
"against_voucher_type": "Stock Entry",
"against_voucher": self.stock_entry,
"company": self.company,

View File

@@ -4,7 +4,7 @@
import unittest
import frappe
from frappe.utils import add_days, flt, nowdate
from frappe.utils import flt, nowdate
from erpnext.assets.doctype.asset.asset import (
get_asset_account,
@@ -288,7 +288,6 @@ def create_asset_repair(**args):
if args.submit:
asset_repair.repair_status = "Completed"
asset_repair.completion_date = add_days(args.failure_date, 1)
asset_repair.cost_center = frappe.db.get_value("Company", asset.company, "cost_center")
if args.stock_consumption:

View File

@@ -456,8 +456,9 @@ class PurchaseOrder(BuyingController):
if not self.is_against_so():
return
for item in removed_items:
prev_ordered_qty = flt(
prev_ordered_qty = (
frappe.get_cached_value("Sales Order Item", item.get("sales_order_item"), "ordered_qty")
or 0.0
)
frappe.db.set_value(

View File

@@ -526,8 +526,12 @@ class TestPurchaseOrder(FrappeTestCase):
self.assertRaises(frappe.ValidationError, pr.submit)
self.assertRaises(frappe.ValidationError, pi.submit)
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
def test_make_purchase_invoice_with_terms(self):
from erpnext.selling.doctype.sales_order.test_sales_order import (
automatically_fetch_payment_terms,
)
automatically_fetch_payment_terms()
po = create_purchase_order(do_not_save=True)
self.assertRaises(frappe.ValidationError, make_pi_from_po, po.name)
@@ -551,6 +555,7 @@ class TestPurchaseOrder(FrappeTestCase):
self.assertEqual(getdate(pi.payment_schedule[0].due_date), getdate(po.transaction_date))
self.assertEqual(pi.payment_schedule[1].payment_amount, 2500.0)
self.assertEqual(getdate(pi.payment_schedule[1].due_date), add_days(getdate(po.transaction_date), 30))
automatically_fetch_payment_terms(enable=0)
def test_warehouse_company_validation(self):
from erpnext.stock.utils import InvalidWarehouseCompany
@@ -698,7 +703,6 @@ class TestPurchaseOrder(FrappeTestCase):
)
self.assertEqual(due_date, "2023-03-31")
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 0})
def test_terms_are_not_copied_if_automatically_fetch_payment_terms_is_unchecked(self):
po = create_purchase_order(do_not_save=1)
po.payment_terms_template = "_Test Payment Term Template"
@@ -830,16 +834,18 @@ class TestPurchaseOrder(FrappeTestCase):
bo.load_from_db()
self.assertEqual(bo.items[0].ordered_qty, 5)
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
def test_payment_terms_are_fetched_when_creating_purchase_invoice(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
create_payment_terms_template,
)
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.selling.doctype.sales_order.test_sales_order import (
automatically_fetch_payment_terms,
compare_payment_schedules,
)
automatically_fetch_payment_terms()
po = create_purchase_order(qty=10, rate=100, do_not_save=1)
create_payment_terms_template()
po.payment_terms_template = "Test Receivable Template"
@@ -853,6 +859,8 @@ class TestPurchaseOrder(FrappeTestCase):
# self.assertEqual(po.payment_terms_template, pi.payment_terms_template)
compare_payment_schedules(self, po, pi)
automatically_fetch_payment_terms(enable=0)
def test_internal_transfer_flow(self):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
make_inter_company_purchase_invoice,

View File

@@ -9,7 +9,6 @@ from frappe import _
from frappe.core.doctype.communication.email import make
from frappe.desk.form.load import get_attachments
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import Order
from frappe.utils import get_url
from frappe.utils.print_format import download_pdf
from frappe.utils.user import get_user_fullname
@@ -503,32 +502,35 @@ def get_supplier_tag():
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filters):
rfq = frappe.qb.DocType("Request for Quotation")
rfq_supplier = frappe.qb.DocType("Request for Quotation Supplier")
query = (
frappe.qb.from_(rfq)
.from_(rfq_supplier)
.select(rfq.name)
.distinct()
.select(rfq.transaction_date, rfq.company)
.where(
(rfq.name == rfq_supplier.parent)
& (rfq_supplier.supplier == filters.get("supplier"))
& (rfq.docstatus == 1)
& (rfq.company == filters.get("company"))
)
.orderby(rfq.transaction_date, order=Order.asc)
.limit(page_len)
.offset(start)
)
conditions = ""
if txt:
query = query.where(rfq.name.like(f"%%{txt}%%"))
conditions += "and rfq.name like '%%" + txt + "%%' "
if filters.get("transaction_date"):
query = query.where(rfq.transaction_date == filters.get("transaction_date"))
conditions += "and rfq.transaction_date = '{}'".format(filters.get("transaction_date"))
rfq_data = query.run(as_dict=1)
rfq_data = frappe.db.sql(
f"""
select
distinct rfq.name, rfq.transaction_date,
rfq.company
from
`tabRequest for Quotation` rfq, `tabRequest for Quotation Supplier` rfq_supplier
where
rfq.name = rfq_supplier.parent
and rfq_supplier.supplier = %(supplier)s
and rfq.docstatus = 1
and rfq.company = %(company)s
{conditions}
order by rfq.transaction_date ASC
limit %(page_len)s offset %(start)s """,
{
"page_len": page_len,
"start": start,
"company": filters.get("company"),
"supplier": filters.get("supplier"),
},
as_dict=1,
)
return rfq_data

View File

@@ -42,11 +42,6 @@ frappe.ui.form.on("Supplier", {
},
};
});
frm.make_methods = {
"Bank Account": () => erpnext.utils.make_bank_account(frm.doc.doctype, frm.doc.name),
"Pricing Rule": () => erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name),
};
},
refresh: function (frm) {
@@ -89,9 +84,21 @@ frappe.ui.form.on("Supplier", {
__("View")
);
frm.add_custom_button(__("Bank Account"), () => frm.make_methods["Bank Account"](), __("Create"));
frm.add_custom_button(
__("Bank Account"),
function () {
erpnext.utils.make_bank_account(frm.doc.doctype, frm.doc.name);
},
__("Create")
);
frm.add_custom_button(__("Pricing Rule"), () => frm.make_methods["Pricing Rule"](), __("Create"));
frm.add_custom_button(
__("Pricing Rule"),
function () {
erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name);
},
__("Create")
);
frm.add_custom_button(
__("Get Supplier Group Details"),
@@ -101,10 +108,7 @@ frappe.ui.form.on("Supplier", {
__("Actions")
);
if (
cint(frappe.defaults.get_default("enable_common_party_accounting")) &&
frappe.model.can_create("Party Link")
) {
if (cint(frappe.defaults.get_default("enable_common_party_accounting"))) {
frm.add_custom_button(
__("Link with Customer"),
function () {

View File

@@ -20,9 +20,6 @@ def update_last_purchase_rate(doc, is_submit) -> None:
this_purchase_date = getdate(doc.get("posting_date") or doc.get("transaction_date"))
for d in doc.get("items"):
if d.get("is_free_item"):
continue
# get last purchase details
last_purchase_details = get_last_purchase_details(d.item_code, doc.name)

View File

@@ -213,11 +213,6 @@ class AccountsController(TransactionBase):
self.validate_date_with_fiscal_year()
self.validate_party_accounts()
if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
if self.is_return:
self.validate_qty()
else:
self.validate_deferred_start_and_end_date()
self.validate_inter_company_reference()
@@ -263,6 +258,11 @@ class AccountsController(TransactionBase):
self.set_advance_gain_or_loss()
if self.is_return:
self.validate_qty()
else:
self.validate_deferred_start_and_end_date()
self.validate_deferred_income_expense_account()
self.set_inter_company_account()
@@ -1742,50 +1742,69 @@ class AccountsController(TransactionBase):
def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on):
from erpnext.controllers.status_updater import get_allowance_for
ref_wise_billed_amount = self.get_reference_wise_billed_amt(ref_dt, item_ref_dn, based_on)
item_allowance = {}
global_qty_allowance, global_amount_allowance = None, None
if not ref_wise_billed_amount:
return
total_overbilled_amt = 0.0
overbilled_items = []
precision = self.precision(based_on, "items")
precision_allowance = 1 / (10**precision)
role_allowed_to_overbill = frappe.get_cached_value(
role_allowed_to_over_bill = frappe.get_cached_value(
"Accounts Settings", None, "role_allowed_to_over_bill"
)
is_overbilling_allowed = role_allowed_to_overbill in frappe.get_roles()
user_roles = frappe.get_roles()
for row in ref_wise_billed_amount.values():
total_billed_amt = row.billed_amt
allowance = get_allowance_for(row.item_code, {}, None, None, "amount")[0]
total_overbilled_amt = 0.0
max_allowed_amt = flt(row.ref_amt * (100 + allowance) / 100)
reference_names = [d.get(item_ref_dn) for d in self.get("items") if d.get(item_ref_dn)]
reference_details = self.get_billing_reference_details(reference_names, ref_dt + " Item", based_on)
for item in self.get("items"):
if not item.get(item_ref_dn):
continue
ref_amt = flt(reference_details.get(item.get(item_ref_dn)), self.precision(based_on, item))
based_on_amt = flt(item.get(based_on))
if not ref_amt:
if based_on_amt: # Skip warning for free items
frappe.msgprint(
_(
"System will not check over billing since amount for Item {0} in {1} is zero"
).format(item.item_code, ref_dt),
title=_("Warning"),
indicator="orange",
)
continue
already_billed = self.get_billed_amount_for_item(item, item_ref_dn, based_on)
total_billed_amt = flt(flt(already_billed) + based_on_amt, self.precision(based_on, item))
allowance, item_allowance, global_qty_allowance, global_amount_allowance = get_allowance_for(
item.item_code, item_allowance, global_qty_allowance, global_amount_allowance, "amount"
)
max_allowed_amt = flt(ref_amt * (100 + allowance) / 100)
if total_billed_amt < 0 and max_allowed_amt < 0:
# while making debit note against purchase return entry(purchase receipt) getting overbill error
total_billed_amt, max_allowed_amt = abs(total_billed_amt), abs(max_allowed_amt)
total_billed_amt = abs(total_billed_amt)
max_allowed_amt = abs(max_allowed_amt)
overbill_amt = total_billed_amt - max_allowed_amt
row["max_allowed_amt"] = max_allowed_amt
total_overbilled_amt += overbill_amt
if overbill_amt > precision_allowance and not is_overbilling_allowed:
if self.doctype != "Purchase Invoice" or not cint(
if overbill_amt > 0.01 and role_allowed_to_over_bill not in user_roles:
if self.doctype != "Purchase Invoice":
self.throw_overbill_exception(item, max_allowed_amt)
elif not cint(
frappe.db.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
)
):
overbilled_items.append(row)
self.throw_overbill_exception(item, max_allowed_amt)
if overbilled_items:
self.throw_overbill_exception(overbilled_items, precision)
if is_overbilling_allowed and total_overbilled_amt > 0.1:
if role_allowed_to_over_bill in user_roles and total_overbilled_amt > 0.1:
frappe.msgprint(
_("Overbilling of {} ignored because you have {} role.").format(
total_overbilled_amt, role_allowed_to_overbill
total_overbilled_amt, role_allowed_to_over_bill
),
indicator="orange",
alert=True,
@@ -1801,87 +1820,54 @@ class AccountsController(TransactionBase):
)
)
def get_reference_wise_billed_amt(self, ref_dt, item_ref_dn, based_on):
def get_billed_amount_for_item(self, item, item_ref_dn, based_on):
"""
Returns Sum of Amount of
Sales/Purchase Invoice Items
that are linked to `item_ref_dn` (`dn_detail` / `pr_detail`)
that are submitted OR not submitted but are under current invoice
"""
reference_names = [d.get(item_ref_dn) for d in self.items if d.get(item_ref_dn)]
if not reference_names:
return
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Sum
ref_wise_billed_amount = {}
precision = self.precision(based_on, "items")
reference_details = self.get_billing_reference_details(reference_names, ref_dt + " Item", based_on)
already_billed = self.get_already_billed_amount(reference_names, item_ref_dn, based_on)
for item in self.items:
key = item.get(item_ref_dn)
if not key:
continue
ref_amt = flt(reference_details.get(key), precision)
current_amount = flt(item.get(based_on), precision)
if not ref_amt:
if current_amount: # Skip warning for free items
frappe.msgprint(
_(
"System will not check over billing since amount for Item {0} in {1} is zero"
).format(item.item_code, ref_dt),
title=_("Warning"),
indicator="orange",
)
continue
ref_wise_billed_amount.setdefault(
key,
frappe._dict(item_code=item.item_code, billed_amt=0.0, ref_amt=ref_amt, rows=[]),
)
ref_wise_billed_amount[key]["rows"].append(item.idx)
ref_wise_billed_amount[key]["ref_amt"] = ref_amt
ref_wise_billed_amount[key]["billed_amt"] += current_amount
if key in already_billed:
ref_wise_billed_amount[key]["billed_amt"] += flt(already_billed.pop(key, 0), precision)
return ref_wise_billed_amount
def get_already_billed_amount(self, reference_names, item_ref_dn, based_on):
item_doctype = frappe.qb.DocType(self.items[0].doctype)
item_doctype = frappe.qb.DocType(item.doctype)
based_on_field = frappe.qb.Field(based_on)
join_field = frappe.qb.Field(item_ref_dn)
return frappe._dict(
(
frappe.qb.from_(item_doctype)
.select(join_field, Sum(based_on_field))
.where(join_field.isin(reference_names))
.where((item_doctype.docstatus == 1) & (item_doctype.parent != self.name))
.groupby(join_field)
).run()
)
def throw_overbill_exception(self, overbilled_items, precision):
message = (
_("<p>Cannot overbill for the following Items:</p>")
+ "<ul>"
+ "".join(
_("<li>Item {0} in row(s) {1} billed more than {2}</li>").format(
frappe.bold(item.item_code),
", ".join(str(x) for x in item.rows),
frappe.bold(fmt_money(item.max_allowed_amt, precision=precision, currency=self.currency)),
result = (
frappe.qb.from_(item_doctype)
.select(Sum(based_on_field))
.where(join_field == item.get(item_ref_dn))
.where(
Criterion.any(
[ # select all items from other invoices OR current invoices
Criterion.all(
[ # for selecting items from other invoices
item_doctype.docstatus == 1,
item_doctype.parent != self.name,
]
),
Criterion.all(
[ # for selecting items from current invoice, that are linked to same reference
item_doctype.docstatus == 0,
item_doctype.parent == self.name,
item_doctype.name != item.name,
]
),
]
)
for item in overbilled_items
)
+ "</ul>"
)
message += _("<p>To allow over-billing, please set allowance in Accounts Settings.</p>")
).run()
frappe.throw(_(message))
return result[0][0] if result else 0
def throw_overbill_exception(self, item, max_allowed_amt):
frappe.throw(
_(
"Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings"
).format(item.item_code, item.idx, max_allowed_amt)
)
def get_company_default(self, fieldname, ignore_validation=False):
from erpnext.accounts.utils import get_company_default
@@ -2239,7 +2225,6 @@ class AccountsController(TransactionBase):
self.payment_schedule = []
self.payment_terms_template = po_or_so.payment_terms_template
posting_date = self.get("bill_date") or self.get("posting_date") or self.get("transaction_date")
for schedule in po_or_so.payment_schedule:
payment_schedule = {
@@ -2252,17 +2237,6 @@ class AccountsController(TransactionBase):
}
if automatically_fetch_payment_terms:
if schedule.due_date_based_on:
payment_schedule["due_date"] = get_due_date(schedule, posting_date)
payment_schedule["due_date_based_on"] = schedule.due_date_based_on
payment_schedule["credit_days"] = cint(schedule.credit_days)
payment_schedule["credit_months"] = cint(schedule.credit_months)
if schedule.discount_validity_based_on:
payment_schedule["discount_date"] = get_discount_date(schedule, posting_date)
payment_schedule["discount_validity_based_on"] = schedule.discount_validity_based_on
payment_schedule["discount_validity"] = cint(schedule.discount_validity)
payment_schedule["payment_amount"] = flt(
grand_total * flt(payment_schedule["invoice_portion"]) / 100,
schedule.precision("payment_amount"),
@@ -2750,7 +2724,9 @@ def set_balance_in_account_currency(
_("Account: {0} with currency: {1} can not be selected").format(gl_dict.account, account_currency)
)
gl_dict["account_currency"] = account_currency
gl_dict["account_currency"] = (
company_currency if account_currency == company_currency else account_currency
)
# set debit/credit in account currency if not provided
if flt(gl_dict.debit) and not flt(gl_dict.debit_in_account_currency):
@@ -2997,26 +2973,14 @@ def get_payment_term_details(
term = frappe.get_doc("Payment Term", term)
else:
term_details.payment_term = term.payment_term
fields_to_copy = [
"description",
"invoice_portion",
"discount_type",
"discount",
"mode_of_payment",
"due_date_based_on",
"credit_days",
"credit_months",
"discount_validity_based_on",
"discount_validity",
]
for field in fields_to_copy:
term_details[field] = term.get(field)
term_details.description = term.description
term_details.invoice_portion = term.invoice_portion
term_details.payment_amount = flt(term.invoice_portion) * flt(grand_total) / 100
term_details.base_payment_amount = flt(term.invoice_portion) * flt(base_grand_total) / 100
term_details.discount_type = term.discount_type
term_details.discount = term.discount
term_details.outstanding = term_details.payment_amount
term_details.mode_of_payment = term.mode_of_payment
if bill_date:
term_details.due_date = get_due_date(term, bill_date)
@@ -3035,11 +2999,11 @@ def get_due_date(term, posting_date=None, bill_date=None):
due_date = None
date = bill_date or posting_date
if term.due_date_based_on == "Day(s) after invoice date":
due_date = add_days(date, cint(term.credit_days))
due_date = add_days(date, term.credit_days)
elif term.due_date_based_on == "Day(s) after the end of the invoice month":
due_date = add_days(get_last_day(date), cint(term.credit_days))
due_date = add_days(get_last_day(date), term.credit_days)
elif term.due_date_based_on == "Month(s) after the end of the invoice month":
due_date = get_last_day(add_months(date, cint(term.credit_months)))
due_date = get_last_day(add_months(date, term.credit_months))
return due_date
@@ -3047,11 +3011,11 @@ def get_discount_date(term, posting_date=None, bill_date=None):
discount_validity = None
date = bill_date or posting_date
if term.discount_validity_based_on == "Day(s) after invoice date":
discount_validity = add_days(date, cint(term.discount_validity))
discount_validity = add_days(date, term.discount_validity)
elif term.discount_validity_based_on == "Day(s) after the end of the invoice month":
discount_validity = add_days(get_last_day(date), cint(term.discount_validity))
discount_validity = add_days(get_last_day(date), term.discount_validity)
elif term.discount_validity_based_on == "Month(s) after the end of the invoice month":
discount_validity = get_last_day(add_months(date, cint(term.discount_validity)))
discount_validity = get_last_day(add_months(date, term.discount_validity))
return discount_validity

View File

@@ -6,7 +6,7 @@ import json
from collections import defaultdict
import frappe
from frappe import cint, qb, scrub
from frappe import qb, scrub
from frappe.desk.reportview import get_filters_cond, get_match_cond
from frappe.query_builder import Criterion, CustomFunction
from frappe.query_builder.functions import Locate
@@ -528,27 +528,21 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters):
bo = frappe.qb.DocType("Blanket Order")
bo_item = frappe.qb.DocType("Blanket Order Item")
blanket_orders = (
frappe.qb.from_(bo)
.from_(bo_item)
.select(bo.name)
.distinct()
.select(bo.blanket_order_type, bo.to_date)
.where(
(bo_item.parent == bo.name)
& (bo_item.item_code == filters.get("item"))
& (bo.blanket_order_type == filters.get("blanket_order_type"))
& (bo.company == filters.get("company"))
& (bo.docstatus == 1)
return frappe.db.sql(
"""select distinct bo.name, bo.blanket_order_type, bo.to_date
from `tabBlanket Order` bo, `tabBlanket Order Item` boi
where
boi.parent = bo.name
and boi.item_code = {item_code}
and bo.blanket_order_type = '{blanket_order_type}'
and bo.company = {company}
and bo.docstatus = 1""".format(
item_code=frappe.db.escape(filters.get("item")),
blanket_order_type=filters.get("blanket_order_type"),
company=frappe.db.escape(filters.get("company")),
)
.run()
)
return blanket_orders
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
@@ -566,7 +560,7 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
if filters.get("company"):
condition += "and tabAccount.company = %(company)s"
condition += " and tabAccount.disabled = %(disabled)s"
condition += f"and tabAccount.disabled = {filters.get('disabled', 0)}"
return frappe.db.sql(
f"""select tabAccount.name from `tabAccount`
@@ -576,11 +570,7 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
and tabAccount.`{searchfield}` LIKE %(txt)s
{condition} {get_match_cond(doctype)}
order by idx desc, name""",
{
"txt": "%" + txt + "%",
"company": filters.get("company", ""),
"disabled": cint(filters.get("disabled", 0)),
},
{"txt": "%" + txt + "%", "company": filters.get("company", "")},
)
@@ -656,7 +646,7 @@ def get_expense_account(doctype, txt, searchfield, start, page_len, filters):
where (tabAccount.report_type = "Profit and Loss"
or tabAccount.account_type in ("Expense Account", "Fixed Asset", "Temporary", "Asset Received But Not Billed", "Capital Work in Progress"))
and tabAccount.is_group=0
and tabAccount.disabled = 0
and tabAccount.docstatus!=2
and tabAccount.{searchfield} LIKE %(txt)s
{condition} {get_match_cond(doctype)}""",
{"company": filters.get("company", ""), "txt": "%" + txt + "%"},

View File

@@ -368,7 +368,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
company = frappe.db.get_value(doctype, source_name, "company")
company = frappe.db.get_value("Delivery Note", source_name, "company")
default_warehouse_for_sales_return = frappe.get_cached_value(
"Company", company, "default_warehouse_for_sales_return"
)

View File

@@ -728,16 +728,7 @@ class SellingController(StockController):
def set_default_income_account_for_item(obj):
"""Set income account as default for items in the transaction.
Updates the item default income account for each item in the transaction
if it differs from the company's default income account.
Args:
obj: Transaction document containing items table with income_account field
"""
company_default = frappe.get_cached_value("Company", obj.company, "default_income_account")
for d in obj.get("items", default=[]):
income_account = getattr(d, "income_account", None)
if d.item_code and income_account and income_account != company_default:
set_item_default(d.item_code, obj.company, "income_account", income_account)
for d in obj.get("items"):
if d.item_code:
if getattr(d, "income_account", None):
set_item_default(d.item_code, obj.company, "income_account", d.income_account)

View File

@@ -520,9 +520,8 @@ class StockController(AccountsController):
make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher)
def make_gl_entries_on_cancel(self, from_repost=False):
if not from_repost:
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
def make_gl_entries_on_cancel(self):
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
if frappe.db.sql(
"""select name from `tabGL Entry` where voucher_type=%s
and voucher_no=%s""",
@@ -1028,7 +1027,12 @@ def is_reposting_pending():
)
def future_sle_exists(args, sl_entries=None):
def future_sle_exists(args, sl_entries=None, allow_force_reposting=True):
if allow_force_reposting and frappe.db.get_single_value(
"Stock Reposting Settings", "do_reposting_for_each_stock_transaction"
):
return True
key = (args.voucher_type, args.voucher_no)
if not hasattr(frappe.local, "future_sle"):
frappe.local.future_sle = {}

View File

@@ -42,23 +42,17 @@ class calculate_taxes_and_totals:
items = list(filter(lambda item: not item.get("is_alternative"), self.doc.get("items")))
return items
def calculate(self, ignore_tax_template_validation=False):
def calculate(self):
if not len(self._items):
return
self.discount_amount_applied = False
self.need_recomputation = False
self.ignore_tax_template_validation = ignore_tax_template_validation
self._calculate()
if self.doc.meta.get_field("discount_amount"):
self.set_discount_amount()
self.apply_discount_amount()
if not ignore_tax_template_validation and self.need_recomputation:
return self.calculate(ignore_tax_template_validation=True)
# Update grand total as per cash and non trade discount
if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"):
self.doc.grand_total -= self.doc.discount_amount
@@ -102,9 +96,6 @@ class calculate_taxes_and_totals:
self.doc.base_tax_withholding_net_total = sum_base_net_amount
def validate_item_tax_template(self):
if self.ignore_tax_template_validation:
return
if self.doc.get("is_return") and self.doc.get("return_against"):
return
@@ -145,10 +136,6 @@ class calculate_taxes_and_totals:
)
)
# For correct tax_amount calculation re-computation is required
if self.discount_amount_applied and self.doc.apply_discount_on == "Grand Total":
self.need_recomputation = True
def update_item_tax_map(self):
for item in self.doc.items:
item.item_tax_rate = get_item_tax_map(

View File

@@ -29,10 +29,4 @@ frappe.ui.form.on("Contract", {
});
}
},
party_name: function (frm) {
let field = frm.doc.party_type.toLowerCase() + "_name";
frappe.db.get_value(frm.doc.party_type, frm.doc.party_name, field, (r) => {
frm.set_value("party_full_name", r[field]);
});
},
});

View File

@@ -14,7 +14,6 @@
"party_user",
"status",
"fulfilment_status",
"party_full_name",
"sb_terms",
"start_date",
"cb_date",
@@ -245,18 +244,11 @@
"fieldname": "authorised_by_section",
"fieldtype": "Section Break",
"label": "Authorised By"
},
{
"fieldname": "party_full_name",
"fieldtype": "Data",
"label": "Party Full Name",
"read_only": 1
}
],
"grid_page_length": 50,
"is_submittable": 1,
"links": [],
"modified": "2025-05-23 13:54:03.346537",
"modified": "2020-12-07 11:15:58.385521",
"modified_by": "Administrator",
"module": "CRM",
"name": "Contract",
@@ -323,10 +315,9 @@
"write": 1
}
],
"row_format": "Dynamic",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 1
}
}

View File

@@ -23,17 +23,10 @@ class Contract(Document):
self.name = _(name)
def validate(self):
self.set_missing_values()
self.validate_dates()
self.update_contract_status()
self.update_fulfilment_status()
def set_missing_values(self):
if not self.party_full_name:
field = self.party_type.lower() + "_name"
if res := frappe.db.get_value(self.party_type, self.party_name, field):
self.party_full_name = res
def before_submit(self):
self.signed_by_company = frappe.session.user

View File

@@ -60,7 +60,7 @@ def import_genericode():
"doctype": "File",
"attached_to_doctype": "Code List",
"attached_to_name": code_list.name,
"folder": frappe.db.get_value("File", {"is_attachments_folder": 1}),
"folder": "Home/Attachments",
"file_name": frappe.local.uploaded_filename,
"file_url": frappe.local.uploaded_file_url,
"is_private": 1,

View File

@@ -6,7 +6,6 @@
"engine": "InnoDB",
"field_order": [
"code_list",
"canonical_uri",
"title",
"common_code",
"description",
@@ -72,17 +71,10 @@
"in_list_view": 1,
"label": "Description",
"max_height": "60px"
},
{
"fetch_from": "code_list.canonical_uri",
"fieldname": "canonical_uri",
"fieldtype": "Data",
"label": "Canonical URI"
}
],
"grid_page_length": 50,
"links": [],
"modified": "2025-10-04 17:22:28.176155",
"modified": "2024-11-06 07:46:17.175687",
"modified_by": "Administrator",
"module": "EDI",
"name": "Common Code",
@@ -102,11 +94,10 @@
"write": 1
}
],
"row_format": "Dynamic",
"search_fields": "common_code,description",
"show_title_field_in_link": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "title"
}
}

View File

@@ -22,7 +22,6 @@ class CommonCode(Document):
additional_data: DF.Code | None
applies_to: DF.Table[DynamicLink]
canonical_uri: DF.Data | None
code_list: DF.Link
common_code: DF.Data
description: DF.SmallText | None

View File

@@ -7,7 +7,7 @@ from collections import deque
from operator import itemgetter
import frappe
from frappe import _, bold
from frappe import _
from frappe.core.doctype.version.version import get_diff
from frappe.model.mapper import get_mapped_doc
from frappe.utils import cint, cstr, flt, today
@@ -554,16 +554,9 @@ class BOM(WebsiteGenerator):
def check_recursion(self, bom_list=None):
"""Check whether recursion occurs in any bom"""
def _throw_error(bom_name, production_item=None):
msg = _("BOM recursion: {1} cannot be parent or child of {0}").format(self.name, bom_name)
if production_item and bom_name != self.name:
msg += "<br><br>"
msg += _(
"Note: If you want to use the finished good {0} as a raw material, then enable the 'Do Not Explode' checkbox in the Items table against the same raw material."
).format(bold(production_item))
def _throw_error(bom_name):
frappe.throw(
msg,
_("BOM recursion: {1} cannot be parent or child of {0}").format(self.name, bom_name),
exc=BOMRecursionError,
)
@@ -580,7 +573,7 @@ class BOM(WebsiteGenerator):
if self.item == item.item_code and item.bom_no:
# Same item but with different BOM should not be allowed.
# Same item can appear recursively once as long as it doesn't have BOM.
_throw_error(item.bom_no, self.item)
_throw_error(item.bom_no)
if self.name in {d.bom_no for d in self.items}:
_throw_error(self.name)

View File

@@ -443,7 +443,7 @@ class JobCard(Document):
op_row.employee.append(time_log.employee)
if time_log.time_in_mins:
op_row.completed_time += time_log.time_in_mins
op_row.completed_qty += flt(time_log.completed_qty)
op_row.completed_qty += time_log.completed_qty
for row in self.sub_operations:
operation_deatils = operation_wise_completed_time.get(row.sub_operation)

View File

@@ -37,14 +37,6 @@ frappe.ui.form.on("Production Plan", {
};
});
frm.set_query("sub_assembly_warehouse", function (doc) {
return {
filters: {
company: doc.company,
},
};
});
frm.set_query("material_request", "material_requests", function () {
return {
filters: {

View File

@@ -1048,7 +1048,6 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p
item.purchase_uom,
item_uom.conversion_factor,
item.safety_stock,
bom.item.as_("main_bom_item"),
)
.where(
(bei.docstatus < 2)
@@ -1116,7 +1115,6 @@ def get_subitems(
item_default.default_warehouse,
item.purchase_uom,
item_uom.conversion_factor,
bom.item.as_("main_bom_item"),
)
.where(
(bom.name == bom_no)
@@ -1230,7 +1228,6 @@ def get_material_request_items(
"sales_order": sales_order,
"description": row.get("description"),
"uom": row.get("purchase_uom") or row.get("stock_uom"),
"main_bom_item": row.get("main_bom_item"),
}
@@ -1760,7 +1757,6 @@ def get_raw_materials_of_sub_assembly_items(
item.purchase_uom,
item_uom.conversion_factor,
item.safety_stock,
bom.item.as_("main_bom_item"),
)
.where(
(bei.docstatus == 1)

View File

@@ -1322,20 +1322,20 @@ def stop_unstop(work_order, status):
@frappe.whitelist()
def query_sales_order(production_item: str) -> list[str]:
return frappe.get_list(
"Sales Order",
filters=[
["Sales Order", "docstatus", "=", 1],
],
or_filters=[
["Sales Order Item", "item_code", "=", production_item],
["Packed Item", "item_code", "=", production_item],
],
pluck="name",
distinct=True,
def query_sales_order(production_item):
out = frappe.db.sql_list(
"""
select distinct so.name from `tabSales Order` so, `tabSales Order Item` so_item
where so_item.parent=so.name and so_item.item_code=%s and so.docstatus=1
union
select distinct so.name from `tabSales Order` so, `tabPacked Item` pi_item
where pi_item.parent=so.name and pi_item.item_code=%s and so.docstatus=1
""",
(production_item, production_item),
)
return out
@frappe.whitelist()
def make_job_card(work_order, operations):

View File

@@ -23,7 +23,6 @@ def get_columns():
"""return columns"""
columns = [
_("Item") + ":Link/Item:150",
_("Item Name") + "::240",
_("Description") + "::300",
_("BOM Qty") + ":Float:160",
_("BOM UoM") + "::160",
@@ -74,12 +73,11 @@ def get_bom_stock(filters):
.on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS))
.select(
BOM_ITEM.item_code,
BOM_ITEM.item_name,
BOM_ITEM.description,
BOM_ITEM.stock_qty,
BOM_ITEM.stock_uom,
BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity,
BIN.actual_qty.as_("actual_qty"),
Sum(BIN.actual_qty).as_("actual_qty"),
Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))),
)
.where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM"))

View File

@@ -94,7 +94,6 @@ def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False):
expected_data.append(
[
item.item_code,
item.item_name,
item.description,
item.stock_qty,
item.stock_uom,

View File

@@ -375,6 +375,3 @@ erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes
erpnext.patches.v14_0.rename_group_by_to_categorize_by
execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor")
erpnext.patches.v14_0.set_update_price_list_based_on
erpnext.patches.v14_0.rename_group_by_to_categorize_by_in_custom_reports
erpnext.patches.v14_0.update_full_name_in_contract
erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter #2025-12-11

View File

@@ -1,24 +0,0 @@
import json
import frappe
def execute():
custom_reports = frappe.get_all(
"Report",
filters={
"report_type": "Custom Report",
"reference_report": ["in", ["General Ledger", "Supplier Quotation Comparison"]],
},
fields=["name", "json"],
)
for report in custom_reports:
report_json = json.loads(report.json)
if "filters" in report_json and "group_by" in report_json["filters"]:
report_json["filters"]["categorize_by"] = (
report_json["filters"].pop("group_by").replace("Group", "Categorize")
)
frappe.db.set_value("Report", report.name, "json", json.dumps(report_json))

View File

@@ -2,15 +2,6 @@ import frappe
def execute():
try:
from erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter import execute
execute()
except ImportError:
update_frankfurter_app_parameter_and_result()
def update_frankfurter_app_parameter_and_result():
settings = frappe.get_doc("Currency Exchange Settings")
if settings.service_provider != "frankfurter.app":
return

View File

@@ -1,15 +0,0 @@
import frappe
from frappe import qb
def execute():
con = qb.DocType("Contract")
for c in (
qb.from_(con)
.select(con.name, con.party_type, con.party_name)
.where(con.party_full_name.isnull())
.run(as_dict=True)
):
field = c.party_type.lower() + "_name"
if res := frappe.db.get_value(c.party_type, c.party_name, field):
frappe.db.set_value("Contract", c.name, "party_full_name", res)

View File

@@ -1,17 +0,0 @@
import frappe
def execute():
settings_meta = frappe.get_meta("Currency Exchange Settings")
settings = frappe.get_doc("Currency Exchange Settings")
if (
"frankfurter.dev" not in settings_meta.get_options("service_provider").split("\n")
or settings.service_provider != "frankfurter.app"
):
return
settings.service_provider = "frankfurter.dev"
settings.set_parameters_and_result()
settings.flags.ignore_validate = True
settings.save()

View File

@@ -292,16 +292,12 @@ def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to
@frappe.whitelist()
def get_timesheet_detail_rate(timelog, currency):
ts = frappe.qb.DocType("Timesheet")
ts_detail = frappe.qb.DocType("Timesheet Detail")
timelog_detail = (
frappe.qb.from_(ts_detail)
.inner_join(ts)
.on(ts.name == ts_detail.parent)
.select(ts_detail.billing_amount.as_("billing_amount"), ts.currency.as_("currency"))
.where(ts_detail.name == timelog)
.run(as_dict=1)
timelog_detail = frappe.db.sql(
f"""SELECT tsd.billing_amount as billing_amount,
ts.currency as currency FROM `tabTimesheet Detail` tsd
INNER JOIN `tabTimesheet` ts ON ts.name=tsd.parent
WHERE tsd.name = '{timelog}'""",
as_dict=1,
)[0]
if timelog_detail.currency:

View File

@@ -1,17 +1,15 @@
import urllib.parse
import frappe
def get_context(context):
if project := frappe.form_dict.project:
title = frappe.utils.data.escape_html(project)
route = "/projects?" + urllib.parse.urlencode({"project": project})
context.parents = [{"title": title, "route": route}]
context.success_url = route
if frappe.form_dict.project:
context.parents = [
{"title": frappe.form_dict.project, "route": "/projects?project=" + frappe.form_dict.project}
]
context.success_url = "/projects?project=" + frappe.form_dict.project
elif context.doc and (project := context.doc.get("project")):
title = frappe.utils.data.escape_html(project)
route = "/projects?" + urllib.parse.urlencode({"project": project})
context.parents = [{"title": title, "route": route}]
context.success_url = route
elif context.doc and context.doc.get("project"):
context.parents = [
{"title": context.doc.project, "route": "/projects?project=" + context.doc.project}
]
context.success_url = "/projects?project=" + context.doc.project

View File

@@ -26,7 +26,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
item.discount_amount = flt(item.rate_with_margin) * flt(item.discount_percentage) / 100;
}
if (item.discount_amount > 0) {
if (item.discount_amount) {
item_rate = flt((item.rate_with_margin) - (item.discount_amount), precision('rate', item));
item.discount_percentage = 100 * flt(item.discount_amount) / flt(item.rate_with_margin);
}
@@ -76,10 +76,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
// Update paid amount on return/debit note creation
if (
this.frm.doc.doctype === "Purchase Invoice" &&
this.frm.doc.is_return &&
this.frm.doc.grand_total < 0 &&
this.frm.doc.grand_total > this.frm.doc.paid_amount
this.frm.doc.doctype === "Purchase Invoice"
&& this.frm.doc.is_return
&& (this.frm.doc.grand_total > this.frm.doc.paid_amount)
) {
this.frm.doc.paid_amount = flt(this.frm.doc.grand_total, precision("grand_total"));
}

View File

@@ -438,7 +438,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
item.weight_per_unit = 0;
item.weight_uom = '';
item.uom = null // make UOM blank to update the existing UOM when item changes
item.conversion_factor = 0;
if(['Sales Invoice'].includes(this.frm.doc.doctype)) {
@@ -879,15 +878,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
}
discount_date(doc, cdt, cdn) {
// Remove fields as discount_date is auto-managed by payment terms
const row = locals[cdt][cdn];
["discount_validity", "discount_validity_based_on"].forEach((field) => {
row[field] = "";
});
this.frm.refresh_field("payment_schedule");
}
due_date() {
// due_date is to be changed, payment terms template and/or payment schedule must
// be removed as due_date is automatically changed based on payment terms
@@ -1089,12 +1079,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
plc_conversion_rate() {
if(this.frm.doc.price_list_currency === this.get_company_currency()) {
this.frm.set_value("plc_conversion_rate", 1.0);
} else if (
this.frm.doc.price_list_currency === this.frm.doc.currency &&
this.frm.doc.plc_conversion_rate &&
flt(this.frm.doc.plc_conversion_rate) != 1 &&
flt(this.frm.doc.plc_conversion_rate) != flt(this.frm.doc.conversion_rate)
) {
} else if(this.frm.doc.price_list_currency === this.frm.doc.currency
&& this.frm.doc.plc_conversion_rate && cint(this.frm.doc.plc_conversion_rate) != 1 &&
cint(this.frm.doc.plc_conversion_rate) != cint(this.frm.doc.conversion_rate)) {
this.frm.set_value("conversion_rate", this.frm.doc.plc_conversion_rate);
}
@@ -2216,7 +2203,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
'item_code': item.item_code,
'valid_from': ["<=", doc.transaction_date || doc.bill_date || doc.posting_date],
'item_group': item.item_group,
'disabled': 0,
}
if (doc.tax_category)
@@ -2258,18 +2244,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
payment_term(doc, cdt, cdn) {
const me = this;
var row = locals[cdt][cdn];
// empty date condition fields
[
"due_date_based_on",
"credit_days",
"credit_months",
"discount_validity",
"discount_validity_based_on",
].forEach(function (field) {
row[field] = "";
});
if (row.payment_term) {
if(row.payment_term) {
frappe.call({
method: "erpnext.controllers.accounts_controller.get_payment_term_details",
args: {
@@ -2279,19 +2254,16 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
grand_total: this.frm.doc.rounded_total || this.frm.doc.grand_total,
base_grand_total: this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total
},
callback: function (r) {
if (r.message && !r.exc) {
const company_currency = me.get_company_currency();
for (let d in r.message) {
row[d] = r.message[d];
callback: function(r) {
if(r.message && !r.exc) {
for (var d in r.message) {
frappe.model.set_value(cdt, cdn, d, r.message[d]);
const company_currency = me.get_company_currency();
me.update_payment_schedule_grid_labels(company_currency);
}
me.update_payment_schedule_grid_labels(company_currency);
me.frm.refresh_field("payment_schedule");
}
},
});
} else {
me.frm.refresh_field("payment_schedule");
}
})
}
}

View File

@@ -47,7 +47,7 @@ frappe.ui.form.on("Event", {
frm.add_custom_button(
__("Add Sales Partners"),
function () {
new frappe.desk.eventParticipants(frm, "Sales Partner");
new frappe.desk.eventParticipants(frm, "Sales Partners");
},
__("Add Participants")
);

View File

@@ -215,9 +215,17 @@ $.extend(erpnext.utils, {
},
make_bank_account: function (doctype, docname) {
frappe.new_doc("Bank Account", {
party_type: doctype,
party: docname,
frappe.call({
method: "erpnext.accounts.doctype.bank_account.bank_account.make_bank_account",
args: {
doctype: doctype,
docname: docname,
},
freeze: true,
callback: function (r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
},
});
},

View File

@@ -381,7 +381,7 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector {
query: "erpnext.controllers.queries.get_batch_no",
};
},
change: function () {
onchange: function () {
const batch_no = this.get_value();
if (!batch_no) {
this.grid_row.on_grid_fields_dict.available_qty.set_value(0);

View File

@@ -14,7 +14,6 @@ frappe.ui.form.on("Customer", {
method: "erpnext.selling.doctype.customer.customer.make_opportunity",
frm: cur_frm,
}),
"Bank Account": () => erpnext.utils.make_bank_account(frm.doc.doctype, frm.doc.name),
};
frm.add_fetch("lead_name", "company_name", "customer_name");
@@ -156,10 +155,7 @@ frappe.ui.form.on("Customer", {
__("Actions")
);
if (
cint(frappe.defaults.get_default("enable_common_party_accounting")) &&
frappe.model.can_create("Party Link")
) {
if (cint(frappe.defaults.get_default("enable_common_party_accounting"))) {
frm.add_custom_button(
__("Link with Supplier"),
function () {

View File

@@ -1266,11 +1266,6 @@ def make_raw_material_request(items, company, sales_order, project=None):
items.update({"company": company, "sales_order": sales_order})
item_wh = {}
for item in items.get("items"):
if item.get("warehouse"):
item_wh[item.get("item_code")] = item.get("warehouse")
raw_materials = get_items_for_material_requests(items)
if not raw_materials:
frappe.msgprint(_("Material Request not created, as quantity for Raw Materials already available."))
@@ -1295,7 +1290,7 @@ def make_raw_material_request(items, company, sales_order, project=None):
"item_code": item.get("item_code"),
"qty": item.get("quantity"),
"schedule_date": schedule_date,
"warehouse": item_wh.get(item.get("main_bom_item")) or item.get("warehouse"),
"warehouse": item.get("warehouse"),
"sales_order": sales_order,
"project": project,
},

View File

@@ -10,7 +10,7 @@ from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, getdate, nowdate, today
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.controllers.accounts_controller import get_due_date, update_child_qty_rate
from erpnext.controllers.accounts_controller import update_child_qty_rate
from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
make_maintenance_schedule,
)
@@ -1759,13 +1759,14 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
so.load_from_db()
self.assertRaises(frappe.LinkExistsError, so.cancel)
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
def test_payment_terms_are_fetched_when_creating_sales_invoice(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
create_payment_terms_template,
)
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
automatically_fetch_payment_terms()
so = make_sales_order(uom="Nos", do_not_save=1)
create_payment_terms_template()
so.payment_terms_template = "Test Receivable Template"
@@ -1779,6 +1780,8 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
self.assertEqual(so.payment_terms_template, si.payment_terms_template)
compare_payment_schedules(self, so, si)
automatically_fetch_payment_terms(enable=0)
def test_zero_amount_sales_order_billing_status(self):
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
@@ -2281,14 +2284,16 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
self.assertRaises(frappe.ValidationError, so1.update_status, "Draft")
def automatically_fetch_payment_terms(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")
accounts_settings.automatically_fetch_payment_terms = enable
accounts_settings.save()
def compare_payment_schedules(doc, doc1, doc2):
for index, schedule in enumerate(doc1.get("payment_schedule")):
posting_date = doc1.get("bill_date") or doc1.get("posting_date") or doc1.get("transaction_date")
due_date = schedule.due_date
if schedule.due_date_based_on:
due_date = get_due_date(schedule, posting_date=posting_date)
doc.assertEqual(schedule.payment_term, doc2.payment_schedule[index].payment_term)
doc.assertEqual(due_date, doc2.payment_schedule[index].due_date)
doc.assertEqual(getdate(schedule.due_date), doc2.payment_schedule[index].due_date)
doc.assertEqual(schedule.invoice_portion, doc2.payment_schedule[index].invoice_portion)
doc.assertEqual(schedule.payment_amount, doc2.payment_schedule[index].payment_amount)

View File

@@ -37,15 +37,15 @@ class SellingSettings(Document):
)
def toggle_hide_tax_id(self):
_hide_tax_id = cint(self.hide_tax_id)
self.hide_tax_id = cint(self.hide_tax_id)
# Make property setters to hide tax_id fields
for doctype in ("Sales Order", "Sales Invoice", "Delivery Note"):
make_property_setter(
doctype, "tax_id", "hidden", _hide_tax_id, "Check", validate_fields_for_doctype=False
doctype, "tax_id", "hidden", self.hide_tax_id, "Check", validate_fields_for_doctype=False
)
make_property_setter(
doctype, "tax_id", "print_hide", _hide_tax_id, "Check", validate_fields_for_doctype=False
doctype, "tax_id", "print_hide", self.hide_tax_id, "Check", validate_fields_for_doctype=False
)
def toggle_editable_rate_for_bundle_items(self):

View File

@@ -11,7 +11,7 @@ from frappe.cache_manager import clear_defaults_cache
from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.desk.page.setup_wizard.setup_wizard import make_records
from frappe.utils import add_months, cint, formatdate, get_first_day, get_link_to_form, get_timestamp, today
from frappe.utils import cint, formatdate, get_link_to_form, get_timestamp, today
from frappe.utils.nestedset import NestedSet, rebuild_tree
from erpnext.accounts.doctype.account.account import get_account_currency
@@ -614,29 +614,27 @@ def install_country_fixtures(company, country):
def update_company_current_month_sales(company):
from_date = get_first_day(today())
to_date = get_first_day(add_months(from_date, 1))
current_month_year = formatdate(today(), "MM-yyyy")
results = frappe.db.sql(
"""
f"""
SELECT
SUM(base_grand_total) AS total,
DATE_FORMAT(posting_date, '%%m-%%Y') AS month_year
DATE_FORMAT(`posting_date`, '%m-%Y') AS month_year
FROM
`tabSales Invoice`
WHERE
posting_date >= %s
AND posting_date < %s
DATE_FORMAT(`posting_date`, '%m-%Y') = '{current_month_year}'
AND docstatus = 1
AND company = %s
AND company = {frappe.db.escape(company)}
GROUP BY
month_year
""",
(from_date, to_date, company),
""",
as_dict=True,
)
monthly_total = results[0]["total"] if len(results) > 0 else 0
frappe.db.set_value("Company", company, "total_monthly_sales", monthly_total)

View File

@@ -68,9 +68,9 @@ def patched_requests_get(*args, **kwargs):
if kwargs["params"].get("date") and kwargs["params"].get("from") and kwargs["params"].get("to"):
if test_exchange_values.get(kwargs["params"]["date"]):
return PatchResponse({"result": test_exchange_values[kwargs["params"]["date"]]}, 200)
elif args[0].startswith("https://api.frankfurter.dev") and kwargs.get("params"):
elif args[0].startswith("https://api.frankfurter.app") and kwargs.get("params"):
if kwargs["params"].get("base") and kwargs["params"].get("symbols"):
date = args[0].replace("https://api.frankfurter.dev/v1/", "")
date = args[0].replace("https://api.frankfurter.app/", "")
if test_exchange_values.get(date):
return PatchResponse(
{"rates": {kwargs["params"].get("symbols"): test_exchange_values.get(date)}}, 200
@@ -149,7 +149,7 @@ class TestCurrencyExchange(unittest.TestCase):
self.assertEqual(flt(exchange_rate, 3), 65.1)
settings = frappe.get_single("Currency Exchange Settings")
settings.service_provider = "frankfurter.dev"
settings.service_provider = "frankfurter.app"
settings.save()
def test_exchange_rate_strict(self, mock_get):

View File

@@ -21,12 +21,6 @@ erpnext.setup.EmployeeController = class EmployeeController extends frappe.ui.fo
};
frappe.ui.form.on("Employee", {
setup: function (frm) {
frm.make_methods = {
"Bank Account": () => erpnext.utils.make_bank_account(frm.doc.doctype, frm.doc.name),
};
},
onload: function (frm) {
frm.set_query("department", function () {
return {

View File

@@ -2,10 +2,8 @@ frappe.listview_settings["Employee"] = {
add_fields: ["status", "branch", "department", "designation", "image"],
filters: [["status", "=", "Active"]],
get_indicator: function (doc) {
return [
__(doc.status, null, "Employee"),
{ Active: "green", Inactive: "red", Left: "gray", Suspended: "orange" }[doc.status],
"status,=," + doc.status,
];
var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status];
indicator[1] = { Active: "green", Inactive: "red", Left: "gray", Suspended: "orange" }[doc.status];
return indicator;
},
};

View File

@@ -5,10 +5,8 @@ import unittest
import frappe
import frappe.utils
from frappe.query_builder import Criterion
import erpnext
from erpnext.accounts.utils import build_qb_match_conditions
from erpnext.setup.doctype.employee.employee import InactiveEmployeeStatusError
test_records = frappe.get_test_records("Employee")
@@ -36,32 +34,6 @@ class TestEmployee(unittest.TestCase):
employee_doc.save()
self.assertTrue("Employee" not in frappe.get_roles(user))
def test_employee_user_permission(self):
employee1 = make_employee("employee_1_test@company.com", create_user_permission=1)
employee2 = make_employee("employee_2_test@company.com", create_user_permission=1)
make_employee("employee_3_test@company.com", create_user_permission=1)
employee1_doc = frappe.get_doc("Employee", employee1)
employee2_doc = frappe.get_doc("Employee", employee2)
employee2_doc.reload()
employee2_doc.reports_to = employee1_doc.name
employee2_doc.save()
frappe.set_user(employee1_doc.user_id)
Employee = frappe.qb.DocType("Employee")
qb_employee_list = (
frappe.qb.from_(Employee)
.select(Employee.name)
.where(Criterion.all(build_qb_match_conditions("Employee")))
.orderby(Employee.Name)
).run(pluck=Employee.name)
employee_list = frappe.db.get_list("Employee", pluck="name", order_by="name")
self.assertEqual(qb_employee_list, employee_list)
frappe.set_user("Administrator")
def tearDown(self):
frappe.db.rollback()

View File

@@ -309,9 +309,8 @@ class TransactionDeletionRecord(Document):
self.db_set("error_log", None)
def get_doctypes_to_be_ignored_list(self):
doctypes_to_be_ignored_list = frappe.get_all(
"DocType", or_filters=[["issingle", "=", 1], ["is_virtual", "=", 1]], pluck="name"
)
singles = frappe.get_all("DocType", filters={"issingle": 1}, pluck="name")
doctypes_to_be_ignored_list = singles
for doctype in self.doctypes_to_be_ignored:
doctypes_to_be_ignored_list.append(doctype.doctype_name)

View File

@@ -80,7 +80,7 @@ def setup_currency_exchange():
ces.set("result_key", [])
ces.set("req_params", [])
ces.api_endpoint = "https://api.frankfurter.dev/v1/{transaction_date}"
ces.api_endpoint = "https://api.frankfurter.app/{transaction_date}"
ces.append("result_key", {"key": "rates"})
ces.append("result_key", {"key": "{to_currency}"})
ces.append("req_params", {"key": "base", "value": "{from_currency}"})

View File

@@ -207,12 +207,13 @@ def get_or_create_account(company_name, account):
default_root_type = "Liability"
root_type = account.get("root_type", default_root_type)
or_filters = {"account_name": account.get("account_name")}
if account.get("account_number"):
or_filters.update({"account_number": account.get("account_number")})
existing_accounts = frappe.get_all(
"Account", filters={"company": company_name, "root_type": root_type}, or_filters=or_filters
"Account",
filters={"company": company_name, "root_type": root_type},
or_filters={
"account_name": account.get("account_name"),
"account_number": account.get("account_number"),
},
)
if existing_accounts:

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