mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-20 05:29:18 +00:00
Merge branch 'version-16-hotfix' into mergify/bp/version-16-hotfix/pr-53588
This commit is contained in:
@@ -52,60 +52,55 @@ frappe.treeview_settings["Account"] = {
|
||||
],
|
||||
root_label: "Accounts",
|
||||
get_tree_nodes: "erpnext.accounts.utils.get_children",
|
||||
on_get_node: function (nodes, deep = false) {
|
||||
if (frappe.boot.user.can_read.indexOf("GL Entry") == -1) return;
|
||||
on_node_render: function (node, deep) {
|
||||
const render_balances = () => {
|
||||
for (let account of cur_tree.account_balance_data) {
|
||||
const node = cur_tree.nodes && cur_tree.nodes[account.value];
|
||||
if (!node || node.is_root) continue;
|
||||
|
||||
let accounts = [];
|
||||
if (deep) {
|
||||
// in case of `get_all_nodes`
|
||||
accounts = nodes.reduce((acc, node) => [...acc, ...node.data], []);
|
||||
} else {
|
||||
accounts = nodes;
|
||||
}
|
||||
// show Dr if positive since balance is calculated as debit - credit else show Cr
|
||||
const balance = account.balance_in_account_currency || account.balance;
|
||||
const dr_or_cr = balance > 0 ? __("Dr") : __("Cr");
|
||||
const format = (value, currency) => format_currency(Math.abs(value), currency);
|
||||
|
||||
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];
|
||||
if (!node || node.is_root) continue;
|
||||
|
||||
// show Dr if positive since balance is calculated as debit - credit else show Cr
|
||||
const balance = account.balance_in_account_currency || account.balance;
|
||||
const dr_or_cr = balance > 0 ? __("Dr") : __("Cr");
|
||||
const format = (value, currency) => format_currency(Math.abs(value), currency);
|
||||
|
||||
if (account.balance !== undefined) {
|
||||
node.parent && node.parent.find(".balance-area").remove();
|
||||
$(
|
||||
'<span class="balance-area pull-right">' +
|
||||
(account.balance_in_account_currency
|
||||
? format(
|
||||
account.balance_in_account_currency,
|
||||
account.account_currency
|
||||
) + " / "
|
||||
: "") +
|
||||
format(account.balance, account.company_currency) +
|
||||
" " +
|
||||
dr_or_cr +
|
||||
"</span>"
|
||||
).insertBefore(node.$ul);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (account.balance !== undefined) {
|
||||
node.parent && node.parent.find(".balance-area").remove();
|
||||
$(
|
||||
'<span class="balance-area pull-right">' +
|
||||
(account.account_currency != account.company_currency
|
||||
? format(account.balance_in_account_currency, account.account_currency) +
|
||||
" / "
|
||||
: "") +
|
||||
format(account.balance, account.company_currency) +
|
||||
" " +
|
||||
dr_or_cr +
|
||||
"</span>"
|
||||
).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",
|
||||
menu_items: [
|
||||
|
||||
@@ -26,8 +26,13 @@
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-15 03:19:47.171349",
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "Account",
|
||||
"link_fieldname": "account_category"
|
||||
}
|
||||
],
|
||||
"modified": "2026-02-23 01:19:49.589393",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account Category",
|
||||
|
||||
@@ -10,9 +10,6 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestAccountingDimension(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
create_dimension()
|
||||
|
||||
def test_dimension_against_sales_invoice(self):
|
||||
si = create_sales_invoice(do_not_save=1)
|
||||
|
||||
@@ -77,63 +74,3 @@ class TestAccountingDimension(ERPNextTestSuite):
|
||||
|
||||
si.save()
|
||||
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
|
||||
|
||||
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.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
@@ -16,7 +12,6 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestAccountingDimensionFilter(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
create_dimension()
|
||||
create_accounting_dimension_filter()
|
||||
self.invoice_list = []
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"payment_options_section",
|
||||
"enable_loyalty_point_program",
|
||||
"column_break_ctam",
|
||||
"fetch_payment_schedule_in_payment_request",
|
||||
"invoicing_settings_tab",
|
||||
"accounts_transactions_settings_section",
|
||||
"over_billing_allowance",
|
||||
@@ -688,6 +689,19 @@
|
||||
"fieldname": "enable_accounting_dimensions",
|
||||
"fieldtype": "Check",
|
||||
"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,
|
||||
@@ -697,7 +711,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-27 01:04:09.415288",
|
||||
"modified": "2026-03-30 07:32:58.182018",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -73,6 +73,7 @@ class AccountsSettings(Document):
|
||||
enable_loyalty_point_program: DF.Check
|
||||
enable_party_matching: DF.Check
|
||||
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
|
||||
general_ledger_remarks_length: DF.Int
|
||||
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
|
||||
|
||||
|
||||
class TestAdvancePaymentLedgerEntry(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestAdvancePaymentLedgerEntry(ERPNextTestSuite, AccountsTestMixin):
|
||||
"""
|
||||
Integration tests for AdvancePaymentLedgerEntry.
|
||||
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()
|
||||
def get_bank_account_details(bank_account):
|
||||
frappe.has_permission("Bank Account", doc=bank_account, ptype="read", throw=True)
|
||||
return frappe.get_cached_value(
|
||||
"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
|
||||
|
||||
|
||||
class TestBankReconciliationTool(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
|
||||
@@ -382,7 +382,7 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_group": "All Customer Groups",
|
||||
"customer_group": "Individual",
|
||||
"customer_type": "Company",
|
||||
"customer_name": "Poore Simon's",
|
||||
}
|
||||
@@ -413,7 +413,7 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_group": "All Customer Groups",
|
||||
"customer_group": "Individual",
|
||||
"customer_type": "Company",
|
||||
"customer_name": "Fayva",
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
# See license.txt
|
||||
|
||||
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):
|
||||
"""A fee that's part of a withdrawal cannot be bigger than the
|
||||
withdrawal itself."""
|
||||
|
||||
@@ -13,7 +13,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestExchangeRateRevaluation(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_usd_receivable_account()
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
frappe.ui.form.on("Financial Report Template", {
|
||||
refresh(frm) {
|
||||
if (frm.is_new() || frm.doc.rows.length === 0) return;
|
||||
|
||||
// add custom button to view missed accounts
|
||||
frm.add_custom_button(__("View Account Coverage"), function () {
|
||||
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) {
|
||||
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_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);
|
||||
},
|
||||
|
||||
@@ -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 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 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 = "";
|
||||
|
||||
@@ -382,8 +378,13 @@ function update_formula_description(frm, data_source) {
|
||||
<li><code>my_app.financial_reports.get_kpi_data</code></li>
|
||||
</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>
|
||||
<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>`;
|
||||
} else if (data_source === "Blank Line") {
|
||||
description_html = `
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:template_name",
|
||||
"creation": "2025-08-02 04:44:15.184541",
|
||||
"doctype": "DocType",
|
||||
@@ -31,7 +30,8 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"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",
|
||||
@@ -66,7 +66,7 @@
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-14 00:11:03.508139",
|
||||
"modified": "2026-02-23 01:04:05.797161",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Financial Report Template",
|
||||
|
||||
@@ -32,6 +32,19 @@ class FinancialReportTemplate(Document):
|
||||
template_name: DF.Data
|
||||
# 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):
|
||||
validator = TemplateValidator(self)
|
||||
result = validator.validate()
|
||||
|
||||
@@ -70,8 +70,8 @@ class ValidationResult:
|
||||
self.warnings.append(issue)
|
||||
|
||||
def notify_user(self) -> None:
|
||||
warnings = "<br><br>".join(str(w) for w in self.warnings)
|
||||
errors = "<br><br>".join(str(e) for e in self.issues)
|
||||
warnings = "<br><br>".join(str(w) for w in self.warnings if w)
|
||||
errors = "<br><br>".join(str(e) for e in self.issues if e)
|
||||
|
||||
if warnings:
|
||||
frappe.msgprint(warnings, title=_("Warnings"), indicator="orange")
|
||||
@@ -99,9 +99,8 @@ class TemplateValidator:
|
||||
result.merge(validator.validate(self.template))
|
||||
|
||||
# Run row-level validations
|
||||
account_fields = {field.fieldname for field in frappe.get_meta("Account").fields}
|
||||
for row in self.template.rows:
|
||||
result.merge(self.formula_validator.validate(row, account_fields))
|
||||
result.merge(self.formula_validator.validate(row))
|
||||
|
||||
return result
|
||||
|
||||
@@ -383,7 +382,8 @@ class AccountFilterValidator(Validator):
|
||||
"""Validates account filter expressions used in Account Data rows"""
|
||||
|
||||
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:
|
||||
result = ValidationResult()
|
||||
@@ -403,7 +403,11 @@ class AccountFilterValidator(Validator):
|
||||
|
||||
try:
|
||||
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:
|
||||
result.add_error(
|
||||
@@ -425,7 +429,12 @@ class AccountFilterValidator(Validator):
|
||||
|
||||
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]
|
||||
if isinstance(filter_config, list):
|
||||
if len(filter_config) != 3:
|
||||
@@ -436,8 +445,10 @@ class AccountFilterValidator(Validator):
|
||||
if not isinstance(field, str) or not isinstance(operator, str):
|
||||
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:
|
||||
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:
|
||||
return f"Invalid operator '{operator}'"
|
||||
@@ -460,7 +471,7 @@ class AccountFilterValidator(Validator):
|
||||
|
||||
# recursive
|
||||
for condition in conditions:
|
||||
error = self._validate_filter_structure(condition, account_fields)
|
||||
error = self._validate_filter_structure(condition, account_fields, advanced_filtering)
|
||||
if error:
|
||||
return error
|
||||
else:
|
||||
@@ -476,7 +487,7 @@ class FormulaValidator(Validator):
|
||||
self.calculation_validator = CalculationFormulaValidator(reference_codes)
|
||||
self.account_filter_validator = AccountFilterValidator()
|
||||
|
||||
def validate(self, row, account_fields: set) -> ValidationResult:
|
||||
def validate(self, row) -> ValidationResult:
|
||||
result = ValidationResult()
|
||||
|
||||
if not row.calculation_formula:
|
||||
@@ -486,9 +497,6 @@ class FormulaValidator(Validator):
|
||||
return self.calculation_validator.validate(row)
|
||||
|
||||
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)
|
||||
|
||||
elif row.data_source == "Custom API":
|
||||
|
||||
@@ -1295,6 +1295,7 @@ class TestFilterExpressionParser(FinancialReportTemplateTestCase):
|
||||
self.data_source = "Account Data"
|
||||
self.idx = 1
|
||||
self.reverse_sign = 0
|
||||
self.advanced_filtering = True
|
||||
|
||||
return MockReportRow(formula, reference_code)
|
||||
|
||||
|
||||
@@ -489,4 +489,5 @@ def rename_temporarily_named_docs(doctype):
|
||||
for hook in frappe.get_hooks(hook_type):
|
||||
frappe.call(hook, newname=newname, oldname=oldname)
|
||||
|
||||
frappe.db.commit()
|
||||
if not frappe.in_test:
|
||||
frappe.db.commit()
|
||||
|
||||
@@ -353,8 +353,11 @@ class JournalEntry(AccountsController):
|
||||
frappe.throw(_("Account {0} should be of type Expense").format(d.account))
|
||||
|
||||
def validate_stock_accounts(self):
|
||||
if self.voucher_type == "Periodic Accounting Entry":
|
||||
# Skip validation for periodic accounting entry
|
||||
if (
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
||||
class TestLedgerHealth(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestLedgerHealth(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
|
||||
@@ -71,14 +71,16 @@ def start_merge(docname):
|
||||
ledger_merge.account,
|
||||
)
|
||||
row.db_set("merged", 1)
|
||||
frappe.db.commit()
|
||||
if not frappe.in_test:
|
||||
frappe.db.commit()
|
||||
successful_merges += 1
|
||||
frappe.publish_realtime(
|
||||
"ledger_merge_progress",
|
||||
{"ledger_merge": ledger_merge.name, "current": successful_merges, "total": total},
|
||||
)
|
||||
except Exception:
|
||||
frappe.db.rollback()
|
||||
if not frappe.in_test:
|
||||
frappe.db.rollback()
|
||||
ledger_merge.log_error("Ledger merge failed")
|
||||
finally:
|
||||
if successful_merges == total:
|
||||
|
||||
@@ -70,9 +70,7 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
|
||||
});
|
||||
});
|
||||
|
||||
if (frm.doc.create_missing_party) {
|
||||
frm.set_df_property("party", "fieldtype", "Data", frm.doc.name, "invoices");
|
||||
}
|
||||
frm.trigger("update_party_labels");
|
||||
},
|
||||
|
||||
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_name", "");
|
||||
});
|
||||
frm.clear_table("invoices");
|
||||
frm.refresh_fields();
|
||||
frm.trigger("update_party_labels");
|
||||
},
|
||||
|
||||
make_dashboard: function (frm) {
|
||||
@@ -175,6 +175,32 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
|
||||
}
|
||||
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", {
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"section_break_ynel",
|
||||
"company",
|
||||
"create_missing_party",
|
||||
"column_break_3",
|
||||
"invoice_type",
|
||||
"create_missing_party",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
@@ -25,11 +26,11 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"remember_last_selected_value": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If party does not exist, create it using the Party Name field.",
|
||||
"fieldname": "create_missing_party",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create Missing Party"
|
||||
@@ -79,12 +80,17 @@
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ynel",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-23 00:32:15.600086",
|
||||
"modified": "2026-03-31 01:47:20.360352",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Opening Invoice Creation Tool",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
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 erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -86,6 +86,11 @@ class OpeningInvoiceCreationTool(Document):
|
||||
)
|
||||
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
|
||||
|
||||
def validate_company(self):
|
||||
@@ -274,7 +279,8 @@ def start_import(invoices):
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.insert(set_name=invoice_number)
|
||||
doc.submit()
|
||||
frappe.db.commit()
|
||||
if not frappe.in_test:
|
||||
frappe.db.commit()
|
||||
names.append(doc.name)
|
||||
except Exception:
|
||||
errors += 1
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
|
||||
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 (
|
||||
get_temporary_opening_account,
|
||||
)
|
||||
@@ -14,11 +10,6 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestOpeningInvoiceCreationTool(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
|
||||
make_company()
|
||||
create_dimension()
|
||||
|
||||
def make_invoices(
|
||||
self,
|
||||
invoice_type="Sales",
|
||||
@@ -183,26 +174,13 @@ def get_opening_invoice_creation_dict(**args):
|
||||
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):
|
||||
customer_name = customer or "Opening Customer"
|
||||
customer = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_name": customer_name,
|
||||
"customer_group": "All Customer Groups",
|
||||
"customer_group": "Individual",
|
||||
"customer_type": "Company",
|
||||
"territory": "All Territories",
|
||||
}
|
||||
|
||||
@@ -824,7 +824,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
paid_amount: function (frm) {
|
||||
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;
|
||||
if (!frm.doc.received_amount) {
|
||||
if (frm.doc.paid_amount) {
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("received_amount", frm.doc.paid_amount);
|
||||
} 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)
|
||||
);
|
||||
|
||||
if (!frm.doc.paid_amount) {
|
||||
if (frm.doc.received_amount) {
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("paid_amount", frm.doc.received_amount);
|
||||
if (frm.doc.target_exchange_rate) {
|
||||
|
||||
@@ -2376,9 +2376,7 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
vouchers=args.get("vouchers") or None,
|
||||
)
|
||||
|
||||
outstanding_invoices = split_invoices_based_on_payment_terms(
|
||||
outstanding_invoices, args.get("company")
|
||||
)
|
||||
outstanding_invoices = split_refdocs_based_on_payment_terms(outstanding_invoices, args.get("company"))
|
||||
|
||||
for d in outstanding_invoices:
|
||||
d["exchange_rate"] = 1
|
||||
@@ -2416,6 +2414,8 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
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
|
||||
|
||||
if not data:
|
||||
@@ -2438,13 +2438,13 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
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."""
|
||||
exc_rates = get_currency_data(outstanding_invoices, company)
|
||||
exc_rates = get_currency_data(refdocs, company)
|
||||
|
||||
outstanding_invoices_after_split = []
|
||||
for entry in outstanding_invoices:
|
||||
if entry.voucher_type in ["Sales Invoice", "Purchase Invoice"]:
|
||||
outstanding_refdoc_after_split = []
|
||||
for entry in refdocs:
|
||||
if entry.voucher_type in ["Sales Invoice", "Purchase Invoice", "Sales Order", "Purchase Order"]:
|
||||
if payment_term_template := frappe.db.get_value(
|
||||
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,
|
||||
)
|
||||
outstanding_invoices_after_split += split_rows
|
||||
outstanding_refdoc_after_split += split_rows
|
||||
continue
|
||||
|
||||
# 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."""
|
||||
exc_rates = frappe._dict()
|
||||
company_currency = frappe.db.get_value("Company", company, "default_currency") if company else None
|
||||
|
||||
for doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype]
|
||||
for doctype in ["Sales Invoice", "Purchase Invoice", "Sales Order", "Purchase Order"]:
|
||||
refdoc = [x.voucher_no for x in outstanding_refdocs if x.voucher_type == doctype]
|
||||
for x in frappe.db.get_all(
|
||||
doctype,
|
||||
filters={"name": ["in", invoices]},
|
||||
filters={"name": ["in", refdoc]},
|
||||
fields=["name", "currency", "conversion_rate", "party_account_currency"],
|
||||
):
|
||||
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, "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):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
|
||||
@@ -750,7 +750,8 @@ def make_payment_request(**args):
|
||||
pr.submit()
|
||||
|
||||
if args.order_type == "Shopping Cart":
|
||||
frappe.db.commit()
|
||||
if not frappe.in_test:
|
||||
frappe.db.commit()
|
||||
frappe.local.response["type"] = "redirect"
|
||||
frappe.local.response["location"] = pr.get_payment_url()
|
||||
|
||||
|
||||
@@ -4,10 +4,6 @@ import unittest
|
||||
|
||||
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 (
|
||||
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
|
||||
"""
|
||||
|
||||
create_dimension()
|
||||
location = frappe.get_doc("Accounting Dimension", "Location")
|
||||
location.dimension_defaults[0].mandatory_for_bs = True
|
||||
location.save()
|
||||
@@ -198,7 +193,6 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
)
|
||||
accounting_dimension_department.mandatory_for_bs = 0
|
||||
accounting_dimension_department.save()
|
||||
disable_dimension()
|
||||
|
||||
def test_merging_into_sales_invoice_for_batched_item(self):
|
||||
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)
|
||||
doc.add_comment("Comment", "Emails sent on: " + frappe.utils.format_datetime(frappe.utils.now()))
|
||||
if doc.report == "General Ledger":
|
||||
doc.db_set("to_date", new_to_date, commit=True)
|
||||
doc.db_set("from_date", new_from_date, commit=True)
|
||||
frappe.db.set_value(doc.doctype, doc.name, "to_date", new_to_date)
|
||||
frappe.db.set_value(doc.doctype, doc.name, "from_date", new_from_date)
|
||||
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
|
||||
else:
|
||||
return False
|
||||
|
||||
@@ -14,7 +14,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestProcessStatementOfAccounts(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestProcessStatementOfAccounts(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
frappe.db.set_single_value("Selling Settings", "validate_selling_price", 0)
|
||||
letterhead = frappe.get_doc("Letter Head", "Company Letterhead - Grey")
|
||||
|
||||
@@ -21,10 +21,12 @@ frappe.ui.form.on("Promotional Scheme", {
|
||||
|
||||
selling: function (frm) {
|
||||
frm.trigger("set_options_for_applicable_for");
|
||||
frm.toggle_enable("buying", !frm.doc.selling);
|
||||
},
|
||||
|
||||
buying: function (frm) {
|
||||
frm.trigger("set_options_for_applicable_for");
|
||||
frm.toggle_enable("selling", !frm.doc.buying);
|
||||
},
|
||||
|
||||
set_options_for_applicable_for: function (frm) {
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"email_append_to": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"naming_series",
|
||||
"supplier",
|
||||
"supplier_name",
|
||||
@@ -209,16 +208,6 @@
|
||||
"connections_tab"
|
||||
],
|
||||
"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",
|
||||
"fieldtype": "Select",
|
||||
@@ -1693,7 +1682,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-17 20:44:00.221219",
|
||||
"modified": "2026-03-25 11:45:38.696888",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
@@ -1756,6 +1745,6 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"timeline_field": "supplier",
|
||||
"title_field": "title",
|
||||
"title_field": "supplier_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
||||
@@ -203,7 +203,6 @@ class PurchaseInvoice(BuyingController):
|
||||
taxes_and_charges_deducted: DF.Currency
|
||||
tc_name: DF.Link | None
|
||||
terms: DF.TextEditor | None
|
||||
title: DF.Data | None
|
||||
to_date: DF.Date | None
|
||||
total: DF.Currency
|
||||
total_advance: DF.Currency
|
||||
@@ -333,9 +332,6 @@ class PurchaseInvoice(BuyingController):
|
||||
if 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):
|
||||
if not self.credit_to:
|
||||
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)
|
||||
|
||||
def po_required(self):
|
||||
if frappe.db.get_single_value("Buying Settings", "po_required") == "Yes":
|
||||
if frappe.get_value(
|
||||
if (
|
||||
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"
|
||||
):
|
||||
return
|
||||
|
||||
)
|
||||
):
|
||||
for d in self.get("items"):
|
||||
if not d.purchase_order:
|
||||
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:
|
||||
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"):
|
||||
if flt(item.base_net_amount) or (self.get("update_stock") and item.valuation_rate):
|
||||
if item.item_code:
|
||||
@@ -1161,7 +1162,11 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
|
||||
# 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 (
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
self.auto_accounting_for_stock
|
||||
and self.is_opening == "No"
|
||||
|
||||
@@ -350,6 +350,12 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
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(
|
||||
company="_Test Company with perpetual inventory",
|
||||
warehouse="Stores - TCP1",
|
||||
@@ -368,14 +374,19 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
|
||||
# fetching the latest GL Entry with exchange gain and loss account account
|
||||
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(
|
||||
pi.items[0].base_net_amount - pr.items[0].base_net_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):
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_invoice as create_purchase_invoice,
|
||||
@@ -2189,11 +2200,6 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
|
||||
def test_offsetting_entries_for_accounting_dimensions(self):
|
||||
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(
|
||||
account_name="Offsetting",
|
||||
@@ -2201,7 +2207,16 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
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.branch = "Location 1"
|
||||
@@ -2238,8 +2253,6 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
voucher_type="Purchase Invoice",
|
||||
additional_columns=["branch"],
|
||||
)
|
||||
clear_dimension_defaults("Branch")
|
||||
disable_dimension()
|
||||
|
||||
def test_repost_accounting_entries(self):
|
||||
# update repost settings
|
||||
|
||||
@@ -190,6 +190,7 @@
|
||||
"fieldtype": "Float",
|
||||
"label": "Received Qty",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -206,7 +207,8 @@
|
||||
{
|
||||
"fieldname": "rejected_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Rejected Qty"
|
||||
"label": "Rejected Qty",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||
@@ -226,6 +228,7 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "UOM",
|
||||
"options": "UOM",
|
||||
"print_hide": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -261,14 +264,16 @@
|
||||
"depends_on": "price_list_rate",
|
||||
"fieldname": "discount_percentage",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Discount on Price List Rate (%)"
|
||||
"label": "Discount on Price List Rate (%)",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "price_list_rate",
|
||||
"fieldname": "discount_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Discount Amount",
|
||||
"options": "currency"
|
||||
"options": "currency",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break3",
|
||||
@@ -401,12 +406,14 @@
|
||||
{
|
||||
"fieldname": "weight_per_unit",
|
||||
"fieldtype": "Float",
|
||||
"label": "Weight Per Unit"
|
||||
"label": "Weight Per Unit",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_weight",
|
||||
"fieldtype": "Float",
|
||||
"label": "Total Weight",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -417,7 +424,8 @@
|
||||
"fieldname": "weight_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Weight UOM",
|
||||
"options": "UOM"
|
||||
"options": "UOM",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.update_stock",
|
||||
@@ -429,7 +437,8 @@
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Accepted Warehouse",
|
||||
"options": "Warehouse"
|
||||
"options": "Warehouse",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "rejected_warehouse",
|
||||
@@ -674,7 +683,8 @@
|
||||
"fieldname": "asset_location",
|
||||
"fieldtype": "Link",
|
||||
"label": "Asset Location",
|
||||
"options": "Location"
|
||||
"options": "Location",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "po_detail",
|
||||
@@ -730,7 +740,6 @@
|
||||
"label": "Valuation Rate",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"precision": "6",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -796,6 +805,7 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Asset Category",
|
||||
"options": "Asset Category",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -828,6 +838,7 @@
|
||||
"label": "Rate of Stock UOM",
|
||||
"no_copy": 1,
|
||||
"options": "currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -866,6 +877,7 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rate With Margin",
|
||||
"options": "currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -892,7 +904,8 @@
|
||||
"default": "1",
|
||||
"fieldname": "apply_tds",
|
||||
"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",
|
||||
@@ -918,7 +931,8 @@
|
||||
"fieldname": "wip_composite_asset",
|
||||
"fieldtype": "Link",
|
||||
"label": "WIP Composite Asset",
|
||||
"options": "Asset"
|
||||
"options": "Asset",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 && doc.docstatus === 0",
|
||||
@@ -930,7 +944,8 @@
|
||||
"default": "0",
|
||||
"fieldname": "use_serial_batch_fields",
|
||||
"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",
|
||||
@@ -977,7 +992,8 @@
|
||||
"fieldname": "distributed_discount_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Distributed Discount Amount",
|
||||
"options": "currency"
|
||||
"options": "currency",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_category",
|
||||
@@ -991,7 +1007,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-15 21:07:49.455930",
|
||||
"modified": "2026-04-07 15:40:45.687554",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"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_request.payment_request import make_payment_request
|
||||
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.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.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestRepostAccountingLedger(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
frappe.db.set_single_value("Selling Settings", "validate_selling_price", 0)
|
||||
update_repost_settings()
|
||||
|
||||
def test_01_basic_functions(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
item="_Test Item",
|
||||
company="_Test Company",
|
||||
customer="_Test Customer",
|
||||
debit_to="Debtors - _TC",
|
||||
parent_cost_center="Main - _TC",
|
||||
cost_center="Main - _TC",
|
||||
rate=100,
|
||||
)
|
||||
|
||||
@@ -48,7 +44,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
# Test Validation Error
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.company = "_Test Company"
|
||||
ral.delete_cancelled_entries = True
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append(
|
||||
@@ -65,7 +61,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
||||
ral.save()
|
||||
|
||||
# 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)
|
||||
|
||||
gl = qb.DocType("GL Entry")
|
||||
@@ -94,23 +90,23 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
def test_02_deferred_accounting_valiations(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
item="_Test Item",
|
||||
company="_Test Company",
|
||||
customer="_Test Customer",
|
||||
debit_to="Debtors - _TC",
|
||||
parent_cost_center="Main - _TC",
|
||||
cost_center="Main - _TC",
|
||||
rate=100,
|
||||
do_not_submit=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_end_date = add_days(nowdate(), 90)
|
||||
si.save().submit()
|
||||
|
||||
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})
|
||||
self.assertRaises(frappe.ValidationError, ral.save)
|
||||
|
||||
@@ -118,35 +114,35 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
||||
def test_04_pcv_validation(self):
|
||||
# Clear old GL entries so PCV can be submitted.
|
||||
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(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
item="_Test Item",
|
||||
company="_Test Company",
|
||||
customer="_Test Customer",
|
||||
debit_to="Debtors - _TC",
|
||||
parent_cost_center="Main - _TC",
|
||||
cost_center="Main - _TC",
|
||||
rate=100,
|
||||
)
|
||||
fy = get_fiscal_year(today(), company=self.company)
|
||||
fy = get_fiscal_year(today(), company="_Test Company")
|
||||
pcv = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Period Closing Voucher",
|
||||
"transaction_date": today(),
|
||||
"period_start_date": fy[1],
|
||||
"period_end_date": today(),
|
||||
"company": self.company,
|
||||
"company": "_Test Company",
|
||||
"fiscal_year": fy[0],
|
||||
"cost_center": self.cost_center,
|
||||
"closing_account_head": self.retained_earnings,
|
||||
"cost_center": "Main - _TC",
|
||||
"closing_account_head": "Retained Earnings - _TC",
|
||||
"remarks": "test",
|
||||
}
|
||||
)
|
||||
pcv.save().submit()
|
||||
|
||||
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})
|
||||
self.assertRaises(frappe.ValidationError, ral.save)
|
||||
|
||||
@@ -156,12 +152,12 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
def test_03_deletion_flag_and_preview_function(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
item="_Test Item",
|
||||
company="_Test Company",
|
||||
customer="_Test Customer",
|
||||
debit_to="Debtors - _TC",
|
||||
parent_cost_center="Main - _TC",
|
||||
cost_center="Main - _TC",
|
||||
rate=100,
|
||||
)
|
||||
|
||||
@@ -170,7 +166,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
# with deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.company = "_Test Company"
|
||||
ral.delete_cancelled_entries = True
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.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):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
item="_Test Item",
|
||||
company="_Test Company",
|
||||
customer="_Test Customer",
|
||||
debit_to="Debtors - _TC",
|
||||
parent_cost_center="Main - _TC",
|
||||
cost_center="Main - _TC",
|
||||
rate=100,
|
||||
)
|
||||
|
||||
@@ -195,7 +191,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
# without deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.company = "_Test Company"
|
||||
ral.delete_cancelled_entries = False
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
@@ -210,16 +206,16 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
||||
provisional_account = create_account(
|
||||
account_name="Provision Account",
|
||||
parent_account="Current Liabilities - _TC",
|
||||
company=self.company,
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
another_provisional_account = create_account(
|
||||
account_name="Another Provision Account",
|
||||
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.default_provisional_account = provisional_account
|
||||
company.save()
|
||||
@@ -229,7 +225,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
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)
|
||||
expected_pr_gles = [
|
||||
{"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.company = self.company
|
||||
repost_doc.company = "_Test Company"
|
||||
repost_doc.delete_cancelled_entries = True
|
||||
repost_doc.append("vouchers", {"voucher_type": pr.doctype, "voucher_no": pr.name})
|
||||
repost_doc.save().submit()
|
||||
|
||||
@@ -165,13 +165,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 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.toggle_get_items();
|
||||
|
||||
this.set_default_print_format();
|
||||
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() {
|
||||
var me = this;
|
||||
|
||||
@@ -331,6 +412,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
this.$delivery_note_btn = this.frm.add_custom_button(
|
||||
__("Delivery Note"),
|
||||
function () {
|
||||
if (!me.frm.doc.customer) {
|
||||
frappe.throw({
|
||||
title: __("Mandatory"),
|
||||
message: __("Please Select a Customer"),
|
||||
});
|
||||
}
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
|
||||
source_doctype: "Delivery Note",
|
||||
@@ -343,7 +430,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
var filters = {
|
||||
docstatus: 1,
|
||||
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;
|
||||
return {
|
||||
@@ -610,6 +697,10 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
apply_tds(frm) {
|
||||
this.frm.clear_table("tax_withholding_entries");
|
||||
}
|
||||
|
||||
is_return() {
|
||||
this.toggle_get_items();
|
||||
}
|
||||
};
|
||||
|
||||
// for backward compatibility: combine new and previous states
|
||||
@@ -1061,71 +1152,6 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
},
|
||||
|
||||
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) {
|
||||
frm.set_df_property("return_against", "label", __("Adjustment Against"));
|
||||
}
|
||||
|
||||
@@ -1101,9 +1101,6 @@ class SalesInvoice(SellingController):
|
||||
if self.po_date:
|
||||
self.remarks += " " + _("dated {0}").format(formatdate(self.po_date))
|
||||
|
||||
else:
|
||||
self.remarks = _("No Remarks")
|
||||
|
||||
def validate_auto_set_posting_time(self):
|
||||
# Don't auto set the posting date and time if invoice is amended
|
||||
if self.is_new() and self.amended_from:
|
||||
|
||||
@@ -2246,13 +2246,6 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
|
||||
@ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": True})
|
||||
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.items = []
|
||||
for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]:
|
||||
@@ -2894,7 +2887,7 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
si.submit()
|
||||
|
||||
# Check if adjustment entry is created
|
||||
self.assertTrue(
|
||||
self.assertFalse(
|
||||
frappe.db.exists(
|
||||
"GL Entry",
|
||||
{
|
||||
|
||||
@@ -207,6 +207,7 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock UOM",
|
||||
"options": "UOM",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -310,7 +311,8 @@
|
||||
"fieldname": "discount_amount",
|
||||
"fieldtype": "Currency",
|
||||
"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",
|
||||
@@ -853,6 +855,7 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rate of Stock UOM",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -869,6 +872,7 @@
|
||||
"fieldname": "grant_commission",
|
||||
"fieldtype": "Check",
|
||||
"label": "Grant Commission",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -926,7 +930,8 @@
|
||||
"default": "0",
|
||||
"fieldname": "use_serial_batch_fields",
|
||||
"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",
|
||||
@@ -941,7 +946,8 @@
|
||||
"fieldname": "distributed_discount_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Distributed Discount Amount",
|
||||
"options": "currency"
|
||||
"options": "currency",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "available_quantity_section",
|
||||
@@ -1010,7 +1016,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-23 14:37:14.853941",
|
||||
"modified": "2026-02-24 14:37:16.853941",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
||||
@@ -772,7 +772,8 @@ def process_all(subscription: list, posting_date: DateTimeLikeObject | None = No
|
||||
try:
|
||||
subscription = frappe.get_doc("Subscription", subscription_name)
|
||||
subscription.process(posting_date)
|
||||
frappe.db.commit()
|
||||
if not frappe.in_test:
|
||||
frappe.db.commit()
|
||||
except frappe.ValidationError:
|
||||
frappe.db.rollback()
|
||||
subscription.log_error("Subscription failed")
|
||||
|
||||
@@ -128,6 +128,7 @@ class TaxWithholdingDetails:
|
||||
self.party_type = party_type
|
||||
self.party = party
|
||||
self.company = company
|
||||
self.tax_id = get_tax_id_for_party(self.party_type, self.party)
|
||||
|
||||
def get(self) -> list:
|
||||
"""
|
||||
@@ -161,6 +162,7 @@ class TaxWithholdingDetails:
|
||||
disable_cumulative_threshold=doc.disable_cumulative_threshold,
|
||||
disable_transaction_threshold=doc.disable_transaction_threshold,
|
||||
taxable_amount=0,
|
||||
tax_id=self.tax_id,
|
||||
)
|
||||
|
||||
# ldc (only if valid based on posting date)
|
||||
@@ -181,17 +183,13 @@ class TaxWithholdingDetails:
|
||||
if self.party_type != "Supplier":
|
||||
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_records = self.get_valid_ldc_records(tax_id)
|
||||
ldc_records = self.get_valid_ldc_records(self.tax_id)
|
||||
if not ldc_records:
|
||||
return ldc_details
|
||||
|
||||
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
|
||||
for ldc in ldc_records:
|
||||
@@ -254,4 +252,5 @@ class TaxWithholdingDetails:
|
||||
|
||||
@allow_regional
|
||||
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
|
||||
|
||||
import datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
@@ -18,7 +19,6 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
|
||||
# create relevant supplier, etc
|
||||
create_records()
|
||||
create_tax_withholding_category_records()
|
||||
make_pan_no_field()
|
||||
|
||||
def validate_tax_withholding_entries(self, doctype, docname, expected_entries):
|
||||
"""Validate tax withholding entries for a document"""
|
||||
@@ -3542,6 +3542,47 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
|
||||
entry.withholding_amount = 5001 # Should be 5000 (10% of 50000)
|
||||
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):
|
||||
# return sales invoice doc object
|
||||
@@ -3998,18 +4039,3 @@ def create_lower_deduction_certificate(
|
||||
"certificate_limit": limit,
|
||||
}
|
||||
).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 (
|
||||
TaxWithholdingDetails,
|
||||
get_tax_id_for_party,
|
||||
)
|
||||
|
||||
|
||||
@@ -646,8 +645,11 @@ class TaxWithholdingController:
|
||||
|
||||
# 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)
|
||||
query = query.where(entry.tax_id == tax_id) if tax_id else query.where(entry.party == self.party)
|
||||
query = (
|
||||
query.where(entry.tax_id == category.tax_id)
|
||||
if category.tax_id
|
||||
else query.where(entry.party == self.party)
|
||||
)
|
||||
|
||||
return query
|
||||
|
||||
@@ -686,6 +688,7 @@ class TaxWithholdingController:
|
||||
"company": self.doc.company,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"tax_id": category.tax_id,
|
||||
"tax_withholding_category": category.name,
|
||||
"tax_withholding_group": category.tax_withholding_group,
|
||||
"tax_rate": category.tax_rate,
|
||||
@@ -1052,6 +1055,7 @@ class TaxWithholdingController:
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"company": self.doc.company,
|
||||
"tax_id": category.tax_id,
|
||||
}
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
class TestUnreconcilePayment(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestUnreconcilePayment(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
|
||||
@@ -7,7 +7,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestAccountsPayable(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestAccountsPayable(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
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
|
||||
|
||||
|
||||
class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
@@ -120,12 +120,12 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
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):
|
||||
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
|
||||
self.create_payment_entry(si.name)
|
||||
report = execute(filters)
|
||||
@@ -178,11 +178,11 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
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):
|
||||
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
|
||||
cr_note = self.create_credit_note(si.name, do_not_submit=True)
|
||||
@@ -225,9 +225,10 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
||||
report = execute(filters)
|
||||
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
|
||||
self.create_customer(
|
||||
@@ -258,18 +259,20 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
||||
report = execute(filters)
|
||||
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
|
||||
filters.pop("in_party_currency")
|
||||
report = execute(filters)
|
||||
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):
|
||||
filters = {
|
||||
@@ -285,11 +288,12 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
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):
|
||||
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
|
||||
self.create_payment_entry(si.name)
|
||||
@@ -348,11 +352,12 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data = [100, 100, "No Remarks"]
|
||||
expected_data = [100, 100]
|
||||
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
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
|
||||
self.create_payment_entry(si.name)
|
||||
|
||||
@@ -8,7 +8,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
self.create_company()
|
||||
|
||||
@@ -10,7 +10,7 @@ from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestCustomerLedgerSummary(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestCustomerLedgerSummary(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
|
||||
@@ -48,6 +48,9 @@ class Deferred_Item:
|
||||
Generate report data for output
|
||||
"""
|
||||
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:
|
||||
ret_data[period.key] = period.total
|
||||
ret_data.indent = 1
|
||||
@@ -205,6 +208,9 @@ class Deferred_Invoice:
|
||||
for item in self.uniq_items:
|
||||
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):
|
||||
"""
|
||||
calculate deferred revenue/expense for all items in invoice
|
||||
@@ -232,7 +238,7 @@ class Deferred_Invoice:
|
||||
generate report data for invoice, includes invoice total
|
||||
"""
|
||||
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:
|
||||
inv_total[x.key] = x.total
|
||||
inv_total.indent = 0
|
||||
@@ -386,6 +392,24 @@ class Deferred_Revenue_and_Expense_Report:
|
||||
def get_columns(self):
|
||||
columns = []
|
||||
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:
|
||||
columns.append(
|
||||
{
|
||||
@@ -415,6 +439,8 @@ class Deferred_Revenue_and_Expense_Report:
|
||||
elif self.filters.type == "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):
|
||||
total_row[period.key] = self.period_total[idx].total
|
||||
ret.append(total_row)
|
||||
|
||||
@@ -142,6 +142,7 @@ def prepare_data(accounts, filters, company_currency, dimension_list):
|
||||
total = 0
|
||||
row = {
|
||||
"account": d.name,
|
||||
"is_group": d.is_group,
|
||||
"parent_account": d.parent_account,
|
||||
"indent": d.indent,
|
||||
"from_date": filters.from_date,
|
||||
|
||||
@@ -74,6 +74,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
depends_on: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestItemWisePurchaseRegister(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestItemWisePurchaseRegister(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_supplier()
|
||||
|
||||
@@ -7,7 +7,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestItemWiseSalesRegister(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
|
||||
@@ -12,7 +12,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestProfitAndLossStatement(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestProfitAndLossStatement(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
|
||||
@@ -7,7 +7,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestItemWiseSalesRegister(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
|
||||
@@ -7,7 +7,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestSupplierLedgerSummary(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestSupplierLedgerSummary(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_supplier()
|
||||
|
||||
@@ -16,7 +16,7 @@ from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestTaxWithholdingDetails(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
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.utils import get_fiscal_year
|
||||
|
||||
self.company = create_company()
|
||||
create_cost_center(
|
||||
cost_center_name="Test Cost Center",
|
||||
company="Trial Balance Company",
|
||||
@@ -26,7 +25,16 @@ class TestTrialBalance(ERPNextTestSuite):
|
||||
parent_account="Temporary Accounts - TBC",
|
||||
)
|
||||
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):
|
||||
"""
|
||||
@@ -45,7 +53,7 @@ class TestTrialBalance(ERPNextTestSuite):
|
||||
branch2.insert(ignore_if_duplicate=True)
|
||||
|
||||
si = create_sales_invoice(
|
||||
company=self.company,
|
||||
company="Trial Balance Company",
|
||||
debit_to="Debtors - TBC",
|
||||
cost_center="Test Cost Center - TBC",
|
||||
income_account="Sales - TBC",
|
||||
@@ -57,60 +65,7 @@ class TestTrialBalance(ERPNextTestSuite):
|
||||
si.submit()
|
||||
|
||||
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]
|
||||
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:
|
||||
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
|
||||
|
||||
|
||||
@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):
|
||||
from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account
|
||||
|
||||
@@ -1531,6 +1603,10 @@ def parse_naming_series_variable(doc, variable):
|
||||
|
||||
else:
|
||||
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 = (
|
||||
(
|
||||
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,
|
||||
)
|
||||
|
||||
frappe.db.commit()
|
||||
if not frappe.in_test:
|
||||
frappe.db.commit()
|
||||
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
failed_assets.append(asset_name)
|
||||
@@ -71,7 +73,8 @@ def book_depreciation_entries(date):
|
||||
if failed_assets:
|
||||
set_depr_entry_posting_status_for_failed_assets(failed_assets)
|
||||
notify_depr_entry_posting_error(failed_assets, error_logs)
|
||||
frappe.db.commit()
|
||||
if not frappe.in_test:
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
def get_depreciable_assets_data(date):
|
||||
|
||||
@@ -380,6 +380,7 @@ class TestAssetCapitalization(ERPNextTestSuite):
|
||||
"asset_type": "Composite Component",
|
||||
"purchase_date": pr.posting_date,
|
||||
"available_for_use_date": pr.posting_date,
|
||||
"location": "Test Location",
|
||||
}
|
||||
)
|
||||
consumed_asset_doc.save()
|
||||
|
||||
@@ -41,7 +41,7 @@ frappe.ui.form.on("Asset Movement", {
|
||||
});
|
||||
},
|
||||
|
||||
onload: (frm) => {
|
||||
refresh: (frm) => {
|
||||
frm.trigger("set_required_fields");
|
||||
},
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ class TestAssetMovement(ERPNextTestSuite):
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC"
|
||||
)
|
||||
make_location()
|
||||
|
||||
def test_movement(self):
|
||||
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:
|
||||
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(
|
||||
purpose="Transfer",
|
||||
company=asset.company,
|
||||
@@ -122,9 +117,6 @@ class TestAssetMovement(ERPNextTestSuite):
|
||||
if asset.docstatus == 0:
|
||||
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})
|
||||
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.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 Movement",
|
||||
[["Asset Movement Item", "asset", "=", asset.name], ["docstatus", "=", 1]],
|
||||
@@ -197,9 +186,3 @@ def create_asset_movement(**args):
|
||||
movement.submit()
|
||||
|
||||
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",
|
||||
"in_list_view": 1,
|
||||
"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",
|
||||
"reqd": 1
|
||||
},
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"supplier_section",
|
||||
"title",
|
||||
"naming_series",
|
||||
"supplier",
|
||||
"supplier_name",
|
||||
@@ -172,17 +171,6 @@
|
||||
"fieldtype": "Section Break",
|
||||
"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",
|
||||
"fieldtype": "Select",
|
||||
@@ -1328,7 +1316,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-09 17:15:29.184682",
|
||||
"modified": "2026-03-25 11:46:18.748951",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
|
||||
@@ -159,7 +159,6 @@ class PurchaseOrder(BuyingController):
|
||||
taxes_and_charges_deducted: DF.Currency
|
||||
tc_name: DF.Link | None
|
||||
terms: DF.TextEditor | None
|
||||
title: DF.Data
|
||||
to_date: DF.Date | None
|
||||
total: DF.Currency
|
||||
total_net_weight: DF.Float
|
||||
@@ -780,7 +779,8 @@ 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"):
|
||||
frappe.throw(_("Not Permitted"), frappe.PermissionError)
|
||||
doc.save()
|
||||
frappe.db.commit()
|
||||
if not frappe.in_test:
|
||||
frappe.db.commit()
|
||||
frappe.response["type"] = "redirect"
|
||||
frappe.response.location = "/purchase-invoices/" + doc.name
|
||||
|
||||
@@ -802,18 +802,18 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
target.set_payment_schedule()
|
||||
target.credit_to = get_party_account("Supplier", source.supplier, source.company)
|
||||
|
||||
def get_billed_qty(po_item_name):
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
table = frappe.qb.DocType("Purchase Invoice Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(Sum(table.qty).as_("qty"))
|
||||
.where((table.docstatus == 1) & (table.po_detail == po_item_name))
|
||||
)
|
||||
return query.run(pluck="qty")[0] or 0
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
def get_billed_qty(po_item_name):
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
table = frappe.qb.DocType("Purchase Invoice Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(Sum(table.qty).as_("qty"))
|
||||
.where((table.docstatus == 1) & (table.po_detail == po_item_name))
|
||||
)
|
||||
return query.run(pluck="qty")[0] or 0
|
||||
|
||||
billed_qty = flt(get_billed_qty(obj.name))
|
||||
target.qty = flt(obj.qty) - billed_qty
|
||||
|
||||
@@ -853,7 +853,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
"wip_composite_asset": "wip_composite_asset",
|
||||
},
|
||||
"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),
|
||||
},
|
||||
"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
|
||||
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):
|
||||
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(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():
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
|
||||
@@ -280,14 +280,16 @@
|
||||
"depends_on": "price_list_rate",
|
||||
"fieldname": "discount_percentage",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Discount on Price List Rate (%)"
|
||||
"label": "Discount on Price List Rate (%)",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "price_list_rate",
|
||||
"fieldname": "discount_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Discount Amount",
|
||||
"options": "currency"
|
||||
"options": "currency",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break3",
|
||||
@@ -428,6 +430,7 @@
|
||||
"fieldname": "weight_per_unit",
|
||||
"fieldtype": "Float",
|
||||
"label": "Weight Per Unit",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -763,6 +766,7 @@
|
||||
"label": "Rate of Stock UOM",
|
||||
"no_copy": 1,
|
||||
"options": "currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -779,6 +783,7 @@
|
||||
"fieldtype": "Float",
|
||||
"label": "Available Qty at Company",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -878,7 +883,8 @@
|
||||
"fieldname": "fg_item_qty",
|
||||
"fieldtype": "Float",
|
||||
"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",
|
||||
@@ -923,7 +929,8 @@
|
||||
"fieldname": "distributed_discount_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Distributed Discount Amount",
|
||||
"options": "currency"
|
||||
"options": "currency",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -934,6 +941,7 @@
|
||||
"label": "Subcontracted Quantity",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
@@ -942,7 +950,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-30 16:51:56.761673",
|
||||
"modified": "2025-11-30 16:51:57.761673",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
||||
@@ -100,6 +100,7 @@ frappe.ui.form.on("Request for Quotation", {
|
||||
fieldname: "print_format",
|
||||
options: "Print Format",
|
||||
placeholder: "Standard",
|
||||
default: frappe.get_meta("Request for Quotation").default_print_format || "",
|
||||
get_query: () => {
|
||||
return {
|
||||
filters: {
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
"field_order": [
|
||||
"naming_series",
|
||||
"company",
|
||||
"billing_address",
|
||||
"billing_address_display",
|
||||
"vendor",
|
||||
"column_break1",
|
||||
"transaction_date",
|
||||
@@ -43,7 +41,13 @@
|
||||
"select_print_heading",
|
||||
"letter_head",
|
||||
"more_info",
|
||||
"opportunity"
|
||||
"opportunity",
|
||||
"address_and_contact_tab",
|
||||
"billing_address",
|
||||
"billing_address_display",
|
||||
"column_break_czul",
|
||||
"shipping_address",
|
||||
"shipping_address_display"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -346,6 +350,27 @@
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"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,
|
||||
@@ -353,7 +378,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-09 17:15:29.774614",
|
||||
"modified": "2026-03-19 15:27:56.730649",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Request for Quotation",
|
||||
|
||||
@@ -56,6 +56,8 @@ class RequestforQuotation(BuyingController):
|
||||
select_print_heading: DF.Link | None
|
||||
send_attached_files: DF.Check
|
||||
send_document_print: DF.Check
|
||||
shipping_address: DF.Link | None
|
||||
shipping_address_display: DF.TextEditor | None
|
||||
status: DF.Literal["", "Draft", "Submitted", "Cancelled"]
|
||||
subject: DF.Data
|
||||
suppliers: DF.Table[RequestforQuotationSupplier]
|
||||
@@ -283,7 +285,7 @@ class RequestforQuotation(BuyingController):
|
||||
}
|
||||
)
|
||||
user.save(ignore_permissions=True)
|
||||
update_password_link = user.reset_password()
|
||||
update_password_link = user._reset_password()
|
||||
|
||||
return user, update_password_link
|
||||
|
||||
|
||||
@@ -167,6 +167,15 @@ def create_supplier(**args):
|
||||
if not args.without_supplier_group:
|
||||
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()
|
||||
|
||||
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")
|
||||
if doctype in ["Purchase Order", "Purchase Invoice"]:
|
||||
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:
|
||||
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
|
||||
)
|
||||
|
||||
net_rate = item.base_net_amount
|
||||
net_rate = item.qty * item.base_net_rate
|
||||
if item.sales_incoming_rate: # for internal transfer
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
|
||||
@@ -503,11 +503,15 @@ class BuyingController(SubcontractingController):
|
||||
if d.category not in ["Valuation", "Valuation and Total"]:
|
||||
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":
|
||||
total_valuation_amount += flt(d.base_tax_amount_after_discount_amount)
|
||||
total_valuation_amount += amount
|
||||
tax_accounts.append(d.account_head)
|
||||
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
|
||||
|
||||
|
||||
@@ -364,38 +364,43 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, filters, as_dict):
|
||||
doctype = "Delivery Note"
|
||||
def get_delivery_notes_to_be_billed(
|
||||
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"])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select {fields}
|
||||
from `tabDelivery Note`
|
||||
where `tabDelivery Note`.`{key}` like {txt} and
|
||||
`tabDelivery Note`.docstatus = 1
|
||||
and status not in ('Stopped', 'Closed') {fcond}
|
||||
and (
|
||||
(`tabDelivery Note`.is_return = 0 and `tabDelivery Note`.per_billed < 100)
|
||||
or (`tabDelivery Note`.grand_total = 0 and `tabDelivery Note`.per_billed < 100)
|
||||
or (
|
||||
`tabDelivery Note`.is_return = 1
|
||||
and return_against in (select name from `tabDelivery Note` where per_billed < 100)
|
||||
original_dn = (
|
||||
frappe.qb.from_(DeliveryNote)
|
||||
.select(DeliveryNote.name)
|
||||
.where((DeliveryNote.docstatus == 1) & (DeliveryNote.is_return == 0) & (DeliveryNote.per_billed > 0))
|
||||
)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(DeliveryNote)
|
||||
.select(*[DeliveryNote[f] for f in fields])
|
||||
.where(
|
||||
(DeliveryNote.docstatus == 1)
|
||||
& (DeliveryNote.status.notin(["Stopped", "Closed"]))
|
||||
& (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()
|
||||
@@ -1002,3 +1007,26 @@ def get_item_uom_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
limit_page_length=page_len,
|
||||
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:
|
||||
continue
|
||||
|
||||
rate = flt(
|
||||
flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor,
|
||||
d.precision("rate"),
|
||||
)
|
||||
if d.rate != rate:
|
||||
rate = flt(flt(d.incoming_rate) * flt(d.conversion_factor or 1.0))
|
||||
|
||||
if flt(d.rate, d.precision("incoming_rate")) != flt(
|
||||
rate, d.precision("incoming_rate")
|
||||
):
|
||||
d.rate = rate
|
||||
frappe.msgprint(
|
||||
_(
|
||||
|
||||
@@ -1435,7 +1435,7 @@ class StockController(AccountsController):
|
||||
elif self.doctype == "Stock Entry" and row.t_warehouse:
|
||||
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
|
||||
|
||||
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))
|
||||
)
|
||||
|
||||
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:
|
||||
frappe.throw(
|
||||
_("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)
|
||||
)
|
||||
|
||||
if self.doctype != "Subcontracting Inward Order":
|
||||
if self.doctype not in ["Subcontracting Inward Order", "Subcontracting Receipt"]:
|
||||
item.amount = item.qty * item.rate
|
||||
|
||||
if item.bom:
|
||||
@@ -238,7 +238,7 @@ class SubcontractingController(StockController):
|
||||
and self._doc_before_save
|
||||
):
|
||||
for row in self._doc_before_save.get("items"):
|
||||
item_dict[row.name] = (row.item_code, row.qty + (row.get("rejected_qty") or 0))
|
||||
item_dict[row.name] = (row.item_code, row.received_qty)
|
||||
|
||||
return item_dict
|
||||
|
||||
@@ -264,7 +264,7 @@ class SubcontractingController(StockController):
|
||||
self.__reference_name.append(row.name)
|
||||
if (row.name not in item_dict) or (
|
||||
row.item_code,
|
||||
row.qty + (row.get("rejected_qty") or 0),
|
||||
row.received_qty,
|
||||
) != item_dict[row.name]:
|
||||
self.__changed_name.append(row.name)
|
||||
|
||||
@@ -962,7 +962,7 @@ class SubcontractingController(StockController):
|
||||
):
|
||||
qty = (
|
||||
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
|
||||
)
|
||||
bom_item.main_item_code = row.item_code
|
||||
@@ -1285,22 +1285,28 @@ class SubcontractingController(StockController):
|
||||
if self.total_additional_costs:
|
||||
if self.distribute_additional_costs_based_on == "Amount":
|
||||
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:
|
||||
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.amount * self.total_additional_costs) / total_amt
|
||||
) / item.qty
|
||||
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
|
||||
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
|
||||
else:
|
||||
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
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _, bold
|
||||
from frappe.query_builder import Case
|
||||
@@ -18,7 +20,7 @@ class SubcontractingInwardController:
|
||||
def on_submit_subcontracting_inward(self):
|
||||
self.update_inward_order_item()
|
||||
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.update_inward_order_status()
|
||||
|
||||
@@ -28,7 +30,7 @@ class SubcontractingInwardController:
|
||||
self.validate_delivery()
|
||||
self.validate_receive_from_customer_cancel()
|
||||
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.update_inward_order_status()
|
||||
|
||||
@@ -239,7 +241,8 @@ class SubcontractingInwardController:
|
||||
item
|
||||
for item in self.get("items")
|
||||
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")
|
||||
]
|
||||
|
||||
@@ -368,7 +371,9 @@ class SubcontractingInwardController:
|
||||
if self.subcontracting_inward_order:
|
||||
if self.purpose in ["Subcontracting Delivery", "Subcontracting Return", "Manufacture"]:
|
||||
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
|
||||
|
||||
def validate_warehouse_(self):
|
||||
@@ -467,7 +472,7 @@ class SubcontractingInwardController:
|
||||
self.validate_delivery_on_save()
|
||||
else:
|
||||
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(
|
||||
"Subcontracting Inward Order Item",
|
||||
item.scio_detail,
|
||||
@@ -519,7 +524,7 @@ class SubcontractingInwardController:
|
||||
if max_allowed_qty:
|
||||
max_allowed_qty = max_allowed_qty[0]
|
||||
else:
|
||||
table = frappe.qb.DocType("Subcontracting Inward Order Scrap Item")
|
||||
table = frappe.qb.DocType("Subcontracting Inward Order Secondary Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select((table.produced_qty - table.delivered_qty).as_("max_allowed_qty"))
|
||||
@@ -538,8 +543,8 @@ class SubcontractingInwardController:
|
||||
bold(
|
||||
frappe.get_cached_value(
|
||||
"Subcontracting Inward Order Item"
|
||||
if not item.is_scrap_item
|
||||
else "Subcontracting Inward Order Scrap Item",
|
||||
if not item.type and not item.is_legacy_scrap_item
|
||||
else "Subcontracting Inward Order Secondary Item",
|
||||
item.scio_detail,
|
||||
"stock_uom",
|
||||
)
|
||||
@@ -590,9 +595,9 @@ class SubcontractingInwardController:
|
||||
)
|
||||
|
||||
for item in [item for item in self.items if not item.is_finished_item]:
|
||||
if item.is_scrap_item:
|
||||
scio_scrap_item = frappe.get_value(
|
||||
"Subcontracting Inward Order Scrap Item",
|
||||
if item.type or item.is_legacy_scrap_item:
|
||||
scio_secondary_item = frappe.get_value(
|
||||
"Subcontracting Inward Order Secondary Item",
|
||||
{
|
||||
"docstatus": 1,
|
||||
"item_code": item.item_code,
|
||||
@@ -603,12 +608,13 @@ class SubcontractingInwardController:
|
||||
as_dict=True,
|
||||
)
|
||||
if (
|
||||
scio_scrap_item
|
||||
and scio_scrap_item.delivered_qty > scio_scrap_item.produced_qty - item.transfer_qty
|
||||
scio_secondary_item
|
||||
and scio_secondary_item.delivered_qty
|
||||
> scio_secondary_item.produced_qty - item.transfer_qty
|
||||
):
|
||||
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))
|
||||
)
|
||||
else:
|
||||
@@ -648,8 +654,8 @@ class SubcontractingInwardController:
|
||||
for item in self.items:
|
||||
doctype = (
|
||||
"Subcontracting Inward Order Item"
|
||||
if not item.is_scrap_item
|
||||
else "Subcontracting Inward Order Scrap Item"
|
||||
if not item.type and not item.is_legacy_scrap_item
|
||||
else "Subcontracting Inward Order Secondary Item"
|
||||
)
|
||||
frappe.db.set_value(
|
||||
doctype,
|
||||
@@ -763,7 +769,11 @@ class SubcontractingInwardController:
|
||||
customer_warehouse = frappe.get_cached_value(
|
||||
"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(
|
||||
{
|
||||
(
|
||||
@@ -860,24 +870,24 @@ class SubcontractingInwardController:
|
||||
doc.insert()
|
||||
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":
|
||||
scrap_items_list = [item for item in self.items if item.is_scrap_item]
|
||||
scrap_items = frappe._dict(
|
||||
{
|
||||
(item.item_code, item.t_warehouse): item.transfer_qty
|
||||
if self._action == "submit"
|
||||
else -item.transfer_qty
|
||||
for item in scrap_items_list
|
||||
}
|
||||
)
|
||||
if scrap_items:
|
||||
item_codes, warehouses = zip(*list(scrap_items.keys()), strict=True)
|
||||
secondary_items_list = [item for item in self.items if item.type or item.is_legacy_scrap_item]
|
||||
|
||||
secondary_items = defaultdict(float)
|
||||
for item in secondary_items_list:
|
||||
secondary_items[(item.item_code, item.t_warehouse)] += (
|
||||
item.transfer_qty if self._action == "submit" else -item.transfer_qty
|
||||
)
|
||||
secondary_items = frappe._dict(secondary_items)
|
||||
|
||||
if secondary_items:
|
||||
item_codes, warehouses = zip(*list(secondary_items.keys()), strict=True)
|
||||
item_codes = list(item_codes)
|
||||
warehouses = list(warehouses)
|
||||
|
||||
result = frappe.get_all(
|
||||
"Subcontracting Inward Order Scrap Item",
|
||||
"Subcontracting Inward Order Secondary Item",
|
||||
filters={
|
||||
"item_code": ["in", item_codes],
|
||||
"warehouse": ["in", warehouses],
|
||||
@@ -890,7 +900,7 @@ class SubcontractingInwardController:
|
||||
)
|
||||
|
||||
if result:
|
||||
scrap_item_dict = frappe._dict(
|
||||
secondary_items_dict = frappe._dict(
|
||||
{
|
||||
(d.item_code, d.warehouse): frappe._dict(
|
||||
{"name": d.name, "produced_qty": d.produced_qty}
|
||||
@@ -900,40 +910,45 @@ class SubcontractingInwardController:
|
||||
)
|
||||
deleted_docs = []
|
||||
case_expr = Case()
|
||||
table = frappe.qb.DocType("Subcontracting Inward Order Scrap Item")
|
||||
for key, value in scrap_item_dict.items():
|
||||
if self._action == "cancel" and value.produced_qty - abs(scrap_items.get(key)) == 0:
|
||||
table = frappe.qb.DocType("Subcontracting Inward Order Secondary Item")
|
||||
for key, value in secondary_items_dict.items():
|
||||
if (
|
||||
self._action == "cancel"
|
||||
and value.produced_qty - abs(secondary_items.get(key)) == 0
|
||||
):
|
||||
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:
|
||||
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(
|
||||
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(
|
||||
(table.name.isin(final_list)) & (table.docstatus == 1)
|
||||
).run()
|
||||
|
||||
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
|
||||
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]
|
||||
]:
|
||||
doc = frappe.new_doc(
|
||||
"Subcontracting Inward Order Scrap Item",
|
||||
"Subcontracting Inward Order Secondary Item",
|
||||
parent=scio,
|
||||
parenttype="Subcontracting Inward Order",
|
||||
parentfield="scrap_items",
|
||||
idx=frappe.db.count("Subcontracting Inward Order Scrap Item", {"parent": scio}) + 1,
|
||||
item_code=scrap_item.item_code,
|
||||
parentfield="secondary_items",
|
||||
idx=frappe.db.count("Subcontracting Inward Order Secondary Item", {"parent": scio})
|
||||
+ 1,
|
||||
item_code=secondary_item.item_code,
|
||||
fg_item_code=fg_item_code,
|
||||
stock_uom=scrap_item.stock_uom,
|
||||
warehouse=scrap_item.t_warehouse,
|
||||
produced_qty=scrap_item.transfer_qty,
|
||||
stock_uom=secondary_item.stock_uom,
|
||||
warehouse=secondary_item.t_warehouse,
|
||||
produced_qty=secondary_item.transfer_qty,
|
||||
type=secondary_item.type,
|
||||
delivered_qty=0,
|
||||
reference_name=frappe.get_value(
|
||||
"Work Order", self.work_order, "subcontracting_inward_order_item"
|
||||
@@ -965,7 +980,7 @@ class SubcontractingInwardController:
|
||||
and (
|
||||
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 Scrap Item", item.scio_detail)
|
||||
and not frappe.db.exists("Subcontracting Inward Order Secondary Item", item.scio_detail)
|
||||
)
|
||||
]
|
||||
for item in items:
|
||||
|
||||
@@ -164,8 +164,14 @@ class calculate_taxes_and_totals:
|
||||
return
|
||||
|
||||
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:
|
||||
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:
|
||||
item.rate = 0.0
|
||||
@@ -225,7 +231,13 @@ class calculate_taxes_and_totals:
|
||||
elif not item.qty and self.doc.get("is_debit_note"):
|
||||
item.amount = flt(item.rate, item.precision("amount"))
|
||||
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
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
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):
|
||||
if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")):
|
||||
return
|
||||
@@ -372,9 +391,16 @@ class calculate_taxes_and_totals:
|
||||
self.doc.total
|
||||
) = 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:
|
||||
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.net_total += item.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
|
||||
diff = flt(expected_amount - actual_breakup, 5)
|
||||
|
||||
# TODO: fix rounding difference issues
|
||||
if abs(diff) <= 0.5:
|
||||
detail_row = self.doc._item_wise_tax_details[last_idx]
|
||||
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):
|
||||
# store tax breakup for each item
|
||||
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":
|
||||
item_wise_taxable_amount = flt(
|
||||
current_net_amount * self.doc.conversion_rate * multiplier, tax.precision("tax_amount")
|
||||
tax._running_txn_taxable_total += current_net_amount * multiplier
|
||||
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:
|
||||
item_wise_taxable_amount = 0.0
|
||||
|
||||
@@ -788,7 +828,8 @@ class calculate_taxes_and_totals:
|
||||
discount_amount += total_return_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)
|
||||
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)
|
||||
|
||||
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):
|
||||
"""
|
||||
Test workings of dimension filters
|
||||
"""
|
||||
self.setup_dimensions()
|
||||
rate_in_account_currency = 1
|
||||
|
||||
# Invoices
|
||||
@@ -1653,7 +1638,6 @@ class TestAccountsController(ERPNextTestSuite):
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
def test_91_cr_note_should_inherit_dimension(self):
|
||||
self.setup_dimensions()
|
||||
rate_in_account_currency = 1
|
||||
|
||||
# Invoice
|
||||
@@ -1698,7 +1682,6 @@ class TestAccountsController(ERPNextTestSuite):
|
||||
|
||||
def test_92_dimension_inhertiance_exc_gain_loss(self):
|
||||
# Sales Invoice in Foreign Currency
|
||||
self.setup_dimensions()
|
||||
rate_in_account_currency = 1
|
||||
dpt = "Research & Development - _TC"
|
||||
|
||||
@@ -1734,7 +1717,6 @@ class TestAccountsController(ERPNextTestSuite):
|
||||
)
|
||||
|
||||
def test_93_dimension_inheritance_on_advance(self):
|
||||
self.setup_dimensions()
|
||||
dpt = "Research & Development - _TC"
|
||||
|
||||
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.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestTaxesAndTotals(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestTaxesAndTotals(ERPNextTestSuite):
|
||||
@ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1})
|
||||
def test_distributed_discount_amount(self):
|
||||
so = make_sales_order(do_not_save=1)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
from erpnext.tests.utils import ERPNextTestSuite, change_settings
|
||||
|
||||
|
||||
class TestTaxesAndTotals(ERPNextTestSuite):
|
||||
@@ -124,3 +124,180 @@ class TestTaxesAndTotals(ERPNextTestSuite):
|
||||
]
|
||||
|
||||
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",
|
||||
"customer_name": uuid4(),
|
||||
"customer_group": "All Customer Groups",
|
||||
"customer_group": "Individual",
|
||||
}
|
||||
).insert()
|
||||
self.supplier = frappe.get_doc(
|
||||
|
||||
@@ -2,36 +2,17 @@ import frappe
|
||||
from frappe import qb
|
||||
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
|
||||
|
||||
|
||||
class TestReactivity(AccountsTestMixin, 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()
|
||||
|
||||
class TestReactivity(ERPNextTestSuite):
|
||||
def test_01_basic_item_details(self):
|
||||
self.disable_dimensions()
|
||||
|
||||
# set Item Price
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Item Price",
|
||||
"item_code": self.item,
|
||||
"price_list": self.price_list,
|
||||
"item_code": "_Test Item",
|
||||
"price_list": "Standard Selling",
|
||||
"price_list_rate": 90,
|
||||
"selling": True,
|
||||
"rate": 90,
|
||||
@@ -42,17 +23,18 @@ class TestReactivity(AccountsTestMixin, ERPNextTestSuite):
|
||||
si = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Sales Invoice",
|
||||
"company": self.company,
|
||||
"customer": self.customer,
|
||||
"debit_to": self.debit_to,
|
||||
"company": "_Test Company",
|
||||
"customer": "_Test Customer",
|
||||
"debit_to": "Debtors - _TC",
|
||||
"posting_date": today(),
|
||||
"cost_center": self.cost_center,
|
||||
"cost_center": "Main - _TC",
|
||||
"currency": "INR",
|
||||
"conversion_rate": 1,
|
||||
"selling_price_list": self.price_list,
|
||||
"selling_price_list": "Standard Selling",
|
||||
}
|
||||
)
|
||||
itm = si.append("items")
|
||||
itm.item_code = self.item
|
||||
itm.item_code = "_Test Item"
|
||||
si.process_item_selection(itm.idx)
|
||||
self.assertEqual(itm.rate, 90)
|
||||
|
||||
|
||||
@@ -501,8 +501,8 @@ class TestSubcontractingController(ERPNextTestSuite):
|
||||
scr1.items[0].qty = 2
|
||||
add_second_row_in_scr(scr1)
|
||||
scr1.flags.ignore_mandatory = True
|
||||
scr1.save()
|
||||
scr1.set_missing_values()
|
||||
scr1.save()
|
||||
scr1.submit()
|
||||
|
||||
for _key, value in get_supplied_items(scr1).items():
|
||||
@@ -513,8 +513,8 @@ class TestSubcontractingController(ERPNextTestSuite):
|
||||
scr2.items[0].qty = 2
|
||||
add_second_row_in_scr(scr2)
|
||||
scr2.flags.ignore_mandatory = True
|
||||
scr2.save()
|
||||
scr2.set_missing_values()
|
||||
scr2.save()
|
||||
scr2.submit()
|
||||
|
||||
for _key, value in get_supplied_items(scr2).items():
|
||||
@@ -523,8 +523,8 @@ class TestSubcontractingController(ERPNextTestSuite):
|
||||
scr3 = make_subcontracting_receipt(sco.name)
|
||||
scr3.items[0].qty = 2
|
||||
scr3.flags.ignore_mandatory = True
|
||||
scr3.save()
|
||||
scr3.set_missing_values()
|
||||
scr3.save()
|
||||
scr3.submit()
|
||||
|
||||
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)
|
||||
|
||||
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):
|
||||
item_dict = {}
|
||||
|
||||
@@ -120,7 +120,8 @@ class Appointment(Document):
|
||||
self.auto_assign()
|
||||
self.create_calendar_event()
|
||||
self.save(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
if not frappe.in_test:
|
||||
frappe.db.commit()
|
||||
|
||||
def create_lead_and_link(self):
|
||||
# Return if already linked
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:06:46.495091",
|
||||
"modified": "2026-03-25 19:27:19.162421",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Contract Template",
|
||||
@@ -75,44 +75,36 @@
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Purchase Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "HR Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,8 +204,22 @@ def send_mail(entry, email_campaign):
|
||||
|
||||
# called from hooks on doc_event Email Unsubscribe
|
||||
def unsubscribe_recipient(unsubscribe, method):
|
||||
if unsubscribe.reference_doctype == "Email Campaign":
|
||||
frappe.db.set_value("Email Campaign", unsubscribe.reference_name, "status", "Unsubscribed")
|
||||
if unsubscribe.reference_doctype != "Email Campaign":
|
||||
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
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
|
||||
import frappe
|
||||
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):
|
||||
@@ -66,58 +72,48 @@ def get_columns():
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT
|
||||
`tabOpportunity`.name,
|
||||
`tabOpportunity`.opportunity_from,
|
||||
`tabOpportunity`.party_name,
|
||||
`tabOpportunity`.customer_name,
|
||||
`tabOpportunity`.opportunity_type,
|
||||
GROUP_CONCAT(`tabOpportunity Lost Reason Detail`.lost_reason separator ', ') lost_reason,
|
||||
`tabOpportunity`.sales_stage,
|
||||
`tabOpportunity`.territory
|
||||
FROM
|
||||
`tabOpportunity`
|
||||
{get_join(filters)}
|
||||
WHERE
|
||||
`tabOpportunity`.status = 'Lost' and `tabOpportunity`.company = %(company)s
|
||||
AND DATE(`tabOpportunity`.modified) BETWEEN %(from_date)s AND %(to_date)s
|
||||
{get_conditions(filters)}
|
||||
GROUP BY
|
||||
`tabOpportunity`.name
|
||||
ORDER BY
|
||||
`tabOpportunity`.creation asc """,
|
||||
filters,
|
||||
as_dict=1,
|
||||
query = (
|
||||
frappe.qb.from_(Opportunity)
|
||||
.left_join(OpportunityLostReasonDetail)
|
||||
.on(
|
||||
(OpportunityLostReasonDetail.parenttype == "Opportunity")
|
||||
& (OpportunityLostReasonDetail.parent == Opportunity.name)
|
||||
)
|
||||
.select(
|
||||
Opportunity.name,
|
||||
Opportunity.opportunity_from,
|
||||
Opportunity.party_name,
|
||||
Opportunity.customer_name,
|
||||
Opportunity.opportunity_type,
|
||||
GROUP_CONCAT(OpportunityLostReasonDetail.lost_reason, alias="lost_reason").separator(", "),
|
||||
Opportunity.sales_stage,
|
||||
Opportunity.territory,
|
||||
)
|
||||
.where(
|
||||
(Opportunity.status == "Lost")
|
||||
& (Opportunity.company == filters.get("company"))
|
||||
& (Date(Opportunity.modified).between(filters.get("from_date"), filters.get("to_date")))
|
||||
)
|
||||
.groupby(Opportunity.name)
|
||||
.orderby(Opportunity.creation)
|
||||
)
|
||||
|
||||
query = get_conditions(filters, query)
|
||||
|
||||
def get_conditions(filters):
|
||||
conditions = []
|
||||
return query.run(as_dict=1)
|
||||
|
||||
|
||||
def get_conditions(filters, query):
|
||||
if filters.get("territory"):
|
||||
conditions.append(" and `tabOpportunity`.territory=%(territory)s")
|
||||
query = query.where(Opportunity.territory == filters.get("territory"))
|
||||
|
||||
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"):
|
||||
conditions.append(" and `tabOpportunity`.party_name=%(party_name)s")
|
||||
|
||||
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"""
|
||||
query = query.where(Opportunity.party_name == filters.get("party_name"))
|
||||
|
||||
if filters.get("lost_reason"):
|
||||
join = """JOIN `tabOpportunity Lost Reason Detail`
|
||||
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"))
|
||||
query = query.where(OpportunityLostReasonDetail.lost_reason == filters.get("lost_reason"))
|
||||
|
||||
return join
|
||||
return query
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestCodeList(FrappeTestCase):
|
||||
class TestCodeList(ERPNextTestSuite):
|
||||
pass
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestCommonCode(FrappeTestCase):
|
||||
class TestCommonCode(ERPNextTestSuite):
|
||||
pass
|
||||
|
||||
@@ -62,7 +62,6 @@ welcome_email = "erpnext.setup.utils.welcome_email"
|
||||
# setup wizard
|
||||
setup_wizard_requires = "assets/erpnext/js/setup_wizard.js"
|
||||
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"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user