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

chore: release v15
This commit is contained in:
diptanilsaha
2026-05-20 09:38:58 +05:30
committed by GitHub
18 changed files with 123 additions and 336 deletions

View File

@@ -30,16 +30,6 @@ frappe.ui.form.on("Accounts Settings", {
add_taxes_from_item_tax_template(frm) { add_taxes_from_item_tax_template(frm) {
toggle_tax_settings(frm, "add_taxes_from_item_tax_template"); toggle_tax_settings(frm, "add_taxes_from_item_tax_template");
}, },
drop_ar_procedures: function (frm) {
frm.call({
doc: frm.doc,
method: "drop_ar_sql_procedures",
callback: function (r) {
frappe.show_alert(__("Procedures dropped"), 5);
},
});
},
}); });
function toggle_tax_settings(frm, field_name) { function toggle_tax_settings(frm, field_name) {

View File

@@ -95,7 +95,6 @@
"receivable_payable_fetch_method", "receivable_payable_fetch_method",
"default_ageing_range", "default_ageing_range",
"column_break_ntmi", "column_break_ntmi",
"drop_ar_procedures",
"legacy_section", "legacy_section",
"ignore_is_opening_check_for_reporting", "ignore_is_opening_check_for_reporting",
"payment_request_settings", "payment_request_settings",
@@ -561,7 +560,7 @@
"fieldname": "receivable_payable_fetch_method", "fieldname": "receivable_payable_fetch_method",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Data Fetch Method", "label": "Data Fetch Method",
"options": "Buffered Cursor\nUnBuffered Cursor\nRaw SQL" "options": "Buffered Cursor\nUnBuffered Cursor"
}, },
{ {
"fieldname": "accounts_receivable_payable_tuning_section", "fieldname": "accounts_receivable_payable_tuning_section",
@@ -623,13 +622,6 @@
"fieldname": "column_break_ntmi", "fieldname": "column_break_ntmi",
"fieldtype": "Column Break" "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"
},
{ {
"default": "0", "default": "0",
"fieldname": "fetch_valuation_rate_for_internal_transaction", "fieldname": "fetch_valuation_rate_for_internal_transaction",
@@ -671,7 +663,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2026-03-06 14:49:11.467716", "modified": "2026-05-18 12:16:33.679345",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",
@@ -701,4 +693,4 @@
"sort_order": "ASC", "sort_order": "ASC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -60,7 +60,7 @@ class AccountsSettings(Document):
merge_similar_account_heads: DF.Check merge_similar_account_heads: DF.Check
over_billing_allowance: DF.Currency over_billing_allowance: DF.Currency
post_change_gl_entries: DF.Check post_change_gl_entries: DF.Check
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor", "Raw SQL"] receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"]
receivable_payable_remarks_length: DF.Int receivable_payable_remarks_length: DF.Int
reconciliation_queue_size: DF.Int reconciliation_queue_size: DF.Int
role_allowed_to_over_bill: DF.Link | None role_allowed_to_over_bill: DF.Link | None
@@ -170,11 +170,3 @@ class AccountsSettings(Document):
), ),
title=_("Auto Tax Settings Error"), title=_("Auto Tax Settings Error"),
) )
@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

@@ -725,31 +725,12 @@ frappe.ui.form.on("Payment Entry", {
if (!frm.doc.paid_from_account_currency || !frm.doc.company) return; if (!frm.doc.paid_from_account_currency || !frm.doc.company) return;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.paid_from_account_currency == company_currency) { frm.events.set_current_exchange_rate(
frm.set_value("source_exchange_rate", 1); frm,
} else if (frm.doc.paid_from) { "source_exchange_rate",
if (["Internal Transfer", "Pay"].includes(frm.doc.payment_type)) { frm.doc.paid_from_account_currency,
let company_currency = frappe.get_doc(":Company", frm.doc.company)?.default_currency; company_currency
frappe.call({ );
method: "erpnext.setup.utils.get_exchange_rate",
args: {
from_currency: frm.doc.paid_from_account_currency,
to_currency: company_currency,
transaction_date: frm.doc.posting_date,
},
callback: function (r, rt) {
frm.set_value("source_exchange_rate", r.message);
},
});
} else {
frm.events.set_current_exchange_rate(
frm,
"source_exchange_rate",
frm.doc.paid_from_account_currency,
company_currency
);
}
}
}, },
paid_to_account_currency: function (frm) { paid_to_account_currency: function (frm) {
@@ -781,49 +762,24 @@ frappe.ui.form.on("Payment Entry", {
posting_date: function (frm) { posting_date: function (frm) {
frm.events.paid_from_account_currency(frm); frm.events.paid_from_account_currency(frm);
frm.events.paid_to_account_currency(frm);
}, },
source_exchange_rate: function (frm) { source_exchange_rate: function (frm) {
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.paid_amount) {
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
// target exchange rate should always be same as source if both account currencies is same
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate);
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
} else if (company_currency == frm.doc.paid_to_account_currency) {
frm.set_value("received_amount", frm.doc.base_paid_amount);
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
}
// set_unallocated_amount is called by below method,
// no need trigger separately
frm.events.set_total_allocated_amount(frm);
}
// Make read only if Accounts Settings doesn't allow stale rates
frm.set_df_property("source_exchange_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
},
target_exchange_rate: function (frm) {
frm.set_paid_amount_based_on_received_amount = true; frm.set_paid_amount_based_on_received_amount = true;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.received_amount) { if (frm.doc.base_received_amount && frm.doc.source_exchange_rate) {
frm.set_value( frm.set_value("base_paid_amount", frm.doc.base_received_amount);
"base_received_amount",
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate)
);
if ( // target exchange rate should always be same as source if both account currencies is same
!frm.doc.source_exchange_rate && if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate);
) { } else {
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate); frm.set_value(
frm.set_value("base_paid_amount", frm.doc.base_received_amount); "paid_amount",
} else if (company_currency == frm.doc.paid_from_account_currency) { flt(frm.doc.base_paid_amount) / flt(frm.doc.source_exchange_rate)
frm.set_value("paid_amount", frm.doc.base_received_amount); );
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
} }
// set_unallocated_amount is called by below method, // set_unallocated_amount is called by below method,
@@ -832,6 +788,32 @@ frappe.ui.form.on("Payment Entry", {
} }
frm.set_paid_amount_based_on_received_amount = false; frm.set_paid_amount_based_on_received_amount = false;
// Make read only if Accounts Settings doesn't allow stale rates
frm.set_df_property("source_exchange_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
},
target_exchange_rate: function (frm) {
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.base_paid_amount && frm.doc.target_exchange_rate) {
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
if (
!frm.doc.source_exchange_rate &&
frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency
) {
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
} else {
frm.set_value(
"received_amount",
flt(frm.doc.base_received_amount) / flt(frm.doc.target_exchange_rate)
);
}
// set_unallocated_amount is called by below method,
// no need trigger separately
frm.events.set_total_allocated_amount(frm);
}
// Make read only if Accounts Settings doesn't allow stale rates // Make read only if Accounts Settings doesn't allow stale rates
frm.set_df_property("target_exchange_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1); frm.set_df_property("target_exchange_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
}, },

View File

@@ -350,7 +350,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"depends_on": "doc.received_amount", "depends_on": "eval:doc.received_amount;",
"fieldname": "base_received_amount", "fieldname": "base_received_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Received Amount (Company Currency)", "label": "Received Amount (Company Currency)",
@@ -800,7 +800,7 @@
"table_fieldname": "payment_entries" "table_fieldname": "payment_entries"
} }
], ],
"modified": "2025-05-15 18:01:04.013025", "modified": "2026-05-15 13:31:01.166010",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry", "name": "Payment Entry",

View File

@@ -669,7 +669,7 @@ def validate_due_date_with_template(posting_date, due_date, bill_date, template_
if not default_due_date: if not default_due_date:
return return
if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date): if getdate(default_due_date) != getdate(posting_date) and getdate(due_date) > getdate(default_due_date):
if frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles(): if frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles():
party_type = "supplier" if doctype == "Purchase Invoice" else "customer" party_type = "supplier" if doctype == "Purchase Invoice" else "customer"

View File

@@ -129,8 +129,6 @@ class ReceivablePayableReport:
self.fetch_ple_in_buffered_cursor() self.fetch_ple_in_buffered_cursor()
elif self.ple_fetch_method == "UnBuffered Cursor": elif self.ple_fetch_method == "UnBuffered Cursor":
self.fetch_ple_in_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 # Build delivery note map against all sales invoices
self.build_delivery_note_map() self.build_delivery_note_map()
@@ -321,81 +319,6 @@ class ReceivablePayableReport:
row.paid -= amount row.paid -= amount
row.paid_in_account_currency -= amount_in_account_currency 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,
project,
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",
"project",
]:
_d[field] = x.get(field)
self.voucher_balance[key] = _d
def update_sub_total_row(self, row, party): def update_sub_total_row(self, row, party):
total_row = self.total_row_map.get(party) total_row = self.total_row_map.get(party)
@@ -1390,136 +1313,3 @@ def get_party_group_with_children(party, party_groups):
frappe.throw(_("{0}: {1} does not exist").format(group_dtype, d)) frappe.throw(_("{0}: {1} does not exist").format(group_dtype, d))
return list(set(all_party_groups)) return list(set(all_party_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},
project {_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},
project {_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, ple.project, 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

@@ -786,19 +786,11 @@ class GrossProfitGenerator:
return self.calculate_buying_amount_from_sle( return self.calculate_buying_amount_from_sle(
row, my_sle, parenttype, parent, row.item_row, item_code row, my_sle, parenttype, parent, row.item_row, item_code
) )
elif self.delivery_notes.get((row.parent, row.item_code), None): elif row.item_row and self.delivery_notes.get(row.item_row):
# check if Invoice has delivery notes dn = self.delivery_notes[row.item_row]
dn = self.delivery_notes.get((row.parent, row.item_code)) if flt(dn.total_qty):
parenttype, parent, item_row, dn_warehouse = ( return flt(row.qty) * flt(dn.total_incoming_value) / flt(dn.total_qty)
"Delivery Note", return flt(row.qty) * self.get_average_buying_rate(row, item_code)
dn["delivery_note"],
dn["item_row"],
dn["warehouse"],
)
my_sle = self.get_stock_ledger_entries(item_code, dn_warehouse)
return self.calculate_buying_amount_from_sle(
row, my_sle, parenttype, parent, item_row, item_code
)
elif row.sales_order and row.so_detail: elif row.sales_order and row.so_detail:
incoming_amount = self.get_buying_amount_from_so_dn(row.sales_order, row.so_detail, item_code) incoming_amount = self.get_buying_amount_from_so_dn(row.sales_order, row.so_detail, item_code)
if incoming_amount: if incoming_amount:
@@ -1049,25 +1041,29 @@ class GrossProfitGenerator:
def get_delivery_notes(self): def get_delivery_notes(self):
self.delivery_notes = frappe._dict({}) self.delivery_notes = frappe._dict({})
if self.si_list: if self.si_list:
from frappe.query_builder.functions import Sum
invoices = [x.parent for x in self.si_list] invoices = [x.parent for x in self.si_list]
dni = qb.DocType("Delivery Note Item") dni = qb.DocType("Delivery Note Item")
delivery_notes = ( delivery_notes = (
qb.from_(dni) qb.from_(dni)
.select( .select(
dni.against_sales_invoice.as_("sales_invoice"), dni.si_detail,
dni.item_code, Sum(dni.stock_qty * dni.incoming_rate).as_("total_incoming_value"),
dni.warehouse, Sum(dni.stock_qty).as_("total_qty"),
dni.parent.as_("delivery_note"),
dni.name.as_("item_row"),
) )
.where((dni.docstatus == 1) & (dni.against_sales_invoice.isin(invoices))) .where(
.groupby(dni.against_sales_invoice, dni.item_code) (dni.docstatus == 1)
.orderby(dni.creation, order=Order.desc) & (dni.against_sales_invoice.isin(invoices))
& (dni.si_detail.isnotnull())
& (dni.si_detail != "")
)
.groupby(dni.si_detail)
.run(as_dict=True) .run(as_dict=True)
) )
for entry in delivery_notes: for entry in delivery_notes:
self.delivery_notes[(entry.sales_invoice, entry.item_code)] = entry self.delivery_notes[entry.si_detail] = entry
def group_items_by_invoice(self): def group_items_by_invoice(self):
""" """

View File

@@ -433,3 +433,4 @@ erpnext.patches.v15_0.replace_http_with_https_in_sales_partner
erpnext.patches.v16_0.add_portal_redirects erpnext.patches.v16_0.add_portal_redirects
erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po
erpnext.patches.v16_0.depends_on_inv_dimensions erpnext.patches.v16_0.depends_on_inv_dimensions
erpnext.patches.v16_0.clear_procedures_from_receivable_report

View File

@@ -0,0 +1,12 @@
import frappe
def execute():
frappe.db.sql("drop function if exists ar_genkey")
frappe.db.sql("drop procedure if exists ar_init_tmp_table")
frappe.db.sql("drop procedure if exists ar_allocate_to_tmp_table")
if frappe.db.get_single_value("Accounts Settings", "receivable_payable_fetch_method") == "Raw SQL":
frappe.db.set_single_value(
"Accounts Settings", "receivable_payable_fetch_method", "UnBuffered Cursor"
)

View File

@@ -6,6 +6,7 @@ import erpnext
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.regional.report.uae_vat_201.uae_vat_201 import ( from erpnext.regional.report.uae_vat_201.uae_vat_201 import (
execute,
get_exempt_total, get_exempt_total,
get_standard_rated_expenses_tax, get_standard_rated_expenses_tax,
get_standard_rated_expenses_total, get_standard_rated_expenses_total,
@@ -39,6 +40,13 @@ class TestUaeVat201(TestCase):
make_item("_Test UAE VAT Zero Rated Item", properties={"is_zero_rated": 1, "is_exempt": 0}) make_item("_Test UAE VAT Zero Rated Item", properties={"is_zero_rated": 1, "is_exempt": 0})
make_item("_Test UAE VAT Exempt Item", properties={"is_zero_rated": 0, "is_exempt": 1}) make_item("_Test UAE VAT Exempt Item", properties={"is_zero_rated": 0, "is_exempt": 1})
def test_validate_company_region(self):
self.assertRaises(
frappe.exceptions.ValidationError,
execute,
{"company": "_Test Company"},
)
def test_uae_vat_201_report(self): def test_uae_vat_201_report(self):
make_sales_invoices() make_sales_invoices()
create_purchase_invoices() create_purchase_invoices()

View File

@@ -10,6 +10,13 @@ frappe.query_reports["UAE VAT 201"] = {
options: "Company", options: "Company",
reqd: 1, reqd: 1,
default: frappe.defaults.get_user_default("Company"), default: frappe.defaults.get_user_default("Company"),
get_query: function () {
return {
filters: {
country: "United Arab Emirates",
},
};
},
}, },
{ {
fieldname: "from_date", fieldname: "from_date",

View File

@@ -5,13 +5,25 @@
import frappe import frappe
from frappe import _ from frappe import _
from erpnext import get_region
def execute(filters=None): def execute(filters=None):
validate_company_region(filters)
columns = get_columns() columns = get_columns()
data, emirates, amounts_by_emirate = get_data(filters) data, emirates, amounts_by_emirate = get_data(filters)
return columns, data return columns, data
def validate_company_region(filters):
if filters.get("company") and get_region(filters.get("company")) != "United Arab Emirates":
frappe.throw(
_(
"The company {0} is not in United Arab Emirates. UAE VAT 201 report is only available for companies in United Arab Emirates."
).format(frappe.bold(filters.get("company")))
)
def get_columns(): def get_columns():
"""Creates a list of dictionaries that are used to generate column headers of the data table.""" """Creates a list of dictionaries that are used to generate column headers of the data table."""
return [ return [

View File

@@ -88,7 +88,7 @@ def get_all_customers(date_range, company, field, limit=None):
@frappe.whitelist() @frappe.whitelist()
def get_all_items(date_range, company, field, limit=None): def get_all_items(date_range: str, company: str, field: str, limit: int | None = None):
if field in ("available_stock_qty", "available_stock_value"): if field in ("available_stock_qty", "available_stock_value"):
select_field = "sum(actual_qty)" if field == "available_stock_qty" else "sum(stock_value)" select_field = "sum(actual_qty)" if field == "available_stock_qty" else "sum(stock_value)"
results = frappe.db.get_all( results = frappe.db.get_all(
@@ -103,21 +103,21 @@ def get_all_items(date_range, company, field, limit=None):
else: else:
if field == "total_sales_amount": if field == "total_sales_amount":
select_field = "base_net_amount" select_field = "base_net_amount"
select_doctype = "Sales Order" select_doctype = "Sales Invoice"
elif field == "total_purchase_amount": elif field == "total_purchase_amount":
select_field = "base_net_amount" select_field = "base_net_amount"
select_doctype = "Purchase Order" select_doctype = "Purchase Invoice"
elif field == "total_qty_sold": elif field == "total_qty_sold":
select_field = "stock_qty" select_field = "stock_qty"
select_doctype = "Sales Order" select_doctype = "Sales Invoice"
elif field == "total_qty_purchased": elif field == "total_qty_purchased":
select_field = "stock_qty" select_field = "stock_qty"
select_doctype = "Purchase Order" select_doctype = "Purchase Invoice"
filters = [["docstatus", "=", "1"], ["company", "=", company]] filters = [["docstatus", "=", "1"], ["company", "=", company]]
from_date, to_date = parse_date_range(date_range) from_date, to_date = parse_date_range(date_range)
if from_date and to_date: if from_date and to_date:
filters.append(["transaction_date", "between", [from_date, to_date]]) filters.append(["posting_date", "between", [from_date, to_date]])
child_doctype = f"{select_doctype} Item" child_doctype = f"{select_doctype} Item"
return frappe.get_list( return frappe.get_list(

View File

@@ -774,7 +774,7 @@ class StockEntry(StockController):
else: else:
frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx)) frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx))
if self.purpose == "Manufacture": if self.purpose in ["Manufacture", "Repack"]:
if d.is_finished_item or d.is_scrap_item: if d.is_finished_item or d.is_scrap_item:
d.s_warehouse = None d.s_warehouse = None
if not d.t_warehouse: if not d.t_warehouse:

View File

@@ -82,6 +82,7 @@ class StockReconciliation(StockController):
self.set_total_qty_and_amount() self.set_total_qty_and_amount()
self.validate_putaway_capacity() self.validate_putaway_capacity()
self.validate_inventory_dimension() self.validate_inventory_dimension()
self.validate_uom_is_integer("stock_uom", "qty")
if self._action == "submit": if self._action == "submit":
self.validate_reserved_stock() self.validate_reserved_stock()

View File

@@ -1044,7 +1044,7 @@ def insert_item_price(args):
) )
item_price.insert() item_price.insert()
frappe.msgprint( frappe.msgprint(
_("Item Price Added for {0} in Price List {1}").format( _("Item Price added for {0} in Price List - {1}").format(
get_link_to_form("Item", args.item_code), args.price_list get_link_to_form("Item", args.item_code), args.price_list
), ),
alert=True, alert=True,
@@ -1070,7 +1070,9 @@ def insert_item_price(args):
) )
item_price.insert() item_price.insert()
frappe.msgprint( frappe.msgprint(
_("Item Price added for {0} in Price List {1}").format(args.item_code, args.price_list), _("Item Price added for {0} in Price List - {1}").format(
get_link_to_form("Item", args.item_code), args.price_list
),
alert=True, alert=True,
) )

View File

@@ -2480,7 +2480,9 @@ def get_stock_value_difference(
) )
if voucher_detail_no: if voucher_detail_no:
query = query.where(table.voucher_detail_no != voucher_detail_no) query = query.where(
(table.voucher_detail_no != voucher_detail_no) | (table.voucher_detail_no.isnull())
)
elif voucher_no: elif voucher_no:
query = query.where(table.voucher_no != voucher_no) query = query.where(table.voucher_no != voucher_no)