mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-08 23:52:57 +00:00
Merge branch 'version-16-hotfix' into mergify/bp/version-16-hotfix/pr-53588
This commit is contained in:
3
.github/workflows/linters.yml
vendored
3
.github/workflows/linters.yml
vendored
@@ -43,3 +43,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Semgrep rules
|
- name: Run Semgrep rules
|
||||||
run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness
|
run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness
|
||||||
|
|
||||||
|
- name: Semgrep for Test Correctness
|
||||||
|
run: semgrep ci --include=**/test_*.py --config ./semgrep/test-correctness.yml
|
||||||
|
|||||||
@@ -52,32 +52,9 @@ frappe.treeview_settings["Account"] = {
|
|||||||
],
|
],
|
||||||
root_label: "Accounts",
|
root_label: "Accounts",
|
||||||
get_tree_nodes: "erpnext.accounts.utils.get_children",
|
get_tree_nodes: "erpnext.accounts.utils.get_children",
|
||||||
on_get_node: function (nodes, deep = false) {
|
on_node_render: function (node, deep) {
|
||||||
if (frappe.boot.user.can_read.indexOf("GL Entry") == -1) return;
|
const render_balances = () => {
|
||||||
|
for (let account of cur_tree.account_balance_data) {
|
||||||
let accounts = [];
|
|
||||||
if (deep) {
|
|
||||||
// in case of `get_all_nodes`
|
|
||||||
accounts = nodes.reduce((acc, node) => [...acc, ...node.data], []);
|
|
||||||
} else {
|
|
||||||
accounts = nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
frappe.db.get_single_value("Accounts Settings", "show_balance_in_coa").then((value) => {
|
|
||||||
if (value) {
|
|
||||||
const get_balances = frappe.call({
|
|
||||||
method: "erpnext.accounts.utils.get_account_balances",
|
|
||||||
args: {
|
|
||||||
accounts: accounts,
|
|
||||||
company: cur_tree.args.company,
|
|
||||||
include_default_fb_balances: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
get_balances.then((r) => {
|
|
||||||
if (!r.message || r.message.length == 0) return;
|
|
||||||
|
|
||||||
for (let account of r.message) {
|
|
||||||
const node = cur_tree.nodes && cur_tree.nodes[account.value];
|
const node = cur_tree.nodes && cur_tree.nodes[account.value];
|
||||||
if (!node || node.is_root) continue;
|
if (!node || node.is_root) continue;
|
||||||
|
|
||||||
@@ -90,11 +67,9 @@ frappe.treeview_settings["Account"] = {
|
|||||||
node.parent && node.parent.find(".balance-area").remove();
|
node.parent && node.parent.find(".balance-area").remove();
|
||||||
$(
|
$(
|
||||||
'<span class="balance-area pull-right">' +
|
'<span class="balance-area pull-right">' +
|
||||||
(account.balance_in_account_currency
|
(account.account_currency != account.company_currency
|
||||||
? format(
|
? format(account.balance_in_account_currency, account.account_currency) +
|
||||||
account.balance_in_account_currency,
|
" / "
|
||||||
account.account_currency
|
|
||||||
) + " / "
|
|
||||||
: "") +
|
: "") +
|
||||||
format(account.balance, account.company_currency) +
|
format(account.balance, account.company_currency) +
|
||||||
" " +
|
" " +
|
||||||
@@ -103,9 +78,29 @@ frappe.treeview_settings["Account"] = {
|
|||||||
).insertBefore(node.$ul);
|
).insertBefore(node.$ul);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (frappe.boot.user.can_read.indexOf("GL Entry") == -1) return;
|
||||||
|
if (!cur_tree.account_balance_data) {
|
||||||
|
frappe.db.get_single_value("Accounts Settings", "show_balance_in_coa").then((value) => {
|
||||||
|
if (value) {
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.accounts.utils.get_account_balances_coa",
|
||||||
|
args: {
|
||||||
|
company: cur_tree.args.company,
|
||||||
|
include_default_fb_balances: true,
|
||||||
|
},
|
||||||
|
callback: function (r) {
|
||||||
|
if (!r.message || r.message.length === 0) return;
|
||||||
|
cur_tree.account_balance_data = r.message || [];
|
||||||
|
render_balances();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
render_balances();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
add_tree_node: "erpnext.accounts.utils.add_ac",
|
add_tree_node: "erpnext.accounts.utils.add_ac",
|
||||||
menu_items: [
|
menu_items: [
|
||||||
|
|||||||
@@ -26,8 +26,13 @@
|
|||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [
|
||||||
"modified": "2025-10-15 03:19:47.171349",
|
{
|
||||||
|
"link_doctype": "Account",
|
||||||
|
"link_fieldname": "account_category"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"modified": "2026-02-23 01:19:49.589393",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Account Category",
|
"name": "Account Category",
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ from erpnext.tests.utils import ERPNextTestSuite
|
|||||||
|
|
||||||
|
|
||||||
class TestAccountingDimension(ERPNextTestSuite):
|
class TestAccountingDimension(ERPNextTestSuite):
|
||||||
def setUp(self):
|
|
||||||
create_dimension()
|
|
||||||
|
|
||||||
def test_dimension_against_sales_invoice(self):
|
def test_dimension_against_sales_invoice(self):
|
||||||
si = create_sales_invoice(do_not_save=1)
|
si = create_sales_invoice(do_not_save=1)
|
||||||
|
|
||||||
@@ -77,63 +74,3 @@ class TestAccountingDimension(ERPNextTestSuite):
|
|||||||
|
|
||||||
si.save()
|
si.save()
|
||||||
self.assertRaises(frappe.ValidationError, si.submit)
|
self.assertRaises(frappe.ValidationError, si.submit)
|
||||||
|
|
||||||
|
|
||||||
def create_dimension():
|
|
||||||
frappe.set_user("Administrator")
|
|
||||||
|
|
||||||
if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
|
|
||||||
dimension = frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "Accounting Dimension",
|
|
||||||
"document_type": "Department",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
dimension.append(
|
|
||||||
"dimension_defaults",
|
|
||||||
{
|
|
||||||
"company": "_Test Company",
|
|
||||||
"reference_document": "Department",
|
|
||||||
"default_dimension": "_Test Department - _TC",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
dimension.insert()
|
|
||||||
dimension.save()
|
|
||||||
else:
|
|
||||||
dimension = frappe.get_doc("Accounting Dimension", "Department")
|
|
||||||
dimension.disabled = 0
|
|
||||||
dimension.save()
|
|
||||||
|
|
||||||
if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}):
|
|
||||||
dimension1 = frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "Accounting Dimension",
|
|
||||||
"document_type": "Location",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
dimension1.append(
|
|
||||||
"dimension_defaults",
|
|
||||||
{
|
|
||||||
"company": "_Test Company",
|
|
||||||
"reference_document": "Location",
|
|
||||||
"default_dimension": "Block 1",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
dimension1.insert()
|
|
||||||
dimension1.save()
|
|
||||||
else:
|
|
||||||
dimension1 = frappe.get_doc("Accounting Dimension", "Location")
|
|
||||||
dimension1.disabled = 0
|
|
||||||
dimension1.save()
|
|
||||||
|
|
||||||
|
|
||||||
def disable_dimension():
|
|
||||||
dimension1 = frappe.get_doc("Accounting Dimension", "Department")
|
|
||||||
dimension1.disabled = 1
|
|
||||||
dimension1.save()
|
|
||||||
|
|
||||||
dimension2 = frappe.get_doc("Accounting Dimension", "Location")
|
|
||||||
dimension2.disabled = 1
|
|
||||||
dimension2.save()
|
|
||||||
|
|||||||
@@ -5,10 +5,6 @@ import unittest
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
|
|
||||||
create_dimension,
|
|
||||||
disable_dimension,
|
|
||||||
)
|
|
||||||
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.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
|
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
|
||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
@@ -16,7 +12,6 @@ from erpnext.tests.utils import ERPNextTestSuite
|
|||||||
|
|
||||||
class TestAccountingDimensionFilter(ERPNextTestSuite):
|
class TestAccountingDimensionFilter(ERPNextTestSuite):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
create_dimension()
|
|
||||||
create_accounting_dimension_filter()
|
create_accounting_dimension_filter()
|
||||||
self.invoice_list = []
|
self.invoice_list = []
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@
|
|||||||
"payment_options_section",
|
"payment_options_section",
|
||||||
"enable_loyalty_point_program",
|
"enable_loyalty_point_program",
|
||||||
"column_break_ctam",
|
"column_break_ctam",
|
||||||
|
"fetch_payment_schedule_in_payment_request",
|
||||||
"invoicing_settings_tab",
|
"invoicing_settings_tab",
|
||||||
"accounts_transactions_settings_section",
|
"accounts_transactions_settings_section",
|
||||||
"over_billing_allowance",
|
"over_billing_allowance",
|
||||||
@@ -688,6 +689,19 @@
|
|||||||
"fieldname": "enable_accounting_dimensions",
|
"fieldname": "enable_accounting_dimensions",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Enable Accounting Dimensions"
|
"label": "Enable Accounting Dimensions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"description": "Enable Subscription tracking in invoice",
|
||||||
|
"fieldname": "enable_subscription",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Enable Subscription"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "fetch_payment_schedule_in_payment_request",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Fetch Payment Schedule In Payment Request"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
@@ -697,7 +711,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-02-27 01:04:09.415288",
|
"modified": "2026-03-30 07:32:58.182018",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts Settings",
|
"name": "Accounts Settings",
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ class AccountsSettings(Document):
|
|||||||
enable_loyalty_point_program: DF.Check
|
enable_loyalty_point_program: DF.Check
|
||||||
enable_party_matching: DF.Check
|
enable_party_matching: DF.Check
|
||||||
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
|
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
|
||||||
|
fetch_payment_schedule_in_payment_request: DF.Check
|
||||||
fetch_valuation_rate_for_internal_transaction: DF.Check
|
fetch_valuation_rate_for_internal_transaction: DF.Check
|
||||||
general_ledger_remarks_length: DF.Int
|
general_ledger_remarks_length: DF.Int
|
||||||
ignore_account_closing_balance: DF.Check
|
ignore_account_closing_balance: DF.Check
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
|
|||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
|
||||||
class TestAdvancePaymentLedgerEntry(AccountsTestMixin, ERPNextTestSuite):
|
class TestAdvancePaymentLedgerEntry(ERPNextTestSuite, AccountsTestMixin):
|
||||||
"""
|
"""
|
||||||
Integration tests for AdvancePaymentLedgerEntry.
|
Integration tests for AdvancePaymentLedgerEntry.
|
||||||
Use this class for testing interactions between multiple components.
|
Use this class for testing interactions between multiple components.
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ def get_default_company_bank_account(company, party_type, party):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_bank_account_details(bank_account):
|
def get_bank_account_details(bank_account):
|
||||||
|
frappe.has_permission("Bank Account", doc=bank_account, ptype="read", throw=True)
|
||||||
return frappe.get_cached_value(
|
return frappe.get_cached_value(
|
||||||
"Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1
|
"Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
|||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
|
||||||
class TestBankReconciliationTool(AccountsTestMixin, ERPNextTestSuite):
|
class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.create_company()
|
self.create_company()
|
||||||
self.create_customer()
|
self.create_customer()
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
|
|||||||
frappe.get_doc(
|
frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Customer",
|
"doctype": "Customer",
|
||||||
"customer_group": "All Customer Groups",
|
"customer_group": "Individual",
|
||||||
"customer_type": "Company",
|
"customer_type": "Company",
|
||||||
"customer_name": "Poore Simon's",
|
"customer_name": "Poore Simon's",
|
||||||
}
|
}
|
||||||
@@ -413,7 +413,7 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
|
|||||||
frappe.get_doc(
|
frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Customer",
|
"doctype": "Customer",
|
||||||
"customer_group": "All Customer Groups",
|
"customer_group": "Individual",
|
||||||
"customer_type": "Company",
|
"customer_type": "Company",
|
||||||
"customer_name": "Fayva",
|
"customer_name": "Fayva",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests import UnitTestCase
|
|
||||||
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
|
||||||
class TestBankTransactionFees(UnitTestCase):
|
class TestBankTransactionFees(ERPNextTestSuite):
|
||||||
def test_included_fee_throws(self):
|
def test_included_fee_throws(self):
|
||||||
"""A fee that's part of a withdrawal cannot be bigger than the
|
"""A fee that's part of a withdrawal cannot be bigger than the
|
||||||
withdrawal itself."""
|
withdrawal itself."""
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
|||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
|
||||||
class TestExchangeRateRevaluation(AccountsTestMixin, ERPNextTestSuite):
|
class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.create_company()
|
self.create_company()
|
||||||
self.create_usd_receivable_account()
|
self.create_usd_receivable_account()
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
frappe.ui.form.on("Financial Report Template", {
|
frappe.ui.form.on("Financial Report Template", {
|
||||||
refresh(frm) {
|
refresh(frm) {
|
||||||
|
if (frm.is_new() || frm.doc.rows.length === 0) return;
|
||||||
|
|
||||||
// add custom button to view missed accounts
|
// add custom button to view missed accounts
|
||||||
frm.add_custom_button(__("View Account Coverage"), function () {
|
frm.add_custom_button(__("View Account Coverage"), function () {
|
||||||
let selected_rows = frm.get_field("rows").grid.get_selected_children();
|
let selected_rows = frm.get_field("rows").grid.get_selected_children();
|
||||||
@@ -20,7 +22,7 @@ frappe.ui.form.on("Financial Report Template", {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
validate(frm) {
|
after_save(frm) {
|
||||||
if (!frm.doc.rows || frm.doc.rows.length === 0) {
|
if (!frm.doc.rows || frm.doc.rows.length === 0) {
|
||||||
frappe.msgprint(__("At least one row is required for a financial report template"));
|
frappe.msgprint(__("At least one row is required for a financial report template"));
|
||||||
}
|
}
|
||||||
@@ -34,14 +36,6 @@ frappe.ui.form.on("Financial Report Row", {
|
|||||||
update_formula_label(frm, row.data_source);
|
update_formula_label(frm, row.data_source);
|
||||||
update_formula_description(frm, row.data_source);
|
update_formula_description(frm, row.data_source);
|
||||||
|
|
||||||
if (row.data_source !== "Account Data") {
|
|
||||||
frappe.model.set_value(cdt, cdn, "balance_type", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (["Blank Line", "Column Break", "Section Break"].includes(row.data_source)) {
|
|
||||||
frappe.model.set_value(cdt, cdn, "calculation_formula", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
set_up_filters_editor(frm, cdt, cdn);
|
set_up_filters_editor(frm, cdt, cdn);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -322,6 +316,8 @@ function update_formula_description(frm, data_source) {
|
|||||||
const list_style = `style="margin-bottom: var(--margin-sm); color: var(--text-muted); font-size: 0.9em;"`;
|
const list_style = `style="margin-bottom: var(--margin-sm); color: var(--text-muted); font-size: 0.9em;"`;
|
||||||
const note_style = `style="margin-bottom: 0; color: var(--text-muted); font-size: 0.9em;"`;
|
const note_style = `style="margin-bottom: 0; color: var(--text-muted); font-size: 0.9em;"`;
|
||||||
const tip_style = `style="margin-bottom: 0; color: var(--text-color); font-size: 0.85em;"`;
|
const tip_style = `style="margin-bottom: 0; color: var(--text-color); font-size: 0.85em;"`;
|
||||||
|
const code_style = `style="background: var(--bg-light-gray); padding: var(--padding-xs); border-radius: var(--border-radius); font-size: 0.85em; width: max-content; margin-bottom: var(--margin-sm);"`;
|
||||||
|
const pre_style = `style="margin: 0; border-radius: var(--border-radius)"`;
|
||||||
|
|
||||||
let description_html = "";
|
let description_html = "";
|
||||||
|
|
||||||
@@ -382,8 +378,13 @@ function update_formula_description(frm, data_source) {
|
|||||||
<li><code>my_app.financial_reports.get_kpi_data</code></li>
|
<li><code>my_app.financial_reports.get_kpi_data</code></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<h6 ${subtitle_style}>Method Signature:</h6>
|
||||||
|
<div ${code_style}>
|
||||||
|
<pre ${pre_style}>def get_custom_data(filters, periods, row): <br> # filters: dict — report filters (company, period, etc.) <br> # periods: list[dict] — period definitions <br> # row: dict — the current report row <br><br> return [1000.0, 1200.0, 1150.0] # one value per period</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h6 ${subtitle_style}>Return Format:</h6>
|
<h6 ${subtitle_style}>Return Format:</h6>
|
||||||
<p ${text_style}>Numbers for each period: <code>[1000.0, 1200.0, 1150.0]</code></p>
|
<p ${text_style}>A list of numbers, one for each period: <code>[1000.0, 1200.0, 1150.0]</code></p>
|
||||||
</div>`;
|
</div>`;
|
||||||
} else if (data_source === "Blank Line") {
|
} else if (data_source === "Blank Line") {
|
||||||
description_html = `
|
description_html = `
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_rename": 1,
|
|
||||||
"autoname": "field:template_name",
|
"autoname": "field:template_name",
|
||||||
"creation": "2025-08-02 04:44:15.184541",
|
"creation": "2025-08-02 04:44:15.184541",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
@@ -31,7 +30,8 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Report Type",
|
"label": "Report Type",
|
||||||
"options": "\nProfit and Loss Statement\nBalance Sheet\nCash Flow\nCustom Financial Statement"
|
"options": "\nProfit and Loss Statement\nBalance Sheet\nCash Flow\nCustom Financial Statement",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:frappe.boot.developer_mode",
|
"depends_on": "eval:frappe.boot.developer_mode",
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-11-14 00:11:03.508139",
|
"modified": "2026-02-23 01:04:05.797161",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Financial Report Template",
|
"name": "Financial Report Template",
|
||||||
|
|||||||
@@ -32,6 +32,19 @@ class FinancialReportTemplate(Document):
|
|||||||
template_name: DF.Data
|
template_name: DF.Data
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
|
def before_validate(self):
|
||||||
|
self.clear_hidden_fields()
|
||||||
|
|
||||||
|
def clear_hidden_fields(self):
|
||||||
|
style_data_sources = {"Blank Line", "Column Break", "Section Break"}
|
||||||
|
|
||||||
|
for row in self.rows:
|
||||||
|
if row.data_source != "Account Data":
|
||||||
|
row.balance_type = None
|
||||||
|
|
||||||
|
if row.data_source in style_data_sources:
|
||||||
|
row.calculation_formula = None
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
validator = TemplateValidator(self)
|
validator = TemplateValidator(self)
|
||||||
result = validator.validate()
|
result = validator.validate()
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ class ValidationResult:
|
|||||||
self.warnings.append(issue)
|
self.warnings.append(issue)
|
||||||
|
|
||||||
def notify_user(self) -> None:
|
def notify_user(self) -> None:
|
||||||
warnings = "<br><br>".join(str(w) for w in self.warnings)
|
warnings = "<br><br>".join(str(w) for w in self.warnings if w)
|
||||||
errors = "<br><br>".join(str(e) for e in self.issues)
|
errors = "<br><br>".join(str(e) for e in self.issues if e)
|
||||||
|
|
||||||
if warnings:
|
if warnings:
|
||||||
frappe.msgprint(warnings, title=_("Warnings"), indicator="orange")
|
frappe.msgprint(warnings, title=_("Warnings"), indicator="orange")
|
||||||
@@ -99,9 +99,8 @@ class TemplateValidator:
|
|||||||
result.merge(validator.validate(self.template))
|
result.merge(validator.validate(self.template))
|
||||||
|
|
||||||
# Run row-level validations
|
# Run row-level validations
|
||||||
account_fields = {field.fieldname for field in frappe.get_meta("Account").fields}
|
|
||||||
for row in self.template.rows:
|
for row in self.template.rows:
|
||||||
result.merge(self.formula_validator.validate(row, account_fields))
|
result.merge(self.formula_validator.validate(row))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -383,7 +382,8 @@ class AccountFilterValidator(Validator):
|
|||||||
"""Validates account filter expressions used in Account Data rows"""
|
"""Validates account filter expressions used in Account Data rows"""
|
||||||
|
|
||||||
def __init__(self, account_fields: set | None = None):
|
def __init__(self, account_fields: set | None = None):
|
||||||
self.account_fields = account_fields or set(frappe.get_meta("Account")._valid_columns)
|
self.account_meta = frappe.get_meta("Account")
|
||||||
|
self.account_fields = account_fields or set(self.account_meta._valid_columns)
|
||||||
|
|
||||||
def validate(self, row) -> ValidationResult:
|
def validate(self, row) -> ValidationResult:
|
||||||
result = ValidationResult()
|
result = ValidationResult()
|
||||||
@@ -403,7 +403,11 @@ class AccountFilterValidator(Validator):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
filter_config = json.loads(row.calculation_formula)
|
filter_config = json.loads(row.calculation_formula)
|
||||||
error = self._validate_filter_structure(filter_config, self.account_fields)
|
error = self._validate_filter_structure(
|
||||||
|
filter_config,
|
||||||
|
self.account_fields,
|
||||||
|
row.advanced_filtering,
|
||||||
|
)
|
||||||
|
|
||||||
if error:
|
if error:
|
||||||
result.add_error(
|
result.add_error(
|
||||||
@@ -425,7 +429,12 @@ class AccountFilterValidator(Validator):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _validate_filter_structure(self, filter_config, account_fields: set) -> str | None:
|
def _validate_filter_structure(
|
||||||
|
self,
|
||||||
|
filter_config,
|
||||||
|
account_fields: set,
|
||||||
|
advanced_filtering: bool = False,
|
||||||
|
) -> str | None:
|
||||||
# simple condition: [field, operator, value]
|
# simple condition: [field, operator, value]
|
||||||
if isinstance(filter_config, list):
|
if isinstance(filter_config, list):
|
||||||
if len(filter_config) != 3:
|
if len(filter_config) != 3:
|
||||||
@@ -436,8 +445,10 @@ class AccountFilterValidator(Validator):
|
|||||||
if not isinstance(field, str) or not isinstance(operator, str):
|
if not isinstance(field, str) or not isinstance(operator, str):
|
||||||
return "Field and operator must be strings"
|
return "Field and operator must be strings"
|
||||||
|
|
||||||
|
display = (field if advanced_filtering else self.account_meta.get_label(field)) or field
|
||||||
|
|
||||||
if field not in account_fields:
|
if field not in account_fields:
|
||||||
return f"Field '{field}' is not a valid account field"
|
return f"Field '{display}' is not a valid Account field"
|
||||||
|
|
||||||
if operator.casefold() not in OPERATOR_MAP:
|
if operator.casefold() not in OPERATOR_MAP:
|
||||||
return f"Invalid operator '{operator}'"
|
return f"Invalid operator '{operator}'"
|
||||||
@@ -460,7 +471,7 @@ class AccountFilterValidator(Validator):
|
|||||||
|
|
||||||
# recursive
|
# recursive
|
||||||
for condition in conditions:
|
for condition in conditions:
|
||||||
error = self._validate_filter_structure(condition, account_fields)
|
error = self._validate_filter_structure(condition, account_fields, advanced_filtering)
|
||||||
if error:
|
if error:
|
||||||
return error
|
return error
|
||||||
else:
|
else:
|
||||||
@@ -476,7 +487,7 @@ class FormulaValidator(Validator):
|
|||||||
self.calculation_validator = CalculationFormulaValidator(reference_codes)
|
self.calculation_validator = CalculationFormulaValidator(reference_codes)
|
||||||
self.account_filter_validator = AccountFilterValidator()
|
self.account_filter_validator = AccountFilterValidator()
|
||||||
|
|
||||||
def validate(self, row, account_fields: set) -> ValidationResult:
|
def validate(self, row) -> ValidationResult:
|
||||||
result = ValidationResult()
|
result = ValidationResult()
|
||||||
|
|
||||||
if not row.calculation_formula:
|
if not row.calculation_formula:
|
||||||
@@ -486,9 +497,6 @@ class FormulaValidator(Validator):
|
|||||||
return self.calculation_validator.validate(row)
|
return self.calculation_validator.validate(row)
|
||||||
|
|
||||||
elif row.data_source == "Account Data":
|
elif row.data_source == "Account Data":
|
||||||
# Update account fields if provided
|
|
||||||
if account_fields:
|
|
||||||
self.account_filter_validator.account_fields = account_fields
|
|
||||||
return self.account_filter_validator.validate(row)
|
return self.account_filter_validator.validate(row)
|
||||||
|
|
||||||
elif row.data_source == "Custom API":
|
elif row.data_source == "Custom API":
|
||||||
|
|||||||
@@ -1295,6 +1295,7 @@ class TestFilterExpressionParser(FinancialReportTemplateTestCase):
|
|||||||
self.data_source = "Account Data"
|
self.data_source = "Account Data"
|
||||||
self.idx = 1
|
self.idx = 1
|
||||||
self.reverse_sign = 0
|
self.reverse_sign = 0
|
||||||
|
self.advanced_filtering = True
|
||||||
|
|
||||||
return MockReportRow(formula, reference_code)
|
return MockReportRow(formula, reference_code)
|
||||||
|
|
||||||
|
|||||||
@@ -489,4 +489,5 @@ def rename_temporarily_named_docs(doctype):
|
|||||||
for hook in frappe.get_hooks(hook_type):
|
for hook in frappe.get_hooks(hook_type):
|
||||||
frappe.call(hook, newname=newname, oldname=oldname)
|
frappe.call(hook, newname=newname, oldname=oldname)
|
||||||
|
|
||||||
|
if not frappe.in_test:
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|||||||
@@ -353,8 +353,11 @@ class JournalEntry(AccountsController):
|
|||||||
frappe.throw(_("Account {0} should be of type Expense").format(d.account))
|
frappe.throw(_("Account {0} should be of type Expense").format(d.account))
|
||||||
|
|
||||||
def validate_stock_accounts(self):
|
def validate_stock_accounts(self):
|
||||||
if self.voucher_type == "Periodic Accounting Entry":
|
if (
|
||||||
# Skip validation for periodic accounting entry
|
not erpnext.is_perpetual_inventory_enabled(self.company)
|
||||||
|
or self.voucher_type == "Periodic Accounting Entry"
|
||||||
|
):
|
||||||
|
# Skip validation for periodic accounting entry and Perpetual Inventory Disabled Company.
|
||||||
return
|
return
|
||||||
|
|
||||||
stock_accounts = get_stock_accounts(self.company, accounts=self.accounts)
|
stock_accounts = get_stock_accounts(self.company, accounts=self.accounts)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from erpnext.accounts.utils import run_ledger_health_checks
|
|||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
|
||||||
class TestLedgerHealth(AccountsTestMixin, ERPNextTestSuite):
|
class TestLedgerHealth(ERPNextTestSuite, AccountsTestMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.create_company()
|
self.create_company()
|
||||||
self.create_customer()
|
self.create_customer()
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ def start_merge(docname):
|
|||||||
ledger_merge.account,
|
ledger_merge.account,
|
||||||
)
|
)
|
||||||
row.db_set("merged", 1)
|
row.db_set("merged", 1)
|
||||||
|
if not frappe.in_test:
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
successful_merges += 1
|
successful_merges += 1
|
||||||
frappe.publish_realtime(
|
frappe.publish_realtime(
|
||||||
@@ -78,6 +79,7 @@ def start_merge(docname):
|
|||||||
{"ledger_merge": ledger_merge.name, "current": successful_merges, "total": total},
|
{"ledger_merge": ledger_merge.name, "current": successful_merges, "total": total},
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
if not frappe.in_test:
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
ledger_merge.log_error("Ledger merge failed")
|
ledger_merge.log_error("Ledger merge failed")
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -70,9 +70,7 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (frm.doc.create_missing_party) {
|
frm.trigger("update_party_labels");
|
||||||
frm.set_df_property("party", "fieldtype", "Data", frm.doc.name, "invoices");
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setup_company_filters: function (frm) {
|
setup_company_filters: function (frm) {
|
||||||
@@ -127,7 +125,9 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
|
|||||||
frappe.model.set_value(row.doctype, row.name, "party", "");
|
frappe.model.set_value(row.doctype, row.name, "party", "");
|
||||||
frappe.model.set_value(row.doctype, row.name, "party_name", "");
|
frappe.model.set_value(row.doctype, row.name, "party_name", "");
|
||||||
});
|
});
|
||||||
|
frm.clear_table("invoices");
|
||||||
frm.refresh_fields();
|
frm.refresh_fields();
|
||||||
|
frm.trigger("update_party_labels");
|
||||||
},
|
},
|
||||||
|
|
||||||
make_dashboard: function (frm) {
|
make_dashboard: function (frm) {
|
||||||
@@ -175,6 +175,32 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
|
|||||||
}
|
}
|
||||||
frm.refresh_field("invoices");
|
frm.refresh_field("invoices");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
update_party_labels: function (frm) {
|
||||||
|
let is_sales = frm.doc.invoice_type == "Sales";
|
||||||
|
|
||||||
|
frm.fields_dict["invoices"].grid.update_docfield_property(
|
||||||
|
"party",
|
||||||
|
"label",
|
||||||
|
is_sales ? "Customer ID" : "Supplier ID"
|
||||||
|
);
|
||||||
|
frm.fields_dict["invoices"].grid.update_docfield_property(
|
||||||
|
"party_name",
|
||||||
|
"label",
|
||||||
|
is_sales ? "Customer Name" : "Supplier Name"
|
||||||
|
);
|
||||||
|
|
||||||
|
frm.set_df_property(
|
||||||
|
"create_missing_party",
|
||||||
|
"description",
|
||||||
|
is_sales
|
||||||
|
? __("If party does not exist, create it using the Customer Name field.")
|
||||||
|
: __("If party does not exist, create it using the Supplier Name field.")
|
||||||
|
);
|
||||||
|
|
||||||
|
frm.refresh_field("invoices");
|
||||||
|
frm.refresh_field("create_missing_party");
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
frappe.ui.form.on("Opening Invoice Creation Tool Item", {
|
frappe.ui.form.on("Opening Invoice Creation Tool Item", {
|
||||||
|
|||||||
@@ -7,10 +7,11 @@
|
|||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
|
"section_break_ynel",
|
||||||
"company",
|
"company",
|
||||||
|
"create_missing_party",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"invoice_type",
|
"invoice_type",
|
||||||
"create_missing_party",
|
|
||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"dimension_col_break",
|
"dimension_col_break",
|
||||||
@@ -25,11 +26,11 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Company",
|
"label": "Company",
|
||||||
"options": "Company",
|
"options": "Company",
|
||||||
|
"remember_last_selected_value": 1,
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"description": "If party does not exist, create it using the Party Name field.",
|
|
||||||
"fieldname": "create_missing_party",
|
"fieldname": "create_missing_party",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Create Missing Party"
|
"label": "Create Missing Party"
|
||||||
@@ -79,12 +80,17 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "dimension_col_break",
|
"fieldname": "dimension_col_break",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_ynel",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"hide_border": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hide_toolbar": 1,
|
"hide_toolbar": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-03-23 00:32:15.600086",
|
"modified": "2026-03-31 01:47:20.360352",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Opening Invoice Creation Tool",
|
"name": "Opening Invoice Creation Tool",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _, scrub
|
from frappe import _, scrub
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import flt, nowdate
|
from frappe.utils import escape_html, flt, nowdate
|
||||||
from frappe.utils.background_jobs import enqueue, is_job_enqueued
|
from frappe.utils.background_jobs import enqueue, is_job_enqueued
|
||||||
|
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
@@ -86,6 +86,11 @@ class OpeningInvoiceCreationTool(Document):
|
|||||||
)
|
)
|
||||||
prepare_invoice_summary(doctype, invoices)
|
prepare_invoice_summary(doctype, invoices)
|
||||||
|
|
||||||
|
invoices_summary_companies = list(invoices_summary.keys())
|
||||||
|
|
||||||
|
for company in invoices_summary_companies:
|
||||||
|
invoices_summary[escape_html(company)] = invoices_summary.pop(company)
|
||||||
|
|
||||||
return invoices_summary, max_count
|
return invoices_summary, max_count
|
||||||
|
|
||||||
def validate_company(self):
|
def validate_company(self):
|
||||||
@@ -274,6 +279,7 @@ def start_import(invoices):
|
|||||||
doc.flags.ignore_mandatory = True
|
doc.flags.ignore_mandatory = True
|
||||||
doc.insert(set_name=invoice_number)
|
doc.insert(set_name=invoice_number)
|
||||||
doc.submit()
|
doc.submit()
|
||||||
|
if not frappe.in_test:
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
names.append(doc.name)
|
names.append(doc.name)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
|
|
||||||
create_dimension,
|
|
||||||
disable_dimension,
|
|
||||||
)
|
|
||||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
|
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
|
||||||
get_temporary_opening_account,
|
get_temporary_opening_account,
|
||||||
)
|
)
|
||||||
@@ -14,11 +10,6 @@ from erpnext.tests.utils import ERPNextTestSuite
|
|||||||
|
|
||||||
|
|
||||||
class TestOpeningInvoiceCreationTool(ERPNextTestSuite):
|
class TestOpeningInvoiceCreationTool(ERPNextTestSuite):
|
||||||
def setUp(self):
|
|
||||||
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
|
|
||||||
make_company()
|
|
||||||
create_dimension()
|
|
||||||
|
|
||||||
def make_invoices(
|
def make_invoices(
|
||||||
self,
|
self,
|
||||||
invoice_type="Sales",
|
invoice_type="Sales",
|
||||||
@@ -183,26 +174,13 @@ def get_opening_invoice_creation_dict(**args):
|
|||||||
return invoice_dict
|
return invoice_dict
|
||||||
|
|
||||||
|
|
||||||
def make_company():
|
|
||||||
if frappe.db.exists("Company", "_Test Opening Invoice Company"):
|
|
||||||
return frappe.get_doc("Company", "_Test Opening Invoice Company")
|
|
||||||
|
|
||||||
company = frappe.new_doc("Company")
|
|
||||||
company.company_name = "_Test Opening Invoice Company"
|
|
||||||
company.abbr = "_TOIC"
|
|
||||||
company.default_currency = "INR"
|
|
||||||
company.country = "Pakistan"
|
|
||||||
company.insert()
|
|
||||||
return company
|
|
||||||
|
|
||||||
|
|
||||||
def make_customer(customer=None):
|
def make_customer(customer=None):
|
||||||
customer_name = customer or "Opening Customer"
|
customer_name = customer or "Opening Customer"
|
||||||
customer = frappe.get_doc(
|
customer = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Customer",
|
"doctype": "Customer",
|
||||||
"customer_name": customer_name,
|
"customer_name": customer_name,
|
||||||
"customer_group": "All Customer Groups",
|
"customer_group": "Individual",
|
||||||
"customer_type": "Company",
|
"customer_type": "Company",
|
||||||
"territory": "All Territories",
|
"territory": "All Territories",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -824,7 +824,7 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
paid_amount: function (frm) {
|
paid_amount: function (frm) {
|
||||||
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
|
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
|
||||||
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.paid_amount) {
|
||||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||||
frm.set_value("received_amount", frm.doc.paid_amount);
|
frm.set_value("received_amount", frm.doc.paid_amount);
|
||||||
} else if (company_currency == frm.doc.paid_to_account_currency) {
|
} else if (company_currency == frm.doc.paid_to_account_currency) {
|
||||||
@@ -845,7 +845,7 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate)
|
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!frm.doc.paid_amount) {
|
if (frm.doc.received_amount) {
|
||||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||||
frm.set_value("paid_amount", frm.doc.received_amount);
|
frm.set_value("paid_amount", frm.doc.received_amount);
|
||||||
if (frm.doc.target_exchange_rate) {
|
if (frm.doc.target_exchange_rate) {
|
||||||
|
|||||||
@@ -2376,9 +2376,7 @@ def get_outstanding_reference_documents(args, validate=False):
|
|||||||
vouchers=args.get("vouchers") or None,
|
vouchers=args.get("vouchers") or None,
|
||||||
)
|
)
|
||||||
|
|
||||||
outstanding_invoices = split_invoices_based_on_payment_terms(
|
outstanding_invoices = split_refdocs_based_on_payment_terms(outstanding_invoices, args.get("company"))
|
||||||
outstanding_invoices, args.get("company")
|
|
||||||
)
|
|
||||||
|
|
||||||
for d in outstanding_invoices:
|
for d in outstanding_invoices:
|
||||||
d["exchange_rate"] = 1
|
d["exchange_rate"] = 1
|
||||||
@@ -2416,6 +2414,8 @@ def get_outstanding_reference_documents(args, validate=False):
|
|||||||
filters=args,
|
filters=args,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
orders_to_be_billed = split_refdocs_based_on_payment_terms(orders_to_be_billed, args.get("company"))
|
||||||
|
|
||||||
data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed
|
data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
@@ -2438,13 +2438,13 @@ def get_outstanding_reference_documents(args, validate=False):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list:
|
def split_refdocs_based_on_payment_terms(refdocs, company) -> list:
|
||||||
"""Split a list of invoices based on their payment terms."""
|
"""Split a list of invoices based on their payment terms."""
|
||||||
exc_rates = get_currency_data(outstanding_invoices, company)
|
exc_rates = get_currency_data(refdocs, company)
|
||||||
|
|
||||||
outstanding_invoices_after_split = []
|
outstanding_refdoc_after_split = []
|
||||||
for entry in outstanding_invoices:
|
for entry in refdocs:
|
||||||
if entry.voucher_type in ["Sales Invoice", "Purchase Invoice"]:
|
if entry.voucher_type in ["Sales Invoice", "Purchase Invoice", "Sales Order", "Purchase Order"]:
|
||||||
if payment_term_template := frappe.db.get_value(
|
if payment_term_template := frappe.db.get_value(
|
||||||
entry.voucher_type, entry.voucher_no, "payment_terms_template"
|
entry.voucher_type, entry.voucher_no, "payment_terms_template"
|
||||||
):
|
):
|
||||||
@@ -2459,25 +2459,25 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list
|
|||||||
),
|
),
|
||||||
alert=True,
|
alert=True,
|
||||||
)
|
)
|
||||||
outstanding_invoices_after_split += split_rows
|
outstanding_refdoc_after_split += split_rows
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# If not an invoice or no payment terms template, add as it is
|
# If not an invoice or no payment terms template, add as it is
|
||||||
outstanding_invoices_after_split.append(entry)
|
outstanding_refdoc_after_split.append(entry)
|
||||||
|
|
||||||
return outstanding_invoices_after_split
|
return outstanding_refdoc_after_split
|
||||||
|
|
||||||
|
|
||||||
def get_currency_data(outstanding_invoices: list, company: str | None = None) -> dict:
|
def get_currency_data(outstanding_refdocs: list, company: str | None = None) -> dict:
|
||||||
"""Get currency and conversion data for a list of invoices."""
|
"""Get currency and conversion data for a list of invoices."""
|
||||||
exc_rates = frappe._dict()
|
exc_rates = frappe._dict()
|
||||||
company_currency = frappe.db.get_value("Company", company, "default_currency") if company else None
|
company_currency = frappe.db.get_value("Company", company, "default_currency") if company else None
|
||||||
|
|
||||||
for doctype in ["Sales Invoice", "Purchase Invoice"]:
|
for doctype in ["Sales Invoice", "Purchase Invoice", "Sales Order", "Purchase Order"]:
|
||||||
invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype]
|
refdoc = [x.voucher_no for x in outstanding_refdocs if x.voucher_type == doctype]
|
||||||
for x in frappe.db.get_all(
|
for x in frappe.db.get_all(
|
||||||
doctype,
|
doctype,
|
||||||
filters={"name": ["in", invoices]},
|
filters={"name": ["in", refdoc]},
|
||||||
fields=["name", "currency", "conversion_rate", "party_account_currency"],
|
fields=["name", "currency", "conversion_rate", "party_account_currency"],
|
||||||
):
|
):
|
||||||
exc_rates[x.name] = frappe._dict(
|
exc_rates[x.name] = frappe._dict(
|
||||||
|
|||||||
@@ -2019,6 +2019,92 @@ class TestPaymentEntry(ERPNextTestSuite):
|
|||||||
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, pe.doctype, pe.name)
|
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, pe.doctype, pe.name)
|
||||||
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, "Journal Entry", jv[0])
|
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, "Journal Entry", jv[0])
|
||||||
|
|
||||||
|
def test_outstanding_orders_split_by_payment_terms(self):
|
||||||
|
create_payment_terms_template()
|
||||||
|
|
||||||
|
so = make_sales_order(do_not_save=1, qty=1, rate=200)
|
||||||
|
so.payment_terms_template = "Test Receivable Template"
|
||||||
|
so.save().submit()
|
||||||
|
|
||||||
|
args = {
|
||||||
|
"posting_date": nowdate(),
|
||||||
|
"company": so.company,
|
||||||
|
"party_type": "Customer",
|
||||||
|
"payment_type": "Receive",
|
||||||
|
"party": so.customer,
|
||||||
|
"party_account": "Debtors - _TC",
|
||||||
|
"get_orders_to_be_billed": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
references = get_outstanding_reference_documents(args)
|
||||||
|
|
||||||
|
self.assertEqual(len(references), 2)
|
||||||
|
self.assertEqual(references[0].voucher_no, so.name)
|
||||||
|
self.assertEqual(references[1].voucher_no, so.name)
|
||||||
|
self.assertEqual(references[0].payment_term, "Basic Amount Receivable")
|
||||||
|
self.assertEqual(references[1].payment_term, "Tax Receivable")
|
||||||
|
|
||||||
|
def test_outstanding_orders_no_split_when_allocate_disabled(self):
|
||||||
|
create_payment_terms_template()
|
||||||
|
|
||||||
|
template = frappe.get_doc("Payment Terms Template", "Test Receivable Template")
|
||||||
|
template.allocate_payment_based_on_payment_terms = 0
|
||||||
|
template.save()
|
||||||
|
|
||||||
|
so = make_sales_order(do_not_save=1, qty=1, rate=200)
|
||||||
|
so.payment_terms_template = "Test Receivable Template"
|
||||||
|
so.save().submit()
|
||||||
|
|
||||||
|
args = {
|
||||||
|
"posting_date": nowdate(),
|
||||||
|
"company": so.company,
|
||||||
|
"party_type": "Customer",
|
||||||
|
"payment_type": "Receive",
|
||||||
|
"party": so.customer,
|
||||||
|
"party_account": "Debtors - _TC",
|
||||||
|
"get_orders_to_be_billed": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
references = get_outstanding_reference_documents(args)
|
||||||
|
|
||||||
|
self.assertEqual(len(references), 1)
|
||||||
|
self.assertIsNone(references[0].payment_term)
|
||||||
|
|
||||||
|
template.allocate_payment_based_on_payment_terms = 1
|
||||||
|
template.save()
|
||||||
|
|
||||||
|
def test_outstanding_multicurrency_sales_order_split(self):
|
||||||
|
create_payment_terms_template()
|
||||||
|
|
||||||
|
so = make_sales_order(
|
||||||
|
customer="_Test Customer USD",
|
||||||
|
currency="USD",
|
||||||
|
qty=1,
|
||||||
|
rate=100,
|
||||||
|
do_not_submit=True,
|
||||||
|
)
|
||||||
|
so.payment_terms_template = "Test Receivable Template"
|
||||||
|
so.conversion_rate = 50
|
||||||
|
so.save().submit()
|
||||||
|
|
||||||
|
args = {
|
||||||
|
"posting_date": nowdate(),
|
||||||
|
"company": so.company,
|
||||||
|
"party_type": "Customer",
|
||||||
|
"payment_type": "Receive",
|
||||||
|
"party": so.customer,
|
||||||
|
"party_account": "Debtors - _TC",
|
||||||
|
"get_orders_to_be_billed": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
references = get_outstanding_reference_documents(args)
|
||||||
|
|
||||||
|
# Should split without throwing currency errors
|
||||||
|
self.assertEqual(len(references), 2)
|
||||||
|
for ref in references:
|
||||||
|
self.assertEqual(ref.voucher_no, so.name)
|
||||||
|
self.assertIsNotNone(ref.payment_term)
|
||||||
|
|
||||||
|
|
||||||
def create_payment_entry(**args):
|
def create_payment_entry(**args):
|
||||||
payment_entry = frappe.new_doc("Payment Entry")
|
payment_entry = frappe.new_doc("Payment Entry")
|
||||||
|
|||||||
@@ -750,6 +750,7 @@ def make_payment_request(**args):
|
|||||||
pr.submit()
|
pr.submit()
|
||||||
|
|
||||||
if args.order_type == "Shopping Cart":
|
if args.order_type == "Shopping Cart":
|
||||||
|
if not frappe.in_test:
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
frappe.local.response["type"] = "redirect"
|
frappe.local.response["type"] = "redirect"
|
||||||
frappe.local.response["location"] = pr.get_payment_url()
|
frappe.local.response["location"] = pr.get_payment_url()
|
||||||
|
|||||||
@@ -4,10 +4,6 @@ import unittest
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
|
|
||||||
create_dimension,
|
|
||||||
disable_dimension,
|
|
||||||
)
|
|
||||||
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import (
|
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import (
|
||||||
make_closing_entry_from_opening,
|
make_closing_entry_from_opening,
|
||||||
)
|
)
|
||||||
@@ -162,7 +158,6 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
|||||||
test case to check whether we can create POS Closing Entry without mandatory accounting dimension
|
test case to check whether we can create POS Closing Entry without mandatory accounting dimension
|
||||||
"""
|
"""
|
||||||
|
|
||||||
create_dimension()
|
|
||||||
location = frappe.get_doc("Accounting Dimension", "Location")
|
location = frappe.get_doc("Accounting Dimension", "Location")
|
||||||
location.dimension_defaults[0].mandatory_for_bs = True
|
location.dimension_defaults[0].mandatory_for_bs = True
|
||||||
location.save()
|
location.save()
|
||||||
@@ -198,7 +193,6 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
|||||||
)
|
)
|
||||||
accounting_dimension_department.mandatory_for_bs = 0
|
accounting_dimension_department.mandatory_for_bs = 0
|
||||||
accounting_dimension_department.save()
|
accounting_dimension_department.save()
|
||||||
disable_dimension()
|
|
||||||
|
|
||||||
def test_merging_into_sales_invoice_for_batched_item(self):
|
def test_merging_into_sales_invoice_for_batched_item(self):
|
||||||
frappe.flags.print_message = False
|
frappe.flags.print_message = False
|
||||||
|
|||||||
@@ -563,10 +563,10 @@ def send_emails(document_name, from_scheduler=False, posting_date=None):
|
|||||||
new_from_date = add_months(new_to_date, -1 * doc.filter_duration)
|
new_from_date = add_months(new_to_date, -1 * doc.filter_duration)
|
||||||
doc.add_comment("Comment", "Emails sent on: " + frappe.utils.format_datetime(frappe.utils.now()))
|
doc.add_comment("Comment", "Emails sent on: " + frappe.utils.format_datetime(frappe.utils.now()))
|
||||||
if doc.report == "General Ledger":
|
if doc.report == "General Ledger":
|
||||||
doc.db_set("to_date", new_to_date, commit=True)
|
frappe.db.set_value(doc.doctype, doc.name, "to_date", new_to_date)
|
||||||
doc.db_set("from_date", new_from_date, commit=True)
|
frappe.db.set_value(doc.doctype, doc.name, "from_date", new_from_date)
|
||||||
else:
|
else:
|
||||||
doc.db_set("posting_date", new_to_date, commit=True)
|
frappe.db.set_value(doc.doctype, doc.name, "posting_date", new_to_date)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
|||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
|
||||||
class TestProcessStatementOfAccounts(AccountsTestMixin, ERPNextTestSuite):
|
class TestProcessStatementOfAccounts(ERPNextTestSuite, AccountsTestMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
frappe.db.set_single_value("Selling Settings", "validate_selling_price", 0)
|
frappe.db.set_single_value("Selling Settings", "validate_selling_price", 0)
|
||||||
letterhead = frappe.get_doc("Letter Head", "Company Letterhead - Grey")
|
letterhead = frappe.get_doc("Letter Head", "Company Letterhead - Grey")
|
||||||
|
|||||||
@@ -21,10 +21,12 @@ frappe.ui.form.on("Promotional Scheme", {
|
|||||||
|
|
||||||
selling: function (frm) {
|
selling: function (frm) {
|
||||||
frm.trigger("set_options_for_applicable_for");
|
frm.trigger("set_options_for_applicable_for");
|
||||||
|
frm.toggle_enable("buying", !frm.doc.selling);
|
||||||
},
|
},
|
||||||
|
|
||||||
buying: function (frm) {
|
buying: function (frm) {
|
||||||
frm.trigger("set_options_for_applicable_for");
|
frm.trigger("set_options_for_applicable_for");
|
||||||
|
frm.toggle_enable("selling", !frm.doc.buying);
|
||||||
},
|
},
|
||||||
|
|
||||||
set_options_for_applicable_for: function (frm) {
|
set_options_for_applicable_for: function (frm) {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
"email_append_to": 1,
|
"email_append_to": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"title",
|
|
||||||
"naming_series",
|
"naming_series",
|
||||||
"supplier",
|
"supplier",
|
||||||
"supplier_name",
|
"supplier_name",
|
||||||
@@ -209,16 +208,6 @@
|
|||||||
"connections_tab"
|
"connections_tab"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
|
||||||
"allow_on_submit": 1,
|
|
||||||
"default": "{supplier_name}",
|
|
||||||
"fieldname": "title",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Title",
|
|
||||||
"no_copy": 1,
|
|
||||||
"print_hide": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "naming_series",
|
"fieldname": "naming_series",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
@@ -1693,7 +1682,7 @@
|
|||||||
"idx": 204,
|
"idx": 204,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-03-17 20:44:00.221219",
|
"modified": "2026-03-25 11:45:38.696888",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice",
|
"name": "Purchase Invoice",
|
||||||
@@ -1756,6 +1745,6 @@
|
|||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"timeline_field": "supplier",
|
"timeline_field": "supplier",
|
||||||
"title_field": "title",
|
"title_field": "supplier_name",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,7 +203,6 @@ class PurchaseInvoice(BuyingController):
|
|||||||
taxes_and_charges_deducted: DF.Currency
|
taxes_and_charges_deducted: DF.Currency
|
||||||
tc_name: DF.Link | None
|
tc_name: DF.Link | None
|
||||||
terms: DF.TextEditor | None
|
terms: DF.TextEditor | None
|
||||||
title: DF.Data | None
|
|
||||||
to_date: DF.Date | None
|
to_date: DF.Date | None
|
||||||
total: DF.Currency
|
total: DF.Currency
|
||||||
total_advance: DF.Currency
|
total_advance: DF.Currency
|
||||||
@@ -333,9 +332,6 @@ class PurchaseInvoice(BuyingController):
|
|||||||
if self.bill_date:
|
if self.bill_date:
|
||||||
self.remarks += " " + _("dated {0}").format(formatdate(self.bill_date))
|
self.remarks += " " + _("dated {0}").format(formatdate(self.bill_date))
|
||||||
|
|
||||||
else:
|
|
||||||
self.remarks = _("No Remarks")
|
|
||||||
|
|
||||||
def set_missing_values(self, for_validate=False):
|
def set_missing_values(self, for_validate=False):
|
||||||
if not self.credit_to:
|
if not self.credit_to:
|
||||||
self.credit_to = get_party_account("Supplier", self.supplier, self.company)
|
self.credit_to = get_party_account("Supplier", self.supplier, self.company)
|
||||||
@@ -617,12 +613,13 @@ class PurchaseInvoice(BuyingController):
|
|||||||
frappe.db.set_value(self.doctype, self.name, "against_expense_account", self.against_expense_account)
|
frappe.db.set_value(self.doctype, self.name, "against_expense_account", self.against_expense_account)
|
||||||
|
|
||||||
def po_required(self):
|
def po_required(self):
|
||||||
if frappe.db.get_single_value("Buying Settings", "po_required") == "Yes":
|
if (
|
||||||
if frappe.get_value(
|
frappe.db.get_single_value("Buying Settings", "po_required") == "Yes"
|
||||||
|
and not self.is_internal_transfer()
|
||||||
|
and not frappe.get_value(
|
||||||
"Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order"
|
"Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order"
|
||||||
|
)
|
||||||
):
|
):
|
||||||
return
|
|
||||||
|
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
if not d.purchase_order:
|
if not d.purchase_order:
|
||||||
msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code))
|
msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code))
|
||||||
@@ -983,6 +980,10 @@ class PurchaseInvoice(BuyingController):
|
|||||||
if provisional_accounting_for_non_stock_items:
|
if provisional_accounting_for_non_stock_items:
|
||||||
self.get_provisional_accounts()
|
self.get_provisional_accounts()
|
||||||
|
|
||||||
|
adjust_incoming_rate = frappe.db.get_single_value(
|
||||||
|
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
|
||||||
|
)
|
||||||
|
|
||||||
for item in self.get("items"):
|
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) or (self.get("update_stock") and item.valuation_rate):
|
||||||
if item.item_code:
|
if item.item_code:
|
||||||
@@ -1161,7 +1162,11 @@ class PurchaseInvoice(BuyingController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# check if the exchange rate has changed
|
# check if the exchange rate has changed
|
||||||
if item.get("purchase_receipt") and self.auto_accounting_for_stock:
|
if (
|
||||||
|
not adjust_incoming_rate
|
||||||
|
and item.get("purchase_receipt")
|
||||||
|
and self.auto_accounting_for_stock
|
||||||
|
):
|
||||||
if (
|
if (
|
||||||
exchange_rate_map[item.purchase_receipt]
|
exchange_rate_map[item.purchase_receipt]
|
||||||
and self.conversion_rate != exchange_rate_map[item.purchase_receipt]
|
and self.conversion_rate != exchange_rate_map[item.purchase_receipt]
|
||||||
@@ -1198,6 +1203,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
item=item,
|
item=item,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.auto_accounting_for_stock
|
self.auto_accounting_for_stock
|
||||||
and self.is_opening == "No"
|
and self.is_opening == "No"
|
||||||
|
|||||||
@@ -350,6 +350,12 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
|||||||
make_purchase_invoice as create_purchase_invoice,
|
make_purchase_invoice as create_purchase_invoice,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
original_value = frappe.db.get_single_value(
|
||||||
|
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
|
||||||
|
|
||||||
pr = make_purchase_receipt(
|
pr = make_purchase_receipt(
|
||||||
company="_Test Company with perpetual inventory",
|
company="_Test Company with perpetual inventory",
|
||||||
warehouse="Stores - TCP1",
|
warehouse="Stores - TCP1",
|
||||||
@@ -368,14 +374,19 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
|||||||
|
|
||||||
# fetching the latest GL Entry with exchange gain and loss account account
|
# fetching the latest GL Entry with exchange gain and loss account account
|
||||||
amount = frappe.db.get_value(
|
amount = frappe.db.get_value(
|
||||||
"GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name}, "credit"
|
"GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name}, "debit"
|
||||||
)
|
)
|
||||||
|
|
||||||
discrepancy_caused_by_exchange_rate_diff = abs(
|
discrepancy_caused_by_exchange_rate_diff = abs(
|
||||||
pi.items[0].base_net_amount - pr.items[0].base_net_amount
|
pi.items[0].base_net_amount - pr.items[0].base_net_amount
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
|
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
|
||||||
|
|
||||||
|
frappe.db.set_single_value(
|
||||||
|
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", original_value
|
||||||
|
)
|
||||||
|
|
||||||
def test_purchase_invoice_with_exchange_rate_difference_for_non_stock_item(self):
|
def test_purchase_invoice_with_exchange_rate_difference_for_non_stock_item(self):
|
||||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||||
make_purchase_invoice as create_purchase_invoice,
|
make_purchase_invoice as create_purchase_invoice,
|
||||||
@@ -2189,11 +2200,6 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
|||||||
|
|
||||||
def test_offsetting_entries_for_accounting_dimensions(self):
|
def test_offsetting_entries_for_accounting_dimensions(self):
|
||||||
from erpnext.accounts.doctype.account.test_account import create_account
|
from erpnext.accounts.doctype.account.test_account import create_account
|
||||||
from erpnext.accounts.report.trial_balance.test_trial_balance import (
|
|
||||||
clear_dimension_defaults,
|
|
||||||
create_accounting_dimension,
|
|
||||||
disable_dimension,
|
|
||||||
)
|
|
||||||
|
|
||||||
create_account(
|
create_account(
|
||||||
account_name="Offsetting",
|
account_name="Offsetting",
|
||||||
@@ -2201,7 +2207,16 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
|||||||
parent_account="Temporary Accounts - _TC",
|
parent_account="Temporary Accounts - _TC",
|
||||||
)
|
)
|
||||||
|
|
||||||
create_accounting_dimension(company="_Test Company", offsetting_account="Offsetting - _TC")
|
dim = frappe.get_doc("Accounting Dimension", "Branch")
|
||||||
|
dim.append(
|
||||||
|
"dimension_defaults",
|
||||||
|
{
|
||||||
|
"company": "_Test Company",
|
||||||
|
"reference_document": "Branch",
|
||||||
|
"offsetting_account": "Offsetting - _TC",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
dim.save()
|
||||||
|
|
||||||
branch1 = frappe.new_doc("Branch")
|
branch1 = frappe.new_doc("Branch")
|
||||||
branch1.branch = "Location 1"
|
branch1.branch = "Location 1"
|
||||||
@@ -2238,8 +2253,6 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
|||||||
voucher_type="Purchase Invoice",
|
voucher_type="Purchase Invoice",
|
||||||
additional_columns=["branch"],
|
additional_columns=["branch"],
|
||||||
)
|
)
|
||||||
clear_dimension_defaults("Branch")
|
|
||||||
disable_dimension()
|
|
||||||
|
|
||||||
def test_repost_accounting_entries(self):
|
def test_repost_accounting_entries(self):
|
||||||
# update repost settings
|
# update repost settings
|
||||||
|
|||||||
@@ -190,6 +190,7 @@
|
|||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Received Qty",
|
"label": "Received Qty",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -206,7 +207,8 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "rejected_qty",
|
"fieldname": "rejected_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Rejected Qty"
|
"label": "Rejected Qty",
|
||||||
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||||
@@ -226,6 +228,7 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "UOM",
|
"label": "UOM",
|
||||||
"options": "UOM",
|
"options": "UOM",
|
||||||
|
"print_hide": 1,
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -261,14 +264,16 @@
|
|||||||
"depends_on": "price_list_rate",
|
"depends_on": "price_list_rate",
|
||||||
"fieldname": "discount_percentage",
|
"fieldname": "discount_percentage",
|
||||||
"fieldtype": "Percent",
|
"fieldtype": "Percent",
|
||||||
"label": "Discount on Price List Rate (%)"
|
"label": "Discount on Price List Rate (%)",
|
||||||
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "price_list_rate",
|
"depends_on": "price_list_rate",
|
||||||
"fieldname": "discount_amount",
|
"fieldname": "discount_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Discount Amount",
|
"label": "Discount Amount",
|
||||||
"options": "currency"
|
"options": "currency",
|
||||||
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "col_break3",
|
"fieldname": "col_break3",
|
||||||
@@ -401,12 +406,14 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "weight_per_unit",
|
"fieldname": "weight_per_unit",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Weight Per Unit"
|
"label": "Weight Per Unit",
|
||||||
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "total_weight",
|
"fieldname": "total_weight",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Total Weight",
|
"label": "Total Weight",
|
||||||
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -417,7 +424,8 @@
|
|||||||
"fieldname": "weight_uom",
|
"fieldname": "weight_uom",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Weight UOM",
|
"label": "Weight UOM",
|
||||||
"options": "UOM"
|
"options": "UOM",
|
||||||
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:parent.update_stock",
|
"depends_on": "eval:parent.update_stock",
|
||||||
@@ -429,7 +437,8 @@
|
|||||||
"fieldname": "warehouse",
|
"fieldname": "warehouse",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Accepted Warehouse",
|
"label": "Accepted Warehouse",
|
||||||
"options": "Warehouse"
|
"options": "Warehouse",
|
||||||
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "rejected_warehouse",
|
"fieldname": "rejected_warehouse",
|
||||||
@@ -674,7 +683,8 @@
|
|||||||
"fieldname": "asset_location",
|
"fieldname": "asset_location",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Asset Location",
|
"label": "Asset Location",
|
||||||
"options": "Location"
|
"options": "Location",
|
||||||
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "po_detail",
|
"fieldname": "po_detail",
|
||||||
@@ -730,7 +740,6 @@
|
|||||||
"label": "Valuation Rate",
|
"label": "Valuation Rate",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"precision": "6",
|
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@@ -796,6 +805,7 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Asset Category",
|
"label": "Asset Category",
|
||||||
"options": "Asset Category",
|
"options": "Asset Category",
|
||||||
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -828,6 +838,7 @@
|
|||||||
"label": "Rate of Stock UOM",
|
"label": "Rate of Stock UOM",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "currency",
|
"options": "currency",
|
||||||
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -866,6 +877,7 @@
|
|||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Rate With Margin",
|
"label": "Rate With Margin",
|
||||||
"options": "currency",
|
"options": "currency",
|
||||||
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -892,7 +904,8 @@
|
|||||||
"default": "1",
|
"default": "1",
|
||||||
"fieldname": "apply_tds",
|
"fieldname": "apply_tds",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Consider for Tax Withholding"
|
"label": "Consider for Tax Withholding",
|
||||||
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||||
@@ -918,7 +931,8 @@
|
|||||||
"fieldname": "wip_composite_asset",
|
"fieldname": "wip_composite_asset",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "WIP Composite Asset",
|
"label": "WIP Composite Asset",
|
||||||
"options": "Asset"
|
"options": "Asset",
|
||||||
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 && doc.docstatus === 0",
|
"depends_on": "eval:doc.use_serial_batch_fields === 0 && doc.docstatus === 0",
|
||||||
@@ -930,7 +944,8 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "use_serial_batch_fields",
|
"fieldname": "use_serial_batch_fields",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Use Serial No / Batch Fields"
|
"label": "Use Serial No / Batch Fields",
|
||||||
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||||
@@ -977,7 +992,8 @@
|
|||||||
"fieldname": "distributed_discount_amount",
|
"fieldname": "distributed_discount_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Distributed Discount Amount",
|
"label": "Distributed Discount Amount",
|
||||||
"options": "currency"
|
"options": "currency",
|
||||||
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "tax_withholding_category",
|
"fieldname": "tax_withholding_category",
|
||||||
@@ -991,7 +1007,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-02-15 21:07:49.455930",
|
"modified": "2026-04-07 15:40:45.687554",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice Item",
|
"name": "Purchase Invoice Item",
|
||||||
|
|||||||
@@ -9,29 +9,25 @@ from frappe.utils import add_days, nowdate, today
|
|||||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries, make_purchase_receipt
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries, make_purchase_receipt
|
||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
|
||||||
class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
class TestRepostAccountingLedger(ERPNextTestSuite):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.create_company()
|
|
||||||
self.create_customer()
|
|
||||||
self.create_item()
|
|
||||||
frappe.db.set_single_value("Selling Settings", "validate_selling_price", 0)
|
frappe.db.set_single_value("Selling Settings", "validate_selling_price", 0)
|
||||||
update_repost_settings()
|
update_repost_settings()
|
||||||
|
|
||||||
def test_01_basic_functions(self):
|
def test_01_basic_functions(self):
|
||||||
si = create_sales_invoice(
|
si = create_sales_invoice(
|
||||||
item=self.item,
|
item="_Test Item",
|
||||||
company=self.company,
|
company="_Test Company",
|
||||||
customer=self.customer,
|
customer="_Test Customer",
|
||||||
debit_to=self.debit_to,
|
debit_to="Debtors - _TC",
|
||||||
parent_cost_center=self.cost_center,
|
parent_cost_center="Main - _TC",
|
||||||
cost_center=self.cost_center,
|
cost_center="Main - _TC",
|
||||||
rate=100,
|
rate=100,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -48,7 +44,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
|||||||
|
|
||||||
# Test Validation Error
|
# Test Validation Error
|
||||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||||
ral.company = self.company
|
ral.company = "_Test Company"
|
||||||
ral.delete_cancelled_entries = True
|
ral.delete_cancelled_entries = True
|
||||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||||
ral.append(
|
ral.append(
|
||||||
@@ -65,7 +61,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
|||||||
ral.save()
|
ral.save()
|
||||||
|
|
||||||
# manually set an incorrect debit amount in DB
|
# manually set an incorrect debit amount in DB
|
||||||
gle = frappe.db.get_all("GL Entry", filters={"voucher_no": si.name, "account": self.debit_to})
|
gle = frappe.db.get_all("GL Entry", filters={"voucher_no": si.name, "account": "Debtors - _TC"})
|
||||||
frappe.db.set_value("GL Entry", gle[0], "debit", 90)
|
frappe.db.set_value("GL Entry", gle[0], "debit", 90)
|
||||||
|
|
||||||
gl = qb.DocType("GL Entry")
|
gl = qb.DocType("GL Entry")
|
||||||
@@ -94,23 +90,23 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
|||||||
|
|
||||||
def test_02_deferred_accounting_valiations(self):
|
def test_02_deferred_accounting_valiations(self):
|
||||||
si = create_sales_invoice(
|
si = create_sales_invoice(
|
||||||
item=self.item,
|
item="_Test Item",
|
||||||
company=self.company,
|
company="_Test Company",
|
||||||
customer=self.customer,
|
customer="_Test Customer",
|
||||||
debit_to=self.debit_to,
|
debit_to="Debtors - _TC",
|
||||||
parent_cost_center=self.cost_center,
|
parent_cost_center="Main - _TC",
|
||||||
cost_center=self.cost_center,
|
cost_center="Main - _TC",
|
||||||
rate=100,
|
rate=100,
|
||||||
do_not_submit=True,
|
do_not_submit=True,
|
||||||
)
|
)
|
||||||
si.items[0].enable_deferred_revenue = True
|
si.items[0].enable_deferred_revenue = True
|
||||||
si.items[0].deferred_revenue_account = self.deferred_revenue
|
si.items[0].deferred_revenue_account = "Deferred Revenue - _TC"
|
||||||
si.items[0].service_start_date = nowdate()
|
si.items[0].service_start_date = nowdate()
|
||||||
si.items[0].service_end_date = add_days(nowdate(), 90)
|
si.items[0].service_end_date = add_days(nowdate(), 90)
|
||||||
si.save().submit()
|
si.save().submit()
|
||||||
|
|
||||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||||
ral.company = self.company
|
ral.company = "_Test Company"
|
||||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||||
self.assertRaises(frappe.ValidationError, ral.save)
|
self.assertRaises(frappe.ValidationError, ral.save)
|
||||||
|
|
||||||
@@ -118,35 +114,35 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
|||||||
def test_04_pcv_validation(self):
|
def test_04_pcv_validation(self):
|
||||||
# Clear old GL entries so PCV can be submitted.
|
# Clear old GL entries so PCV can be submitted.
|
||||||
gl = frappe.qb.DocType("GL Entry")
|
gl = frappe.qb.DocType("GL Entry")
|
||||||
qb.from_(gl).delete().where(gl.company == self.company).run()
|
qb.from_(gl).delete().where(gl.company == "_Test Company").run()
|
||||||
|
|
||||||
si = create_sales_invoice(
|
si = create_sales_invoice(
|
||||||
item=self.item,
|
item="_Test Item",
|
||||||
company=self.company,
|
company="_Test Company",
|
||||||
customer=self.customer,
|
customer="_Test Customer",
|
||||||
debit_to=self.debit_to,
|
debit_to="Debtors - _TC",
|
||||||
parent_cost_center=self.cost_center,
|
parent_cost_center="Main - _TC",
|
||||||
cost_center=self.cost_center,
|
cost_center="Main - _TC",
|
||||||
rate=100,
|
rate=100,
|
||||||
)
|
)
|
||||||
fy = get_fiscal_year(today(), company=self.company)
|
fy = get_fiscal_year(today(), company="_Test Company")
|
||||||
pcv = frappe.get_doc(
|
pcv = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Period Closing Voucher",
|
"doctype": "Period Closing Voucher",
|
||||||
"transaction_date": today(),
|
"transaction_date": today(),
|
||||||
"period_start_date": fy[1],
|
"period_start_date": fy[1],
|
||||||
"period_end_date": today(),
|
"period_end_date": today(),
|
||||||
"company": self.company,
|
"company": "_Test Company",
|
||||||
"fiscal_year": fy[0],
|
"fiscal_year": fy[0],
|
||||||
"cost_center": self.cost_center,
|
"cost_center": "Main - _TC",
|
||||||
"closing_account_head": self.retained_earnings,
|
"closing_account_head": "Retained Earnings - _TC",
|
||||||
"remarks": "test",
|
"remarks": "test",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
pcv.save().submit()
|
pcv.save().submit()
|
||||||
|
|
||||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||||
ral.company = self.company
|
ral.company = "_Test Company"
|
||||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||||
self.assertRaises(frappe.ValidationError, ral.save)
|
self.assertRaises(frappe.ValidationError, ral.save)
|
||||||
|
|
||||||
@@ -156,12 +152,12 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
|||||||
|
|
||||||
def test_03_deletion_flag_and_preview_function(self):
|
def test_03_deletion_flag_and_preview_function(self):
|
||||||
si = create_sales_invoice(
|
si = create_sales_invoice(
|
||||||
item=self.item,
|
item="_Test Item",
|
||||||
company=self.company,
|
company="_Test Company",
|
||||||
customer=self.customer,
|
customer="_Test Customer",
|
||||||
debit_to=self.debit_to,
|
debit_to="Debtors - _TC",
|
||||||
parent_cost_center=self.cost_center,
|
parent_cost_center="Main - _TC",
|
||||||
cost_center=self.cost_center,
|
cost_center="Main - _TC",
|
||||||
rate=100,
|
rate=100,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -170,7 +166,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
|||||||
|
|
||||||
# with deletion flag set
|
# with deletion flag set
|
||||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||||
ral.company = self.company
|
ral.company = "_Test Company"
|
||||||
ral.delete_cancelled_entries = True
|
ral.delete_cancelled_entries = True
|
||||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||||
@@ -181,12 +177,12 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
|||||||
|
|
||||||
def test_05_without_deletion_flag(self):
|
def test_05_without_deletion_flag(self):
|
||||||
si = create_sales_invoice(
|
si = create_sales_invoice(
|
||||||
item=self.item,
|
item="_Test Item",
|
||||||
company=self.company,
|
company="_Test Company",
|
||||||
customer=self.customer,
|
customer="_Test Customer",
|
||||||
debit_to=self.debit_to,
|
debit_to="Debtors - _TC",
|
||||||
parent_cost_center=self.cost_center,
|
parent_cost_center="Main - _TC",
|
||||||
cost_center=self.cost_center,
|
cost_center="Main - _TC",
|
||||||
rate=100,
|
rate=100,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -195,7 +191,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
|||||||
|
|
||||||
# without deletion flag set
|
# without deletion flag set
|
||||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||||
ral.company = self.company
|
ral.company = "_Test Company"
|
||||||
ral.delete_cancelled_entries = False
|
ral.delete_cancelled_entries = False
|
||||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||||
@@ -210,16 +206,16 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
|||||||
provisional_account = create_account(
|
provisional_account = create_account(
|
||||||
account_name="Provision Account",
|
account_name="Provision Account",
|
||||||
parent_account="Current Liabilities - _TC",
|
parent_account="Current Liabilities - _TC",
|
||||||
company=self.company,
|
company="_Test Company",
|
||||||
)
|
)
|
||||||
|
|
||||||
another_provisional_account = create_account(
|
another_provisional_account = create_account(
|
||||||
account_name="Another Provision Account",
|
account_name="Another Provision Account",
|
||||||
parent_account="Current Liabilities - _TC",
|
parent_account="Current Liabilities - _TC",
|
||||||
company=self.company,
|
company="_Test Company",
|
||||||
)
|
)
|
||||||
|
|
||||||
company = frappe.get_doc("Company", self.company)
|
company = frappe.get_doc("Company", "_Test Company")
|
||||||
company.enable_provisional_accounting_for_non_stock_items = 1
|
company.enable_provisional_accounting_for_non_stock_items = 1
|
||||||
company.default_provisional_account = provisional_account
|
company.default_provisional_account = provisional_account
|
||||||
company.save()
|
company.save()
|
||||||
@@ -229,7 +225,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
|||||||
|
|
||||||
item = make_item(properties={"is_stock_item": 0})
|
item = make_item(properties={"is_stock_item": 0})
|
||||||
|
|
||||||
pr = make_purchase_receipt(company=self.company, item_code=item.name, rate=1000.0, qty=1.0)
|
pr = make_purchase_receipt(company="_Test Company", item_code=item.name, rate=1000.0, qty=1.0)
|
||||||
pr_gl_entries = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
pr_gl_entries = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||||
expected_pr_gles = [
|
expected_pr_gles = [
|
||||||
{"account": provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
{"account": provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||||
@@ -246,7 +242,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
|||||||
)
|
)
|
||||||
|
|
||||||
repost_doc = frappe.new_doc("Repost Accounting Ledger")
|
repost_doc = frappe.new_doc("Repost Accounting Ledger")
|
||||||
repost_doc.company = self.company
|
repost_doc.company = "_Test Company"
|
||||||
repost_doc.delete_cancelled_entries = True
|
repost_doc.delete_cancelled_entries = True
|
||||||
repost_doc.append("vouchers", {"voucher_type": pr.doctype, "voucher_no": pr.name})
|
repost_doc.append("vouchers", {"voucher_type": pr.doctype, "voucher_no": pr.name})
|
||||||
repost_doc.save().submit()
|
repost_doc.save().submit()
|
||||||
|
|||||||
@@ -165,13 +165,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.toggle_get_items();
|
||||||
// Show buttons only when pos view is active
|
|
||||||
if (cint(doc.docstatus == 0) && this.frm.page.current_view_name !== "pos" && !doc.is_return) {
|
|
||||||
this.frm.cscript.sales_order_btn();
|
|
||||||
this.frm.cscript.delivery_note_btn();
|
|
||||||
this.frm.cscript.quotation_btn();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.set_default_print_format();
|
this.set_default_print_format();
|
||||||
if (doc.docstatus == 1 && !doc.inter_company_invoice_reference) {
|
if (doc.docstatus == 1 && !doc.inter_company_invoice_reference) {
|
||||||
@@ -260,6 +254,93 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggle_get_items() {
|
||||||
|
const buttons = ["Sales Order", "Quotation", "Timesheet", "Delivery Note"];
|
||||||
|
|
||||||
|
buttons.forEach((label) => {
|
||||||
|
this.frm.remove_custom_button(label, "Get Items From");
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cint(this.frm.doc.docstatus) !== 0 || this.frm.page.current_view_name === "pos") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.frm.doc.is_return) {
|
||||||
|
this.frm.cscript.sales_order_btn();
|
||||||
|
this.frm.cscript.quotation_btn();
|
||||||
|
this.frm.cscript.timesheet_btn();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.frm.cscript.delivery_note_btn();
|
||||||
|
}
|
||||||
|
|
||||||
|
timesheet_btn() {
|
||||||
|
var me = this;
|
||||||
|
|
||||||
|
me.frm.add_custom_button(
|
||||||
|
__("Timesheet"),
|
||||||
|
function () {
|
||||||
|
let d = new frappe.ui.Dialog({
|
||||||
|
title: __("Fetch Timesheet"),
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: __("From"),
|
||||||
|
fieldname: "from_time",
|
||||||
|
fieldtype: "Date",
|
||||||
|
reqd: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Item Code"),
|
||||||
|
fieldname: "item_code",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Item",
|
||||||
|
get_query: () => {
|
||||||
|
return {
|
||||||
|
query: "erpnext.controllers.queries.item_query",
|
||||||
|
filters: {
|
||||||
|
is_sales_item: 1,
|
||||||
|
customer: me.frm.doc.customer,
|
||||||
|
has_variants: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Column Break",
|
||||||
|
fieldname: "col_break_1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("To"),
|
||||||
|
fieldname: "to_time",
|
||||||
|
fieldtype: "Date",
|
||||||
|
reqd: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Project"),
|
||||||
|
fieldname: "project",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Project",
|
||||||
|
default: me.frm.doc.project,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
primary_action: function () {
|
||||||
|
const data = d.get_values();
|
||||||
|
me.frm.events.add_timesheet_data(me.frm, {
|
||||||
|
from_time: data.from_time,
|
||||||
|
to_time: data.to_time,
|
||||||
|
project: data.project,
|
||||||
|
item_code: data.item_code,
|
||||||
|
});
|
||||||
|
d.hide();
|
||||||
|
},
|
||||||
|
primary_action_label: __("Get Timesheets"),
|
||||||
|
});
|
||||||
|
d.show();
|
||||||
|
},
|
||||||
|
__("Get Items From")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
sales_order_btn() {
|
sales_order_btn() {
|
||||||
var me = this;
|
var me = this;
|
||||||
|
|
||||||
@@ -331,6 +412,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
|||||||
this.$delivery_note_btn = this.frm.add_custom_button(
|
this.$delivery_note_btn = this.frm.add_custom_button(
|
||||||
__("Delivery Note"),
|
__("Delivery Note"),
|
||||||
function () {
|
function () {
|
||||||
|
if (!me.frm.doc.customer) {
|
||||||
|
frappe.throw({
|
||||||
|
title: __("Mandatory"),
|
||||||
|
message: __("Please Select a Customer"),
|
||||||
|
});
|
||||||
|
}
|
||||||
erpnext.utils.map_current_doc({
|
erpnext.utils.map_current_doc({
|
||||||
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
|
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
|
||||||
source_doctype: "Delivery Note",
|
source_doctype: "Delivery Note",
|
||||||
@@ -343,7 +430,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
|||||||
var filters = {
|
var filters = {
|
||||||
docstatus: 1,
|
docstatus: 1,
|
||||||
company: me.frm.doc.company,
|
company: me.frm.doc.company,
|
||||||
is_return: 0,
|
is_return: me.frm.doc.is_return,
|
||||||
};
|
};
|
||||||
if (me.frm.doc.customer) filters["customer"] = me.frm.doc.customer;
|
if (me.frm.doc.customer) filters["customer"] = me.frm.doc.customer;
|
||||||
return {
|
return {
|
||||||
@@ -610,6 +697,10 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
|||||||
apply_tds(frm) {
|
apply_tds(frm) {
|
||||||
this.frm.clear_table("tax_withholding_entries");
|
this.frm.clear_table("tax_withholding_entries");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is_return() {
|
||||||
|
this.toggle_get_items();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// for backward compatibility: combine new and previous states
|
// for backward compatibility: combine new and previous states
|
||||||
@@ -1061,71 +1152,6 @@ frappe.ui.form.on("Sales Invoice", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
if (frm.doc.docstatus === 0 && !frm.doc.is_return) {
|
|
||||||
frm.add_custom_button(
|
|
||||||
__("Timesheet"),
|
|
||||||
function () {
|
|
||||||
let d = new frappe.ui.Dialog({
|
|
||||||
title: __("Fetch Timesheet"),
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
label: __("From"),
|
|
||||||
fieldname: "from_time",
|
|
||||||
fieldtype: "Date",
|
|
||||||
reqd: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: __("Item Code"),
|
|
||||||
fieldname: "item_code",
|
|
||||||
fieldtype: "Link",
|
|
||||||
options: "Item",
|
|
||||||
get_query: () => {
|
|
||||||
return {
|
|
||||||
query: "erpnext.controllers.queries.item_query",
|
|
||||||
filters: {
|
|
||||||
is_sales_item: 1,
|
|
||||||
customer: frm.doc.customer,
|
|
||||||
has_variants: 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Column Break",
|
|
||||||
fieldname: "col_break_1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: __("To"),
|
|
||||||
fieldname: "to_time",
|
|
||||||
fieldtype: "Date",
|
|
||||||
reqd: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: __("Project"),
|
|
||||||
fieldname: "project",
|
|
||||||
fieldtype: "Link",
|
|
||||||
options: "Project",
|
|
||||||
default: frm.doc.project,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
primary_action: function () {
|
|
||||||
const data = d.get_values();
|
|
||||||
frm.events.add_timesheet_data(frm, {
|
|
||||||
from_time: data.from_time,
|
|
||||||
to_time: data.to_time,
|
|
||||||
project: data.project,
|
|
||||||
item_code: data.item_code,
|
|
||||||
});
|
|
||||||
d.hide();
|
|
||||||
},
|
|
||||||
primary_action_label: __("Get Timesheets"),
|
|
||||||
});
|
|
||||||
d.show();
|
|
||||||
},
|
|
||||||
__("Get Items From")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frm.doc.is_debit_note) {
|
if (frm.doc.is_debit_note) {
|
||||||
frm.set_df_property("return_against", "label", __("Adjustment Against"));
|
frm.set_df_property("return_against", "label", __("Adjustment Against"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1101,9 +1101,6 @@ class SalesInvoice(SellingController):
|
|||||||
if self.po_date:
|
if self.po_date:
|
||||||
self.remarks += " " + _("dated {0}").format(formatdate(self.po_date))
|
self.remarks += " " + _("dated {0}").format(formatdate(self.po_date))
|
||||||
|
|
||||||
else:
|
|
||||||
self.remarks = _("No Remarks")
|
|
||||||
|
|
||||||
def validate_auto_set_posting_time(self):
|
def validate_auto_set_posting_time(self):
|
||||||
# Don't auto set the posting date and time if invoice is amended
|
# Don't auto set the posting date and time if invoice is amended
|
||||||
if self.is_new() and self.amended_from:
|
if self.is_new() and self.amended_from:
|
||||||
|
|||||||
@@ -2246,13 +2246,6 @@ class TestSalesInvoice(ERPNextTestSuite):
|
|||||||
|
|
||||||
@ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": True})
|
@ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": True})
|
||||||
def test_rounding_adjustment_3(self):
|
def test_rounding_adjustment_3(self):
|
||||||
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import create_dimension
|
|
||||||
|
|
||||||
# Dimension creates custom field, which does an implicit DB commit as it is a DDL command
|
|
||||||
# Ensure dimension don't have any mandatory fields
|
|
||||||
create_dimension()
|
|
||||||
|
|
||||||
# rollback from tearDown() happens till here
|
|
||||||
si = create_sales_invoice(do_not_save=True)
|
si = create_sales_invoice(do_not_save=True)
|
||||||
si.items = []
|
si.items = []
|
||||||
for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]:
|
for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]:
|
||||||
@@ -2894,7 +2887,7 @@ class TestSalesInvoice(ERPNextTestSuite):
|
|||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
# Check if adjustment entry is created
|
# Check if adjustment entry is created
|
||||||
self.assertTrue(
|
self.assertFalse(
|
||||||
frappe.db.exists(
|
frappe.db.exists(
|
||||||
"GL Entry",
|
"GL Entry",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -207,6 +207,7 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Stock UOM",
|
"label": "Stock UOM",
|
||||||
"options": "UOM",
|
"options": "UOM",
|
||||||
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -310,7 +311,8 @@
|
|||||||
"fieldname": "discount_amount",
|
"fieldname": "discount_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Discount Amount",
|
"label": "Discount Amount",
|
||||||
"options": "currency"
|
"options": "currency",
|
||||||
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount",
|
"depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount",
|
||||||
@@ -853,6 +855,7 @@
|
|||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Rate of Stock UOM",
|
"label": "Rate of Stock UOM",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
|
"print_hide": 1,
|
||||||
"options": "currency",
|
"options": "currency",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@@ -869,6 +872,7 @@
|
|||||||
"fieldname": "grant_commission",
|
"fieldname": "grant_commission",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Grant Commission",
|
"label": "Grant Commission",
|
||||||
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -926,7 +930,8 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "use_serial_batch_fields",
|
"fieldname": "use_serial_batch_fields",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Use Serial No / Batch Fields"
|
"label": "Use Serial No / Batch Fields",
|
||||||
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
"depends_on": "eval:doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||||
@@ -941,7 +946,8 @@
|
|||||||
"fieldname": "distributed_discount_amount",
|
"fieldname": "distributed_discount_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Distributed Discount Amount",
|
"label": "Distributed Discount Amount",
|
||||||
"options": "currency"
|
"options": "currency",
|
||||||
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "available_quantity_section",
|
"fieldname": "available_quantity_section",
|
||||||
@@ -1010,7 +1016,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-02-23 14:37:14.853941",
|
"modified": "2026-02-24 14:37:16.853941",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice Item",
|
"name": "Sales Invoice Item",
|
||||||
|
|||||||
@@ -772,6 +772,7 @@ def process_all(subscription: list, posting_date: DateTimeLikeObject | None = No
|
|||||||
try:
|
try:
|
||||||
subscription = frappe.get_doc("Subscription", subscription_name)
|
subscription = frappe.get_doc("Subscription", subscription_name)
|
||||||
subscription.process(posting_date)
|
subscription.process(posting_date)
|
||||||
|
if not frappe.in_test:
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
except frappe.ValidationError:
|
except frappe.ValidationError:
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ class TaxWithholdingDetails:
|
|||||||
self.party_type = party_type
|
self.party_type = party_type
|
||||||
self.party = party
|
self.party = party
|
||||||
self.company = company
|
self.company = company
|
||||||
|
self.tax_id = get_tax_id_for_party(self.party_type, self.party)
|
||||||
|
|
||||||
def get(self) -> list:
|
def get(self) -> list:
|
||||||
"""
|
"""
|
||||||
@@ -161,6 +162,7 @@ class TaxWithholdingDetails:
|
|||||||
disable_cumulative_threshold=doc.disable_cumulative_threshold,
|
disable_cumulative_threshold=doc.disable_cumulative_threshold,
|
||||||
disable_transaction_threshold=doc.disable_transaction_threshold,
|
disable_transaction_threshold=doc.disable_transaction_threshold,
|
||||||
taxable_amount=0,
|
taxable_amount=0,
|
||||||
|
tax_id=self.tax_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ldc (only if valid based on posting date)
|
# ldc (only if valid based on posting date)
|
||||||
@@ -181,17 +183,13 @@ class TaxWithholdingDetails:
|
|||||||
if self.party_type != "Supplier":
|
if self.party_type != "Supplier":
|
||||||
return ldc_details
|
return ldc_details
|
||||||
|
|
||||||
# NOTE: This can be a configurable option
|
|
||||||
# To check if filter by tax_id is needed
|
|
||||||
tax_id = get_tax_id_for_party(self.party_type, self.party)
|
|
||||||
|
|
||||||
# ldc details
|
# ldc details
|
||||||
ldc_records = self.get_valid_ldc_records(tax_id)
|
ldc_records = self.get_valid_ldc_records(self.tax_id)
|
||||||
if not ldc_records:
|
if not ldc_records:
|
||||||
return ldc_details
|
return ldc_details
|
||||||
|
|
||||||
ldc_names = [ldc.name for ldc in ldc_records]
|
ldc_names = [ldc.name for ldc in ldc_records]
|
||||||
ldc_utilization_map = self.get_ldc_utilization_by_category(ldc_names, tax_id)
|
ldc_utilization_map = self.get_ldc_utilization_by_category(ldc_names, self.tax_id)
|
||||||
|
|
||||||
# map
|
# map
|
||||||
for ldc in ldc_records:
|
for ldc in ldc_records:
|
||||||
@@ -254,4 +252,5 @@ class TaxWithholdingDetails:
|
|||||||
|
|
||||||
@allow_regional
|
@allow_regional
|
||||||
def get_tax_id_for_party(party_type, party):
|
def get_tax_id_for_party(party_type, party):
|
||||||
return None
|
# cannot use tax_id from doc because payment and journal entry do not have tax_id field.\
|
||||||
|
return frappe.db.get_value(party_type, party, "tax_id")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||||
@@ -18,7 +19,6 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
|
|||||||
# create relevant supplier, etc
|
# create relevant supplier, etc
|
||||||
create_records()
|
create_records()
|
||||||
create_tax_withholding_category_records()
|
create_tax_withholding_category_records()
|
||||||
make_pan_no_field()
|
|
||||||
|
|
||||||
def validate_tax_withholding_entries(self, doctype, docname, expected_entries):
|
def validate_tax_withholding_entries(self, doctype, docname, expected_entries):
|
||||||
"""Validate tax withholding entries for a document"""
|
"""Validate tax withholding entries for a document"""
|
||||||
@@ -3542,6 +3542,47 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
|
|||||||
entry.withholding_amount = 5001 # Should be 5000 (10% of 50000)
|
entry.withholding_amount = 5001 # Should be 5000 (10% of 50000)
|
||||||
self.assertRaisesRegex(frappe.ValidationError, "Withholding Amount.*does not match", pi.save)
|
self.assertRaisesRegex(frappe.ValidationError, "Withholding Amount.*does not match", pi.save)
|
||||||
|
|
||||||
|
def test_tax_id_is_set_in_all_generated_entries_from_party_doctype(self):
|
||||||
|
self.setup_party_with_category("Supplier", "Test TDS Supplier3", "New TDS Category")
|
||||||
|
frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_id", "ABCTY1234D")
|
||||||
|
|
||||||
|
pi = create_purchase_invoice(supplier="Test TDS Supplier3", rate=40000)
|
||||||
|
pi.submit()
|
||||||
|
|
||||||
|
entries = frappe.get_all(
|
||||||
|
"Tax Withholding Entry",
|
||||||
|
filters={"parenttype": "Purchase Invoice", "parent": pi.name},
|
||||||
|
fields=["name", "tax_id"],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(entries)
|
||||||
|
self.assertTrue(all(entry.tax_id == "ABCTY1234D" for entry in entries))
|
||||||
|
|
||||||
|
def test_threshold_considers_two_parties_with_same_tax_id_with_overrided_hook(self):
|
||||||
|
self.setup_party_with_category("Supplier", "Test TDS Supplier1", "Cumulative Threshold TDS")
|
||||||
|
self.setup_party_with_category("Supplier", "Test TDS Supplier2", "Cumulative Threshold TDS")
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category.get_tax_id_for_party",
|
||||||
|
return_value="AAAPL1234C",
|
||||||
|
):
|
||||||
|
pi1 = create_purchase_invoice(supplier="Test TDS Supplier1", rate=20000)
|
||||||
|
pi1.submit()
|
||||||
|
|
||||||
|
pi2 = create_purchase_invoice(supplier="Test TDS Supplier2", rate=20000)
|
||||||
|
|
||||||
|
pi2.submit()
|
||||||
|
|
||||||
|
entries = frappe.get_all(
|
||||||
|
"Tax Withholding Entry",
|
||||||
|
filters={"parenttype": "Purchase Invoice", "parent": pi2.name},
|
||||||
|
fields=["status", "withholding_amount"],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(entries), 1)
|
||||||
|
self.assertEqual(entries[0].status, "Settled")
|
||||||
|
self.assertEqual(entries[0].withholding_amount, 2000.0)
|
||||||
|
|
||||||
|
|
||||||
def create_purchase_invoice(**args):
|
def create_purchase_invoice(**args):
|
||||||
# return sales invoice doc object
|
# return sales invoice doc object
|
||||||
@@ -3998,18 +4039,3 @@ def create_lower_deduction_certificate(
|
|||||||
"certificate_limit": limit,
|
"certificate_limit": limit,
|
||||||
}
|
}
|
||||||
).insert()
|
).insert()
|
||||||
|
|
||||||
|
|
||||||
def make_pan_no_field():
|
|
||||||
pan_field = {
|
|
||||||
"Supplier": [
|
|
||||||
{
|
|
||||||
"fieldname": "pan",
|
|
||||||
"label": "PAN",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"translatable": 0,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
create_custom_fields(pan_field, update=1)
|
|
||||||
|
|||||||
@@ -344,7 +344,6 @@ class TaxWithholdingEntry(Document):
|
|||||||
|
|
||||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||||
TaxWithholdingDetails,
|
TaxWithholdingDetails,
|
||||||
get_tax_id_for_party,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -646,8 +645,11 @@ class TaxWithholdingController:
|
|||||||
|
|
||||||
# NOTE: This can be a configurable option
|
# NOTE: This can be a configurable option
|
||||||
# To check if filter by tax_id is needed
|
# To check if filter by tax_id is needed
|
||||||
tax_id = get_tax_id_for_party(self.party_type, self.party)
|
query = (
|
||||||
query = query.where(entry.tax_id == tax_id) if tax_id else query.where(entry.party == self.party)
|
query.where(entry.tax_id == category.tax_id)
|
||||||
|
if category.tax_id
|
||||||
|
else query.where(entry.party == self.party)
|
||||||
|
)
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
@@ -686,6 +688,7 @@ class TaxWithholdingController:
|
|||||||
"company": self.doc.company,
|
"company": self.doc.company,
|
||||||
"party_type": self.party_type,
|
"party_type": self.party_type,
|
||||||
"party": self.party,
|
"party": self.party,
|
||||||
|
"tax_id": category.tax_id,
|
||||||
"tax_withholding_category": category.name,
|
"tax_withholding_category": category.name,
|
||||||
"tax_withholding_group": category.tax_withholding_group,
|
"tax_withholding_group": category.tax_withholding_group,
|
||||||
"tax_rate": category.tax_rate,
|
"tax_rate": category.tax_rate,
|
||||||
@@ -1052,6 +1055,7 @@ class TaxWithholdingController:
|
|||||||
"party_type": self.party_type,
|
"party_type": self.party_type,
|
||||||
"party": self.party,
|
"party": self.party,
|
||||||
"company": self.doc.company,
|
"company": self.doc.company,
|
||||||
|
"tax_id": category.tax_id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return entry
|
return entry
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
|
|||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
|
||||||
class TestUnreconcilePayment(AccountsTestMixin, ERPNextTestSuite):
|
class TestUnreconcilePayment(ERPNextTestSuite, AccountsTestMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.create_company()
|
self.create_company()
|
||||||
self.create_customer()
|
self.create_customer()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
|||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
|
||||||
class TestAccountsPayable(AccountsTestMixin, ERPNextTestSuite):
|
class TestAccountsPayable(ERPNextTestSuite, AccountsTestMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.create_company()
|
self.create_company()
|
||||||
self.create_customer()
|
self.create_customer()
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
|
|||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
|
||||||
class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.create_company()
|
self.create_company()
|
||||||
self.create_customer()
|
self.create_customer()
|
||||||
@@ -120,12 +120,12 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
|||||||
|
|
||||||
report = execute(filters)
|
report = execute(filters)
|
||||||
|
|
||||||
expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]]
|
expected_data = [[100, 30], [100, 50], [100, 20]]
|
||||||
|
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
row = report[1][i - 1]
|
row = report[1][i - 1]
|
||||||
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
|
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced])
|
||||||
|
self.assertFalse(row.get("remarks"))
|
||||||
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
||||||
self.create_payment_entry(si.name)
|
self.create_payment_entry(si.name)
|
||||||
report = execute(filters)
|
report = execute(filters)
|
||||||
@@ -178,11 +178,11 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
|||||||
|
|
||||||
report = execute(filters)
|
report = execute(filters)
|
||||||
|
|
||||||
expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]]
|
expected_data = [[100, 30], [100, 50], [100, 20]]
|
||||||
|
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
row = report[1][i - 1]
|
row = report[1][i - 1]
|
||||||
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
|
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced])
|
||||||
|
|
||||||
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
|
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
|
||||||
cr_note = self.create_credit_note(si.name, do_not_submit=True)
|
cr_note = self.create_credit_note(si.name, do_not_submit=True)
|
||||||
@@ -225,9 +225,10 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
|||||||
report = execute(filters)
|
report = execute(filters)
|
||||||
row = report[1][0]
|
row = report[1][0]
|
||||||
|
|
||||||
expected_data = [8000, 8000, "No Remarks"] # Data in company currency
|
expected_data = [8000, 8000] # Data in company currency
|
||||||
|
|
||||||
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks])
|
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced])
|
||||||
|
self.assertFalse(row.get("remarks"))
|
||||||
|
|
||||||
# CASE 2: Transaction currency and party account currency are the same
|
# CASE 2: Transaction currency and party account currency are the same
|
||||||
self.create_customer(
|
self.create_customer(
|
||||||
@@ -258,18 +259,20 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
|||||||
report = execute(filters)
|
report = execute(filters)
|
||||||
row = report[1][0]
|
row = report[1][0]
|
||||||
|
|
||||||
expected_data = [100, 100, "No Remarks"] # Data in Part Account Currency
|
expected_data = [100, 100] # Data in Part Account Currency
|
||||||
|
|
||||||
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks])
|
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced])
|
||||||
|
self.assertFalse(row.get("remarks"))
|
||||||
|
|
||||||
# View in Company currency
|
# View in Company currency
|
||||||
filters.pop("in_party_currency")
|
filters.pop("in_party_currency")
|
||||||
report = execute(filters)
|
report = execute(filters)
|
||||||
row = report[1][0]
|
row = report[1][0]
|
||||||
|
|
||||||
expected_data = [8000, 8000, "No Remarks"] # Data in Company Currency
|
expected_data = [8000, 8000] # Data in Company Currency
|
||||||
|
|
||||||
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks])
|
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced])
|
||||||
|
self.assertFalse(row.get("remarks"))
|
||||||
|
|
||||||
def test_accounts_receivable_with_partial_payment(self):
|
def test_accounts_receivable_with_partial_payment(self):
|
||||||
filters = {
|
filters = {
|
||||||
@@ -285,11 +288,12 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
|||||||
|
|
||||||
report = execute(filters)
|
report = execute(filters)
|
||||||
|
|
||||||
expected_data = [[200, 60, "No Remarks"], [200, 100, "No Remarks"], [200, 40, "No Remarks"]]
|
expected_data = [[200, 60], [200, 100], [200, 40]]
|
||||||
|
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
row = report[1][i - 1]
|
row = report[1][i - 1]
|
||||||
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
|
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced])
|
||||||
|
self.assertFalse(row.get("remarks"))
|
||||||
|
|
||||||
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
||||||
self.create_payment_entry(si.name)
|
self.create_payment_entry(si.name)
|
||||||
@@ -348,11 +352,12 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
|||||||
|
|
||||||
report = execute(filters)
|
report = execute(filters)
|
||||||
|
|
||||||
expected_data = [100, 100, "No Remarks"]
|
expected_data = [100, 100]
|
||||||
|
|
||||||
self.assertEqual(len(report[1]), 1)
|
self.assertEqual(len(report[1]), 1)
|
||||||
row = report[1][0]
|
row = report[1][0]
|
||||||
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks])
|
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced])
|
||||||
|
self.assertFalse(row.get("remarks"))
|
||||||
|
|
||||||
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
||||||
self.create_payment_entry(si.name)
|
self.create_payment_entry(si.name)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
|||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
|
||||||
class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.maxDiff = None
|
self.maxDiff = None
|
||||||
self.create_company()
|
self.create_company()
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
|||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
|
||||||
class TestCustomerLedgerSummary(AccountsTestMixin, ERPNextTestSuite):
|
class TestCustomerLedgerSummary(ERPNextTestSuite, AccountsTestMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.create_company()
|
self.create_company()
|
||||||
self.create_customer()
|
self.create_customer()
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ class Deferred_Item:
|
|||||||
Generate report data for output
|
Generate report data for output
|
||||||
"""
|
"""
|
||||||
ret_data = frappe._dict({"name": self.item_name})
|
ret_data = frappe._dict({"name": self.item_name})
|
||||||
|
ret_data.service_start_date = self.service_start_date
|
||||||
|
ret_data.service_end_date = self.service_end_date
|
||||||
|
ret_data.amount = self.base_net_amount
|
||||||
for period in self.period_total:
|
for period in self.period_total:
|
||||||
ret_data[period.key] = period.total
|
ret_data[period.key] = period.total
|
||||||
ret_data.indent = 1
|
ret_data.indent = 1
|
||||||
@@ -205,6 +208,9 @@ class Deferred_Invoice:
|
|||||||
for item in self.uniq_items:
|
for item in self.uniq_items:
|
||||||
self.items.append(Deferred_Item(item, self, [x for x in items if x.item == item]))
|
self.items.append(Deferred_Item(item, self, [x for x in items if x.item == item]))
|
||||||
|
|
||||||
|
# roll-up amount from all deferred items
|
||||||
|
self.amount_total = sum(item.base_net_amount for item in self.items)
|
||||||
|
|
||||||
def calculate_invoice_revenue_expense_for_period(self):
|
def calculate_invoice_revenue_expense_for_period(self):
|
||||||
"""
|
"""
|
||||||
calculate deferred revenue/expense for all items in invoice
|
calculate deferred revenue/expense for all items in invoice
|
||||||
@@ -232,7 +238,7 @@ class Deferred_Invoice:
|
|||||||
generate report data for invoice, includes invoice total
|
generate report data for invoice, includes invoice total
|
||||||
"""
|
"""
|
||||||
ret_data = []
|
ret_data = []
|
||||||
inv_total = frappe._dict({"name": self.name})
|
inv_total = frappe._dict({"name": self.name, "amount": self.amount_total})
|
||||||
for x in self.period_total:
|
for x in self.period_total:
|
||||||
inv_total[x.key] = x.total
|
inv_total[x.key] = x.total
|
||||||
inv_total.indent = 0
|
inv_total.indent = 0
|
||||||
@@ -386,6 +392,24 @@ class Deferred_Revenue_and_Expense_Report:
|
|||||||
def get_columns(self):
|
def get_columns(self):
|
||||||
columns = []
|
columns = []
|
||||||
columns.append({"label": _("Name"), "fieldname": "name", "fieldtype": "Data", "read_only": 1})
|
columns.append({"label": _("Name"), "fieldname": "name", "fieldtype": "Data", "read_only": 1})
|
||||||
|
columns.append(
|
||||||
|
{
|
||||||
|
"label": _("Service Start Date"),
|
||||||
|
"fieldname": "service_start_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"read_only": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
columns.append(
|
||||||
|
{
|
||||||
|
"label": _("Service End Date"),
|
||||||
|
"fieldname": "service_end_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"read_only": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
columns.append({"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "read_only": 1})
|
||||||
|
|
||||||
for period in self.period_list:
|
for period in self.period_list:
|
||||||
columns.append(
|
columns.append(
|
||||||
{
|
{
|
||||||
@@ -415,6 +439,8 @@ class Deferred_Revenue_and_Expense_Report:
|
|||||||
elif self.filters.type == "Expense":
|
elif self.filters.type == "Expense":
|
||||||
total_row = frappe._dict({"name": "Total Deferred Expense"})
|
total_row = frappe._dict({"name": "Total Deferred Expense"})
|
||||||
|
|
||||||
|
total_row["amount"] = sum(inv.amount_total for inv in self.deferred_invoices)
|
||||||
|
|
||||||
for idx, period in enumerate(self.period_list, 0):
|
for idx, period in enumerate(self.period_list, 0):
|
||||||
total_row[period.key] = self.period_total[idx].total
|
total_row[period.key] = self.period_total[idx].total
|
||||||
ret.append(total_row)
|
ret.append(total_row)
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ def prepare_data(accounts, filters, company_currency, dimension_list):
|
|||||||
total = 0
|
total = 0
|
||||||
row = {
|
row = {
|
||||||
"account": d.name,
|
"account": d.name,
|
||||||
|
"is_group": d.is_group,
|
||||||
"parent_account": d.parent_account,
|
"parent_account": d.parent_account,
|
||||||
"indent": d.indent,
|
"indent": d.indent,
|
||||||
"from_date": filters.from_date,
|
"from_date": filters.from_date,
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ frappe.query_reports["General Ledger"] = {
|
|||||||
label: __("Party"),
|
label: __("Party"),
|
||||||
fieldtype: "MultiSelectList",
|
fieldtype: "MultiSelectList",
|
||||||
options: "party_type",
|
options: "party_type",
|
||||||
|
depends_on: "party_type",
|
||||||
get_data: function (txt) {
|
get_data: function (txt) {
|
||||||
if (!frappe.query_report.filters) return;
|
if (!frappe.query_report.filters) return;
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
|||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
|
||||||
class TestItemWisePurchaseRegister(AccountsTestMixin, ERPNextTestSuite):
|
class TestItemWisePurchaseRegister(ERPNextTestSuite, AccountsTestMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.create_company()
|
self.create_company()
|
||||||
self.create_supplier()
|
self.create_supplier()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
|||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
|
||||||
class TestItemWiseSalesRegister(AccountsTestMixin, ERPNextTestSuite):
|
class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.create_company()
|
self.create_company()
|
||||||
self.create_customer()
|
self.create_customer()
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
|||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
|
||||||
class TestProfitAndLossStatement(AccountsTestMixin, ERPNextTestSuite):
|
class TestProfitAndLossStatement(ERPNextTestSuite, AccountsTestMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.create_company()
|
self.create_company()
|
||||||
self.create_customer()
|
self.create_customer()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
|||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
|
||||||
class TestItemWiseSalesRegister(AccountsTestMixin, ERPNextTestSuite):
|
class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.create_company()
|
self.create_company()
|
||||||
self.create_customer()
|
self.create_customer()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
|||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
|
||||||
class TestSupplierLedgerSummary(AccountsTestMixin, ERPNextTestSuite):
|
class TestSupplierLedgerSummary(ERPNextTestSuite, AccountsTestMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.create_company()
|
self.create_company()
|
||||||
self.create_supplier()
|
self.create_supplier()
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from erpnext.accounts.utils import get_fiscal_year
|
|||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
|
||||||
class TestTaxWithholdingDetails(AccountsTestMixin, ERPNextTestSuite):
|
class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.create_company()
|
self.create_company()
|
||||||
self.clear_old_entries()
|
self.clear_old_entries()
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ class TestTrialBalance(ERPNextTestSuite):
|
|||||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
|
|
||||||
self.company = create_company()
|
|
||||||
create_cost_center(
|
create_cost_center(
|
||||||
cost_center_name="Test Cost Center",
|
cost_center_name="Test Cost Center",
|
||||||
company="Trial Balance Company",
|
company="Trial Balance Company",
|
||||||
@@ -26,7 +25,16 @@ class TestTrialBalance(ERPNextTestSuite):
|
|||||||
parent_account="Temporary Accounts - TBC",
|
parent_account="Temporary Accounts - TBC",
|
||||||
)
|
)
|
||||||
self.fiscal_year = get_fiscal_year(today(), company="Trial Balance Company")[0]
|
self.fiscal_year = get_fiscal_year(today(), company="Trial Balance Company")[0]
|
||||||
create_accounting_dimension()
|
dim = frappe.get_doc("Accounting Dimension", "Branch")
|
||||||
|
dim.append(
|
||||||
|
"dimension_defaults",
|
||||||
|
{
|
||||||
|
"company": "Trial Balance Company",
|
||||||
|
"automatically_post_balancing_accounting_entry": 1,
|
||||||
|
"offsetting_account": "Offsetting - TBC",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
dim.save()
|
||||||
|
|
||||||
def test_offsetting_entries_for_accounting_dimensions(self):
|
def test_offsetting_entries_for_accounting_dimensions(self):
|
||||||
"""
|
"""
|
||||||
@@ -45,7 +53,7 @@ class TestTrialBalance(ERPNextTestSuite):
|
|||||||
branch2.insert(ignore_if_duplicate=True)
|
branch2.insert(ignore_if_duplicate=True)
|
||||||
|
|
||||||
si = create_sales_invoice(
|
si = create_sales_invoice(
|
||||||
company=self.company,
|
company="Trial Balance Company",
|
||||||
debit_to="Debtors - TBC",
|
debit_to="Debtors - TBC",
|
||||||
cost_center="Test Cost Center - TBC",
|
cost_center="Test Cost Center - TBC",
|
||||||
income_account="Sales - TBC",
|
income_account="Sales - TBC",
|
||||||
@@ -57,60 +65,7 @@ class TestTrialBalance(ERPNextTestSuite):
|
|||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
filters = frappe._dict(
|
filters = frappe._dict(
|
||||||
{"company": self.company, "fiscal_year": self.fiscal_year, "branch": ["Location 1"]}
|
{"company": "Trial Balance Company", "fiscal_year": self.fiscal_year, "branch": ["Location 1"]}
|
||||||
)
|
)
|
||||||
total_row = execute(filters)[1][-1]
|
total_row = execute(filters)[1][-1]
|
||||||
self.assertEqual(total_row["debit"], total_row["credit"])
|
self.assertEqual(total_row["debit"], total_row["credit"])
|
||||||
|
|
||||||
|
|
||||||
def create_company(**args):
|
|
||||||
args = frappe._dict(args)
|
|
||||||
company = frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "Company",
|
|
||||||
"company_name": args.company_name or "Trial Balance Company",
|
|
||||||
"country": args.country or "India",
|
|
||||||
"default_currency": args.currency or "INR",
|
|
||||||
"parent_company": args.get("parent_company"),
|
|
||||||
"is_group": args.get("is_group"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
company.insert(ignore_if_duplicate=True)
|
|
||||||
return company.name
|
|
||||||
|
|
||||||
|
|
||||||
def create_accounting_dimension(**args):
|
|
||||||
args = frappe._dict(args)
|
|
||||||
document_type = args.document_type or "Branch"
|
|
||||||
if frappe.db.exists("Accounting Dimension", document_type):
|
|
||||||
accounting_dimension = frappe.get_doc("Accounting Dimension", document_type)
|
|
||||||
accounting_dimension.disabled = 0
|
|
||||||
else:
|
|
||||||
accounting_dimension = frappe.new_doc("Accounting Dimension")
|
|
||||||
accounting_dimension.document_type = document_type
|
|
||||||
accounting_dimension.insert()
|
|
||||||
|
|
||||||
accounting_dimension.set("dimension_defaults", [])
|
|
||||||
accounting_dimension.append(
|
|
||||||
"dimension_defaults",
|
|
||||||
{
|
|
||||||
"company": args.company or "Trial Balance Company",
|
|
||||||
"automatically_post_balancing_accounting_entry": 1,
|
|
||||||
"offsetting_account": args.offsetting_account or "Offsetting - TBC",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
accounting_dimension.save()
|
|
||||||
|
|
||||||
|
|
||||||
def disable_dimension(**args):
|
|
||||||
args = frappe._dict(args)
|
|
||||||
document_type = args.document_type or "Branch"
|
|
||||||
dimension = frappe.get_doc("Accounting Dimension", document_type)
|
|
||||||
dimension.disabled = 1
|
|
||||||
dimension.save()
|
|
||||||
|
|
||||||
|
|
||||||
def clear_dimension_defaults(dimension_name):
|
|
||||||
accounting_dimension = frappe.get_doc("Accounting Dimension", dimension_name)
|
|
||||||
accounting_dimension.dimension_defaults = []
|
|
||||||
accounting_dimension.save()
|
|
||||||
|
|||||||
@@ -229,23 +229,3 @@ class AccountsTestMixin:
|
|||||||
]
|
]
|
||||||
for doctype in doctype_list:
|
for doctype in doctype_list:
|
||||||
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
|
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
|
||||||
|
|
||||||
def create_price_list(self):
|
|
||||||
pl_name = "Mixin Price List"
|
|
||||||
if not frappe.db.exists("Price List", pl_name):
|
|
||||||
self.price_list = (
|
|
||||||
frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "Price List",
|
|
||||||
"currency": "INR",
|
|
||||||
"enabled": True,
|
|
||||||
"selling": True,
|
|
||||||
"buying": True,
|
|
||||||
"price_list_name": pl_name,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.insert()
|
|
||||||
.name
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.price_list = frappe.get_doc("Price List", pl_name).name
|
|
||||||
|
|||||||
@@ -1404,6 +1404,78 @@ def get_account_balances(accounts, company, finance_book=None, include_default_f
|
|||||||
return accounts
|
return accounts
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_account_balances_coa(company: str, include_default_fb_balances: bool = False):
|
||||||
|
company_currency = frappe.get_cached_value("Company", company, "default_currency")
|
||||||
|
|
||||||
|
Account = DocType("Account")
|
||||||
|
account_list = (
|
||||||
|
frappe.qb.from_(Account)
|
||||||
|
.select(Account.name, Account.parent_account, Account.account_currency)
|
||||||
|
.where(Account.company == company)
|
||||||
|
.orderby(Account.lft)
|
||||||
|
.run(as_dict=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
account_balances_cc = {account.get("name"): 0 for account in account_list}
|
||||||
|
|
||||||
|
account_balances_ac = {account.get("name"): 0 for account in account_list}
|
||||||
|
|
||||||
|
GLEntry = DocType("GL Entry")
|
||||||
|
precision = get_currency_precision()
|
||||||
|
get_ledger_balances_query = (
|
||||||
|
frappe.qb.from_(GLEntry)
|
||||||
|
.select(
|
||||||
|
GLEntry.account,
|
||||||
|
(Sum(Round(GLEntry.debit, precision)) - Sum(Round(GLEntry.credit, precision))).as_("balance"),
|
||||||
|
(
|
||||||
|
Sum(Round(GLEntry.debit_in_account_currency, precision))
|
||||||
|
- Sum(Round(GLEntry.credit_in_account_currency, precision))
|
||||||
|
).as_("balance_in_account_currency"),
|
||||||
|
)
|
||||||
|
.groupby(GLEntry.account)
|
||||||
|
)
|
||||||
|
|
||||||
|
condition_list = [GLEntry.company == company, GLEntry.is_cancelled == 0]
|
||||||
|
|
||||||
|
default_finance_book = None
|
||||||
|
|
||||||
|
if include_default_fb_balances:
|
||||||
|
default_finance_book = frappe.get_cached_value("Company", company, "default_finance_book")
|
||||||
|
|
||||||
|
if default_finance_book:
|
||||||
|
condition_list.append(
|
||||||
|
(GLEntry.finance_book == default_finance_book) | (GLEntry.finance_book.isnull())
|
||||||
|
)
|
||||||
|
|
||||||
|
for condition in condition_list:
|
||||||
|
get_ledger_balances_query = get_ledger_balances_query.where(condition)
|
||||||
|
|
||||||
|
ledger_balances = get_ledger_balances_query.run(as_dict=True)
|
||||||
|
|
||||||
|
for ledger_entry in ledger_balances:
|
||||||
|
account_balances_cc[ledger_entry.get("account")] = ledger_entry.get("balance")
|
||||||
|
account_balances_ac[ledger_entry.get("account")] = ledger_entry.get("balance_in_account_currency")
|
||||||
|
|
||||||
|
for account in reversed(account_list):
|
||||||
|
parent = account.get("parent_account")
|
||||||
|
if parent:
|
||||||
|
account_balances_cc[parent] += account_balances_cc.get(account.get("name"))
|
||||||
|
|
||||||
|
accounts_data = [
|
||||||
|
{
|
||||||
|
"value": account.get("name"),
|
||||||
|
"company_currency": company_currency,
|
||||||
|
"balance": account_balances_cc.get(account.get("name")),
|
||||||
|
"account_currency": account.get("account_currency"),
|
||||||
|
"balance_in_account_currency": account_balances_ac.get(account.get("name")),
|
||||||
|
}
|
||||||
|
for account in account_list
|
||||||
|
]
|
||||||
|
|
||||||
|
return accounts_data
|
||||||
|
|
||||||
|
|
||||||
def create_payment_gateway_account(gateway, payment_channel="Email", company=None):
|
def create_payment_gateway_account(gateway, payment_channel="Email", company=None):
|
||||||
from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account
|
from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account
|
||||||
|
|
||||||
@@ -1531,6 +1603,10 @@ def parse_naming_series_variable(doc, variable):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
data = {"YY": "%y", "YYYY": "%Y", "MM": "%m", "DD": "%d", "JJJ": "%j"}
|
data = {"YY": "%y", "YYYY": "%Y", "MM": "%m", "DD": "%d", "JJJ": "%j"}
|
||||||
|
|
||||||
|
if doc and doc.doctype in ["Batch", "Serial No"] and doc.reference_doctype and doc.reference_name:
|
||||||
|
doc = frappe.get_doc(doc.reference_doctype, doc.reference_name)
|
||||||
|
|
||||||
date = (
|
date = (
|
||||||
(
|
(
|
||||||
getdate(doc.get("posting_date") or doc.get("transaction_date") or doc.get("posting_datetime"))
|
getdate(doc.get("posting_date") or doc.get("transaction_date") or doc.get("posting_datetime"))
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ def book_depreciation_entries(date):
|
|||||||
accounting_dimensions,
|
accounting_dimensions,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not frappe.in_test:
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
failed_assets.append(asset_name)
|
failed_assets.append(asset_name)
|
||||||
@@ -71,6 +73,7 @@ def book_depreciation_entries(date):
|
|||||||
if failed_assets:
|
if failed_assets:
|
||||||
set_depr_entry_posting_status_for_failed_assets(failed_assets)
|
set_depr_entry_posting_status_for_failed_assets(failed_assets)
|
||||||
notify_depr_entry_posting_error(failed_assets, error_logs)
|
notify_depr_entry_posting_error(failed_assets, error_logs)
|
||||||
|
if not frappe.in_test:
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -380,6 +380,7 @@ class TestAssetCapitalization(ERPNextTestSuite):
|
|||||||
"asset_type": "Composite Component",
|
"asset_type": "Composite Component",
|
||||||
"purchase_date": pr.posting_date,
|
"purchase_date": pr.posting_date,
|
||||||
"available_for_use_date": pr.posting_date,
|
"available_for_use_date": pr.posting_date,
|
||||||
|
"location": "Test Location",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
consumed_asset_doc.save()
|
consumed_asset_doc.save()
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ frappe.ui.form.on("Asset Movement", {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onload: (frm) => {
|
refresh: (frm) => {
|
||||||
frm.trigger("set_required_fields");
|
frm.trigger("set_required_fields");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ class TestAssetMovement(ERPNextTestSuite):
|
|||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
"Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC"
|
"Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC"
|
||||||
)
|
)
|
||||||
make_location()
|
|
||||||
|
|
||||||
def test_movement(self):
|
def test_movement(self):
|
||||||
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location")
|
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location")
|
||||||
@@ -40,10 +39,6 @@ class TestAssetMovement(ERPNextTestSuite):
|
|||||||
if asset.docstatus == 0:
|
if asset.docstatus == 0:
|
||||||
asset.submit()
|
asset.submit()
|
||||||
|
|
||||||
# check asset movement is created
|
|
||||||
if not frappe.db.exists("Location", "Test Location 2"):
|
|
||||||
frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert()
|
|
||||||
|
|
||||||
create_asset_movement(
|
create_asset_movement(
|
||||||
purpose="Transfer",
|
purpose="Transfer",
|
||||||
company=asset.company,
|
company=asset.company,
|
||||||
@@ -122,9 +117,6 @@ class TestAssetMovement(ERPNextTestSuite):
|
|||||||
if asset.docstatus == 0:
|
if asset.docstatus == 0:
|
||||||
asset.submit()
|
asset.submit()
|
||||||
|
|
||||||
if not frappe.db.exists("Location", "Test Location 2"):
|
|
||||||
frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert()
|
|
||||||
|
|
||||||
movement = frappe.get_doc({"doctype": "Asset Movement", "reference_name": pr.name})
|
movement = frappe.get_doc({"doctype": "Asset Movement", "reference_name": pr.name})
|
||||||
self.assertRaises(frappe.ValidationError, movement.cancel)
|
self.assertRaises(frappe.ValidationError, movement.cancel)
|
||||||
|
|
||||||
@@ -150,9 +142,6 @@ class TestAssetMovement(ERPNextTestSuite):
|
|||||||
asset = create_asset(item_code="Macbook Pro", do_not_save=1)
|
asset = create_asset(item_code="Macbook Pro", do_not_save=1)
|
||||||
asset.save().submit()
|
asset.save().submit()
|
||||||
|
|
||||||
if not frappe.db.exists("Location", "Test Location 2"):
|
|
||||||
frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert()
|
|
||||||
|
|
||||||
asset_creation_date = frappe.db.get_value(
|
asset_creation_date = frappe.db.get_value(
|
||||||
"Asset Movement",
|
"Asset Movement",
|
||||||
[["Asset Movement Item", "asset", "=", asset.name], ["docstatus", "=", 1]],
|
[["Asset Movement Item", "asset", "=", asset.name], ["docstatus", "=", 1]],
|
||||||
@@ -197,9 +186,3 @@ def create_asset_movement(**args):
|
|||||||
movement.submit()
|
movement.submit()
|
||||||
|
|
||||||
return movement
|
return movement
|
||||||
|
|
||||||
|
|
||||||
def make_location():
|
|
||||||
for location in ["Pune", "Mumbai", "Nagpur"]:
|
|
||||||
if not frappe.db.exists("Location", location):
|
|
||||||
frappe.get_doc({"doctype": "Location", "location_name": location}).insert(ignore_permissions=True)
|
|
||||||
|
|||||||
@@ -130,7 +130,7 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Asset",
|
"label": "Asset",
|
||||||
"link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",\"Cancelled\",null]]]",
|
"link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",\"Cancelled\"]]]",
|
||||||
"options": "Asset",
|
"options": "Asset",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"supplier_section",
|
"supplier_section",
|
||||||
"title",
|
|
||||||
"naming_series",
|
"naming_series",
|
||||||
"supplier",
|
"supplier",
|
||||||
"supplier_name",
|
"supplier_name",
|
||||||
@@ -172,17 +171,6 @@
|
|||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"options": "fa fa-user"
|
"options": "fa fa-user"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"allow_on_submit": 1,
|
|
||||||
"default": "{supplier_name}",
|
|
||||||
"fieldname": "title",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Title",
|
|
||||||
"no_copy": 1,
|
|
||||||
"print_hide": 1,
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "naming_series",
|
"fieldname": "naming_series",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
@@ -1328,7 +1316,7 @@
|
|||||||
"idx": 105,
|
"idx": 105,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-03-09 17:15:29.184682",
|
"modified": "2026-03-25 11:46:18.748951",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Purchase Order",
|
"name": "Purchase Order",
|
||||||
|
|||||||
@@ -159,7 +159,6 @@ class PurchaseOrder(BuyingController):
|
|||||||
taxes_and_charges_deducted: DF.Currency
|
taxes_and_charges_deducted: DF.Currency
|
||||||
tc_name: DF.Link | None
|
tc_name: DF.Link | None
|
||||||
terms: DF.TextEditor | None
|
terms: DF.TextEditor | None
|
||||||
title: DF.Data
|
|
||||||
to_date: DF.Date | None
|
to_date: DF.Date | None
|
||||||
total: DF.Currency
|
total: DF.Currency
|
||||||
total_net_weight: DF.Float
|
total_net_weight: DF.Float
|
||||||
@@ -780,6 +779,7 @@ def make_purchase_invoice_from_portal(purchase_order_name):
|
|||||||
if frappe.session.user not in frappe.get_all("Portal User", {"parent": doc.supplier}, pluck="user"):
|
if frappe.session.user not in frappe.get_all("Portal User", {"parent": doc.supplier}, pluck="user"):
|
||||||
frappe.throw(_("Not Permitted"), frappe.PermissionError)
|
frappe.throw(_("Not Permitted"), frappe.PermissionError)
|
||||||
doc.save()
|
doc.save()
|
||||||
|
if not frappe.in_test:
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
frappe.response["type"] = "redirect"
|
frappe.response["type"] = "redirect"
|
||||||
frappe.response.location = "/purchase-invoices/" + doc.name
|
frappe.response.location = "/purchase-invoices/" + doc.name
|
||||||
@@ -802,7 +802,6 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
|||||||
target.set_payment_schedule()
|
target.set_payment_schedule()
|
||||||
target.credit_to = get_party_account("Supplier", source.supplier, source.company)
|
target.credit_to = get_party_account("Supplier", source.supplier, source.company)
|
||||||
|
|
||||||
def update_item(obj, target, source_parent):
|
|
||||||
def get_billed_qty(po_item_name):
|
def get_billed_qty(po_item_name):
|
||||||
from frappe.query_builder.functions import Sum
|
from frappe.query_builder.functions import Sum
|
||||||
|
|
||||||
@@ -814,6 +813,7 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
|||||||
)
|
)
|
||||||
return query.run(pluck="qty")[0] or 0
|
return query.run(pluck="qty")[0] or 0
|
||||||
|
|
||||||
|
def update_item(obj, target, source_parent):
|
||||||
billed_qty = flt(get_billed_qty(obj.name))
|
billed_qty = flt(get_billed_qty(obj.name))
|
||||||
target.qty = flt(obj.qty) - billed_qty
|
target.qty = flt(obj.qty) - billed_qty
|
||||||
|
|
||||||
@@ -853,7 +853,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
|||||||
"wip_composite_asset": "wip_composite_asset",
|
"wip_composite_asset": "wip_composite_asset",
|
||||||
},
|
},
|
||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
"condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount))
|
"condition": lambda doc: (
|
||||||
|
doc.base_amount == 0
|
||||||
|
or abs(doc.billed_amt) < abs(doc.amount)
|
||||||
|
or doc.qty > flt(get_billed_qty(doc.name))
|
||||||
|
)
|
||||||
and select_item(doc),
|
and select_item(doc),
|
||||||
},
|
},
|
||||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
|
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
|
||||||
|
|||||||
@@ -291,6 +291,30 @@ class TestPurchaseOrder(ERPNextTestSuite):
|
|||||||
# ordered qty should decrease (back to initial) on row deletion
|
# ordered qty should decrease (back to initial) on row deletion
|
||||||
self.assertEqual(get_ordered_qty(), existing_ordered_qty)
|
self.assertEqual(get_ordered_qty(), existing_ordered_qty)
|
||||||
|
|
||||||
|
def test_discount_amount_partial_purchase_receipt(self):
|
||||||
|
po = create_purchase_order(qty=4, rate=100, do_not_save=1)
|
||||||
|
po.apply_discount_on = "Grand Total"
|
||||||
|
po.discount_amount = 120
|
||||||
|
po.save()
|
||||||
|
po.submit()
|
||||||
|
|
||||||
|
self.assertEqual(po.grand_total, 280)
|
||||||
|
|
||||||
|
pr1 = make_purchase_receipt(po.name)
|
||||||
|
pr1.items[0].qty = 3
|
||||||
|
pr1.save()
|
||||||
|
pr1.submit()
|
||||||
|
|
||||||
|
self.assertEqual(pr1.discount_amount, 120)
|
||||||
|
self.assertEqual(pr1.grand_total, 180)
|
||||||
|
|
||||||
|
pr2 = make_purchase_receipt(po.name)
|
||||||
|
pr2.save()
|
||||||
|
pr2.submit()
|
||||||
|
|
||||||
|
self.assertEqual(pr2.discount_amount, 0)
|
||||||
|
self.assertEqual(pr2.grand_total, 100)
|
||||||
|
|
||||||
def test_update_child_perm(self):
|
def test_update_child_perm(self):
|
||||||
po = create_purchase_order(item_code="_Test Item", qty=4)
|
po = create_purchase_order(item_code="_Test Item", qty=4)
|
||||||
|
|
||||||
@@ -1386,6 +1410,34 @@ class TestPurchaseOrder(ERPNextTestSuite):
|
|||||||
self.assertEqual(pi_2.status, "Paid")
|
self.assertEqual(pi_2.status, "Paid")
|
||||||
self.assertEqual(po.status, "Completed")
|
self.assertEqual(po.status, "Completed")
|
||||||
|
|
||||||
|
def test_purchase_order_over_billing_missing_item(self):
|
||||||
|
item1 = make_item(
|
||||||
|
"_Test Item for Overbilling",
|
||||||
|
).name
|
||||||
|
|
||||||
|
item2 = make_item(
|
||||||
|
"_Test Item for Overbilling 2",
|
||||||
|
).name
|
||||||
|
|
||||||
|
po = create_purchase_order(qty=10, rate=1000, item_code=item1, do_not_save=1)
|
||||||
|
po.append("items", {"item_code": item2, "qty": 5, "rate": 20, "warehouse": "_Test Warehouse - _TC"})
|
||||||
|
po.taxes = []
|
||||||
|
po.insert()
|
||||||
|
po.submit()
|
||||||
|
|
||||||
|
pi1 = make_pi_from_po(po.name)
|
||||||
|
pi1.items[0].qty = 8
|
||||||
|
pi1.items[0].rate = 1250
|
||||||
|
pi1.remove(pi1.items[1])
|
||||||
|
pi1.insert()
|
||||||
|
pi1.submit()
|
||||||
|
|
||||||
|
self.assertEqual(pi1.grand_total, 10000.0)
|
||||||
|
self.assertTrue(len(pi1.items) == 1)
|
||||||
|
|
||||||
|
pi2 = make_pi_from_po(po.name)
|
||||||
|
self.assertEqual(len(pi2.items), 2)
|
||||||
|
|
||||||
|
|
||||||
def create_po_for_sc_testing():
|
def create_po_for_sc_testing():
|
||||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||||
|
|||||||
@@ -280,14 +280,16 @@
|
|||||||
"depends_on": "price_list_rate",
|
"depends_on": "price_list_rate",
|
||||||
"fieldname": "discount_percentage",
|
"fieldname": "discount_percentage",
|
||||||
"fieldtype": "Percent",
|
"fieldtype": "Percent",
|
||||||
"label": "Discount on Price List Rate (%)"
|
"label": "Discount on Price List Rate (%)",
|
||||||
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "price_list_rate",
|
"depends_on": "price_list_rate",
|
||||||
"fieldname": "discount_amount",
|
"fieldname": "discount_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Discount Amount",
|
"label": "Discount Amount",
|
||||||
"options": "currency"
|
"options": "currency",
|
||||||
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "col_break3",
|
"fieldname": "col_break3",
|
||||||
@@ -428,6 +430,7 @@
|
|||||||
"fieldname": "weight_per_unit",
|
"fieldname": "weight_per_unit",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Weight Per Unit",
|
"label": "Weight Per Unit",
|
||||||
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -763,6 +766,7 @@
|
|||||||
"label": "Rate of Stock UOM",
|
"label": "Rate of Stock UOM",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "currency",
|
"options": "currency",
|
||||||
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -779,6 +783,7 @@
|
|||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Available Qty at Company",
|
"label": "Available Qty at Company",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -878,7 +883,8 @@
|
|||||||
"fieldname": "fg_item_qty",
|
"fieldname": "fg_item_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Finished Good Qty",
|
"label": "Finished Good Qty",
|
||||||
"mandatory_depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow"
|
"mandatory_depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow",
|
||||||
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:parent.is_internal_supplier",
|
"depends_on": "eval:parent.is_internal_supplier",
|
||||||
@@ -923,7 +929,8 @@
|
|||||||
"fieldname": "distributed_discount_amount",
|
"fieldname": "distributed_discount_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Distributed Discount Amount",
|
"label": "Distributed Discount Amount",
|
||||||
"options": "currency"
|
"options": "currency",
|
||||||
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
@@ -934,6 +941,7 @@
|
|||||||
"label": "Subcontracted Quantity",
|
"label": "Subcontracted Quantity",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"non_negative": 1,
|
"non_negative": 1,
|
||||||
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -942,7 +950,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-10-30 16:51:56.761673",
|
"modified": "2025-11-30 16:51:57.761673",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Purchase Order Item",
|
"name": "Purchase Order Item",
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ frappe.ui.form.on("Request for Quotation", {
|
|||||||
fieldname: "print_format",
|
fieldname: "print_format",
|
||||||
options: "Print Format",
|
options: "Print Format",
|
||||||
placeholder: "Standard",
|
placeholder: "Standard",
|
||||||
|
default: frappe.get_meta("Request for Quotation").default_print_format || "",
|
||||||
get_query: () => {
|
get_query: () => {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
|
|||||||
@@ -9,8 +9,6 @@
|
|||||||
"field_order": [
|
"field_order": [
|
||||||
"naming_series",
|
"naming_series",
|
||||||
"company",
|
"company",
|
||||||
"billing_address",
|
|
||||||
"billing_address_display",
|
|
||||||
"vendor",
|
"vendor",
|
||||||
"column_break1",
|
"column_break1",
|
||||||
"transaction_date",
|
"transaction_date",
|
||||||
@@ -43,7 +41,13 @@
|
|||||||
"select_print_heading",
|
"select_print_heading",
|
||||||
"letter_head",
|
"letter_head",
|
||||||
"more_info",
|
"more_info",
|
||||||
"opportunity"
|
"opportunity",
|
||||||
|
"address_and_contact_tab",
|
||||||
|
"billing_address",
|
||||||
|
"billing_address_display",
|
||||||
|
"column_break_czul",
|
||||||
|
"shipping_address",
|
||||||
|
"shipping_address_display"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -346,6 +350,27 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Use HTML"
|
"label": "Use HTML"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "address_and_contact_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Address & Contact"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_czul",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "shipping_address",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Company Shipping Address",
|
||||||
|
"options": "Address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "shipping_address_display",
|
||||||
|
"fieldtype": "Text Editor",
|
||||||
|
"label": "Shipping Address Details",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
@@ -353,7 +378,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-03-09 17:15:29.774614",
|
"modified": "2026-03-19 15:27:56.730649",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Request for Quotation",
|
"name": "Request for Quotation",
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ class RequestforQuotation(BuyingController):
|
|||||||
select_print_heading: DF.Link | None
|
select_print_heading: DF.Link | None
|
||||||
send_attached_files: DF.Check
|
send_attached_files: DF.Check
|
||||||
send_document_print: DF.Check
|
send_document_print: DF.Check
|
||||||
|
shipping_address: DF.Link | None
|
||||||
|
shipping_address_display: DF.TextEditor | None
|
||||||
status: DF.Literal["", "Draft", "Submitted", "Cancelled"]
|
status: DF.Literal["", "Draft", "Submitted", "Cancelled"]
|
||||||
subject: DF.Data
|
subject: DF.Data
|
||||||
suppliers: DF.Table[RequestforQuotationSupplier]
|
suppliers: DF.Table[RequestforQuotationSupplier]
|
||||||
@@ -283,7 +285,7 @@ class RequestforQuotation(BuyingController):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
user.save(ignore_permissions=True)
|
user.save(ignore_permissions=True)
|
||||||
update_password_link = user.reset_password()
|
update_password_link = user._reset_password()
|
||||||
|
|
||||||
return user, update_password_link
|
return user, update_password_link
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,15 @@ def create_supplier(**args):
|
|||||||
if not args.without_supplier_group:
|
if not args.without_supplier_group:
|
||||||
doc.supplier_group = args.supplier_group or "Services"
|
doc.supplier_group = args.supplier_group or "Services"
|
||||||
|
|
||||||
|
if args.get("party_account"):
|
||||||
|
doc.append(
|
||||||
|
"accounts",
|
||||||
|
{
|
||||||
|
"company": frappe.db.get_value("Account", args.get("party_account"), "company"),
|
||||||
|
"account": args.get("party_account"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
doc.insert()
|
doc.insert()
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -4324,6 +4324,8 @@ def get_missing_company_details(doctype, docname):
|
|||||||
company = frappe.db.get_value(doctype, docname, "company")
|
company = frappe.db.get_value(doctype, docname, "company")
|
||||||
if doctype in ["Purchase Order", "Purchase Invoice"]:
|
if doctype in ["Purchase Order", "Purchase Invoice"]:
|
||||||
company_address = frappe.db.get_value(doctype, docname, "billing_address")
|
company_address = frappe.db.get_value(doctype, docname, "billing_address")
|
||||||
|
elif doctype in ["Request for Quotation"]:
|
||||||
|
company_address = frappe.db.get_value(doctype, docname, "shipping_address")
|
||||||
else:
|
else:
|
||||||
company_address = frappe.db.get_value(doctype, docname, "company_address")
|
company_address = frappe.db.get_value(doctype, docname, "company_address")
|
||||||
|
|
||||||
|
|||||||
@@ -457,7 +457,7 @@ class BuyingController(SubcontractingController):
|
|||||||
get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
|
get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
|
||||||
)
|
)
|
||||||
|
|
||||||
net_rate = item.base_net_amount
|
net_rate = item.qty * item.base_net_rate
|
||||||
if item.sales_incoming_rate: # for internal transfer
|
if item.sales_incoming_rate: # for internal transfer
|
||||||
net_rate = item.qty * item.sales_incoming_rate
|
net_rate = item.qty * item.sales_incoming_rate
|
||||||
|
|
||||||
@@ -503,11 +503,15 @@ class BuyingController(SubcontractingController):
|
|||||||
if d.category not in ["Valuation", "Valuation and Total"]:
|
if d.category not in ["Valuation", "Valuation and Total"]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
amount = flt(d.base_tax_amount_after_discount_amount) * (
|
||||||
|
-1 if d.get("add_deduct_tax") == "Deduct" else 1
|
||||||
|
)
|
||||||
|
|
||||||
if d.charge_type == "On Net Total":
|
if d.charge_type == "On Net Total":
|
||||||
total_valuation_amount += flt(d.base_tax_amount_after_discount_amount)
|
total_valuation_amount += amount
|
||||||
tax_accounts.append(d.account_head)
|
tax_accounts.append(d.account_head)
|
||||||
else:
|
else:
|
||||||
total_actual_tax_amount += flt(d.base_tax_amount_after_discount_amount)
|
total_actual_tax_amount += amount
|
||||||
|
|
||||||
return tax_accounts, total_valuation_amount, total_actual_tax_amount
|
return tax_accounts, total_valuation_amount, total_actual_tax_amount
|
||||||
|
|
||||||
|
|||||||
@@ -364,38 +364,43 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@frappe.validate_and_sanitize_search_inputs
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, filters, as_dict):
|
def get_delivery_notes_to_be_billed(
|
||||||
doctype = "Delivery Note"
|
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict, as_dict: bool = False
|
||||||
|
):
|
||||||
|
DeliveryNote = frappe.qb.DocType("Delivery Note")
|
||||||
|
|
||||||
fields = get_fields(doctype, ["name", "customer", "posting_date"])
|
fields = get_fields(doctype, ["name", "customer", "posting_date"])
|
||||||
|
|
||||||
return frappe.db.sql(
|
original_dn = (
|
||||||
"""
|
frappe.qb.from_(DeliveryNote)
|
||||||
select {fields}
|
.select(DeliveryNote.name)
|
||||||
from `tabDelivery Note`
|
.where((DeliveryNote.docstatus == 1) & (DeliveryNote.is_return == 0) & (DeliveryNote.per_billed > 0))
|
||||||
where `tabDelivery Note`.`{key}` like {txt} and
|
)
|
||||||
`tabDelivery Note`.docstatus = 1
|
|
||||||
and status not in ('Stopped', 'Closed') {fcond}
|
query = (
|
||||||
and (
|
frappe.qb.from_(DeliveryNote)
|
||||||
(`tabDelivery Note`.is_return = 0 and `tabDelivery Note`.per_billed < 100)
|
.select(*[DeliveryNote[f] for f in fields])
|
||||||
or (`tabDelivery Note`.grand_total = 0 and `tabDelivery Note`.per_billed < 100)
|
.where(
|
||||||
or (
|
(DeliveryNote.docstatus == 1)
|
||||||
`tabDelivery Note`.is_return = 1
|
& (DeliveryNote.status.notin(["Stopped", "Closed"]))
|
||||||
and return_against in (select name from `tabDelivery Note` where per_billed < 100)
|
& (DeliveryNote[searchfield].like(f"%{txt}%"))
|
||||||
|
& (
|
||||||
|
((DeliveryNote.is_return == 0) & (DeliveryNote.per_billed < 100))
|
||||||
|
| ((DeliveryNote.grand_total == 0) & (DeliveryNote.per_billed < 100))
|
||||||
|
| (
|
||||||
|
(DeliveryNote.is_return == 1)
|
||||||
|
& (DeliveryNote.per_billed < 100)
|
||||||
|
& (DeliveryNote.return_against.isin(original_dn))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
{mcond} order by `tabDelivery Note`.`{key}` asc limit {page_len} offset {start}
|
|
||||||
""".format(
|
|
||||||
fields=", ".join([f"`tabDelivery Note`.{f}" for f in fields]),
|
|
||||||
key=searchfield,
|
|
||||||
fcond=get_filters_cond(doctype, filters, []),
|
|
||||||
mcond=get_match_cond(doctype),
|
|
||||||
start=start,
|
|
||||||
page_len=page_len,
|
|
||||||
txt="%(txt)s",
|
|
||||||
),
|
|
||||||
{"txt": ("%%%s%%" % txt)},
|
|
||||||
as_dict=as_dict,
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
if filters and isinstance(filters, dict):
|
||||||
|
for key, value in filters.items():
|
||||||
|
query = query.where(DeliveryNote[key] == value)
|
||||||
|
|
||||||
|
query = query.orderby(DeliveryNote[searchfield], order=Order.asc).limit(page_len).offset(start)
|
||||||
|
return query.run(as_dict=as_dict)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@@ -1002,3 +1007,26 @@ def get_item_uom_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
limit_page_length=page_len,
|
limit_page_length=page_len,
|
||||||
as_list=1,
|
as_list=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
|
def get_warehouse_address(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
|
||||||
|
table = frappe.qb.DocType(doctype)
|
||||||
|
child_table = frappe.qb.DocType("Dynamic Link")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(table)
|
||||||
|
.inner_join(child_table)
|
||||||
|
.on((table.name == child_table.parent) & (child_table.parenttype == doctype))
|
||||||
|
.select(table.name)
|
||||||
|
.where(
|
||||||
|
(child_table.link_name == filters.get("warehouse"))
|
||||||
|
& (table.disabled == 0)
|
||||||
|
& (child_table.link_doctype == "Warehouse")
|
||||||
|
& (table.name.like(f"%{txt}%"))
|
||||||
|
)
|
||||||
|
.offset(start)
|
||||||
|
.limit(page_len)
|
||||||
|
)
|
||||||
|
return query.run(as_list=1)
|
||||||
|
|||||||
@@ -634,11 +634,11 @@ class SellingController(StockController):
|
|||||||
if allow_at_arms_length_price:
|
if allow_at_arms_length_price:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
rate = flt(
|
rate = flt(flt(d.incoming_rate) * flt(d.conversion_factor or 1.0))
|
||||||
flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor,
|
|
||||||
d.precision("rate"),
|
if flt(d.rate, d.precision("incoming_rate")) != flt(
|
||||||
)
|
rate, d.precision("incoming_rate")
|
||||||
if d.rate != rate:
|
):
|
||||||
d.rate = rate
|
d.rate = rate
|
||||||
frappe.msgprint(
|
frappe.msgprint(
|
||||||
_(
|
_(
|
||||||
|
|||||||
@@ -1435,7 +1435,7 @@ class StockController(AccountsController):
|
|||||||
elif self.doctype == "Stock Entry" and row.t_warehouse:
|
elif self.doctype == "Stock Entry" and row.t_warehouse:
|
||||||
qi_required = True # inward stock needs inspection
|
qi_required = True # inward stock needs inspection
|
||||||
|
|
||||||
if row.get("is_scrap_item"):
|
if row.get("type") or row.get("is_legacy_scrap_item"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if qi_required: # validate row only if inspection is required on item level
|
if qi_required: # validate row only if inspection is required on item level
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ class SubcontractingController(StockController):
|
|||||||
).format(item.idx, get_link_to_form("Item", item.item_code))
|
).format(item.idx, get_link_to_form("Item", item.item_code))
|
||||||
)
|
)
|
||||||
|
|
||||||
if not item.get("is_scrap_item"):
|
if not item.get("type") and not item.get("is_legacy_scrap_item"):
|
||||||
if not is_sub_contracted_item:
|
if not is_sub_contracted_item:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
|
_("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
|
||||||
@@ -206,7 +206,7 @@ class SubcontractingController(StockController):
|
|||||||
).format(item.idx, item.item_name)
|
).format(item.idx, item.item_name)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.doctype != "Subcontracting Inward Order":
|
if self.doctype not in ["Subcontracting Inward Order", "Subcontracting Receipt"]:
|
||||||
item.amount = item.qty * item.rate
|
item.amount = item.qty * item.rate
|
||||||
|
|
||||||
if item.bom:
|
if item.bom:
|
||||||
@@ -238,7 +238,7 @@ class SubcontractingController(StockController):
|
|||||||
and self._doc_before_save
|
and self._doc_before_save
|
||||||
):
|
):
|
||||||
for row in self._doc_before_save.get("items"):
|
for row in self._doc_before_save.get("items"):
|
||||||
item_dict[row.name] = (row.item_code, row.qty + (row.get("rejected_qty") or 0))
|
item_dict[row.name] = (row.item_code, row.received_qty)
|
||||||
|
|
||||||
return item_dict
|
return item_dict
|
||||||
|
|
||||||
@@ -264,7 +264,7 @@ class SubcontractingController(StockController):
|
|||||||
self.__reference_name.append(row.name)
|
self.__reference_name.append(row.name)
|
||||||
if (row.name not in item_dict) or (
|
if (row.name not in item_dict) or (
|
||||||
row.item_code,
|
row.item_code,
|
||||||
row.qty + (row.get("rejected_qty") or 0),
|
row.received_qty,
|
||||||
) != item_dict[row.name]:
|
) != item_dict[row.name]:
|
||||||
self.__changed_name.append(row.name)
|
self.__changed_name.append(row.name)
|
||||||
|
|
||||||
@@ -962,7 +962,7 @@ class SubcontractingController(StockController):
|
|||||||
):
|
):
|
||||||
qty = (
|
qty = (
|
||||||
flt(bom_item.qty_consumed_per_unit)
|
flt(bom_item.qty_consumed_per_unit)
|
||||||
* flt(row.qty + (row.get("rejected_qty") or 0))
|
* flt(row.get("received_qty") or (row.qty + (row.get("rejected_qty") or 0)))
|
||||||
* row.conversion_factor
|
* row.conversion_factor
|
||||||
)
|
)
|
||||||
bom_item.main_item_code = row.item_code
|
bom_item.main_item_code = row.item_code
|
||||||
@@ -1285,22 +1285,28 @@ class SubcontractingController(StockController):
|
|||||||
if self.total_additional_costs:
|
if self.total_additional_costs:
|
||||||
if self.distribute_additional_costs_based_on == "Amount":
|
if self.distribute_additional_costs_based_on == "Amount":
|
||||||
total_amt = sum(
|
total_amt = sum(
|
||||||
flt(item.amount) for item in self.get("items") if not item.get("is_scrap_item")
|
flt(item.amount)
|
||||||
|
for item in self.get("items")
|
||||||
|
if not item.get("type") and not item.get("is_legacy_scrap_item")
|
||||||
)
|
)
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
if not item.get("is_scrap_item"):
|
if not item.get("type") and not item.get("is_legacy_scrap_item"):
|
||||||
item.additional_cost_per_qty = (
|
item.additional_cost_per_qty = (
|
||||||
(item.amount * self.total_additional_costs) / total_amt
|
(item.amount * self.total_additional_costs) / total_amt
|
||||||
) / item.qty
|
) / item.qty
|
||||||
else:
|
else:
|
||||||
total_qty = sum(flt(item.qty) for item in self.get("items") if not item.get("is_scrap_item"))
|
total_qty = sum(
|
||||||
|
flt(item.qty)
|
||||||
|
for item in self.get("items")
|
||||||
|
if not item.get("type") and not item.get("is_legacy_scrap_item")
|
||||||
|
)
|
||||||
additional_cost_per_qty = self.total_additional_costs / total_qty
|
additional_cost_per_qty = self.total_additional_costs / total_qty
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
if not item.get("is_scrap_item"):
|
if not item.get("type") and not item.get("is_legacy_scrap_item"):
|
||||||
item.additional_cost_per_qty = additional_cost_per_qty
|
item.additional_cost_per_qty = additional_cost_per_qty
|
||||||
else:
|
else:
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
if not item.get("is_scrap_item"):
|
if not item.get("type") and not item.get("is_legacy_scrap_item"):
|
||||||
item.additional_cost_per_qty = 0
|
item.additional_cost_per_qty = 0
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, bold
|
from frappe import _, bold
|
||||||
from frappe.query_builder import Case
|
from frappe.query_builder import Case
|
||||||
@@ -18,7 +20,7 @@ class SubcontractingInwardController:
|
|||||||
def on_submit_subcontracting_inward(self):
|
def on_submit_subcontracting_inward(self):
|
||||||
self.update_inward_order_item()
|
self.update_inward_order_item()
|
||||||
self.update_inward_order_received_items()
|
self.update_inward_order_received_items()
|
||||||
self.update_inward_order_scrap_items()
|
self.update_inward_order_secondary_items()
|
||||||
self.create_stock_reservation_entries_for_inward()
|
self.create_stock_reservation_entries_for_inward()
|
||||||
self.update_inward_order_status()
|
self.update_inward_order_status()
|
||||||
|
|
||||||
@@ -28,7 +30,7 @@ class SubcontractingInwardController:
|
|||||||
self.validate_delivery()
|
self.validate_delivery()
|
||||||
self.validate_receive_from_customer_cancel()
|
self.validate_receive_from_customer_cancel()
|
||||||
self.update_inward_order_received_items()
|
self.update_inward_order_received_items()
|
||||||
self.update_inward_order_scrap_items()
|
self.update_inward_order_secondary_items()
|
||||||
self.remove_reference_for_additional_items()
|
self.remove_reference_for_additional_items()
|
||||||
self.update_inward_order_status()
|
self.update_inward_order_status()
|
||||||
|
|
||||||
@@ -239,7 +241,8 @@ class SubcontractingInwardController:
|
|||||||
item
|
item
|
||||||
for item in self.get("items")
|
for item in self.get("items")
|
||||||
if not item.is_finished_item
|
if not item.is_finished_item
|
||||||
and not item.is_scrap_item
|
and not item.type
|
||||||
|
and not item.is_legacy_scrap_item
|
||||||
and frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item")
|
and frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item")
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -368,7 +371,9 @@ class SubcontractingInwardController:
|
|||||||
if self.subcontracting_inward_order:
|
if self.subcontracting_inward_order:
|
||||||
if self.purpose in ["Subcontracting Delivery", "Subcontracting Return", "Manufacture"]:
|
if self.purpose in ["Subcontracting Delivery", "Subcontracting Return", "Manufacture"]:
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
if (item.is_finished_item or item.is_scrap_item) and item.valuation_rate == 0:
|
if (
|
||||||
|
item.is_finished_item or item.type or item.is_legacy_scrap_item
|
||||||
|
) and item.valuation_rate == 0:
|
||||||
item.allow_zero_valuation_rate = 1
|
item.allow_zero_valuation_rate = 1
|
||||||
|
|
||||||
def validate_warehouse_(self):
|
def validate_warehouse_(self):
|
||||||
@@ -467,7 +472,7 @@ class SubcontractingInwardController:
|
|||||||
self.validate_delivery_on_save()
|
self.validate_delivery_on_save()
|
||||||
else:
|
else:
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
if not item.is_scrap_item:
|
if not item.type and not item.is_legacy_scrap_item:
|
||||||
delivered_qty, returned_qty = frappe.get_value(
|
delivered_qty, returned_qty = frappe.get_value(
|
||||||
"Subcontracting Inward Order Item",
|
"Subcontracting Inward Order Item",
|
||||||
item.scio_detail,
|
item.scio_detail,
|
||||||
@@ -519,7 +524,7 @@ class SubcontractingInwardController:
|
|||||||
if max_allowed_qty:
|
if max_allowed_qty:
|
||||||
max_allowed_qty = max_allowed_qty[0]
|
max_allowed_qty = max_allowed_qty[0]
|
||||||
else:
|
else:
|
||||||
table = frappe.qb.DocType("Subcontracting Inward Order Scrap Item")
|
table = frappe.qb.DocType("Subcontracting Inward Order Secondary Item")
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(table)
|
frappe.qb.from_(table)
|
||||||
.select((table.produced_qty - table.delivered_qty).as_("max_allowed_qty"))
|
.select((table.produced_qty - table.delivered_qty).as_("max_allowed_qty"))
|
||||||
@@ -538,8 +543,8 @@ class SubcontractingInwardController:
|
|||||||
bold(
|
bold(
|
||||||
frappe.get_cached_value(
|
frappe.get_cached_value(
|
||||||
"Subcontracting Inward Order Item"
|
"Subcontracting Inward Order Item"
|
||||||
if not item.is_scrap_item
|
if not item.type and not item.is_legacy_scrap_item
|
||||||
else "Subcontracting Inward Order Scrap Item",
|
else "Subcontracting Inward Order Secondary Item",
|
||||||
item.scio_detail,
|
item.scio_detail,
|
||||||
"stock_uom",
|
"stock_uom",
|
||||||
)
|
)
|
||||||
@@ -590,9 +595,9 @@ class SubcontractingInwardController:
|
|||||||
)
|
)
|
||||||
|
|
||||||
for item in [item for item in self.items if not item.is_finished_item]:
|
for item in [item for item in self.items if not item.is_finished_item]:
|
||||||
if item.is_scrap_item:
|
if item.type or item.is_legacy_scrap_item:
|
||||||
scio_scrap_item = frappe.get_value(
|
scio_secondary_item = frappe.get_value(
|
||||||
"Subcontracting Inward Order Scrap Item",
|
"Subcontracting Inward Order Secondary Item",
|
||||||
{
|
{
|
||||||
"docstatus": 1,
|
"docstatus": 1,
|
||||||
"item_code": item.item_code,
|
"item_code": item.item_code,
|
||||||
@@ -603,12 +608,13 @@ class SubcontractingInwardController:
|
|||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
scio_scrap_item
|
scio_secondary_item
|
||||||
and scio_scrap_item.delivered_qty > scio_scrap_item.produced_qty - item.transfer_qty
|
and scio_secondary_item.delivered_qty
|
||||||
|
> scio_secondary_item.produced_qty - item.transfer_qty
|
||||||
):
|
):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
"Row #{0}: Cannot cancel this Manufacturing Stock Entry as quantity of Scrap Item {1} produced cannot be less than quantity delivered."
|
"Row #{0}: Cannot cancel this Manufacturing Stock Entry as quantity of Secondary Item {1} produced cannot be less than quantity delivered."
|
||||||
).format(item.idx, get_link_to_form("Item", item.item_code))
|
).format(item.idx, get_link_to_form("Item", item.item_code))
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -648,8 +654,8 @@ class SubcontractingInwardController:
|
|||||||
for item in self.items:
|
for item in self.items:
|
||||||
doctype = (
|
doctype = (
|
||||||
"Subcontracting Inward Order Item"
|
"Subcontracting Inward Order Item"
|
||||||
if not item.is_scrap_item
|
if not item.type and not item.is_legacy_scrap_item
|
||||||
else "Subcontracting Inward Order Scrap Item"
|
else "Subcontracting Inward Order Secondary Item"
|
||||||
)
|
)
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
doctype,
|
doctype,
|
||||||
@@ -763,7 +769,11 @@ class SubcontractingInwardController:
|
|||||||
customer_warehouse = frappe.get_cached_value(
|
customer_warehouse = frappe.get_cached_value(
|
||||||
"Subcontracting Inward Order", self.subcontracting_inward_order, "customer_warehouse"
|
"Subcontracting Inward Order", self.subcontracting_inward_order, "customer_warehouse"
|
||||||
)
|
)
|
||||||
items = [item for item in self.items if not item.is_finished_item and not item.is_scrap_item]
|
items = [
|
||||||
|
item
|
||||||
|
for item in self.items
|
||||||
|
if not item.is_finished_item and not item.type and not item.is_legacy_scrap_item
|
||||||
|
]
|
||||||
item_code_wh = frappe._dict(
|
item_code_wh = frappe._dict(
|
||||||
{
|
{
|
||||||
(
|
(
|
||||||
@@ -860,24 +870,24 @@ class SubcontractingInwardController:
|
|||||||
doc.insert()
|
doc.insert()
|
||||||
doc.submit()
|
doc.submit()
|
||||||
|
|
||||||
def update_inward_order_scrap_items(self):
|
def update_inward_order_secondary_items(self):
|
||||||
if (scio := self.subcontracting_inward_order) and self.purpose == "Manufacture":
|
if (scio := self.subcontracting_inward_order) and self.purpose == "Manufacture":
|
||||||
scrap_items_list = [item for item in self.items if item.is_scrap_item]
|
secondary_items_list = [item for item in self.items if item.type or item.is_legacy_scrap_item]
|
||||||
scrap_items = frappe._dict(
|
|
||||||
{
|
secondary_items = defaultdict(float)
|
||||||
(item.item_code, item.t_warehouse): item.transfer_qty
|
for item in secondary_items_list:
|
||||||
if self._action == "submit"
|
secondary_items[(item.item_code, item.t_warehouse)] += (
|
||||||
else -item.transfer_qty
|
item.transfer_qty if self._action == "submit" else -item.transfer_qty
|
||||||
for item in scrap_items_list
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
if scrap_items:
|
secondary_items = frappe._dict(secondary_items)
|
||||||
item_codes, warehouses = zip(*list(scrap_items.keys()), strict=True)
|
|
||||||
|
if secondary_items:
|
||||||
|
item_codes, warehouses = zip(*list(secondary_items.keys()), strict=True)
|
||||||
item_codes = list(item_codes)
|
item_codes = list(item_codes)
|
||||||
warehouses = list(warehouses)
|
warehouses = list(warehouses)
|
||||||
|
|
||||||
result = frappe.get_all(
|
result = frappe.get_all(
|
||||||
"Subcontracting Inward Order Scrap Item",
|
"Subcontracting Inward Order Secondary Item",
|
||||||
filters={
|
filters={
|
||||||
"item_code": ["in", item_codes],
|
"item_code": ["in", item_codes],
|
||||||
"warehouse": ["in", warehouses],
|
"warehouse": ["in", warehouses],
|
||||||
@@ -890,7 +900,7 @@ class SubcontractingInwardController:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
scrap_item_dict = frappe._dict(
|
secondary_items_dict = frappe._dict(
|
||||||
{
|
{
|
||||||
(d.item_code, d.warehouse): frappe._dict(
|
(d.item_code, d.warehouse): frappe._dict(
|
||||||
{"name": d.name, "produced_qty": d.produced_qty}
|
{"name": d.name, "produced_qty": d.produced_qty}
|
||||||
@@ -900,40 +910,45 @@ class SubcontractingInwardController:
|
|||||||
)
|
)
|
||||||
deleted_docs = []
|
deleted_docs = []
|
||||||
case_expr = Case()
|
case_expr = Case()
|
||||||
table = frappe.qb.DocType("Subcontracting Inward Order Scrap Item")
|
table = frappe.qb.DocType("Subcontracting Inward Order Secondary Item")
|
||||||
for key, value in scrap_item_dict.items():
|
for key, value in secondary_items_dict.items():
|
||||||
if self._action == "cancel" and value.produced_qty - abs(scrap_items.get(key)) == 0:
|
if (
|
||||||
|
self._action == "cancel"
|
||||||
|
and value.produced_qty - abs(secondary_items.get(key)) == 0
|
||||||
|
):
|
||||||
deleted_docs.append(value.name)
|
deleted_docs.append(value.name)
|
||||||
frappe.delete_doc("Subcontracting Inward Order Scrap Item", value.name)
|
frappe.delete_doc("Subcontracting Inward Order Secondary Item", value.name)
|
||||||
else:
|
else:
|
||||||
case_expr = case_expr.when(
|
case_expr = case_expr.when(
|
||||||
table.name == value.name, value.produced_qty + scrap_items.get(key)
|
table.name == value.name, value.produced_qty + secondary_items.get(key)
|
||||||
)
|
)
|
||||||
|
|
||||||
if final_list := list(
|
if final_list := list(
|
||||||
set([v.name for v in scrap_item_dict.values()]) - set(deleted_docs)
|
set([v.name for v in secondary_items_dict.values()]) - set(deleted_docs)
|
||||||
):
|
):
|
||||||
frappe.qb.update(table).set(table.produced_qty, case_expr).where(
|
frappe.qb.update(table).set(table.produced_qty, case_expr).where(
|
||||||
(table.name.isin(final_list)) & (table.docstatus == 1)
|
(table.name.isin(final_list)) & (table.docstatus == 1)
|
||||||
).run()
|
).run()
|
||||||
|
|
||||||
fg_item_code = next(fg for fg in self.items if fg.is_finished_item).item_code
|
fg_item_code = next(fg for fg in self.items if fg.is_finished_item).item_code
|
||||||
for scrap_item in [
|
for secondary_item in [
|
||||||
item
|
item
|
||||||
for item in scrap_items_list
|
for item in secondary_items_list
|
||||||
if (item.item_code, item.t_warehouse) not in [(d.item_code, d.warehouse) for d in result]
|
if (item.item_code, item.t_warehouse) not in [(d.item_code, d.warehouse) for d in result]
|
||||||
]:
|
]:
|
||||||
doc = frappe.new_doc(
|
doc = frappe.new_doc(
|
||||||
"Subcontracting Inward Order Scrap Item",
|
"Subcontracting Inward Order Secondary Item",
|
||||||
parent=scio,
|
parent=scio,
|
||||||
parenttype="Subcontracting Inward Order",
|
parenttype="Subcontracting Inward Order",
|
||||||
parentfield="scrap_items",
|
parentfield="secondary_items",
|
||||||
idx=frappe.db.count("Subcontracting Inward Order Scrap Item", {"parent": scio}) + 1,
|
idx=frappe.db.count("Subcontracting Inward Order Secondary Item", {"parent": scio})
|
||||||
item_code=scrap_item.item_code,
|
+ 1,
|
||||||
|
item_code=secondary_item.item_code,
|
||||||
fg_item_code=fg_item_code,
|
fg_item_code=fg_item_code,
|
||||||
stock_uom=scrap_item.stock_uom,
|
stock_uom=secondary_item.stock_uom,
|
||||||
warehouse=scrap_item.t_warehouse,
|
warehouse=secondary_item.t_warehouse,
|
||||||
produced_qty=scrap_item.transfer_qty,
|
produced_qty=secondary_item.transfer_qty,
|
||||||
|
type=secondary_item.type,
|
||||||
delivered_qty=0,
|
delivered_qty=0,
|
||||||
reference_name=frappe.get_value(
|
reference_name=frappe.get_value(
|
||||||
"Work Order", self.work_order, "subcontracting_inward_order_item"
|
"Work Order", self.work_order, "subcontracting_inward_order_item"
|
||||||
@@ -965,7 +980,7 @@ class SubcontractingInwardController:
|
|||||||
and (
|
and (
|
||||||
not frappe.db.exists("Subcontracting Inward Order Received Item", item.scio_detail)
|
not frappe.db.exists("Subcontracting Inward Order Received Item", item.scio_detail)
|
||||||
and not frappe.db.exists("Subcontracting Inward Order Item", item.scio_detail)
|
and not frappe.db.exists("Subcontracting Inward Order Item", item.scio_detail)
|
||||||
and not frappe.db.exists("Subcontracting Inward Order Scrap Item", item.scio_detail)
|
and not frappe.db.exists("Subcontracting Inward Order Secondary Item", item.scio_detail)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
for item in items:
|
for item in items:
|
||||||
|
|||||||
@@ -164,8 +164,14 @@ class calculate_taxes_and_totals:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if not self.discount_amount_applied:
|
if not self.discount_amount_applied:
|
||||||
|
bill_for_rejected_quantity_in_purchase_invoice = frappe.get_single_value(
|
||||||
|
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
|
||||||
|
)
|
||||||
|
|
||||||
|
do_not_round_fields = ["valuation_rate", "incoming_rate"]
|
||||||
|
|
||||||
for item in self.doc.items:
|
for item in self.doc.items:
|
||||||
self.doc.round_floats_in(item)
|
self.doc.round_floats_in(item, do_not_round_fields=do_not_round_fields)
|
||||||
|
|
||||||
if item.discount_percentage == 100:
|
if item.discount_percentage == 100:
|
||||||
item.rate = 0.0
|
item.rate = 0.0
|
||||||
@@ -225,7 +231,13 @@ class calculate_taxes_and_totals:
|
|||||||
elif not item.qty and self.doc.get("is_debit_note"):
|
elif not item.qty and self.doc.get("is_debit_note"):
|
||||||
item.amount = flt(item.rate, item.precision("amount"))
|
item.amount = flt(item.rate, item.precision("amount"))
|
||||||
else:
|
else:
|
||||||
item.amount = flt(item.rate * item.qty, item.precision("amount"))
|
qty = (
|
||||||
|
(item.qty + item.rejected_qty)
|
||||||
|
if bill_for_rejected_quantity_in_purchase_invoice
|
||||||
|
and self.doc.doctype == "Purchase Receipt"
|
||||||
|
else item.qty
|
||||||
|
)
|
||||||
|
item.amount = flt(item.rate * qty, item.precision("amount"))
|
||||||
|
|
||||||
item.net_amount = item.amount
|
item.net_amount = item.amount
|
||||||
|
|
||||||
@@ -285,6 +297,13 @@ class calculate_taxes_and_totals:
|
|||||||
self.doc._item_wise_tax_details = item_wise_tax_details
|
self.doc._item_wise_tax_details = item_wise_tax_details
|
||||||
self.doc.item_wise_tax_details = []
|
self.doc.item_wise_tax_details = []
|
||||||
|
|
||||||
|
for tax in self.doc.get("taxes"):
|
||||||
|
if not tax.get("dont_recompute_tax"):
|
||||||
|
tax._running_txn_tax_total = 0.0
|
||||||
|
tax._running_base_tax_total = 0.0
|
||||||
|
tax._running_txn_taxable_total = 0.0
|
||||||
|
tax._running_base_taxable_total = 0.0
|
||||||
|
|
||||||
def determine_exclusive_rate(self):
|
def determine_exclusive_rate(self):
|
||||||
if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")):
|
if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")):
|
||||||
return
|
return
|
||||||
@@ -372,9 +391,16 @@ class calculate_taxes_and_totals:
|
|||||||
self.doc.total
|
self.doc.total
|
||||||
) = self.doc.base_total = self.doc.net_total = self.doc.base_net_total = 0.0
|
) = self.doc.base_total = self.doc.net_total = self.doc.base_net_total = 0.0
|
||||||
|
|
||||||
|
bill_for_rejected_quantity_in_purchase_invoice = frappe.get_single_value(
|
||||||
|
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
|
||||||
|
)
|
||||||
for item in self._items:
|
for item in self._items:
|
||||||
self.doc.total += item.amount
|
self.doc.total += item.amount
|
||||||
self.doc.total_qty += item.qty
|
self.doc.total_qty += (
|
||||||
|
(item.qty + item.rejected_qty)
|
||||||
|
if bill_for_rejected_quantity_in_purchase_invoice and self.doc.doctype == "Purchase Receipt"
|
||||||
|
else item.qty
|
||||||
|
)
|
||||||
self.doc.base_total += item.base_amount
|
self.doc.base_total += item.base_amount
|
||||||
self.doc.net_total += item.net_amount
|
self.doc.net_total += item.net_amount
|
||||||
self.doc.base_net_total += item.base_net_amount
|
self.doc.base_net_total += item.base_net_amount
|
||||||
@@ -521,7 +547,6 @@ class calculate_taxes_and_totals:
|
|||||||
actual_breakup = tax._total_tax_breakup
|
actual_breakup = tax._total_tax_breakup
|
||||||
diff = flt(expected_amount - actual_breakup, 5)
|
diff = flt(expected_amount - actual_breakup, 5)
|
||||||
|
|
||||||
# TODO: fix rounding difference issues
|
|
||||||
if abs(diff) <= 0.5:
|
if abs(diff) <= 0.5:
|
||||||
detail_row = self.doc._item_wise_tax_details[last_idx]
|
detail_row = self.doc._item_wise_tax_details[last_idx]
|
||||||
detail_row["amount"] = flt(detail_row["amount"] + diff, 5)
|
detail_row["amount"] = flt(detail_row["amount"] + diff, 5)
|
||||||
@@ -597,14 +622,29 @@ class calculate_taxes_and_totals:
|
|||||||
def set_item_wise_tax(self, item, tax, tax_rate, current_tax_amount, current_net_amount):
|
def set_item_wise_tax(self, item, tax, tax_rate, current_tax_amount, current_net_amount):
|
||||||
# store tax breakup for each item
|
# store tax breakup for each item
|
||||||
multiplier = -1 if tax.get("add_deduct_tax") == "Deduct" else 1
|
multiplier = -1 if tax.get("add_deduct_tax") == "Deduct" else 1
|
||||||
item_wise_tax_amount = flt(
|
|
||||||
current_tax_amount * self.doc.conversion_rate * multiplier, tax.precision("tax_amount")
|
# Error diffusion: derive each item's base amount as a delta of the running cumulative total
|
||||||
|
# so the sum always equals base_tax_amount_after_discount_amount.
|
||||||
|
tax._running_txn_tax_total += current_tax_amount * multiplier
|
||||||
|
new_base_tax_total = flt(
|
||||||
|
flt(tax._running_txn_tax_total, tax.precision("tax_amount")) * self.doc.conversion_rate,
|
||||||
|
tax.precision("base_tax_amount"),
|
||||||
)
|
)
|
||||||
|
item_wise_tax_amount = flt(
|
||||||
|
new_base_tax_total - tax._running_base_tax_total, tax.precision("base_tax_amount")
|
||||||
|
)
|
||||||
|
tax._running_base_tax_total = new_base_tax_total
|
||||||
|
|
||||||
if tax.charge_type != "On Item Quantity":
|
if tax.charge_type != "On Item Quantity":
|
||||||
item_wise_taxable_amount = flt(
|
tax._running_txn_taxable_total += current_net_amount * multiplier
|
||||||
current_net_amount * self.doc.conversion_rate * multiplier, tax.precision("tax_amount")
|
new_base_taxable_total = flt(
|
||||||
|
flt(tax._running_txn_taxable_total, tax.precision("net_amount")) * self.doc.conversion_rate,
|
||||||
|
tax.precision("base_net_amount"),
|
||||||
)
|
)
|
||||||
|
item_wise_taxable_amount = flt(
|
||||||
|
new_base_taxable_total - tax._running_base_taxable_total, tax.precision("base_net_amount")
|
||||||
|
)
|
||||||
|
tax._running_base_taxable_total = new_base_taxable_total
|
||||||
else:
|
else:
|
||||||
item_wise_taxable_amount = 0.0
|
item_wise_taxable_amount = 0.0
|
||||||
|
|
||||||
@@ -788,7 +828,8 @@ class calculate_taxes_and_totals:
|
|||||||
discount_amount += total_return_discount
|
discount_amount += total_return_discount
|
||||||
|
|
||||||
# validate that discount amount cannot exceed the total before discount
|
# validate that discount amount cannot exceed the total before discount
|
||||||
if (
|
# only during save (i.e. when `_action` is set)
|
||||||
|
if self.doc.get("_action") and (
|
||||||
(grand_total >= 0 and discount_amount > grand_total)
|
(grand_total >= 0 and discount_amount > grand_total)
|
||||||
or (grand_total < 0 and discount_amount < grand_total) # returns
|
or (grand_total < 0 and discount_amount < grand_total) # returns
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -1567,25 +1567,10 @@ class TestAccountsController(ERPNextTestSuite):
|
|||||||
|
|
||||||
frappe.db.set_value("Company", self.company, "cost_center", cc)
|
frappe.db.set_value("Company", self.company, "cost_center", cc)
|
||||||
|
|
||||||
def setup_dimensions(self):
|
|
||||||
# create dimension
|
|
||||||
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
|
|
||||||
create_dimension,
|
|
||||||
)
|
|
||||||
|
|
||||||
create_dimension()
|
|
||||||
# make it non-mandatory
|
|
||||||
loc = frappe.get_doc("Accounting Dimension", "Location")
|
|
||||||
for x in loc.dimension_defaults:
|
|
||||||
x.mandatory_for_bs = False
|
|
||||||
x.mandatory_for_pl = False
|
|
||||||
loc.save()
|
|
||||||
|
|
||||||
def test_90_dimensions_filter(self):
|
def test_90_dimensions_filter(self):
|
||||||
"""
|
"""
|
||||||
Test workings of dimension filters
|
Test workings of dimension filters
|
||||||
"""
|
"""
|
||||||
self.setup_dimensions()
|
|
||||||
rate_in_account_currency = 1
|
rate_in_account_currency = 1
|
||||||
|
|
||||||
# Invoices
|
# Invoices
|
||||||
@@ -1653,7 +1638,6 @@ class TestAccountsController(ERPNextTestSuite):
|
|||||||
self.assertEqual(len(pr.payments), 1)
|
self.assertEqual(len(pr.payments), 1)
|
||||||
|
|
||||||
def test_91_cr_note_should_inherit_dimension(self):
|
def test_91_cr_note_should_inherit_dimension(self):
|
||||||
self.setup_dimensions()
|
|
||||||
rate_in_account_currency = 1
|
rate_in_account_currency = 1
|
||||||
|
|
||||||
# Invoice
|
# Invoice
|
||||||
@@ -1698,7 +1682,6 @@ class TestAccountsController(ERPNextTestSuite):
|
|||||||
|
|
||||||
def test_92_dimension_inhertiance_exc_gain_loss(self):
|
def test_92_dimension_inhertiance_exc_gain_loss(self):
|
||||||
# Sales Invoice in Foreign Currency
|
# Sales Invoice in Foreign Currency
|
||||||
self.setup_dimensions()
|
|
||||||
rate_in_account_currency = 1
|
rate_in_account_currency = 1
|
||||||
dpt = "Research & Development - _TC"
|
dpt = "Research & Development - _TC"
|
||||||
|
|
||||||
@@ -1734,7 +1717,6 @@ class TestAccountsController(ERPNextTestSuite):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_93_dimension_inheritance_on_advance(self):
|
def test_93_dimension_inheritance_on_advance(self):
|
||||||
self.setup_dimensions()
|
|
||||||
dpt = "Research & Development - _TC"
|
dpt = "Research & Development - _TC"
|
||||||
|
|
||||||
adv = self.create_payment_entry(amount=1, source_exc_rate=85)
|
adv = self.create_payment_entry(amount=1, source_exc_rate=85)
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
|
||||||
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals
|
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals
|
||||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
|
||||||
class TestTaxesAndTotals(AccountsTestMixin, ERPNextTestSuite):
|
class TestTaxesAndTotals(ERPNextTestSuite):
|
||||||
@ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1})
|
@ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1})
|
||||||
def test_distributed_discount_amount(self):
|
def test_distributed_discount_amount(self):
|
||||||
so = make_sales_order(do_not_save=1)
|
so = make_sales_order(do_not_save=1)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.utils import flt
|
||||||
|
|
||||||
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals
|
from erpnext.tests.utils import ERPNextTestSuite, change_settings
|
||||||
from erpnext.tests.utils import ERPNextTestSuite
|
|
||||||
|
|
||||||
|
|
||||||
class TestTaxesAndTotals(ERPNextTestSuite):
|
class TestTaxesAndTotals(ERPNextTestSuite):
|
||||||
@@ -124,3 +124,180 @@ class TestTaxesAndTotals(ERPNextTestSuite):
|
|||||||
]
|
]
|
||||||
|
|
||||||
self.assertEqual(actual_values, expected_values)
|
self.assertEqual(actual_values, expected_values)
|
||||||
|
|
||||||
|
@change_settings("Selling Settings", {"allow_multiple_items": 1})
|
||||||
|
def test_item_wise_tax_detail_high_conversion_rate(self):
|
||||||
|
"""
|
||||||
|
With a high conversion rate (e.g. USD -> KRW ~1300), independently rounding
|
||||||
|
each item's base tax amount causes per-item errors that accumulate and exceed
|
||||||
|
the 0.5-unit safety threshold, raising a validation error.
|
||||||
|
|
||||||
|
Error diffusion fixes this: the cumulative base total after the last item
|
||||||
|
equals base_tax_amount_after_discount_amount exactly, so the sum of all
|
||||||
|
per-item amounts is always exact regardless of item count or rate magnitude.
|
||||||
|
|
||||||
|
Analytically with conversion_rate=1300, rate=7.77 x3 items, VAT 16%:
|
||||||
|
per-item txn tax = 1.2432
|
||||||
|
OLD independent: flt(1.2432 * 1300, 2) = 1616.16 -> sum 4848.48
|
||||||
|
expected base: flt(flt(3.7296, 2) * 1300, 0) = flt(3.73 * 1300, 0) = 4849
|
||||||
|
diff = 0.52 -> exceeds 0.5 threshold -> would throw with old code
|
||||||
|
"""
|
||||||
|
doc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Sales Invoice",
|
||||||
|
"customer": "_Test Customer",
|
||||||
|
"company": "_Test Company",
|
||||||
|
"currency": "USD",
|
||||||
|
"debit_to": "_Test Receivable USD - _TC",
|
||||||
|
"conversion_rate": 1300,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"item_code": "_Test Item",
|
||||||
|
"qty": 1,
|
||||||
|
"rate": 7.77,
|
||||||
|
"income_account": "Sales - _TC",
|
||||||
|
"expense_account": "Cost of Goods Sold - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_code": "_Test Item",
|
||||||
|
"qty": 1,
|
||||||
|
"rate": 7.77,
|
||||||
|
"income_account": "Sales - _TC",
|
||||||
|
"expense_account": "Cost of Goods Sold - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_code": "_Test Item",
|
||||||
|
"qty": 1,
|
||||||
|
"rate": 7.77,
|
||||||
|
"income_account": "Sales - _TC",
|
||||||
|
"expense_account": "Cost of Goods Sold - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"taxes": [
|
||||||
|
{
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"account_head": "_Test Account VAT - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"description": "VAT",
|
||||||
|
"rate": 16,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"charge_type": "On Previous Row Amount",
|
||||||
|
"account_head": "_Test Account Service Tax - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"description": "Service Tax",
|
||||||
|
"rate": 10,
|
||||||
|
"row_id": 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
doc.save()
|
||||||
|
|
||||||
|
details_by_tax = {}
|
||||||
|
for detail in doc.item_wise_tax_details:
|
||||||
|
bucket = details_by_tax.setdefault(detail.tax_row, 0.0)
|
||||||
|
details_by_tax[detail.tax_row] = bucket + detail.amount
|
||||||
|
|
||||||
|
for tax in doc.taxes:
|
||||||
|
self.assertEqual(details_by_tax[tax.name], tax.base_tax_amount_after_discount_amount)
|
||||||
|
|
||||||
|
@change_settings("Selling Settings", {"allow_multiple_items": 1})
|
||||||
|
def test_rounding_in_item_wise_tax_details(self):
|
||||||
|
"""
|
||||||
|
This test verifies the amounts are properly rounded.
|
||||||
|
"""
|
||||||
|
doc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Sales Invoice",
|
||||||
|
"customer": "_Test Customer",
|
||||||
|
"company": "_Test Company",
|
||||||
|
"currency": "INR",
|
||||||
|
"conversion_rate": 1,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"item_code": "_Test Item",
|
||||||
|
"qty": 5,
|
||||||
|
"rate": 20,
|
||||||
|
"income_account": "Sales - _TC",
|
||||||
|
"expense_account": "Cost of Goods Sold - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_code": "_Test Item",
|
||||||
|
"qty": 3,
|
||||||
|
"rate": 19,
|
||||||
|
"income_account": "Sales - _TC",
|
||||||
|
"expense_account": "Cost of Goods Sold - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_code": "_Test Item",
|
||||||
|
"qty": 1,
|
||||||
|
"rate": 1000,
|
||||||
|
"income_account": "Sales - _TC",
|
||||||
|
"expense_account": "Cost of Goods Sold - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"taxes": [
|
||||||
|
{
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"account_head": "_Test Account VAT - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"description": "VAT",
|
||||||
|
"rate": 9,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
doc.save()
|
||||||
|
|
||||||
|
# item 1: taxable=100, tax=9.0; item 2: taxable=57, tax=5.13; item 3: taxable=1000, tax=90.0
|
||||||
|
# error diffusion: 14.13 - 9.0 = 5.130000000000001 without rounding
|
||||||
|
for detail in doc.item_wise_tax_details:
|
||||||
|
self.assertEqual(detail.amount, flt(detail.amount, detail.precision("amount")))
|
||||||
|
|
||||||
|
def test_item_wise_tax_detail_with_multi_currency_with_single_item(self):
|
||||||
|
"""
|
||||||
|
When the tax amount (in transaction currency) has more decimals than
|
||||||
|
the field precision, rounding must happen *before* multiplying by
|
||||||
|
conversion_rate — the same order used by _set_in_company_currency.
|
||||||
|
"""
|
||||||
|
doc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Sales Invoice",
|
||||||
|
"customer": "_Test Customer",
|
||||||
|
"company": "_Test Company",
|
||||||
|
"currency": "USD",
|
||||||
|
"debit_to": "_Test Receivable USD - _TC",
|
||||||
|
"conversion_rate": 129.99,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"item_code": "_Test Item",
|
||||||
|
"qty": 1,
|
||||||
|
"rate": 47.41,
|
||||||
|
"income_account": "Sales - _TC",
|
||||||
|
"expense_account": "Cost of Goods Sold - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"taxes": [
|
||||||
|
{
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"account_head": "_Test Account VAT - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"description": "VAT",
|
||||||
|
"rate": 16,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
doc.save()
|
||||||
|
|
||||||
|
tax = doc.taxes[0]
|
||||||
|
detail = doc.item_wise_tax_details[0]
|
||||||
|
self.assertEqual(detail.amount, tax.base_tax_amount_after_discount_amount)
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class TestTaxes(ERPNextTestSuite):
|
|||||||
{
|
{
|
||||||
"doctype": "Customer",
|
"doctype": "Customer",
|
||||||
"customer_name": uuid4(),
|
"customer_name": uuid4(),
|
||||||
"customer_group": "All Customer Groups",
|
"customer_group": "Individual",
|
||||||
}
|
}
|
||||||
).insert()
|
).insert()
|
||||||
self.supplier = frappe.get_doc(
|
self.supplier = frappe.get_doc(
|
||||||
|
|||||||
@@ -2,36 +2,17 @@ import frappe
|
|||||||
from frappe import qb
|
from frappe import qb
|
||||||
from frappe.utils import today
|
from frappe.utils import today
|
||||||
|
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import disable_dimension
|
|
||||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
|
||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
|
||||||
class TestReactivity(AccountsTestMixin, ERPNextTestSuite):
|
class TestReactivity(ERPNextTestSuite):
|
||||||
def setUp(self):
|
|
||||||
self.create_company()
|
|
||||||
self.create_customer()
|
|
||||||
self.create_item()
|
|
||||||
self.create_usd_receivable_account()
|
|
||||||
self.create_price_list()
|
|
||||||
self.clear_old_entries()
|
|
||||||
|
|
||||||
def disable_dimensions(self):
|
|
||||||
res = frappe.db.get_all("Accounting Dimension", filters={"disabled": False})
|
|
||||||
for x in res:
|
|
||||||
dim = frappe.get_doc("Accounting Dimension", x.name)
|
|
||||||
dim.disabled = True
|
|
||||||
dim.save()
|
|
||||||
|
|
||||||
def test_01_basic_item_details(self):
|
def test_01_basic_item_details(self):
|
||||||
self.disable_dimensions()
|
|
||||||
|
|
||||||
# set Item Price
|
# set Item Price
|
||||||
frappe.get_doc(
|
frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Item Price",
|
"doctype": "Item Price",
|
||||||
"item_code": self.item,
|
"item_code": "_Test Item",
|
||||||
"price_list": self.price_list,
|
"price_list": "Standard Selling",
|
||||||
"price_list_rate": 90,
|
"price_list_rate": 90,
|
||||||
"selling": True,
|
"selling": True,
|
||||||
"rate": 90,
|
"rate": 90,
|
||||||
@@ -42,17 +23,18 @@ class TestReactivity(AccountsTestMixin, ERPNextTestSuite):
|
|||||||
si = frappe.get_doc(
|
si = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Sales Invoice",
|
"doctype": "Sales Invoice",
|
||||||
"company": self.company,
|
"company": "_Test Company",
|
||||||
"customer": self.customer,
|
"customer": "_Test Customer",
|
||||||
"debit_to": self.debit_to,
|
"debit_to": "Debtors - _TC",
|
||||||
"posting_date": today(),
|
"posting_date": today(),
|
||||||
"cost_center": self.cost_center,
|
"cost_center": "Main - _TC",
|
||||||
|
"currency": "INR",
|
||||||
"conversion_rate": 1,
|
"conversion_rate": 1,
|
||||||
"selling_price_list": self.price_list,
|
"selling_price_list": "Standard Selling",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
itm = si.append("items")
|
itm = si.append("items")
|
||||||
itm.item_code = self.item
|
itm.item_code = "_Test Item"
|
||||||
si.process_item_selection(itm.idx)
|
si.process_item_selection(itm.idx)
|
||||||
self.assertEqual(itm.rate, 90)
|
self.assertEqual(itm.rate, 90)
|
||||||
|
|
||||||
|
|||||||
@@ -501,8 +501,8 @@ class TestSubcontractingController(ERPNextTestSuite):
|
|||||||
scr1.items[0].qty = 2
|
scr1.items[0].qty = 2
|
||||||
add_second_row_in_scr(scr1)
|
add_second_row_in_scr(scr1)
|
||||||
scr1.flags.ignore_mandatory = True
|
scr1.flags.ignore_mandatory = True
|
||||||
scr1.save()
|
|
||||||
scr1.set_missing_values()
|
scr1.set_missing_values()
|
||||||
|
scr1.save()
|
||||||
scr1.submit()
|
scr1.submit()
|
||||||
|
|
||||||
for _key, value in get_supplied_items(scr1).items():
|
for _key, value in get_supplied_items(scr1).items():
|
||||||
@@ -513,8 +513,8 @@ class TestSubcontractingController(ERPNextTestSuite):
|
|||||||
scr2.items[0].qty = 2
|
scr2.items[0].qty = 2
|
||||||
add_second_row_in_scr(scr2)
|
add_second_row_in_scr(scr2)
|
||||||
scr2.flags.ignore_mandatory = True
|
scr2.flags.ignore_mandatory = True
|
||||||
scr2.save()
|
|
||||||
scr2.set_missing_values()
|
scr2.set_missing_values()
|
||||||
|
scr2.save()
|
||||||
scr2.submit()
|
scr2.submit()
|
||||||
|
|
||||||
for _key, value in get_supplied_items(scr2).items():
|
for _key, value in get_supplied_items(scr2).items():
|
||||||
@@ -523,8 +523,8 @@ class TestSubcontractingController(ERPNextTestSuite):
|
|||||||
scr3 = make_subcontracting_receipt(sco.name)
|
scr3 = make_subcontracting_receipt(sco.name)
|
||||||
scr3.items[0].qty = 2
|
scr3.items[0].qty = 2
|
||||||
scr3.flags.ignore_mandatory = True
|
scr3.flags.ignore_mandatory = True
|
||||||
scr3.save()
|
|
||||||
scr3.set_missing_values()
|
scr3.set_missing_values()
|
||||||
|
scr3.save()
|
||||||
scr3.submit()
|
scr3.submit()
|
||||||
|
|
||||||
for _key, value in get_supplied_items(scr3).items():
|
for _key, value in get_supplied_items(scr3).items():
|
||||||
@@ -1164,6 +1164,54 @@ class TestSubcontractingController(ERPNextTestSuite):
|
|||||||
|
|
||||||
self.assertEqual([item.rm_item_code for item in sco.supplied_items], expected)
|
self.assertEqual([item.rm_item_code for item in sco.supplied_items], expected)
|
||||||
|
|
||||||
|
def test_co_by_product(self):
|
||||||
|
frappe.set_value("UOM", "Nos", "must_be_whole_number", 0)
|
||||||
|
|
||||||
|
fg_item = make_item("FG Item", properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name
|
||||||
|
rm_item = make_item("RM Item", properties={"is_stock_item": 1}).name
|
||||||
|
scrap_item = make_item("Scrap Item", properties={"is_stock_item": 1}).name
|
||||||
|
make_bom(
|
||||||
|
item=fg_item, raw_materials=[rm_item], scrap_items=[scrap_item], process_loss_percentage=10
|
||||||
|
).name
|
||||||
|
|
||||||
|
service_items = [
|
||||||
|
{
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"item_code": "Subcontracted Service Item 11",
|
||||||
|
"qty": 5,
|
||||||
|
"rate": 100,
|
||||||
|
"fg_item": fg_item,
|
||||||
|
"fg_item_qty": 5,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
sco = get_subcontracting_order(service_items=service_items)
|
||||||
|
rm_items = get_rm_items(sco.supplied_items)
|
||||||
|
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||||
|
make_stock_transfer_entry(
|
||||||
|
sco_no=sco.name,
|
||||||
|
rm_items=rm_items,
|
||||||
|
itemwise_details=copy.deepcopy(itemwise_details),
|
||||||
|
)
|
||||||
|
|
||||||
|
scr1 = make_subcontracting_receipt(sco.name)
|
||||||
|
scr1.get_secondary_items()
|
||||||
|
scr1.save()
|
||||||
|
|
||||||
|
self.assertEqual(scr1.items[0].received_qty, 5)
|
||||||
|
self.assertEqual(scr1.items[0].process_loss_qty, 0.5)
|
||||||
|
self.assertEqual(scr1.items[0].qty, 4.5)
|
||||||
|
self.assertEqual(scr1.items[0].rate, 200)
|
||||||
|
self.assertEqual(scr1.items[0].amount, 900)
|
||||||
|
|
||||||
|
self.assertEqual(scr1.items[1].item_code, scrap_item)
|
||||||
|
self.assertEqual(scr1.items[1].received_qty, 5)
|
||||||
|
self.assertEqual(scr1.items[1].process_loss_qty, 0.5)
|
||||||
|
self.assertEqual(scr1.items[1].qty, 4.5)
|
||||||
|
self.assertEqual(flt(scr1.items[1].rate, 3), 11.111)
|
||||||
|
self.assertEqual(scr1.items[1].amount, 50)
|
||||||
|
|
||||||
|
frappe.set_value("UOM", "Nos", "must_be_whole_number", 1)
|
||||||
|
|
||||||
|
|
||||||
def add_second_row_in_scr(scr):
|
def add_second_row_in_scr(scr):
|
||||||
item_dict = {}
|
item_dict = {}
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ class Appointment(Document):
|
|||||||
self.auto_assign()
|
self.auto_assign()
|
||||||
self.create_calendar_event()
|
self.create_calendar_event()
|
||||||
self.save(ignore_permissions=True)
|
self.save(ignore_permissions=True)
|
||||||
|
if not frappe.in_test:
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
def create_lead_and_link(self):
|
def create_lead_and_link(self):
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:06:46.495091",
|
"modified": "2026-03-25 19:27:19.162421",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "Contract Template",
|
"name": "Contract Template",
|
||||||
@@ -75,42 +75,34 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "Sales Manager",
|
"role": "Sales Manager",
|
||||||
"share": 1,
|
"share": 1
|
||||||
"write": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "Purchase Manager",
|
"role": "Purchase Manager",
|
||||||
"share": 1,
|
"share": 1
|
||||||
"write": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "HR Manager",
|
"role": "HR Manager",
|
||||||
"share": 1,
|
"share": 1
|
||||||
"write": 1
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
|
|||||||
@@ -204,8 +204,22 @@ def send_mail(entry, email_campaign):
|
|||||||
|
|
||||||
# called from hooks on doc_event Email Unsubscribe
|
# called from hooks on doc_event Email Unsubscribe
|
||||||
def unsubscribe_recipient(unsubscribe, method):
|
def unsubscribe_recipient(unsubscribe, method):
|
||||||
if unsubscribe.reference_doctype == "Email Campaign":
|
if unsubscribe.reference_doctype != "Email Campaign":
|
||||||
frappe.db.set_value("Email Campaign", unsubscribe.reference_name, "status", "Unsubscribed")
|
return
|
||||||
|
|
||||||
|
email_campaign = frappe.get_doc("Email Campaign", unsubscribe.reference_name)
|
||||||
|
|
||||||
|
if email_campaign.email_campaign_for == "Email Group":
|
||||||
|
if unsubscribe.email:
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Email Group Member",
|
||||||
|
{"email_group": email_campaign.recipient, "email": unsubscribe.email},
|
||||||
|
"unsubscribed",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# For Lead or Contact
|
||||||
|
frappe.db.set_value("Email Campaign", email_campaign.name, "status", "Unsubscribed")
|
||||||
|
|
||||||
|
|
||||||
# called through hooks to update email campaign status daily
|
# called through hooks to update email campaign status daily
|
||||||
|
|||||||
@@ -4,6 +4,12 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.query_builder import DocType
|
||||||
|
from frappe.query_builder.custom import GROUP_CONCAT
|
||||||
|
from frappe.query_builder.functions import Date
|
||||||
|
|
||||||
|
Opportunity = DocType("Opportunity")
|
||||||
|
OpportunityLostReasonDetail = DocType("Opportunity Lost Reason Detail")
|
||||||
|
|
||||||
|
|
||||||
def execute(filters=None):
|
def execute(filters=None):
|
||||||
@@ -66,58 +72,48 @@ def get_columns():
|
|||||||
|
|
||||||
|
|
||||||
def get_data(filters):
|
def get_data(filters):
|
||||||
return frappe.db.sql(
|
query = (
|
||||||
f"""
|
frappe.qb.from_(Opportunity)
|
||||||
SELECT
|
.left_join(OpportunityLostReasonDetail)
|
||||||
`tabOpportunity`.name,
|
.on(
|
||||||
`tabOpportunity`.opportunity_from,
|
(OpportunityLostReasonDetail.parenttype == "Opportunity")
|
||||||
`tabOpportunity`.party_name,
|
& (OpportunityLostReasonDetail.parent == Opportunity.name)
|
||||||
`tabOpportunity`.customer_name,
|
)
|
||||||
`tabOpportunity`.opportunity_type,
|
.select(
|
||||||
GROUP_CONCAT(`tabOpportunity Lost Reason Detail`.lost_reason separator ', ') lost_reason,
|
Opportunity.name,
|
||||||
`tabOpportunity`.sales_stage,
|
Opportunity.opportunity_from,
|
||||||
`tabOpportunity`.territory
|
Opportunity.party_name,
|
||||||
FROM
|
Opportunity.customer_name,
|
||||||
`tabOpportunity`
|
Opportunity.opportunity_type,
|
||||||
{get_join(filters)}
|
GROUP_CONCAT(OpportunityLostReasonDetail.lost_reason, alias="lost_reason").separator(", "),
|
||||||
WHERE
|
Opportunity.sales_stage,
|
||||||
`tabOpportunity`.status = 'Lost' and `tabOpportunity`.company = %(company)s
|
Opportunity.territory,
|
||||||
AND DATE(`tabOpportunity`.modified) BETWEEN %(from_date)s AND %(to_date)s
|
)
|
||||||
{get_conditions(filters)}
|
.where(
|
||||||
GROUP BY
|
(Opportunity.status == "Lost")
|
||||||
`tabOpportunity`.name
|
& (Opportunity.company == filters.get("company"))
|
||||||
ORDER BY
|
& (Date(Opportunity.modified).between(filters.get("from_date"), filters.get("to_date")))
|
||||||
`tabOpportunity`.creation asc """,
|
)
|
||||||
filters,
|
.groupby(Opportunity.name)
|
||||||
as_dict=1,
|
.orderby(Opportunity.creation)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
query = get_conditions(filters, query)
|
||||||
|
|
||||||
def get_conditions(filters):
|
return query.run(as_dict=1)
|
||||||
conditions = []
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_conditions(filters, query):
|
||||||
if filters.get("territory"):
|
if filters.get("territory"):
|
||||||
conditions.append(" and `tabOpportunity`.territory=%(territory)s")
|
query = query.where(Opportunity.territory == filters.get("territory"))
|
||||||
|
|
||||||
if filters.get("opportunity_from"):
|
if filters.get("opportunity_from"):
|
||||||
conditions.append(" and `tabOpportunity`.opportunity_from=%(opportunity_from)s")
|
query = query.where(Opportunity.opportunity_from == filters.get("opportunity_from"))
|
||||||
|
|
||||||
if filters.get("party_name"):
|
if filters.get("party_name"):
|
||||||
conditions.append(" and `tabOpportunity`.party_name=%(party_name)s")
|
query = query.where(Opportunity.party_name == filters.get("party_name"))
|
||||||
|
|
||||||
return " ".join(conditions) if conditions else ""
|
|
||||||
|
|
||||||
|
|
||||||
def get_join(filters):
|
|
||||||
join = """LEFT JOIN `tabOpportunity Lost Reason Detail`
|
|
||||||
ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and
|
|
||||||
`tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name"""
|
|
||||||
|
|
||||||
if filters.get("lost_reason"):
|
if filters.get("lost_reason"):
|
||||||
join = """JOIN `tabOpportunity Lost Reason Detail`
|
query = query.where(OpportunityLostReasonDetail.lost_reason == filters.get("lost_reason"))
|
||||||
ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and
|
|
||||||
`tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name and
|
|
||||||
`tabOpportunity Lost Reason Detail`.lost_reason = '{}'
|
|
||||||
""".format(filters.get("lost_reason"))
|
|
||||||
|
|
||||||
return join
|
return query
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
|
||||||
class TestCodeList(FrappeTestCase):
|
class TestCodeList(ERPNextTestSuite):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
|
||||||
class TestCommonCode(FrappeTestCase):
|
class TestCommonCode(ERPNextTestSuite):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ welcome_email = "erpnext.setup.utils.welcome_email"
|
|||||||
# setup wizard
|
# setup wizard
|
||||||
setup_wizard_requires = "assets/erpnext/js/setup_wizard.js"
|
setup_wizard_requires = "assets/erpnext/js/setup_wizard.js"
|
||||||
setup_wizard_stages = "erpnext.setup.setup_wizard.setup_wizard.get_setup_stages"
|
setup_wizard_stages = "erpnext.setup.setup_wizard.setup_wizard.get_setup_stages"
|
||||||
setup_wizard_complete = "erpnext.setup.setup_wizard.setup_wizard.setup_demo"
|
|
||||||
|
|
||||||
after_install = "erpnext.setup.install.after_install"
|
after_install = "erpnext.setup.install.after_install"
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user