mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-23 23:19:20 +00:00
Merge branch 'develop' into fix-test
This commit is contained in:
@@ -57,7 +57,7 @@ def get_company_currency(company):
|
||||
|
||||
def set_perpetual_inventory(enable=1, company=None):
|
||||
if not company:
|
||||
company = "_Test Company" if frappe.flags.in_test else get_default_company()
|
||||
company = "_Test Company" if frappe.in_test else get_default_company()
|
||||
|
||||
company = frappe.get_doc("Company", company)
|
||||
company.enable_perpetual_inventory = enable
|
||||
@@ -77,7 +77,7 @@ def encode_company_abbr(name, company=None, abbr=None):
|
||||
|
||||
def is_perpetual_inventory_enabled(company):
|
||||
if not company:
|
||||
company = "_Test Company" if frappe.flags.in_test else get_default_company()
|
||||
company = "_Test Company" if frappe.in_test else get_default_company()
|
||||
|
||||
if not hasattr(frappe.local, "enable_perpetual_inventory"):
|
||||
frappe.local.enable_perpetual_inventory = {}
|
||||
|
||||
@@ -526,7 +526,7 @@ def make_gl_entries(
|
||||
make_gl_entries(gl_entries, cancel=(doc.docstatus == 2), merge_entries=True)
|
||||
frappe.db.commit()
|
||||
except Exception as e:
|
||||
if frappe.flags.in_test:
|
||||
if frappe.in_test:
|
||||
doc.log_error(f"Error while processing deferred accounting for Invoice {doc.name}")
|
||||
raise e
|
||||
else:
|
||||
|
||||
@@ -602,7 +602,7 @@ def _ensure_idle_system():
|
||||
# 1. Correctness: It's next to impossible to ensure that renamed account is not being used *right now*.
|
||||
# 2. Performance: Renaming requires locking out many tables entirely and severely degrades performance.
|
||||
|
||||
if frappe.flags.in_test:
|
||||
if frappe.in_test:
|
||||
return
|
||||
|
||||
last_gl_update = None
|
||||
|
||||
@@ -139,6 +139,11 @@ frappe.treeview_settings["Account"] = {
|
||||
description: __(
|
||||
"Further accounts can be made under Groups, but entries can be made against non-Groups"
|
||||
),
|
||||
onchange: function () {
|
||||
if (!this.value) {
|
||||
this.layout.set_value("root_type", "");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Select",
|
||||
|
||||
@@ -83,7 +83,7 @@ class AccountingDimension(Document):
|
||||
frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company)))
|
||||
|
||||
def after_insert(self):
|
||||
if frappe.flags.in_test:
|
||||
if frappe.in_test:
|
||||
make_dimension_in_accounting_doctypes(doc=self)
|
||||
else:
|
||||
frappe.enqueue(
|
||||
@@ -91,7 +91,7 @@ class AccountingDimension(Document):
|
||||
)
|
||||
|
||||
def on_trash(self):
|
||||
if frappe.flags.in_test:
|
||||
if frappe.in_test:
|
||||
delete_accounting_dimension(doc=self)
|
||||
else:
|
||||
frappe.enqueue(delete_accounting_dimension, doc=self, queue="long", enqueue_after_commit=True)
|
||||
@@ -213,7 +213,7 @@ def delete_accounting_dimension(doc):
|
||||
|
||||
@frappe.whitelist()
|
||||
def disable_dimension(doc):
|
||||
if frappe.flags.in_test:
|
||||
if frappe.in_test:
|
||||
toggle_disabling(doc=doc)
|
||||
else:
|
||||
frappe.enqueue(toggle_disabling, doc=doc)
|
||||
|
||||
@@ -22,4 +22,21 @@ frappe.ui.form.on("Accounts Settings", {
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
add_taxes_from_taxes_and_charges_template(frm) {
|
||||
toggle_tax_settings(frm, "add_taxes_from_taxes_and_charges_template");
|
||||
},
|
||||
add_taxes_from_item_tax_template(frm) {
|
||||
toggle_tax_settings(frm, "add_taxes_from_item_tax_template");
|
||||
},
|
||||
});
|
||||
|
||||
function toggle_tax_settings(frm, field_name) {
|
||||
if (frm.doc[field_name]) {
|
||||
const other_field =
|
||||
field_name === "add_taxes_from_item_tax_template"
|
||||
? "add_taxes_from_taxes_and_charges_template"
|
||||
: "add_taxes_from_item_tax_template";
|
||||
frm.set_value(other_field, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"determine_address_tax_category_from",
|
||||
"column_break_19",
|
||||
"add_taxes_from_item_tax_template",
|
||||
"add_taxes_from_taxes_and_charges_template",
|
||||
"book_tax_discount_loss",
|
||||
"round_row_wise_tax",
|
||||
"print_settings",
|
||||
@@ -46,6 +47,7 @@
|
||||
"role_to_override_stop_action",
|
||||
"currency_exchange_section",
|
||||
"allow_stale",
|
||||
"allow_pegged_currencies_exchange_rates",
|
||||
"column_break_yuug",
|
||||
"stale_days",
|
||||
"section_break_jpd0",
|
||||
@@ -614,6 +616,21 @@
|
||||
{
|
||||
"fieldname": "column_break_feyo",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "System will do an implicit conversion using the pegged currency. <br>\nEx: Instead of AED -> INR, system will do AED -> USD -> INR using the pegged exchange rate of AED against USD.",
|
||||
"documentation_url": "/app/pegged-currencies/Pegged Currencies",
|
||||
"fieldname": "allow_pegged_currencies_exchange_rates",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Implicit Pegged Currency Conversion"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If no taxes are set, and Taxes and Charges Template is selected, the system will automatically apply the taxes from the chosen template.",
|
||||
"fieldname": "add_taxes_from_taxes_and_charges_template",
|
||||
"fieldtype": "Check",
|
||||
"label": "Automatically Add Taxes from Taxes and Charges Template"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -622,7 +639,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-06 11:03:28.095723",
|
||||
"modified": "2025-06-23 15:55:33.346398",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -25,7 +25,9 @@ class AccountsSettings(Document):
|
||||
|
||||
acc_frozen_upto: DF.Date | None
|
||||
add_taxes_from_item_tax_template: DF.Check
|
||||
add_taxes_from_taxes_and_charges_template: DF.Check
|
||||
allow_multi_currency_invoices_against_single_party_account: DF.Check
|
||||
allow_pegged_currencies_exchange_rates: DF.Check
|
||||
allow_stale: DF.Check
|
||||
auto_reconcile_payments: DF.Check
|
||||
auto_reconciliation_job_trigger: DF.Int
|
||||
@@ -75,6 +77,7 @@ class AccountsSettings(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_auto_tax_settings()
|
||||
old_doc = self.get_doc_before_save()
|
||||
clear_cache = False
|
||||
|
||||
@@ -141,3 +144,13 @@ class AccountsSettings(Document):
|
||||
if self.has_value_changed("reconciliation_queue_size"):
|
||||
if cint(self.reconciliation_queue_size) < 5 or cint(self.reconciliation_queue_size) > 100:
|
||||
frappe.throw(_("Queue Size should be between 5 and 100"))
|
||||
|
||||
def validate_auto_tax_settings(self):
|
||||
if self.add_taxes_from_item_tax_template and self.add_taxes_from_taxes_and_charges_template:
|
||||
frappe.throw(
|
||||
_("You cannot enable both the settings '{0}' and '{1}'.").format(
|
||||
frappe.bold(self.meta.get_label("add_taxes_from_item_tax_template")),
|
||||
frappe.bold(self.meta.get_label("add_taxes_from_taxes_and_charges_template")),
|
||||
),
|
||||
title=_("Auto Tax Settings Error"),
|
||||
)
|
||||
|
||||
@@ -70,7 +70,7 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
|
||||
frm.get_field("import_file").df.options = {
|
||||
restrictions: {
|
||||
allowed_file_types: [".csv", ".xls", ".xlsx"],
|
||||
allowed_file_types: [".csv", ".xls", ".xlsx", ".TXT", ".txt"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -81,6 +81,7 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
|
||||
refresh(frm) {
|
||||
frm.page.hide_icon_group();
|
||||
frm.trigger("toggle_mt940_note");
|
||||
frm.trigger("update_indicators");
|
||||
frm.trigger("import_file");
|
||||
frm.trigger("show_import_log");
|
||||
@@ -192,6 +193,24 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
});
|
||||
},
|
||||
|
||||
import_mt940_fromat(frm) {
|
||||
frm.trigger("toggle_mt940_note");
|
||||
frm.save();
|
||||
},
|
||||
|
||||
toggle_mt940_note(frm) {
|
||||
if (!frm.doc.import_mt940_fromat) {
|
||||
frm.set_df_property("custom_delimiters", "hidden", 0);
|
||||
frm.set_df_property("google_sheets_url", "hidden", 0);
|
||||
frm.set_df_property("html_5", "hidden", 0);
|
||||
} else {
|
||||
frm.set_df_property("custom_delimiters", "hidden", 1);
|
||||
frm.set_df_property("google_sheets_url", "hidden", 1);
|
||||
frm.set_df_property("html_5", "hidden", 1);
|
||||
}
|
||||
frm.set_value("import_mt940_fromat", frm.doc.import_mt940_fromat);
|
||||
},
|
||||
|
||||
show_report_error_button(frm) {
|
||||
if (frm.doc.status === "Error") {
|
||||
frappe.db
|
||||
@@ -290,23 +309,45 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
.html(__("Loading import file..."))
|
||||
.appendTo(frm.get_field("import_preview").$wrapper);
|
||||
|
||||
frm.call({
|
||||
method: "get_preview_from_template",
|
||||
args: {
|
||||
data_import: frm.doc.name,
|
||||
import_file: frm.doc.import_file,
|
||||
google_sheets_url: frm.doc.google_sheets_url,
|
||||
frappe.run_serially([
|
||||
// Convert MT940 to CSV if .txt file
|
||||
() => {
|
||||
if (frm.doc.import_file && frm.doc.import_file.toLowerCase().endsWith(".txt")) {
|
||||
return frm
|
||||
.call({
|
||||
method: "convert_mt940_to_csv",
|
||||
args: {
|
||||
data_import: frm.doc.name,
|
||||
mt940_file_path: frm.doc.import_file,
|
||||
},
|
||||
})
|
||||
.then((r) => {
|
||||
const file_url = r.message;
|
||||
frm.set_value("import_file", file_url);
|
||||
frm.save();
|
||||
});
|
||||
}
|
||||
},
|
||||
error_handlers: {
|
||||
TimestampMismatchError() {
|
||||
// ignore this error
|
||||
},
|
||||
() => {
|
||||
frm.call({
|
||||
method: "get_preview_from_template",
|
||||
args: {
|
||||
data_import: frm.doc.name,
|
||||
import_file: frm.doc.import_file,
|
||||
google_sheets_url: frm.doc.google_sheets_url,
|
||||
},
|
||||
error_handlers: {
|
||||
TimestampMismatchError() {
|
||||
// ignore this error
|
||||
},
|
||||
},
|
||||
}).then((r) => {
|
||||
let preview_data = r.message;
|
||||
frm.events.show_import_preview(frm, preview_data);
|
||||
frm.events.show_import_warnings(frm, preview_data);
|
||||
});
|
||||
},
|
||||
}).then((r) => {
|
||||
let preview_data = r.message;
|
||||
frm.events.show_import_preview(frm, preview_data);
|
||||
frm.events.show_import_warnings(frm, preview_data);
|
||||
});
|
||||
]);
|
||||
},
|
||||
// method: 'frappe.core.doctype.data_import.data_import.get_preview_from_template',
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"bank_account",
|
||||
"bank",
|
||||
"column_break_4",
|
||||
"import_mt940_fromat",
|
||||
"custom_delimiters",
|
||||
"delimiter_options",
|
||||
"google_sheets_url",
|
||||
@@ -20,6 +21,7 @@
|
||||
"download_template",
|
||||
"status",
|
||||
"template_options",
|
||||
"use_csv_sniffer",
|
||||
"import_warnings_section",
|
||||
"template_warnings",
|
||||
"import_warnings",
|
||||
@@ -207,14 +209,28 @@
|
||||
"fieldname": "delimiter_options",
|
||||
"fieldtype": "Data",
|
||||
"label": "Delimiter options"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_csv_sniffer",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Use CSV Sniffer"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "import_mt940_fromat",
|
||||
"fieldtype": "Check",
|
||||
"label": "Import MT940 Fromat"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-25 17:32:07.658250",
|
||||
"modified": "2025-06-11 02:23:22.159961",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Statement Import",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -230,8 +246,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,19 @@
|
||||
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
from datetime import date, datetime
|
||||
|
||||
import frappe
|
||||
import mt940
|
||||
import openpyxl
|
||||
from frappe import _
|
||||
from frappe.core.doctype.data_import.data_import import DataImport
|
||||
from frappe.core.doctype.data_import.importer import Importer, ImportFile
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.file_manager import get_file, save_file
|
||||
from frappe.utils.xlsxutils import ILLEGAL_CHARACTERS_RE, handle_html
|
||||
from openpyxl.styles import Font
|
||||
from openpyxl.utils import get_column_letter
|
||||
@@ -35,6 +39,7 @@ class BankStatementImport(DataImport):
|
||||
delimiter_options: DF.Data | None
|
||||
google_sheets_url: DF.Data | None
|
||||
import_file: DF.Attach | None
|
||||
import_mt940_fromat: DF.Check
|
||||
import_type: DF.Literal["", "Insert New Records", "Update Existing Records"]
|
||||
mute_emails: DF.Check
|
||||
reference_doctype: DF.Link
|
||||
@@ -43,6 +48,7 @@ class BankStatementImport(DataImport):
|
||||
submit_after_import: DF.Check
|
||||
template_options: DF.Code | None
|
||||
template_warnings: DF.Code | None
|
||||
use_csv_sniffer: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -65,8 +71,9 @@ class BankStatementImport(DataImport):
|
||||
|
||||
self.template_warnings = ""
|
||||
|
||||
self.validate_import_file()
|
||||
self.validate_google_sheets_url()
|
||||
if self.import_file and not self.import_file.lower().endswith(".txt"):
|
||||
self.validate_import_file()
|
||||
self.validate_google_sheets_url()
|
||||
|
||||
def start_import(self):
|
||||
preview = frappe.get_doc("Bank Statement Import", self.name).get_preview_from_template(
|
||||
@@ -79,7 +86,7 @@ class BankStatementImport(DataImport):
|
||||
from frappe.utils.background_jobs import is_job_enqueued
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
run_now = frappe.flags.in_test or frappe.conf.developer_mode
|
||||
run_now = frappe.in_test or frappe.conf.developer_mode
|
||||
if is_scheduler_inactive() and not run_now:
|
||||
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
|
||||
|
||||
@@ -104,6 +111,68 @@ class BankStatementImport(DataImport):
|
||||
return None
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def convert_mt940_to_csv(data_import, mt940_file_path):
|
||||
doc = frappe.get_doc("Bank Statement Import", data_import)
|
||||
|
||||
file_doc, content = get_file(mt940_file_path)
|
||||
|
||||
if not is_mt940_format(content):
|
||||
frappe.throw(_("The uploaded file does not appear to be in valid MT940 format."))
|
||||
|
||||
if is_mt940_format(content) and not doc.import_mt940_fromat:
|
||||
frappe.throw(_("MT940 file detected. Please enable 'Import MT940 Format' to proceed."))
|
||||
|
||||
try:
|
||||
transactions = mt940.parse(content)
|
||||
except Exception as e:
|
||||
frappe.throw(_("Failed to parse MT940 format. Error: {0}").format(str(e)))
|
||||
|
||||
if not transactions:
|
||||
frappe.throw(_("Parsed file is not in valid MT940 format or contains no transactions."))
|
||||
|
||||
# Use in-memory file buffer instead of writing to temp file
|
||||
csv_buffer = io.StringIO()
|
||||
writer = csv.writer(csv_buffer)
|
||||
|
||||
headers = ["Date", "Deposit", "Withdrawal", "Description", "Reference Number", "Bank Account", "Currency"]
|
||||
writer.writerow(headers)
|
||||
|
||||
for txn in transactions:
|
||||
txn_date = getattr(txn, "date", None)
|
||||
raw_date = txn.data.get("date", "")
|
||||
|
||||
if txn_date:
|
||||
date_str = txn_date.strftime("%Y-%m-%d")
|
||||
elif isinstance(raw_date, date | datetime):
|
||||
date_str = raw_date.strftime("%Y-%m-%d")
|
||||
else:
|
||||
date_str = str(raw_date)
|
||||
|
||||
raw_amount = str(txn.data.get("amount", ""))
|
||||
parts = raw_amount.strip().split()
|
||||
amount_value = float(parts[0]) if parts else 0.0
|
||||
|
||||
deposit = amount_value if amount_value > 0 else ""
|
||||
withdrawal = abs(amount_value) if amount_value < 0 else ""
|
||||
description = txn.data.get("extra_details") or ""
|
||||
reference = txn.data.get("transaction_reference") or ""
|
||||
currency = txn.data.get("currency", "")
|
||||
|
||||
writer.writerow([date_str, deposit, withdrawal, description, reference, doc.bank_account, currency])
|
||||
|
||||
# Prepare in-memory CSV for upload
|
||||
csv_content = csv_buffer.getvalue().encode("utf-8")
|
||||
csv_buffer.close()
|
||||
|
||||
filename = f"{frappe.utils.now_datetime().strftime('%Y%m%d%H%M%S')}_converted_mt940.csv"
|
||||
|
||||
# Save to File Manager
|
||||
saved_file = save_file(filename, csv_content, doc.doctype, doc.name, is_private=True, df="import_file")
|
||||
|
||||
return saved_file.file_url
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_preview_from_template(data_import, import_file=None, google_sheets_url=None):
|
||||
return frappe.get_doc("Bank Statement Import", data_import).get_preview_from_template(
|
||||
@@ -128,6 +197,12 @@ def download_import_log(data_import_name):
|
||||
return frappe.get_doc("Bank Statement Import", data_import_name).download_import_log()
|
||||
|
||||
|
||||
def is_mt940_format(content: str) -> bool:
|
||||
"""Check if the content has key MT940 tags"""
|
||||
required_tags = [":20:", ":25:", ":28C:", ":61:"]
|
||||
return all(tag in content for tag in required_tags)
|
||||
|
||||
|
||||
def parse_data_from_template(raw_data):
|
||||
data = []
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import frappe
|
||||
from frappe.utils import flt
|
||||
from rapidfuzz import fuzz, process
|
||||
from rapidfuzz.utils import default_process
|
||||
|
||||
|
||||
class AutoMatchParty:
|
||||
@@ -132,6 +133,7 @@ class AutoMatchbyPartyNameDescription:
|
||||
query=self.get(field),
|
||||
choices={row.get("name"): row.get("party_name") for row in names},
|
||||
scorer=fuzz.token_set_ratio,
|
||||
processor=default_process,
|
||||
)
|
||||
party_name, skip = self.process_fuzzy_result(result)
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
"default": "ACC-BTN-.YYYY.-",
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Series",
|
||||
"no_copy": 1,
|
||||
"options": "ACC-BTN-.YYYY.-",
|
||||
@@ -236,9 +235,10 @@
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-18 18:32:47.203694",
|
||||
"modified": "2025-06-18 17:24:57.044666",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
@@ -287,9 +287,10 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "date",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "bank_account",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"naming_series",
|
||||
"budget_against",
|
||||
"company",
|
||||
"cost_center",
|
||||
"naming_series",
|
||||
"project",
|
||||
"fiscal_year",
|
||||
"column_break_3",
|
||||
@@ -199,12 +199,12 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"fieldtype": "Select",
|
||||
"label": "Series",
|
||||
"no_copy": 1,
|
||||
"options": "BUDGET-.YYYY.-",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
@@ -238,7 +238,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-05-22 13:46:28.510566",
|
||||
"modified": "2025-06-16 15:57:13.114981",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Budget",
|
||||
|
||||
@@ -51,7 +51,7 @@ class Budget(Document):
|
||||
cost_center: DF.Link | None
|
||||
fiscal_year: DF.Link
|
||||
monthly_distribution: DF.Link | None
|
||||
naming_series: DF.Data | None
|
||||
naming_series: DF.Literal["BUDGET-.YYYY.-"]
|
||||
project: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -139,9 +139,6 @@ class Budget(Document):
|
||||
):
|
||||
self.applicable_on_booking_actual_expenses = 1
|
||||
|
||||
def before_naming(self):
|
||||
self.naming_series = f"{{{frappe.scrub(self.budget_against)}}}./.{self.fiscal_year}/.###"
|
||||
|
||||
|
||||
def validate_expense_against_budget(args, expense_amount=0):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -36,7 +36,7 @@ class CurrencyExchangeSettings(Document):
|
||||
|
||||
def validate(self):
|
||||
self.set_parameters_and_result()
|
||||
if frappe.flags.in_test or frappe.flags.in_install or frappe.flags.in_setup_wizard:
|
||||
if frappe.in_test or frappe.flags.in_install or frappe.flags.in_setup_wizard:
|
||||
return
|
||||
response, value = self.validate_parameters()
|
||||
self.validate_result(response, value)
|
||||
|
||||
@@ -123,7 +123,7 @@ def check_duplicate_fiscal_year(doc):
|
||||
)
|
||||
for fiscal_year, ysd, yed in year_start_end_dates:
|
||||
if (getdate(doc.year_start_date) == ysd and getdate(doc.year_end_date) == yed) and (
|
||||
not frappe.flags.in_test
|
||||
not frappe.in_test
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
|
||||
@@ -39,7 +39,16 @@ class ItemTaxTemplate(Document):
|
||||
check_list = []
|
||||
for d in self.get("taxes"):
|
||||
if d.tax_type:
|
||||
account_type = frappe.get_cached_value("Account", d.tax_type, "account_type")
|
||||
account_type, account_company = frappe.get_cached_value(
|
||||
"Account", d.tax_type, ["account_type", "company"]
|
||||
)
|
||||
|
||||
if account_company != self.company:
|
||||
frappe.throw(
|
||||
_("Item Tax Row {0}: Account must belong to Company - {1}").format(
|
||||
d.idx, frappe.bold(self.company)
|
||||
)
|
||||
)
|
||||
|
||||
if account_type not in [
|
||||
"Tax",
|
||||
|
||||
@@ -20,6 +20,39 @@ frappe.ui.form.on("Journal Entry", {
|
||||
"Unreconcile Payment Entries",
|
||||
"Bank Transaction",
|
||||
];
|
||||
|
||||
frm.trigger("set_queries");
|
||||
},
|
||||
|
||||
set_queries(frm) {
|
||||
frm.set_query("periodic_entry_difference_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("stock_asset_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
account_type: "Stock",
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
get_balance_for_periodic_accounting(frm) {
|
||||
frm.call({
|
||||
method: "get_balance_for_periodic_accounting",
|
||||
doc: frm.doc,
|
||||
callback: function (r) {
|
||||
refresh_field("accounts");
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
|
||||
@@ -13,15 +13,21 @@
|
||||
"title",
|
||||
"voucher_type",
|
||||
"naming_series",
|
||||
"finance_book",
|
||||
"process_deferred_accounting",
|
||||
"reversal_of",
|
||||
"tax_withholding_category",
|
||||
"column_break1",
|
||||
"from_template",
|
||||
"company",
|
||||
"posting_date",
|
||||
"finance_book",
|
||||
"apply_tds",
|
||||
"tax_withholding_category",
|
||||
"section_break_tcvw",
|
||||
"for_all_stock_asset_accounts",
|
||||
"column_break_wpau",
|
||||
"stock_asset_account",
|
||||
"periodic_entry_difference_account",
|
||||
"get_balance_for_periodic_accounting",
|
||||
"2_add_edit_gl_entries",
|
||||
"accounts",
|
||||
"section_break99",
|
||||
@@ -89,7 +95,7 @@
|
||||
"label": "Entry Type",
|
||||
"oldfieldname": "voucher_type",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nAsset Disposal\nExchange Rate Revaluation\nExchange Gain Or Loss\nDeferred Revenue\nDeferred Expense",
|
||||
"options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nAsset Disposal\nPeriodic Accounting Entry\nExchange Rate Revaluation\nExchange Gain Or Loss\nDeferred Revenue\nDeferred Expense",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
@@ -543,6 +549,42 @@
|
||||
"label": "Is System Generated",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.voucher_type === \"Periodic Accounting Entry\"",
|
||||
"fieldname": "periodic_entry_difference_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Periodic Entry Difference Account",
|
||||
"mandatory_depends_on": "eval:doc.voucher_type === \"Periodic Accounting Entry\"",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.voucher_type === \"Periodic Accounting Entry\"",
|
||||
"fieldname": "section_break_tcvw",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Periodic Accounting"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "for_all_stock_asset_accounts",
|
||||
"fieldtype": "Check",
|
||||
"label": "For All Stock Asset Accounts"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.for_all_stock_asset_accounts === 0",
|
||||
"fieldname": "stock_asset_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock Asset Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_wpau",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "get_balance_for_periodic_accounting",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Balance"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -557,7 +599,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2024-12-26 15:32:20.730666",
|
||||
"modified": "2025-06-17 15:18:13.322681",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
@@ -602,10 +644,11 @@
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "voucher_type,posting_date, due_date, cheque_no",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ class JournalEntry(AccountsController):
|
||||
difference: DF.Currency
|
||||
due_date: DF.Date | None
|
||||
finance_book: DF.Link | None
|
||||
for_all_stock_asset_accounts: DF.Check
|
||||
from_template: DF.Link | None
|
||||
inter_company_journal_entry_reference: DF.Link | None
|
||||
is_opening: DF.Literal["No", "Yes"]
|
||||
@@ -73,11 +74,13 @@ class JournalEntry(AccountsController):
|
||||
paid_loan: DF.Data | None
|
||||
pay_to_recd_from: DF.Data | None
|
||||
payment_order: DF.Link | None
|
||||
periodic_entry_difference_account: DF.Link | None
|
||||
posting_date: DF.Date
|
||||
process_deferred_accounting: DF.Link | None
|
||||
remark: DF.SmallText | None
|
||||
reversal_of: DF.Link | None
|
||||
select_print_heading: DF.Link | None
|
||||
stock_asset_account: DF.Link | None
|
||||
stock_entry: DF.Link | None
|
||||
tax_withholding_category: DF.Link | None
|
||||
title: DF.Data | None
|
||||
@@ -101,6 +104,7 @@ class JournalEntry(AccountsController):
|
||||
"Opening Entry",
|
||||
"Depreciation Entry",
|
||||
"Asset Disposal",
|
||||
"Periodic Accounting Entry",
|
||||
"Exchange Rate Revaluation",
|
||||
"Exchange Gain Or Loss",
|
||||
"Deferred Revenue",
|
||||
@@ -148,8 +152,7 @@ class JournalEntry(AccountsController):
|
||||
if self.docstatus == 0:
|
||||
self.apply_tax_withholding()
|
||||
|
||||
if not self.title:
|
||||
self.title = self.get_title()
|
||||
self.title = self.get_title()
|
||||
|
||||
def validate_advance_accounts(self):
|
||||
journal_accounts = set([x.account for x in self.accounts])
|
||||
@@ -198,6 +201,76 @@ class JournalEntry(AccountsController):
|
||||
self.update_inter_company_jv()
|
||||
self.update_invoice_discounting()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_balance_for_periodic_accounting(self):
|
||||
self.validate_company_for_periodic_accounting()
|
||||
|
||||
stock_accounts = self.get_stock_accounts_for_periodic_accounting()
|
||||
self.set("accounts", [])
|
||||
for account in stock_accounts:
|
||||
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(
|
||||
account, self.posting_date, self.company
|
||||
)
|
||||
|
||||
difference_value = flt(stock_bal - account_bal, self.precision("difference"))
|
||||
|
||||
if difference_value == 0:
|
||||
frappe.msgprint(
|
||||
_("No difference found for stock account {0}").format(frappe.bold(account)),
|
||||
alert=True,
|
||||
)
|
||||
continue
|
||||
|
||||
self.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": account,
|
||||
"debit_in_account_currency": difference_value if difference_value > 0 else 0,
|
||||
"credit_in_account_currency": abs(difference_value) if difference_value < 0 else 0,
|
||||
},
|
||||
)
|
||||
|
||||
self.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": self.periodic_entry_difference_account,
|
||||
"credit_in_account_currency": difference_value if difference_value > 0 else 0,
|
||||
"debit_in_account_currency": abs(difference_value) if difference_value < 0 else 0,
|
||||
},
|
||||
)
|
||||
|
||||
def validate_company_for_periodic_accounting(self):
|
||||
if erpnext.is_perpetual_inventory_enabled(self.company):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Periodic Accounting Entry is not allowed for company {0} with perpetual inventory enabled"
|
||||
).format(self.company)
|
||||
)
|
||||
|
||||
if not self.periodic_entry_difference_account:
|
||||
frappe.throw(_("Please select Periodic Accounting Entry Difference Account"))
|
||||
|
||||
def get_stock_accounts_for_periodic_accounting(self):
|
||||
if self.voucher_type != "Periodic Accounting Entry":
|
||||
return []
|
||||
|
||||
if self.for_all_stock_asset_accounts:
|
||||
return frappe.get_all(
|
||||
"Account",
|
||||
filters={
|
||||
"company": self.company,
|
||||
"account_type": "Stock",
|
||||
"root_type": "Asset",
|
||||
"is_group": 0,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
if not self.stock_asset_account:
|
||||
frappe.throw(_("Please select Stock Asset Account"))
|
||||
|
||||
return [self.stock_asset_account]
|
||||
|
||||
def on_update_after_submit(self):
|
||||
# Flag will be set on Reconciliation
|
||||
# Reconciliation tool will anyways repost ledger entries. So, no need to check and do implicit repost.
|
||||
@@ -280,6 +353,10 @@ 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
|
||||
return
|
||||
|
||||
stock_accounts = get_stock_accounts(self.company, accounts=self.accounts)
|
||||
for account in stock_accounts:
|
||||
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(
|
||||
|
||||
@@ -35,7 +35,7 @@ class LedgerMerge(Document):
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
if is_scheduler_inactive() and not frappe.flags.in_test:
|
||||
if is_scheduler_inactive() and not frappe.in_test:
|
||||
frappe.throw(_("Scheduler is inactive. Cannot merge accounts."), title=_("Scheduler Inactive"))
|
||||
|
||||
job_id = f"ledger_merge::{self.name}"
|
||||
@@ -47,7 +47,7 @@ class LedgerMerge(Document):
|
||||
event="ledger_merge",
|
||||
job_id=job_id,
|
||||
docname=self.name,
|
||||
now=frappe.conf.developer_mode or frappe.flags.in_test,
|
||||
now=frappe.conf.developer_mode or frappe.in_test,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ class OpeningInvoiceCreationTool(Document):
|
||||
else:
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
if is_scheduler_inactive() and not frappe.flags.in_test:
|
||||
if is_scheduler_inactive() and not frappe.in_test:
|
||||
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
|
||||
|
||||
job_id = f"opening_invoice::{self.name}"
|
||||
@@ -242,7 +242,7 @@ class OpeningInvoiceCreationTool(Document):
|
||||
event="opening_invoice_creation",
|
||||
job_id=job_id,
|
||||
invoices=invoices,
|
||||
now=frappe.conf.developer_mode or frappe.flags.in_test,
|
||||
now=frappe.conf.developer_mode or frappe.in_test,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1979,7 +1979,9 @@ class TestPaymentReconciliation(IntegrationTestCase):
|
||||
|
||||
def test_reconciliation_on_closed_period_payment(self):
|
||||
# create backdated fiscal year
|
||||
first_fy_start_date = frappe.db.get_value("Fiscal Year", {"disabled": 0}, "min(year_start_date)")
|
||||
first_fy_start_date = frappe.db.get_value(
|
||||
"Fiscal Year", {"disabled": 0}, [{"MIN": "year_start_date"}]
|
||||
)
|
||||
prev_fy_start_date = add_years(first_fy_start_date, -1)
|
||||
prev_fy_end_date = add_days(first_fy_start_date, -1)
|
||||
create_fiscal_year(
|
||||
|
||||
@@ -788,29 +788,28 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
pr = make_payment_request(dt="Sales Invoice", dn=si.name, mute_email=1)
|
||||
self.assertEqual(pr.grand_total, si.outstanding_amount)
|
||||
|
||||
def test_partial_paid_invoice_with_submitted_payment_entry(self):
|
||||
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
def test_partial_paid_invoice_with_submitted_payment_entry(self):
|
||||
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0001"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
pe.cancel()
|
||||
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0001"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
pe.cancel()
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0002"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0002"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
|
||||
pi.load_from_db()
|
||||
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
pi.load_from_db()
|
||||
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Pegged Currencies", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-05-30 11:47:03.670913",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"pegged_currencies_item_section",
|
||||
"pegged_currency_item"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "pegged_currencies_item_section",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "pegged_currency_item",
|
||||
"fieldtype": "Table",
|
||||
"options": "Pegged Currency Details"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-02 11:46:31.936714",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pegged Currencies",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PeggedCurrencies(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.pegged_currencies.pegged_currencies import PeggedCurrencies
|
||||
|
||||
pegged_currency_item: DF.Table[PeggedCurrencies]
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,29 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
|
||||
class UnitTestPeggedCurrencies(UnitTestCase):
|
||||
"""
|
||||
Unit tests for PeggedCurrencies.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IntegrationTestPeggedCurrencies(IntegrationTestCase):
|
||||
"""
|
||||
Integration tests for PeggedCurrencies.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-05-30 11:59:28.219277",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"source_currency",
|
||||
"pegged_against",
|
||||
"pegged_exchange_rate"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "source_currency",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "pegged_exchange_rate",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Exchange Rate"
|
||||
},
|
||||
{
|
||||
"fieldname": "pegged_against",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Pegged Against",
|
||||
"options": "Currency"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-17 14:11:16.521193",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pegged Currency Details",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PeggedCurrencyDetails(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
pegged_against: DF.Link | None
|
||||
pegged_exchange_rate: DF.Data | None
|
||||
source_currency: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -259,6 +259,7 @@ class POSInvoiceMergeLog(Document):
|
||||
if not found:
|
||||
tax.charge_type = "Actual"
|
||||
tax.idx = idx
|
||||
tax.row_id = None
|
||||
idx += 1
|
||||
tax.included_in_print_rate = 0
|
||||
tax.tax_amount = tax.tax_amount_after_discount_amount
|
||||
@@ -492,7 +493,7 @@ def split_invoices_by_accounting_dimension(pos_invoices):
|
||||
|
||||
def consolidate_pos_invoices(pos_invoices=None, closing_entry=None):
|
||||
invoices = pos_invoices or (closing_entry and closing_entry.get("pos_invoices"))
|
||||
if frappe.flags.in_test and not invoices:
|
||||
if frappe.in_test and not invoices:
|
||||
invoices = get_all_unconsolidated_invoices()
|
||||
|
||||
invoice_by_customer = get_invoice_customer_map(invoices)
|
||||
@@ -654,7 +655,7 @@ def enqueue_job(job, **kwargs):
|
||||
timeout=10000,
|
||||
event="processing_merge_logs",
|
||||
job_id=job_id,
|
||||
now=frappe.conf.developer_mode or frappe.flags.in_test,
|
||||
now=frappe.conf.developer_mode or frappe.in_test,
|
||||
)
|
||||
|
||||
if job == create_merge_logs:
|
||||
@@ -666,7 +667,7 @@ def enqueue_job(job, **kwargs):
|
||||
|
||||
|
||||
def check_scheduler_status():
|
||||
if is_scheduler_inactive() and not frappe.flags.in_test:
|
||||
if is_scheduler_inactive() and not frappe.in_test:
|
||||
frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive"))
|
||||
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ from erpnext.controllers.accounts_controller import validate_account_head
|
||||
from erpnext.controllers.buying_controller import BuyingController
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
get_item_account_wise_additional_cost,
|
||||
update_billed_amount_based_on_po,
|
||||
)
|
||||
|
||||
@@ -260,6 +259,7 @@ class PurchaseInvoice(BuyingController):
|
||||
self.is_opening = "No"
|
||||
|
||||
self.validate_posting_time()
|
||||
self.validate_posting_date_with_po()
|
||||
|
||||
super().validate()
|
||||
|
||||
@@ -940,7 +940,7 @@ class PurchaseInvoice(BuyingController):
|
||||
if self.update_stock and self.auto_accounting_for_stock:
|
||||
warehouse_account = get_warehouse_account_map(self.company)
|
||||
|
||||
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
|
||||
landed_cost_entries = self.get_item_account_wise_lcv_entries()
|
||||
|
||||
voucher_wise_stock_value = {}
|
||||
if self.update_stock:
|
||||
|
||||
@@ -1660,7 +1660,7 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
pi.set_posting_time = 1
|
||||
pi.posting_date = add_days(pr.posting_date, -1)
|
||||
pi.posting_date = add_days(pr.posting_date, 1)
|
||||
pi.items[0].expense_account = "Cost of Goods Sold - _TC"
|
||||
pi.save()
|
||||
pi.submit()
|
||||
@@ -1669,30 +1669,38 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
|
||||
# Check GLE for Purchase Invoice
|
||||
expected_gle = [
|
||||
["Cost of Goods Sold - _TC", 250, 0, add_days(pr.posting_date, -1)],
|
||||
["Creditors - _TC", 0, 250, add_days(pr.posting_date, -1)],
|
||||
["Cost of Goods Sold - _TC", 250, 0, add_days(pr.posting_date, 1)],
|
||||
["Creditors - _TC", 0, 250, add_days(pr.posting_date, 1)],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
|
||||
|
||||
expected_gle_for_purchase_receipt = [
|
||||
["Provision Account - _TC", 250, 0, pr.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 0, 250, pr.posting_date],
|
||||
["Provision Account - _TC", 0, 250, pi.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 250, 0, pr.posting_date],
|
||||
["Provision Account - _TC", 0, 250, pr.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 0, 250, pi.posting_date],
|
||||
["Provision Account - _TC", 250, 0, pi.posting_date],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
|
||||
check_gl_entries(
|
||||
self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date, voucher_type="Purchase Receipt"
|
||||
)
|
||||
|
||||
# Cancel purchase invoice to check reverse provisional entry cancellation
|
||||
pi.cancel()
|
||||
|
||||
expected_gle_for_purchase_receipt_post_pi_cancel = [
|
||||
["Provision Account - _TC", 0, 250, pi.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date],
|
||||
["Provision Account - _TC", 0, 250, pi.posting_date],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date)
|
||||
check_gl_entries(
|
||||
self,
|
||||
pr.name,
|
||||
expected_gle_for_purchase_receipt_post_pi_cancel,
|
||||
pi.posting_date,
|
||||
voucher_type="Purchase Receipt",
|
||||
)
|
||||
|
||||
toggle_provisional_accounting_setting()
|
||||
|
||||
@@ -1713,7 +1721,7 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
# Overbill PR: rate = 2000, qty = 10
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
pi.set_posting_time = 1
|
||||
pi.posting_date = add_days(pr.posting_date, -1)
|
||||
pi.posting_date = add_days(pr.posting_date, 1)
|
||||
pi.items[0].qty = 10
|
||||
pi.items[0].rate = 2000
|
||||
pi.items[0].expense_account = "Cost of Goods Sold - _TC"
|
||||
@@ -1721,30 +1729,38 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
pi.submit()
|
||||
|
||||
expected_gle = [
|
||||
["Cost of Goods Sold - _TC", 20000, 0, add_days(pr.posting_date, -1)],
|
||||
["Creditors - _TC", 0, 20000, add_days(pr.posting_date, -1)],
|
||||
["Cost of Goods Sold - _TC", 20000, 0, add_days(pr.posting_date, 1)],
|
||||
["Creditors - _TC", 0, 20000, add_days(pr.posting_date, 1)],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
|
||||
|
||||
expected_gle_for_purchase_receipt = [
|
||||
["Provision Account - _TC", 5000, 0, pr.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 0, 5000, pr.posting_date],
|
||||
["Provision Account - _TC", 0, 5000, pi.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 5000, 0, pi.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 5000, 0, pr.posting_date],
|
||||
["Provision Account - _TC", 0, 5000, pr.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 0, 5000, pi.posting_date],
|
||||
["Provision Account - _TC", 5000, 0, pi.posting_date],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
|
||||
check_gl_entries(
|
||||
self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date, voucher_type="Purchase Receipt"
|
||||
)
|
||||
|
||||
# Cancel purchase invoice to check reverse provisional entry cancellation
|
||||
pi.cancel()
|
||||
|
||||
expected_gle_for_purchase_receipt_post_pi_cancel = [
|
||||
["Provision Account - _TC", 0, 5000, pi.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 5000, 0, pi.posting_date],
|
||||
["Provision Account - _TC", 0, 5000, pi.posting_date],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date)
|
||||
check_gl_entries(
|
||||
self,
|
||||
pr.name,
|
||||
expected_gle_for_purchase_receipt_post_pi_cancel,
|
||||
pi.posting_date,
|
||||
voucher_type="Purchase Receipt",
|
||||
)
|
||||
|
||||
toggle_provisional_accounting_setting()
|
||||
|
||||
@@ -1777,13 +1793,76 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
|
||||
|
||||
expected_gle_for_purchase_receipt = [
|
||||
["Provision Account - _TC", 5000, 0, pr.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 0, 5000, pr.posting_date],
|
||||
["Provision Account - _TC", 0, 1000, pi.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 1000, 0, pi.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 5000, 0, pr.posting_date],
|
||||
["Provision Account - _TC", 0, 5000, pr.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 0, 1000, pi.posting_date],
|
||||
["Provision Account - _TC", 1000, 0, pi.posting_date],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
|
||||
check_gl_entries(
|
||||
self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date, voucher_type="Purchase Receipt"
|
||||
)
|
||||
|
||||
toggle_provisional_accounting_setting()
|
||||
|
||||
def test_provisional_accounting_entry_multi_currency(self):
|
||||
setup_provisional_accounting()
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
item_code="_Test Non Stock Item",
|
||||
posting_date=add_days(nowdate(), -2),
|
||||
qty=1000,
|
||||
rate=111.11,
|
||||
currency="USD",
|
||||
do_not_save=1,
|
||||
supplier="_Test Supplier USD",
|
||||
)
|
||||
pr.conversion_rate = 0.014783000
|
||||
pr.save()
|
||||
pr.submit()
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
pi.set_posting_time = 1
|
||||
pi.posting_date = add_days(pr.posting_date, 1)
|
||||
pi.items[0].expense_account = "Cost of Goods Sold - _TC"
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
self.assertEqual(pr.items[0].provisional_expense_account, "Provision Account - _TC")
|
||||
|
||||
# Check GLE for Purchase Invoice
|
||||
expected_gle = [
|
||||
["_Test Payable USD - _TC", 0, 1642.54, add_days(pr.posting_date, 1)],
|
||||
["Cost of Goods Sold - _TC", 1642.54, 0, add_days(pr.posting_date, 1)],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
|
||||
|
||||
expected_gle_for_purchase_receipt = [
|
||||
["_Test Account Cost for Goods Sold - _TC", 1642.54, 0, pr.posting_date],
|
||||
["Provision Account - _TC", 0, 1642.54, pr.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 0, 1642.54, pi.posting_date],
|
||||
["Provision Account - _TC", 1642.54, 0, pi.posting_date],
|
||||
]
|
||||
check_gl_entries(
|
||||
self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date, voucher_type="Purchase Receipt"
|
||||
)
|
||||
|
||||
# Cancel purchase invoice to check reverse provisional entry cancellation
|
||||
pi.cancel()
|
||||
|
||||
expected_gle_for_purchase_receipt_post_pi_cancel = [
|
||||
["_Test Account Cost for Goods Sold - _TC", 1642.54, 0, pi.posting_date],
|
||||
["Provision Account - _TC", 0, 1642.54, pi.posting_date],
|
||||
]
|
||||
|
||||
check_gl_entries(
|
||||
self,
|
||||
pr.name,
|
||||
expected_gle_for_purchase_receipt_post_pi_cancel,
|
||||
pi.posting_date,
|
||||
voucher_type="Purchase Receipt",
|
||||
)
|
||||
|
||||
toggle_provisional_accounting_setting()
|
||||
|
||||
|
||||
@@ -843,6 +843,10 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
w = self.make()
|
||||
self.assertEqual(w.outstanding_amount, w.base_rounded_total)
|
||||
|
||||
@IntegrationTestCase.change_settings(
|
||||
"Accounts Settings",
|
||||
{"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 0},
|
||||
)
|
||||
def test_rounded_total_with_cash_discount(self):
|
||||
si = frappe.copy_doc(self.globalTestRecords["Sales Invoice"][2])
|
||||
|
||||
@@ -3380,6 +3384,7 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
si.posting_date = getdate()
|
||||
si.submit()
|
||||
|
||||
@IntegrationTestCase.change_settings("Accounts Settings", {"over_billing_allowance": 0})
|
||||
def test_over_billing_case_against_delivery_note(self):
|
||||
"""
|
||||
Test a case where duplicating the item with qty = 1 in the invoice
|
||||
@@ -3387,24 +3392,23 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
"""
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||
frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 0)
|
||||
|
||||
dn = create_delivery_note()
|
||||
dn.submit()
|
||||
|
||||
si = make_sales_invoice(dn.name)
|
||||
# make a copy of first item and add it to invoice
|
||||
item_copy = frappe.copy_doc(si.items[0])
|
||||
si.save()
|
||||
|
||||
si.items = [] # Clear existing items
|
||||
si.append("items", item_copy)
|
||||
si.save()
|
||||
|
||||
si.append("items", item_copy)
|
||||
with self.assertRaises(frappe.ValidationError) as err:
|
||||
si.submit()
|
||||
si.save()
|
||||
|
||||
self.assertTrue("cannot overbill" in str(err.exception).lower())
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", over_billing_allowance)
|
||||
dn.cancel()
|
||||
|
||||
@IntegrationTestCase.change_settings(
|
||||
"Accounts Settings",
|
||||
@@ -4442,6 +4446,94 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "amount": 1000})
|
||||
self.assertRaises(frappe.ValidationError, pos.insert)
|
||||
|
||||
def test_stand_alone_credit_note_valuation(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item_code = "_Test Item for Credit Note Valuation"
|
||||
make_item_for_si(
|
||||
item_code,
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "BATCH-TCNV.####",
|
||||
},
|
||||
)
|
||||
|
||||
si = create_sales_invoice(
|
||||
item=item_code,
|
||||
qty=-2,
|
||||
rate=1200,
|
||||
is_return=1,
|
||||
update_stock=1,
|
||||
)
|
||||
|
||||
stock_ledger_entry = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"voucher_type": "Sales Invoice",
|
||||
"voucher_no": si.name,
|
||||
"item_code": item_code,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
},
|
||||
["incoming_rate", "valuation_rate", "actual_qty as qty", "stock_value_difference"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
self.assertEqual(stock_ledger_entry.incoming_rate, 1200.0)
|
||||
self.assertEqual(stock_ledger_entry.valuation_rate, 1200.0)
|
||||
self.assertEqual(stock_ledger_entry.qty, 2.0)
|
||||
self.assertEqual(stock_ledger_entry.stock_value_difference, 2400.0)
|
||||
|
||||
def test_stand_alone_credit_note_zero_valuation(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item_code = "_Test Item for Credit Note Zero Valuation"
|
||||
make_item_for_si(
|
||||
item_code,
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "BATCH-TCNZV.####",
|
||||
},
|
||||
)
|
||||
|
||||
si = create_sales_invoice(
|
||||
item=item_code,
|
||||
qty=-2,
|
||||
rate=1200,
|
||||
is_return=1,
|
||||
update_stock=1,
|
||||
allow_zero_valuation_rate=1,
|
||||
)
|
||||
|
||||
stock_ledger_entry = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"voucher_type": "Sales Invoice",
|
||||
"voucher_no": si.name,
|
||||
"item_code": item_code,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
},
|
||||
["incoming_rate", "valuation_rate", "actual_qty as qty", "stock_value_difference"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
self.assertEqual(stock_ledger_entry.incoming_rate, 0.0)
|
||||
self.assertEqual(stock_ledger_entry.valuation_rate, 0.0)
|
||||
self.assertEqual(stock_ledger_entry.qty, 2.0)
|
||||
self.assertEqual(stock_ledger_entry.stock_value_difference, 0.0)
|
||||
|
||||
|
||||
def make_item_for_si(item_code, properties=None):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item = make_item(item_code, properties=properties)
|
||||
item.is_stock_item = 1
|
||||
item.save()
|
||||
return item
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
@@ -4553,6 +4645,7 @@ def create_sales_invoice(**args):
|
||||
"conversion_factor": args.get("conversion_factor", 1),
|
||||
"incoming_rate": args.incoming_rate or 0,
|
||||
"serial_and_batch_bundle": bundle_id,
|
||||
"allow_zero_valuation_rate": args.allow_zero_valuation_rate or 0,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -657,7 +657,7 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
||||
"company": inv.company,
|
||||
"voucher_no": ["in", vouchers],
|
||||
},
|
||||
"sum(debit)",
|
||||
[{"SUM": "debit"}],
|
||||
)
|
||||
or 0.0
|
||||
)
|
||||
@@ -735,7 +735,7 @@ def get_limit_consumed(ldc, parties):
|
||||
"posting_date": ("between", (ldc.valid_from, ldc.valid_upto)),
|
||||
"company": ldc.company,
|
||||
},
|
||||
"sum(tax_withholding_net_total)",
|
||||
[{"SUM": "tax_withholding_net_total"}],
|
||||
)
|
||||
|
||||
return limit_consumed
|
||||
|
||||
@@ -766,7 +766,7 @@ def validate_against_pcv(is_opening, posting_date, company):
|
||||
)
|
||||
|
||||
last_pcv_date = frappe.db.get_value(
|
||||
"Period Closing Voucher", {"docstatus": 1, "company": company}, "max(period_end_date)"
|
||||
"Period Closing Voucher", {"docstatus": 1, "company": company}, [{"MAX": "period_end_date"}]
|
||||
)
|
||||
|
||||
if last_pcv_date and getdate(posting_date) <= getdate(last_pcv_date):
|
||||
|
||||
@@ -424,6 +424,8 @@ def get_party_account(party_type, party=None, company=None, include_advance=Fals
|
||||
Will first search in party (Customer / Supplier) record, if not found,
|
||||
will search in group (Customer Group / Supplier Group),
|
||||
finally will return default."""
|
||||
if not party_type:
|
||||
frappe.throw(_("Party Type is mandatory"))
|
||||
if not company:
|
||||
frappe.throw(_("Please select a Company"))
|
||||
|
||||
|
||||
@@ -49,7 +49,8 @@ class ReceivablePayableReport:
|
||||
self.filters.report_date = getdate(self.filters.report_date or nowdate())
|
||||
self.age_as_on = (
|
||||
getdate(nowdate())
|
||||
if self.filters.calculate_ageing_with == "Today Date"
|
||||
if "calculate_ageing_with" not in self.filters
|
||||
or self.filters.calculate_ageing_with == "Today Date"
|
||||
else self.filters.report_date
|
||||
)
|
||||
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.tests import IntegrationTestCase, change_settings
|
||||
from frappe.utils import flt, today
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import execute
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||
|
||||
|
||||
class TestGeneralLedger(IntegrationTestCase):
|
||||
@@ -168,6 +171,90 @@ class TestGeneralLedger(IntegrationTestCase):
|
||||
self.assertEqual(data[3]["debit"], 100)
|
||||
self.assertEqual(data[3]["credit"], 100)
|
||||
|
||||
@change_settings("Accounts Settings", {"delete_linked_ledger_entries": True})
|
||||
def test_debit_in_exchange_gain_loss_account(self):
|
||||
company = "_Test Company"
|
||||
|
||||
exchange_gain_loss_account = frappe.db.get_value("Company", "exchange_gain_loss_account")
|
||||
if not exchange_gain_loss_account:
|
||||
frappe.db.set_value(
|
||||
"Company", company, "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
|
||||
)
|
||||
|
||||
account_name = "_Test Receivable USD - _TC"
|
||||
customer_name = "_Test Customer USD"
|
||||
|
||||
sales_invoice = create_sales_invoice(
|
||||
company=company,
|
||||
customer=customer_name,
|
||||
currency="USD",
|
||||
debit_to=account_name,
|
||||
conversion_rate=85,
|
||||
posting_date=today(),
|
||||
)
|
||||
|
||||
payment_entry = create_payment_entry(
|
||||
company=company,
|
||||
party_type="Customer",
|
||||
party=customer_name,
|
||||
payment_type="Receive",
|
||||
paid_from=account_name,
|
||||
paid_from_account_currency="USD",
|
||||
paid_to="Cash - _TC",
|
||||
paid_to_account_currency="INR",
|
||||
paid_amount=10,
|
||||
do_not_submit=True,
|
||||
)
|
||||
payment_entry.base_paid_amount = 800
|
||||
payment_entry.received_amount = 800
|
||||
payment_entry.currency = "USD"
|
||||
payment_entry.source_exchange_rate = 80
|
||||
payment_entry.append(
|
||||
"references",
|
||||
frappe._dict(
|
||||
{
|
||||
"reference_doctype": "Sales Invoice",
|
||||
"reference_name": sales_invoice.name,
|
||||
"total_amount": 10,
|
||||
"outstanding_amount": 10,
|
||||
"exchange_rate": 85,
|
||||
"allocated_amount": 10,
|
||||
"exchange_gain_loss": -50,
|
||||
}
|
||||
),
|
||||
)
|
||||
payment_entry.save()
|
||||
payment_entry.submit()
|
||||
|
||||
journal_entry = frappe.get_all(
|
||||
"Journal Entry Account", filters={"reference_name": sales_invoice.name}, fields=["parent"]
|
||||
)
|
||||
|
||||
columns, data = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"company": company,
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"include_dimensions": 1,
|
||||
"include_default_book_entries": 1,
|
||||
"account": ["_Test Exchange Gain/Loss - _TC"],
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
entry = data[1]
|
||||
self.assertEqual(entry["debit"], 50)
|
||||
self.assertEqual(entry["voucher_type"], "Journal Entry")
|
||||
self.assertEqual(entry["voucher_no"], journal_entry[0]["parent"])
|
||||
|
||||
payment_entry.cancel()
|
||||
payment_entry.delete()
|
||||
sales_invoice.reload()
|
||||
sales_invoice.cancel()
|
||||
sales_invoice.delete()
|
||||
|
||||
def test_ignore_exchange_rate_journals_filter(self):
|
||||
# create a new account with USD currency
|
||||
account_name = "Test Debtors USD"
|
||||
|
||||
@@ -525,7 +525,7 @@ def get_grand_total(filters, doctype):
|
||||
"docstatus": 1,
|
||||
"posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]),
|
||||
},
|
||||
"sum(base_grand_total)",
|
||||
[{"SUM": "base_grand_total"}],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -107,7 +107,11 @@ def convert_to_presentation_currency(gl_entries, currency_info):
|
||||
credit_in_account_currency = flt(entry["credit_in_account_currency"])
|
||||
account_currency = entry["account_currency"]
|
||||
|
||||
if len(account_currencies) == 1 and account_currency == presentation_currency:
|
||||
if (
|
||||
len(account_currencies) == 1
|
||||
and account_currency == presentation_currency
|
||||
and (debit_in_account_currency or credit_in_account_currency)
|
||||
):
|
||||
entry["debit"] = debit_in_account_currency
|
||||
entry["credit"] = credit_in_account_currency
|
||||
else:
|
||||
|
||||
@@ -1480,7 +1480,7 @@ def repost_gle_for_stock_vouchers(
|
||||
else:
|
||||
_delete_accounting_ledger_entries(voucher_type, voucher_no)
|
||||
|
||||
if not frappe.flags.in_test:
|
||||
if not frappe.in_test:
|
||||
frappe.db.commit()
|
||||
|
||||
if repost_doc:
|
||||
|
||||
@@ -208,14 +208,11 @@ class Asset(AccountsController):
|
||||
add_asset_activity(self.name, _("Asset cancelled"))
|
||||
|
||||
def after_insert(self):
|
||||
if (
|
||||
not frappe.db.exists(
|
||||
{
|
||||
"doctype": "Asset Activity",
|
||||
"asset": self.name,
|
||||
}
|
||||
)
|
||||
and not self.flags.asset_created_via_asset_capitalization
|
||||
if not frappe.db.exists(
|
||||
{
|
||||
"doctype": "Asset Activity",
|
||||
"asset": self.name,
|
||||
}
|
||||
):
|
||||
add_asset_activity(self.name, _("Asset created"))
|
||||
|
||||
@@ -1006,7 +1003,6 @@ def create_asset_capitalization(company, asset, asset_name, item_code):
|
||||
{
|
||||
"target_asset": asset,
|
||||
"company": company,
|
||||
"capitalization_method": "Choose a WIP composite asset",
|
||||
"target_asset_name": asset_name,
|
||||
"target_item_code": item_code,
|
||||
}
|
||||
|
||||
@@ -1748,6 +1748,7 @@ def create_asset(**args):
|
||||
"asset_owner": args.asset_owner or "Company",
|
||||
"is_existing_asset": args.is_existing_asset or 1,
|
||||
"is_composite_asset": args.is_composite_asset or 0,
|
||||
"is_composite_component": args.is_composite_component or 0,
|
||||
"asset_quantity": args.get("asset_quantity") or 1,
|
||||
"depr_entry_posting_status": args.depr_entry_posting_status or "",
|
||||
}
|
||||
|
||||
@@ -134,10 +134,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
}
|
||||
|
||||
target_asset() {
|
||||
if (
|
||||
this.frm.doc.target_asset &&
|
||||
this.frm.doc.capitalization_method === "Choose a WIP composite asset"
|
||||
) {
|
||||
if (this.frm.doc.target_asset) {
|
||||
this.set_consumed_stock_items_tagged_to_wip_composite_asset(this.frm.doc.target_asset);
|
||||
this.get_target_asset_details();
|
||||
}
|
||||
|
||||
@@ -9,19 +9,16 @@
|
||||
"field_order": [
|
||||
"title",
|
||||
"naming_series",
|
||||
"capitalization_method",
|
||||
"target_item_code",
|
||||
"target_item_name",
|
||||
"target_asset",
|
||||
"target_asset_name",
|
||||
"target_item_code",
|
||||
"finance_book",
|
||||
"target_qty",
|
||||
"target_asset_location",
|
||||
"column_break_9",
|
||||
"company",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"set_posting_time",
|
||||
"finance_book",
|
||||
"target_batch_no",
|
||||
"target_serial_no",
|
||||
"amended_from",
|
||||
@@ -54,20 +51,12 @@
|
||||
"label": "Title"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.target_item_code && !doc.__islocal && doc.capitalization_method !== 'Choose a WIP composite asset') || doc.capitalization_method=='Create a new composite asset'",
|
||||
"depends_on": "eval:(doc.target_item_code && !doc.__islocal)",
|
||||
"fieldname": "target_item_code",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Target Item Code",
|
||||
"mandatory_depends_on": "eval:doc.capitalization_method=='Create a new composite asset'",
|
||||
"options": "Item"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.target_item_code && doc.target_item_name != doc.target_item_code",
|
||||
"fetch_from": "target_item_code.item_name",
|
||||
"fieldname": "target_item_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Target Item Name",
|
||||
"options": "Item",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -80,18 +69,14 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.target_asset && !doc.__islocal) || doc.capitalization_method=='Choose a WIP composite asset'",
|
||||
"fieldname": "target_asset",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Target Asset",
|
||||
"mandatory_depends_on": "eval:doc.capitalization_method=='Choose a WIP composite asset'",
|
||||
"no_copy": 1,
|
||||
"options": "Asset",
|
||||
"read_only_depends_on": "eval:doc.capitalization_method=='Create a new composite asset'"
|
||||
"options": "Asset"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.target_asset_name && !doc.__islocal) || (doc.target_asset && doc.capitalization_method=='Choose a WIP composite asset')",
|
||||
"fetch_from": "target_asset.asset_name",
|
||||
"fieldname": "target_asset_name",
|
||||
"fieldtype": "Data",
|
||||
@@ -176,7 +161,9 @@
|
||||
"default": "1",
|
||||
"fieldname": "target_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Target Qty"
|
||||
"hidden": 1,
|
||||
"label": "Target Qty",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -298,26 +285,12 @@
|
||||
"label": "Target Fixed Asset Account",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.capitalization_method=='Create a new composite asset'",
|
||||
"fieldname": "target_asset_location",
|
||||
"fieldtype": "Link",
|
||||
"label": "Target Asset Location",
|
||||
"mandatory_depends_on": "eval:doc.capitalization_method=='Create a new composite asset'",
|
||||
"options": "Location"
|
||||
},
|
||||
{
|
||||
"fieldname": "capitalization_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Capitalization Method",
|
||||
"options": "\nCreate a new composite asset\nChoose a WIP composite asset"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-08 13:14:33.008458",
|
||||
"modified": "2025-05-20 15:15:12.110035",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Capitalization",
|
||||
@@ -355,10 +328,11 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import erpnext
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
depreciate_asset,
|
||||
get_disposal_account_and_cost_center,
|
||||
get_gl_entries_on_asset_disposal,
|
||||
get_value_after_depreciation_on_disposal_date,
|
||||
reset_depreciation_schedule,
|
||||
@@ -70,7 +71,6 @@ class AssetCapitalization(StockController):
|
||||
amended_from: DF.Link | None
|
||||
asset_items: DF.Table[AssetCapitalizationAssetItem]
|
||||
asset_items_total: DF.Currency
|
||||
capitalization_method: DF.Literal["", "Create new composite asset", "Use existing composite asset"]
|
||||
company: DF.Link
|
||||
cost_center: DF.Link | None
|
||||
finance_book: DF.Link | None
|
||||
@@ -83,7 +83,6 @@ class AssetCapitalization(StockController):
|
||||
stock_items: DF.Table[AssetCapitalizationStockItem]
|
||||
stock_items_total: DF.Currency
|
||||
target_asset: DF.Link | None
|
||||
target_asset_location: DF.Link | None
|
||||
target_asset_name: DF.Data | None
|
||||
target_batch_no: DF.Link | None
|
||||
target_fixed_asset_account: DF.Link | None
|
||||
@@ -92,7 +91,6 @@ class AssetCapitalization(StockController):
|
||||
target_incoming_rate: DF.Currency
|
||||
target_is_fixed_asset: DF.Check
|
||||
target_item_code: DF.Link | None
|
||||
target_item_name: DF.Data | None
|
||||
target_qty: DF.Float
|
||||
target_serial_no: DF.SmallText | None
|
||||
title: DF.Data | None
|
||||
@@ -118,7 +116,7 @@ class AssetCapitalization(StockController):
|
||||
|
||||
def before_submit(self):
|
||||
self.validate_source_mandatory()
|
||||
self.create_target_asset()
|
||||
# self.create_target_asset()
|
||||
|
||||
def on_submit(self):
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
@@ -143,7 +141,7 @@ class AssetCapitalization(StockController):
|
||||
self.update_target_asset()
|
||||
|
||||
def set_title(self):
|
||||
self.title = self.target_asset_name or self.target_item_name or self.target_item_code
|
||||
self.title = self.target_asset_name or self.target_item_code
|
||||
|
||||
def set_missing_values(self, for_validate=False):
|
||||
target_item_details = get_target_item_details(self.target_item_code, self.company)
|
||||
@@ -301,16 +299,7 @@ class AssetCapitalization(StockController):
|
||||
d.cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
|
||||
|
||||
def validate_source_mandatory(self):
|
||||
if self.capitalization_method == "Create a new composite asset" and not (
|
||||
self.get("stock_items") or self.get("asset_items")
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Consumed Stock Items or Consumed Asset Items are mandatory for creating new composite asset"
|
||||
)
|
||||
)
|
||||
|
||||
elif not (self.get("stock_items") or self.get("asset_items") or self.get("service_items")):
|
||||
if not (self.get("stock_items") or self.get("asset_items") or self.get("service_items")):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Consumed Stock Items, Consumed Asset Items or Consumed Service Items is mandatory for Capitalization"
|
||||
@@ -441,7 +430,11 @@ class AssetCapitalization(StockController):
|
||||
self.get_gl_entries_for_consumed_asset_items(gl_entries, target_account, target_against, precision)
|
||||
self.get_gl_entries_for_consumed_service_items(gl_entries, target_account, target_against, precision)
|
||||
|
||||
self.get_gl_entries_for_target_item(gl_entries, target_account, target_against, precision)
|
||||
composite_component_value = self.get_composite_component_value()
|
||||
|
||||
self.get_gl_entries_for_target_item(
|
||||
gl_entries, target_account, target_against, precision, composite_component_value
|
||||
)
|
||||
|
||||
return gl_entries
|
||||
|
||||
@@ -493,34 +486,34 @@ class AssetCapitalization(StockController):
|
||||
for item in self.asset_items:
|
||||
asset = frappe.get_doc("Asset", item.asset)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
notes = _(
|
||||
"This schedule was created when Asset {0} was consumed through Asset Capitalization {1}."
|
||||
).format(
|
||||
get_link_to_form(asset.doctype, asset.name),
|
||||
get_link_to_form(self.doctype, self.get("name")),
|
||||
)
|
||||
depreciate_asset(asset, self.posting_date, notes)
|
||||
asset.reload()
|
||||
if not asset.is_composite_component:
|
||||
if asset.calculate_depreciation:
|
||||
notes = _(
|
||||
"This schedule was created when Asset {0} was consumed through Asset Capitalization {1}."
|
||||
).format(
|
||||
get_link_to_form(asset.doctype, asset.name),
|
||||
get_link_to_form(self.doctype, self.get("name")),
|
||||
)
|
||||
depreciate_asset(asset, self.posting_date, notes)
|
||||
asset.reload()
|
||||
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
||||
asset,
|
||||
item.asset_value,
|
||||
item.get("finance_book") or self.get("finance_book"),
|
||||
self.get("doctype"),
|
||||
self.get("name"),
|
||||
self.get("posting_date"),
|
||||
)
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
||||
asset,
|
||||
item.asset_value,
|
||||
item.get("finance_book") or self.get("finance_book"),
|
||||
self.get("doctype"),
|
||||
self.get("name"),
|
||||
self.get("posting_date"),
|
||||
)
|
||||
|
||||
for gle in fixed_asset_gl_entries:
|
||||
gle["against"] = target_account
|
||||
gl_entries.append(self.get_gl_dict(gle, item=item))
|
||||
target_against.add(gle["account"])
|
||||
|
||||
asset.db_set("disposal_date", self.posting_date)
|
||||
|
||||
self.set_consumed_asset_status(asset)
|
||||
|
||||
for gle in fixed_asset_gl_entries:
|
||||
gle["against"] = target_account
|
||||
gl_entries.append(self.get_gl_dict(gle, item=item))
|
||||
target_against.add(gle["account"])
|
||||
|
||||
def get_gl_entries_for_consumed_service_items(
|
||||
self, gl_entries, target_account, target_against, precision
|
||||
):
|
||||
@@ -543,65 +536,35 @@ class AssetCapitalization(StockController):
|
||||
)
|
||||
)
|
||||
|
||||
def get_gl_entries_for_target_item(self, gl_entries, target_account, target_against, precision):
|
||||
def get_composite_component_value(self):
|
||||
composite_component_value = 0
|
||||
for item in self.asset_items:
|
||||
asset = frappe.db.get_value("Asset", item.asset, ["is_composite_component"], as_dict=True)
|
||||
if asset and asset.is_composite_component:
|
||||
composite_component_value += flt(item.asset_value, item.precision("asset_value"))
|
||||
return composite_component_value
|
||||
|
||||
def get_gl_entries_for_target_item(
|
||||
self, gl_entries, target_account, target_against, precision, composite_component_value
|
||||
):
|
||||
if self.target_is_fixed_asset:
|
||||
# Capitalization
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": target_account,
|
||||
"against": ", ".join(target_against),
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||
"debit": flt(self.total_value, precision),
|
||||
"cost_center": self.get("cost_center"),
|
||||
},
|
||||
item=self,
|
||||
total_value = flt(self.total_value - composite_component_value, precision)
|
||||
if total_value:
|
||||
# Capitalization
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": target_account,
|
||||
"against": ", ".join(target_against),
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||
"debit": total_value,
|
||||
"cost_center": self.get("cost_center"),
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def create_target_asset(self):
|
||||
if self.capitalization_method != "Create a new composite asset":
|
||||
return
|
||||
|
||||
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
|
||||
|
||||
asset_doc = frappe.new_doc("Asset")
|
||||
asset_doc.company = self.company
|
||||
asset_doc.item_code = self.target_item_code
|
||||
asset_doc.is_composite_asset = 1
|
||||
asset_doc.location = self.target_asset_location
|
||||
asset_doc.available_for_use_date = self.posting_date
|
||||
asset_doc.purchase_date = self.posting_date
|
||||
asset_doc.gross_purchase_amount = total_target_asset_value
|
||||
asset_doc.purchase_amount = total_target_asset_value
|
||||
asset_doc.flags.ignore_validate = True
|
||||
asset_doc.flags.asset_created_via_asset_capitalization = True
|
||||
asset_doc.insert()
|
||||
|
||||
self.target_asset = asset_doc.name
|
||||
|
||||
self.target_fixed_asset_account = get_asset_category_account(
|
||||
"fixed_asset_account", item=self.target_item_code, company=asset_doc.company
|
||||
)
|
||||
asset_doc.set_status("Work In Progress")
|
||||
|
||||
add_asset_activity(
|
||||
asset_doc.name,
|
||||
_("Asset created after Asset Capitalization {0} was submitted").format(
|
||||
get_link_to_form("Asset Capitalization", self.name)
|
||||
),
|
||||
)
|
||||
|
||||
frappe.msgprint(
|
||||
_("Asset {0} has been created. Please set the depreciation details if any and submit it.").format(
|
||||
get_link_to_form("Asset", asset_doc.name)
|
||||
)
|
||||
)
|
||||
|
||||
def update_target_asset(self):
|
||||
if self.capitalization_method != "Choose a WIP composite asset":
|
||||
return
|
||||
|
||||
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
|
||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||
|
||||
|
||||
@@ -59,10 +59,16 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
company=company,
|
||||
)
|
||||
|
||||
wip_composite_asset = create_asset(
|
||||
asset_name="Asset Capitalization WIP Composite Asset",
|
||||
is_composite_asset=1,
|
||||
warehouse="Stores - TCP1",
|
||||
company=company,
|
||||
)
|
||||
|
||||
# Create and submit Asset Captitalization
|
||||
asset_capitalization = create_asset_capitalization(
|
||||
capitalization_method="Create a new composite asset",
|
||||
target_item_code="Macbook Pro",
|
||||
target_asset=wip_composite_asset.name,
|
||||
target_asset_location="Test Location",
|
||||
stock_qty=stock_qty,
|
||||
stock_rate=stock_rate,
|
||||
@@ -148,10 +154,16 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
company=company,
|
||||
)
|
||||
|
||||
wip_composite_asset = create_asset(
|
||||
asset_name="Asset Capitalization WIP Composite Asset",
|
||||
is_composite_asset=1,
|
||||
warehouse="Stores - TCP1",
|
||||
company=company,
|
||||
)
|
||||
|
||||
# Create and submit Asset Captitalization
|
||||
asset_capitalization = create_asset_capitalization(
|
||||
capitalization_method="Create a new composite asset",
|
||||
target_item_code="Macbook Pro",
|
||||
target_asset=wip_composite_asset.name,
|
||||
target_asset_location="Test Location",
|
||||
stock_qty=stock_qty,
|
||||
stock_rate=stock_rate,
|
||||
@@ -240,7 +252,6 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
|
||||
# Create and submit Asset Captitalization
|
||||
asset_capitalization = create_asset_capitalization(
|
||||
capitalization_method="Choose a WIP composite asset",
|
||||
target_asset=wip_composite_asset.name,
|
||||
target_asset_location="Test Location",
|
||||
stock_qty=stock_qty,
|
||||
@@ -251,7 +262,6 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
)
|
||||
|
||||
# Test Asset Capitalization values
|
||||
self.assertEqual(asset_capitalization.capitalization_method, "Choose a WIP composite asset")
|
||||
self.assertEqual(asset_capitalization.target_qty, 1)
|
||||
|
||||
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
|
||||
@@ -310,7 +320,6 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
|
||||
# Create and submit Asset Captitalization
|
||||
asset_capitalization = create_asset_capitalization(
|
||||
capitalization_method="Choose a WIP composite asset",
|
||||
target_asset=wip_composite_asset.name,
|
||||
target_asset_location="Test Location",
|
||||
service_qty=service_qty,
|
||||
@@ -340,6 +349,50 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
|
||||
self.assertFalse(get_actual_sle_dict(asset_capitalization.name))
|
||||
|
||||
def test_capitalize_composite_component(self):
|
||||
company = "_Test Company with perpetual inventory"
|
||||
set_depreciation_settings_in_company(company=company)
|
||||
name = frappe.db.get_value(
|
||||
"Asset Category Account",
|
||||
filters={"parent": "Computers", "company_name": company},
|
||||
fieldname=["name"],
|
||||
)
|
||||
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", "")
|
||||
|
||||
wip_composite_asset = create_asset(
|
||||
asset_name="Asset Capitalization WIP Composite Asset",
|
||||
is_composite_asset=1,
|
||||
warehouse="Stores - TCP1",
|
||||
company=company,
|
||||
)
|
||||
|
||||
consumed_asset_value = 100000
|
||||
|
||||
consumed_asset = create_asset(
|
||||
asset_name="Asset Capitalization Consumable Asset",
|
||||
asset_value=consumed_asset_value,
|
||||
submit=1,
|
||||
warehouse="Stores - _TC",
|
||||
is_composite_component=1,
|
||||
company=company,
|
||||
)
|
||||
|
||||
# Create and submit Asset Captitalization
|
||||
asset_capitalization = create_asset_capitalization(
|
||||
target_asset=wip_composite_asset.name,
|
||||
target_asset_location="Test Location",
|
||||
consumed_asset=consumed_asset.name,
|
||||
company=company,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
# Test Asset Capitalization values
|
||||
self.assertEqual(asset_capitalization.target_qty, 1)
|
||||
self.assertEqual(asset_capitalization.asset_items[0].asset_value, consumed_asset_value)
|
||||
|
||||
actual_gle = get_actual_gle_dict(asset_capitalization.name)
|
||||
self.assertEqual(actual_gle, {})
|
||||
|
||||
|
||||
def create_asset_capitalization_data():
|
||||
create_item("Capitalization Target Stock Item", is_stock_item=1, is_fixed_asset=0, is_purchase_item=0)
|
||||
@@ -362,7 +415,6 @@ def create_asset_capitalization(**args):
|
||||
asset_capitalization = frappe.new_doc("Asset Capitalization")
|
||||
asset_capitalization.update(
|
||||
{
|
||||
"capitalization_method": args.capitalization_method or None,
|
||||
"company": company,
|
||||
"posting_date": args.posting_date or now.strftime("%Y-%m-%d"),
|
||||
"posting_time": args.posting_time or now.strftime("%H:%M:%S.%f"),
|
||||
|
||||
@@ -171,6 +171,7 @@ class AssetValueAdjustment(Document):
|
||||
asset = self.update_asset_value_after_depreciation()
|
||||
note = self.get_adjustment_note()
|
||||
reschedule_depreciation(asset, note)
|
||||
asset.set_status()
|
||||
|
||||
def update_asset_value_after_depreciation(self):
|
||||
difference_amount = self.difference_amount if self.docstatus == 1 else -1 * self.difference_amount
|
||||
@@ -179,13 +180,22 @@ class AssetValueAdjustment(Document):
|
||||
if asset.calculate_depreciation:
|
||||
for row in asset.finance_books:
|
||||
if cstr(row.finance_book) == cstr(self.finance_book):
|
||||
row.value_after_depreciation += flt(difference_amount)
|
||||
salvage_value_adjustment = (
|
||||
self.get_adjusted_salvage_value_amount(row, difference_amount) or 0
|
||||
)
|
||||
row.expected_value_after_useful_life += salvage_value_adjustment
|
||||
row.value_after_depreciation = row.value_after_depreciation + flt(difference_amount)
|
||||
row.db_update()
|
||||
|
||||
asset.value_after_depreciation += flt(difference_amount)
|
||||
asset.db_update()
|
||||
return asset
|
||||
|
||||
def get_adjusted_salvage_value_amount(self, row, difference_amount):
|
||||
if row.expected_value_after_useful_life:
|
||||
salvage_value_adjustment = (difference_amount * row.salvage_value_percentage) / 100
|
||||
return flt(salvage_value_adjustment)
|
||||
|
||||
def get_adjustment_note(self):
|
||||
if self.docstatus == 1:
|
||||
notes = _(
|
||||
|
||||
@@ -299,6 +299,43 @@ class TestAssetValueAdjustment(IntegrationTestCase):
|
||||
asset_doc.load_from_db()
|
||||
self.assertEqual(asset_doc.finance_books[0].value_after_depreciation, 40000.0)
|
||||
|
||||
def test_expected_value_after_useful_life(self):
|
||||
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location")
|
||||
|
||||
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
|
||||
asset_doc = frappe.get_doc("Asset", asset_name)
|
||||
asset_doc.calculate_depreciation = 1
|
||||
asset_doc.available_for_use_date = "2023-01-15"
|
||||
asset_doc.purchase_date = "2023-01-15"
|
||||
|
||||
asset_doc.append(
|
||||
"finance_books",
|
||||
{
|
||||
"expected_value_after_useful_life": 5000,
|
||||
"salvage_value_percentage": 5,
|
||||
"depreciation_method": "Straight Line",
|
||||
"total_number_of_depreciations": 12,
|
||||
"frequency_of_depreciation": 1,
|
||||
"depreciation_start_date": "2023-01-31",
|
||||
},
|
||||
)
|
||||
asset_doc.submit()
|
||||
self.assertEqual(asset_doc.finance_books[0].expected_value_after_useful_life, 5000.0)
|
||||
|
||||
current_asset_value = get_asset_value_after_depreciation(asset_doc.name)
|
||||
adj_doc = make_asset_value_adjustment(
|
||||
asset=asset_doc.name,
|
||||
current_asset_value=current_asset_value,
|
||||
new_asset_value=40000,
|
||||
date="2023-08-21",
|
||||
)
|
||||
adj_doc.submit()
|
||||
difference_amount = adj_doc.new_asset_value - adj_doc.current_asset_value
|
||||
self.assertEqual(difference_amount, -60000)
|
||||
asset_doc.load_from_db()
|
||||
self.assertEqual(asset_doc.finance_books[0].value_after_depreciation, 40000.0)
|
||||
self.assertEqual(asset_doc.finance_books[0].expected_value_after_useful_life, 2000.0)
|
||||
|
||||
|
||||
def make_asset_value_adjustment(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -59,6 +59,19 @@ frappe.ui.form.on("Purchase Order", {
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
},
|
||||
|
||||
schedule_date(frm) {
|
||||
if (frm.doc.schedule_date) {
|
||||
frm.doc.items.forEach((d) => {
|
||||
frappe.model.set_value(d.doctype, d.name, "schedule_date", frm.doc.schedule_date);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
transaction_date(frm) {
|
||||
prevent_past_schedule_dates(frm);
|
||||
frm.set_value("schedule_date", "");
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.is_old_subcontracting_flow) {
|
||||
frm.trigger("get_materials_from_supplier");
|
||||
@@ -75,6 +88,7 @@ frappe.ui.form.on("Purchase Order", {
|
||||
if (frm.doc.docstatus == 0) {
|
||||
erpnext.set_unit_price_items_note(frm);
|
||||
}
|
||||
prevent_past_schedule_dates(frm);
|
||||
},
|
||||
|
||||
supplier: function (frm) {
|
||||
@@ -776,10 +790,6 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
items_on_form_rendered() {
|
||||
set_schedule_date(this.frm);
|
||||
}
|
||||
|
||||
schedule_date() {
|
||||
set_schedule_date(this.frm);
|
||||
}
|
||||
};
|
||||
|
||||
// for backward compatibility: combine new and previous states
|
||||
@@ -835,3 +845,11 @@ frappe.ui.form.on("Purchase Order", "is_subcontracted", function (frm) {
|
||||
erpnext.buying.get_default_bom(frm);
|
||||
}
|
||||
});
|
||||
|
||||
function prevent_past_schedule_dates(frm) {
|
||||
if (frm.doc.transaction_date) {
|
||||
frm.fields_dict["schedule_date"].datepicker.update({
|
||||
minDate: new Date(frm.doc.transaction_date),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,11 +30,15 @@
|
||||
"stock_qty",
|
||||
"sec_break_price_list",
|
||||
"price_list_rate",
|
||||
"base_price_list_rate",
|
||||
"discount_and_margin_section",
|
||||
"margin_type",
|
||||
"margin_rate_or_amount",
|
||||
"rate_with_margin",
|
||||
"col_break_6",
|
||||
"discount_percentage",
|
||||
"discount_amount",
|
||||
"distributed_discount_amount",
|
||||
"col_break_price_list",
|
||||
"base_price_list_rate",
|
||||
"sec_break1",
|
||||
"rate",
|
||||
"amount",
|
||||
@@ -531,10 +535,6 @@
|
||||
"fieldname": "sec_break_price_list",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break_price_list",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "ad_sec_break",
|
||||
@@ -572,21 +572,57 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Distributed Discount Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"depends_on": "price_list_rate",
|
||||
"fieldname": "margin_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Margin Type",
|
||||
"options": "\nPercentage\nAmount",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.margin_type && doc.price_list_rate",
|
||||
"fieldname": "margin_rate_or_amount",
|
||||
"fieldtype": "Float",
|
||||
"label": "Margin Rate or Amount",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount",
|
||||
"fieldname": "discount_and_margin_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Discount and Margin"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount",
|
||||
"fieldname": "rate_with_margin",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rate With Margin",
|
||||
"options": "currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-02 06:22:17.864822",
|
||||
"modified": "2025-06-17 12:05:52.441645",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier Quotation Item",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ class SupplierQuotationItem(Document):
|
||||
lead_time_days: DF.Int
|
||||
manufacturer: DF.Link | None
|
||||
manufacturer_part_no: DF.Data | None
|
||||
margin_rate_or_amount: DF.Float
|
||||
margin_type: DF.Literal["", "Percentage", "Amount"]
|
||||
material_request: DF.Link | None
|
||||
material_request_item: DF.Data | None
|
||||
net_amount: DF.Currency
|
||||
@@ -52,6 +54,7 @@ class SupplierQuotationItem(Document):
|
||||
project: DF.Link | None
|
||||
qty: DF.Float
|
||||
rate: DF.Currency
|
||||
rate_with_margin: DF.Currency
|
||||
request_for_quotation: DF.Link | None
|
||||
request_for_quotation_item: DF.Data | None
|
||||
sales_order: DF.Link | None
|
||||
|
||||
@@ -24,22 +24,28 @@ def get_chart_data(data, conditions, filters):
|
||||
|
||||
datapoints = []
|
||||
|
||||
start = 2 if filters.get("based_on") in ["Item", "Supplier"] else 1
|
||||
if filters.get("based_on") in ["Supplier"]:
|
||||
start = 3
|
||||
elif filters.get("based_on") in ["Item"]:
|
||||
start = 2
|
||||
else:
|
||||
start = 1
|
||||
|
||||
if filters.get("group_by"):
|
||||
start += 1
|
||||
|
||||
# fetch only periodic columns as labels
|
||||
columns = conditions.get("columns")[start:-2][1::2]
|
||||
columns = conditions.get("columns")[start:-2][2::2]
|
||||
labels = [column.split(":")[0] for column in columns]
|
||||
datapoints = [0] * len(labels)
|
||||
|
||||
for row in data:
|
||||
# If group by filter, don't add first row of group (it's already summed)
|
||||
if not row[start - 1]:
|
||||
if not row[start]:
|
||||
continue
|
||||
# Remove None values and compute only periodic data
|
||||
row = [x if x else 0 for x in row[start:-2]]
|
||||
row = row[1::2]
|
||||
row = row[2::2]
|
||||
|
||||
for i in range(len(row)):
|
||||
datapoints[i] += row[i]
|
||||
|
||||
@@ -654,7 +654,7 @@ class AccountsController(TransactionBase):
|
||||
self.base_paid_amount = 0
|
||||
|
||||
def set_missing_values(self, for_validate=False):
|
||||
if frappe.flags.in_test:
|
||||
if frappe.in_test:
|
||||
for fieldname in ["posting_date", "transaction_date"]:
|
||||
if self.meta.get_field(fieldname) and not self.get(fieldname):
|
||||
self.set(fieldname, today())
|
||||
@@ -1138,10 +1138,17 @@ class AccountsController(TransactionBase):
|
||||
return True
|
||||
|
||||
def set_taxes_and_charges(self):
|
||||
if self.get("taxes") or self.get("is_pos"):
|
||||
return
|
||||
|
||||
if frappe.get_single_value(
|
||||
"Accounts Settings", "add_taxes_from_taxes_and_charges_template"
|
||||
) and hasattr(self, "taxes_and_charges"):
|
||||
if tax_master_doctype := self.meta.get_field("taxes_and_charges").options:
|
||||
self.append_taxes_from_master(tax_master_doctype)
|
||||
|
||||
if frappe.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template"):
|
||||
if hasattr(self, "taxes_and_charges") and not self.get("taxes") and not self.get("is_pos"):
|
||||
if tax_master_doctype := self.meta.get_field("taxes_and_charges").options:
|
||||
self.append_taxes_from_master(tax_master_doctype)
|
||||
self.append_taxes_from_item_tax_template()
|
||||
|
||||
def append_taxes_from_master(self, tax_master_doctype=None):
|
||||
if self.get("taxes_and_charges"):
|
||||
@@ -1174,6 +1181,8 @@ class AccountsController(TransactionBase):
|
||||
"rate": 0,
|
||||
"description": account_head,
|
||||
"set_by_item_tax_template": 1,
|
||||
"category": "Total",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2034,69 +2043,48 @@ class AccountsController(TransactionBase):
|
||||
def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on):
|
||||
from erpnext.controllers.status_updater import get_allowance_for
|
||||
|
||||
item_allowance = {}
|
||||
global_qty_allowance, global_amount_allowance = None, None
|
||||
ref_wise_billed_amount = self.get_reference_wise_billed_amt(ref_dt, item_ref_dn, based_on)
|
||||
|
||||
role_allowed_to_over_bill = frappe.get_cached_value(
|
||||
"Accounts Settings", None, "role_allowed_to_over_bill"
|
||||
)
|
||||
user_roles = frappe.get_roles()
|
||||
if not ref_wise_billed_amount:
|
||||
return
|
||||
|
||||
total_overbilled_amt = 0.0
|
||||
overbilled_items = []
|
||||
precision = self.precision(based_on, "items")
|
||||
precision_allowance = 1 / (10**precision)
|
||||
|
||||
reference_names = [d.get(item_ref_dn) for d in self.get("items") if d.get(item_ref_dn)]
|
||||
reference_details = self.get_billing_reference_details(reference_names, ref_dt + " Item", based_on)
|
||||
role_allowed_to_overbill = frappe.get_single_value("Accounts Settings", "role_allowed_to_over_bill")
|
||||
is_overbilling_allowed = role_allowed_to_overbill in frappe.get_roles()
|
||||
|
||||
for item in self.get("items"):
|
||||
if not item.get(item_ref_dn):
|
||||
continue
|
||||
for row in ref_wise_billed_amount.values():
|
||||
total_billed_amt = row.billed_amt
|
||||
allowance = get_allowance_for(row.item_code, {}, None, None, "amount")[0]
|
||||
|
||||
ref_amt = flt(reference_details.get(item.get(item_ref_dn)), self.precision(based_on, item))
|
||||
based_on_amt = flt(item.get(based_on))
|
||||
|
||||
if not ref_amt:
|
||||
if based_on_amt: # Skip warning for free items
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"System will not check over billing since amount for Item {0} in {1} is zero"
|
||||
).format(item.item_code, ref_dt),
|
||||
title=_("Warning"),
|
||||
indicator="orange",
|
||||
)
|
||||
continue
|
||||
|
||||
already_billed = self.get_billed_amount_for_item(item, item_ref_dn, based_on)
|
||||
|
||||
total_billed_amt = flt(flt(already_billed) + based_on_amt, self.precision(based_on, item))
|
||||
|
||||
allowance, item_allowance, global_qty_allowance, global_amount_allowance = get_allowance_for(
|
||||
item.item_code, item_allowance, global_qty_allowance, global_amount_allowance, "amount"
|
||||
)
|
||||
|
||||
max_allowed_amt = flt(ref_amt * (100 + allowance) / 100)
|
||||
max_allowed_amt = flt(row.ref_amt * (100 + allowance) / 100)
|
||||
|
||||
if total_billed_amt < 0 and max_allowed_amt < 0:
|
||||
# while making debit note against purchase return entry(purchase receipt) getting overbill error
|
||||
total_billed_amt = abs(total_billed_amt)
|
||||
max_allowed_amt = abs(max_allowed_amt)
|
||||
total_billed_amt, max_allowed_amt = abs(total_billed_amt), abs(max_allowed_amt)
|
||||
|
||||
overbill_amt = total_billed_amt - max_allowed_amt
|
||||
row["max_allowed_amt"] = max_allowed_amt
|
||||
total_overbilled_amt += overbill_amt
|
||||
|
||||
if overbill_amt > 0.01 and role_allowed_to_over_bill not in user_roles:
|
||||
if self.doctype != "Purchase Invoice":
|
||||
self.throw_overbill_exception(item, max_allowed_amt)
|
||||
elif not cint(
|
||||
if overbill_amt > precision_allowance and not is_overbilling_allowed:
|
||||
if self.doctype != "Purchase Invoice" or not cint(
|
||||
frappe.db.get_single_value(
|
||||
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
|
||||
)
|
||||
):
|
||||
self.throw_overbill_exception(item, max_allowed_amt)
|
||||
overbilled_items.append(row)
|
||||
|
||||
if role_allowed_to_over_bill in user_roles and total_overbilled_amt > 0.1:
|
||||
if overbilled_items:
|
||||
self.throw_overbill_exception(overbilled_items, precision)
|
||||
|
||||
if is_overbilling_allowed and total_overbilled_amt > 0.1:
|
||||
frappe.msgprint(
|
||||
_("Overbilling of {} ignored because you have {} role.").format(
|
||||
total_overbilled_amt, role_allowed_to_over_bill
|
||||
total_overbilled_amt, role_allowed_to_overbill
|
||||
),
|
||||
indicator="orange",
|
||||
alert=True,
|
||||
@@ -2112,55 +2100,88 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
)
|
||||
|
||||
def get_billed_amount_for_item(self, item, item_ref_dn, based_on):
|
||||
def get_reference_wise_billed_amt(self, ref_dt, item_ref_dn, based_on):
|
||||
"""
|
||||
Returns Sum of Amount of
|
||||
Sales/Purchase Invoice Items
|
||||
that are linked to `item_ref_dn` (`dn_detail` / `pr_detail`)
|
||||
that are submitted OR not submitted but are under current invoice
|
||||
"""
|
||||
reference_names = [d.get(item_ref_dn) for d in self.items if d.get(item_ref_dn)]
|
||||
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Sum
|
||||
if not reference_names:
|
||||
return
|
||||
|
||||
item_doctype = frappe.qb.DocType(item.doctype)
|
||||
ref_wise_billed_amount = {}
|
||||
precision = self.precision(based_on, "items")
|
||||
reference_details = self.get_billing_reference_details(reference_names, ref_dt + " Item", based_on)
|
||||
already_billed = self.get_already_billed_amount(reference_names, item_ref_dn, based_on)
|
||||
|
||||
for item in self.items:
|
||||
key = item.get(item_ref_dn)
|
||||
if not key:
|
||||
continue
|
||||
|
||||
ref_amt = flt(reference_details.get(key), precision)
|
||||
current_amount = flt(item.get(based_on), precision)
|
||||
|
||||
if not ref_amt:
|
||||
if current_amount: # Skip warning for free items
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"System will not check over billing since amount for Item {0} in {1} is zero"
|
||||
).format(item.item_code, ref_dt),
|
||||
title=_("Warning"),
|
||||
indicator="orange",
|
||||
)
|
||||
continue
|
||||
|
||||
ref_wise_billed_amount.setdefault(
|
||||
key,
|
||||
frappe._dict(item_code=item.item_code, billed_amt=0.0, ref_amt=ref_amt, rows=[]),
|
||||
)
|
||||
|
||||
ref_wise_billed_amount[key]["rows"].append(item.idx)
|
||||
ref_wise_billed_amount[key]["ref_amt"] = ref_amt
|
||||
ref_wise_billed_amount[key]["billed_amt"] += current_amount
|
||||
if key in already_billed:
|
||||
ref_wise_billed_amount[key]["billed_amt"] += flt(already_billed.pop(key, 0), precision)
|
||||
|
||||
return ref_wise_billed_amount
|
||||
|
||||
def get_already_billed_amount(self, reference_names, item_ref_dn, based_on):
|
||||
item_doctype = frappe.qb.DocType(self.items[0].doctype)
|
||||
based_on_field = frappe.qb.Field(based_on)
|
||||
join_field = frappe.qb.Field(item_ref_dn)
|
||||
|
||||
result = (
|
||||
frappe.qb.from_(item_doctype)
|
||||
.select(Sum(based_on_field))
|
||||
.where(join_field == item.get(item_ref_dn))
|
||||
.where(
|
||||
Criterion.any(
|
||||
[ # select all items from other invoices OR current invoices
|
||||
Criterion.all(
|
||||
[ # for selecting items from other invoices
|
||||
item_doctype.docstatus == 1,
|
||||
item_doctype.parent != self.name,
|
||||
]
|
||||
),
|
||||
Criterion.all(
|
||||
[ # for selecting items from current invoice, that are linked to same reference
|
||||
item_doctype.docstatus == 0,
|
||||
item_doctype.parent == self.name,
|
||||
item_doctype.name != item.name,
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
).run()
|
||||
|
||||
return result[0][0] if result else 0
|
||||
|
||||
def throw_overbill_exception(self, item, max_allowed_amt):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings"
|
||||
).format(item.item_code, item.idx, max_allowed_amt)
|
||||
return frappe._dict(
|
||||
(
|
||||
frappe.qb.from_(item_doctype)
|
||||
.select(join_field, Sum(based_on_field))
|
||||
.where(join_field.isin(reference_names))
|
||||
.where((item_doctype.docstatus == 1) & (item_doctype.parent != self.name))
|
||||
.groupby(join_field)
|
||||
).run()
|
||||
)
|
||||
|
||||
def throw_overbill_exception(self, overbilled_items, precision):
|
||||
message = (
|
||||
_("<p>Cannot overbill for the following Items:</p>")
|
||||
+ "<ul>"
|
||||
+ "".join(
|
||||
_("<li>Item {0} in row(s) {1} billed more than {2}</li>").format(
|
||||
frappe.bold(item.item_code),
|
||||
", ".join(str(x) for x in item.rows),
|
||||
frappe.bold(fmt_money(item.max_allowed_amt, precision=precision, currency=self.currency)),
|
||||
)
|
||||
for item in overbilled_items
|
||||
)
|
||||
+ "</ul>"
|
||||
)
|
||||
message += _("<p>To allow over-billing, please set allowance in Accounts Settings.</p>")
|
||||
|
||||
frappe.throw(_(message))
|
||||
|
||||
def get_company_default(self, fieldname, ignore_validation=False):
|
||||
from erpnext.accounts.utils import get_company_default
|
||||
|
||||
@@ -2232,6 +2253,7 @@ class AccountsController(TransactionBase):
|
||||
def set_advance_payment_status(self):
|
||||
new_status = None
|
||||
|
||||
PaymentRequest = frappe.qb.DocType("Payment Request")
|
||||
paid_amount = frappe.get_value(
|
||||
doctype="Payment Request",
|
||||
filters={
|
||||
@@ -2239,7 +2261,7 @@ class AccountsController(TransactionBase):
|
||||
"reference_name": self.name,
|
||||
"docstatus": 1,
|
||||
},
|
||||
fieldname="sum(grand_total - outstanding_amount)",
|
||||
fieldname=Sum(PaymentRequest.grand_total - PaymentRequest.outstanding_amount),
|
||||
)
|
||||
|
||||
if not paid_amount:
|
||||
|
||||
@@ -80,6 +80,21 @@ class BuyingController(SubcontractingController):
|
||||
),
|
||||
)
|
||||
|
||||
def validate_posting_date_with_po(self):
|
||||
po_list = []
|
||||
for item in self.items:
|
||||
if item.purchase_order and item.purchase_order not in po_list:
|
||||
po_list.append(item.purchase_order)
|
||||
|
||||
for po in po_list:
|
||||
po_posting_date = frappe.get_value("Purchase Order", po, "transaction_date")
|
||||
if getdate(po_posting_date) > getdate(self.posting_date):
|
||||
frappe.throw(
|
||||
_("Posting Date {0} cannot be before Purchase Order Posting Date {1}").format(
|
||||
frappe.bold(self.posting_date), frappe.bold(po_posting_date)
|
||||
)
|
||||
)
|
||||
|
||||
def create_package_for_transfer(self) -> None:
|
||||
"""Create serial and batch package for Sourece Warehouse in case of inter transfer."""
|
||||
|
||||
@@ -241,18 +256,6 @@ class BuyingController(SubcontractingController):
|
||||
|
||||
return [d.item_code for d in self.items if d.is_fixed_asset]
|
||||
|
||||
def set_landed_cost_voucher_amount(self):
|
||||
for d in self.get("items"):
|
||||
lc_voucher_data = frappe.db.sql(
|
||||
"""select sum(applicable_charges), cost_center
|
||||
from `tabLanded Cost Item`
|
||||
where docstatus = 1 and purchase_receipt_item = %s and receipt_document = %s""",
|
||||
(d.name, self.name),
|
||||
)
|
||||
d.landed_cost_voucher_amount = lc_voucher_data[0][0] if lc_voucher_data else 0.0
|
||||
if not d.cost_center and lc_voucher_data and lc_voucher_data[0][1]:
|
||||
d.db_set("cost_center", lc_voucher_data[0][1])
|
||||
|
||||
def validate_from_warehouse(self):
|
||||
for item in self.get("items"):
|
||||
if item.get("from_warehouse") and (item.get("from_warehouse") == item.get("warehouse")):
|
||||
|
||||
@@ -242,7 +242,7 @@ def enqueue_multiple_variant_creation(item, args, use_template_image=False):
|
||||
item=item,
|
||||
args=args,
|
||||
use_template_image=use_template_image,
|
||||
now=frappe.flags.in_test,
|
||||
now=frappe.in_test,
|
||||
)
|
||||
return "queued"
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.query_builder.functions import Abs
|
||||
from frappe.utils import cint, flt, format_datetime, get_datetime
|
||||
|
||||
import erpnext
|
||||
@@ -661,7 +662,8 @@ def get_rate_for_return(
|
||||
if voucher_type in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"):
|
||||
select_field = "incoming_rate"
|
||||
else:
|
||||
select_field = "abs(stock_value_difference / actual_qty)"
|
||||
StockLedgerEntry = frappe.qb.DocType("Stock Ledger Entry")
|
||||
select_field = Abs(StockLedgerEntry.stock_value_difference / StockLedgerEntry.actual_qty)
|
||||
|
||||
rate = flt(frappe.db.get_value("Stock Ledger Entry", filters, select_field))
|
||||
if not (rate and return_against) and voucher_type in ["Sales Invoice", "Delivery Note"]:
|
||||
@@ -683,6 +685,14 @@ def get_rate_for_return(
|
||||
raise_error_if_no_rate=False,
|
||||
)
|
||||
|
||||
if not rate and voucher_type in ["Sales Invoice", "Delivery Note"]:
|
||||
details = frappe.db.get_value(
|
||||
voucher_type + " Item", voucher_detail_no, ["rate", "allow_zero_valuation_rate"], as_dict=1
|
||||
)
|
||||
|
||||
if details and not details.allow_zero_valuation_rate:
|
||||
rate = flt(details.rate)
|
||||
|
||||
return rate
|
||||
|
||||
|
||||
|
||||
@@ -527,6 +527,15 @@ class SellingController(StockController):
|
||||
self.doctype, self.name, d.item_code, self.return_against, item_row=d
|
||||
)
|
||||
|
||||
if (
|
||||
self.get("is_return")
|
||||
and not d.incoming_rate
|
||||
and not self.get("return_against")
|
||||
and not self.is_internal_transfer()
|
||||
and not d.get("allow_zero_valuation_rate")
|
||||
):
|
||||
d.incoming_rate = d.rate
|
||||
|
||||
# For internal transfers use incoming rate as the valuation rate
|
||||
if self.is_internal_transfer():
|
||||
if self.doctype == "Delivery Note" or self.get("update_stock"):
|
||||
|
||||
@@ -164,6 +164,17 @@ status_map = {
|
||||
["Draft", None],
|
||||
["Completed", "eval:self.docstatus == 1"],
|
||||
],
|
||||
"Pick List": [
|
||||
["Draft", None],
|
||||
["Open", "eval:self.docstatus == 1"],
|
||||
["Completed", "stock_entry_exists"],
|
||||
[
|
||||
"Partly Delivered",
|
||||
"eval:self.purpose == 'Delivery' and self.delivery_status == 'Partly Delivered'",
|
||||
],
|
||||
["Completed", "eval:self.purpose == 'Delivery' and self.delivery_status == 'Fully Delivered'"],
|
||||
["Cancelled", "eval:self.docstatus == 2"],
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _, bold
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate
|
||||
|
||||
import erpnext
|
||||
@@ -64,6 +65,8 @@ class StockController(AccountsController):
|
||||
self.validate_internal_transfer()
|
||||
self.validate_putaway_capacity()
|
||||
self.reset_conversion_factor()
|
||||
|
||||
def on_update(self):
|
||||
self.check_zero_rate()
|
||||
|
||||
def reset_conversion_factor(self):
|
||||
@@ -229,7 +232,7 @@ class StockController(AccountsController):
|
||||
return
|
||||
|
||||
# To handle test cases
|
||||
if frappe.flags.in_test and frappe.flags.use_serial_and_batch_fields:
|
||||
if frappe.in_test and frappe.flags.use_serial_and_batch_fields:
|
||||
return
|
||||
|
||||
if not table_name:
|
||||
@@ -243,7 +246,11 @@ class StockController(AccountsController):
|
||||
parent_details = self.get_parent_details_for_packed_items()
|
||||
|
||||
for row in self.get(table_name):
|
||||
if row.serial_and_batch_bundle and (row.serial_no or row.batch_no):
|
||||
if (
|
||||
not via_landed_cost_voucher
|
||||
and row.serial_and_batch_bundle
|
||||
and (row.serial_no or row.batch_no)
|
||||
):
|
||||
self.validate_serial_nos_and_batches_with_bundle(row)
|
||||
|
||||
if not row.serial_no and not row.batch_no and not row.get("rejected_serial_no"):
|
||||
@@ -884,6 +891,91 @@ class StockController(AccountsController):
|
||||
|
||||
return sl_dict
|
||||
|
||||
def set_landed_cost_voucher_amount(self):
|
||||
for d in self.get("items"):
|
||||
lcv_item = frappe.qb.DocType("Landed Cost Item")
|
||||
query = (
|
||||
frappe.qb.from_(lcv_item)
|
||||
.select(Sum(lcv_item.applicable_charges), lcv_item.cost_center)
|
||||
.where((lcv_item.docstatus == 1) & (lcv_item.receipt_document == self.name))
|
||||
)
|
||||
|
||||
if self.doctype == "Stock Entry":
|
||||
query = query.where(lcv_item.stock_entry_item == d.name)
|
||||
else:
|
||||
query = query.where(lcv_item.purchase_receipt_item == d.name)
|
||||
|
||||
lc_voucher_data = query.run(as_list=True)
|
||||
|
||||
d.landed_cost_voucher_amount = lc_voucher_data[0][0] if lc_voucher_data else 0.0
|
||||
if not d.cost_center and lc_voucher_data and lc_voucher_data[0][1]:
|
||||
d.db_set("cost_center", lc_voucher_data[0][1])
|
||||
|
||||
def has_landed_cost_amount(self):
|
||||
for row in self.items:
|
||||
if row.get("landed_cost_voucher_amount"):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_item_account_wise_lcv_entries(self):
|
||||
if not self.has_landed_cost_amount():
|
||||
return
|
||||
|
||||
landed_cost_vouchers = frappe.get_all(
|
||||
"Landed Cost Purchase Receipt",
|
||||
fields=["parent"],
|
||||
filters={"receipt_document": self.name, "docstatus": 1},
|
||||
)
|
||||
|
||||
if not landed_cost_vouchers:
|
||||
return
|
||||
|
||||
item_account_wise_cost = {}
|
||||
|
||||
row_fieldname = "purchase_receipt_item"
|
||||
if self.doctype == "Stock Entry":
|
||||
row_fieldname = "stock_entry_item"
|
||||
|
||||
for lcv in landed_cost_vouchers:
|
||||
landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", lcv.parent)
|
||||
|
||||
based_on_field = "applicable_charges"
|
||||
# Use amount field for total item cost for manually cost distributed LCVs
|
||||
if landed_cost_voucher_doc.distribute_charges_based_on != "Distribute Manually":
|
||||
based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on)
|
||||
|
||||
total_item_cost = 0
|
||||
|
||||
if based_on_field:
|
||||
for item in landed_cost_voucher_doc.items:
|
||||
total_item_cost += item.get(based_on_field)
|
||||
|
||||
for item in landed_cost_voucher_doc.items:
|
||||
if item.receipt_document == self.name:
|
||||
for account in landed_cost_voucher_doc.taxes:
|
||||
exchange_rate = account.exchange_rate or 1
|
||||
item_account_wise_cost.setdefault((item.item_code, item.get(row_fieldname)), {})
|
||||
item_account_wise_cost[(item.item_code, item.get(row_fieldname))].setdefault(
|
||||
account.expense_account, {"amount": 0.0, "base_amount": 0.0}
|
||||
)
|
||||
|
||||
item_row = item_account_wise_cost[(item.item_code, item.get(row_fieldname))][
|
||||
account.expense_account
|
||||
]
|
||||
|
||||
if total_item_cost > 0:
|
||||
item_row["amount"] += account.amount * item.get(based_on_field) / total_item_cost
|
||||
|
||||
item_row["base_amount"] += (
|
||||
account.base_amount * item.get(based_on_field) / total_item_cost
|
||||
)
|
||||
else:
|
||||
item_row["amount"] += item.applicable_charges / exchange_rate
|
||||
item_row["base_amount"] += item.applicable_charges
|
||||
|
||||
return item_account_wise_cost
|
||||
|
||||
def update_inventory_dimensions(self, row, sl_dict) -> None:
|
||||
# To handle delivery note and sales invoice
|
||||
if row.get("item_row"):
|
||||
@@ -934,7 +1026,7 @@ class StockController(AccountsController):
|
||||
fieldname = f"{dimension.source_fieldname}"
|
||||
|
||||
sl_dict[dimension.target_fieldname] = row.get(fieldname)
|
||||
return
|
||||
continue
|
||||
|
||||
sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname)
|
||||
else:
|
||||
|
||||
@@ -935,7 +935,10 @@ class TestAccountsController(IntegrationTestCase):
|
||||
self.assertEqual(exc_je_for_si, [])
|
||||
self.assertEqual(exc_je_for_pe, [])
|
||||
|
||||
@IntegrationTestCase.change_settings("Accounts Settings", {"add_taxes_from_item_tax_template": 1})
|
||||
@IntegrationTestCase.change_settings(
|
||||
"Accounts Settings",
|
||||
{"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 1},
|
||||
)
|
||||
def test_18_fetch_taxes_based_on_taxes_and_charges_template(self):
|
||||
# Create a Sales Taxes and Charges Template
|
||||
if not frappe.db.exists("Sales Taxes and Charges Template", "_Test Tax - _TC"):
|
||||
@@ -964,6 +967,30 @@ class TestAccountsController(IntegrationTestCase):
|
||||
|
||||
self.assertEqual(sinv.total_taxes_and_charges, 4.5)
|
||||
|
||||
@IntegrationTestCase.change_settings(
|
||||
"Accounts Settings",
|
||||
{"add_taxes_from_item_tax_template": 1, "add_taxes_from_taxes_and_charges_template": 0},
|
||||
)
|
||||
def test_19_fetch_taxes_based_on_item_tax_template_template(self):
|
||||
# Create a Sales Invoice
|
||||
sinv = frappe.new_doc("Sales Invoice")
|
||||
sinv.customer = self.customer
|
||||
sinv.company = self.company
|
||||
sinv.currency = "INR"
|
||||
sinv.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"qty": 1,
|
||||
"rate": 50,
|
||||
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
|
||||
},
|
||||
)
|
||||
sinv.insert()
|
||||
|
||||
self.assertEqual(sinv.taxes[0].account_head, "_Test Account Excise Duty - _TC")
|
||||
self.assertEqual(sinv.total_taxes_and_charges, 5)
|
||||
|
||||
def test_20_journal_against_sales_invoice(self):
|
||||
# Invoice in Foreign Currency
|
||||
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)
|
||||
|
||||
@@ -5,7 +5,6 @@ import frappe
|
||||
from frappe.core.doctype.user_permission.test_user_permission import create_user
|
||||
from frappe.core.doctype.user_permission.user_permission import add_user_permissions
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
from erpnext.controllers import queries
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
@@ -120,7 +119,7 @@ class TestQueries(ERPNextTestSuite):
|
||||
}
|
||||
)
|
||||
|
||||
with IntegrationTestCase.set_user(user.name):
|
||||
with self.set_user(user.name):
|
||||
params = {
|
||||
"doctype": "Employee",
|
||||
"txt": "",
|
||||
|
||||
@@ -97,10 +97,13 @@ def get_data(filters, conditions):
|
||||
elif filters.get("group_by") == "Supplier":
|
||||
sel_col = "t1.supplier"
|
||||
|
||||
if filters.get("based_on") in ["Item", "Customer", "Supplier"]:
|
||||
if filters.get("based_on") in ["Customer", "Supplier"]:
|
||||
inc = 3
|
||||
elif filters.get("based_on") in ["Item"]:
|
||||
inc = 2
|
||||
else:
|
||||
inc = 1
|
||||
|
||||
data1 = frappe.db.sql(
|
||||
""" select {} from `tab{}` t1, `tab{} Item` t2 {}
|
||||
where t2.parent = t1.name and t1.company = {} and {} between {} and {} and
|
||||
@@ -157,7 +160,7 @@ def get_data(filters, conditions):
|
||||
|
||||
# get data for group_by filter
|
||||
row1 = frappe.db.sql(
|
||||
""" select t1.currency , {} , {} from `tab{}` t1, `tab{} Item` t2 {}
|
||||
""" select t4.default_currency AS currency , {} , {} from `tab{}` t1, `tab{} Item` t2 {}
|
||||
where t2.parent = t1.name and t1.company = {} and {} between {} and {}
|
||||
and t1.docstatus = 1 and {} = {} and {} = {} {} {}
|
||||
""".format(
|
||||
@@ -330,11 +333,20 @@ def based_wise_columns_query(based_on, trans):
|
||||
based_on_details["addl_tables"] = ""
|
||||
|
||||
elif based_on == "Customer":
|
||||
based_on_details["based_on_cols"] = [
|
||||
"Customer:Link/Customer:120",
|
||||
"Territory:Link/Territory:120",
|
||||
]
|
||||
based_on_details["based_on_select"] = "t1.customer_name, t1.territory, "
|
||||
if trans == "Quotation":
|
||||
based_on_details["based_on_cols"] = [
|
||||
"Party:Link/Customer:120",
|
||||
"Party Name:Data:120",
|
||||
"Territory:Link/Territory:120",
|
||||
]
|
||||
based_on_details["based_on_select"] = "t1.party_name, t1.customer_name, t1.territory,"
|
||||
else:
|
||||
based_on_details["based_on_cols"] = [
|
||||
"Customer:Link/Customer:120",
|
||||
"Customer Name:Data:120",
|
||||
"Territory:Link/Territory:120",
|
||||
]
|
||||
based_on_details["based_on_select"] = "t1.customer, t1.customer_name, t1.territory,"
|
||||
based_on_details["based_on_group_by"] = "t1.party_name" if trans == "Quotation" else "t1.customer"
|
||||
based_on_details["addl_tables"] = ""
|
||||
|
||||
@@ -347,9 +359,10 @@ def based_wise_columns_query(based_on, trans):
|
||||
elif based_on == "Supplier":
|
||||
based_on_details["based_on_cols"] = [
|
||||
"Supplier:Link/Supplier:120",
|
||||
"Supplier Name:Data:120",
|
||||
"Supplier Group:Link/Supplier Group:140",
|
||||
]
|
||||
based_on_details["based_on_select"] = "t1.supplier, t3.supplier_group,"
|
||||
based_on_details["based_on_select"] = "t1.supplier, t1.supplier_name, t3.supplier_group,"
|
||||
based_on_details["based_on_group_by"] = "t1.supplier"
|
||||
based_on_details["addl_tables"] = ",`tabSupplier` t3"
|
||||
based_on_details["addl_tables_relational_cond"] = " and t1.supplier = t3.name"
|
||||
@@ -381,8 +394,12 @@ def based_wise_columns_query(based_on, trans):
|
||||
else:
|
||||
frappe.throw(_("Project-wise data is not available for Quotation"))
|
||||
|
||||
based_on_details["based_on_select"] += "t1.currency,"
|
||||
based_on_details["based_on_select"] += "t4.default_currency as currency,"
|
||||
based_on_details["based_on_cols"].append("Currency:Link/Currency:120")
|
||||
based_on_details["addl_tables"] += ", `tabCompany` t4"
|
||||
based_on_details["addl_tables_relational_cond"] = (
|
||||
based_on_details.get("addl_tables_relational_cond", "") + " and t1.company = t4.name"
|
||||
)
|
||||
|
||||
return based_on_details
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "CON-.YYYY.-.#####",
|
||||
"creation": "2018-04-12 06:32:04.582486",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -256,10 +257,11 @@
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-05-23 13:54:03.346537",
|
||||
"modified": "2025-06-19 17:48:45.049007",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Contract",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -324,10 +326,12 @@
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "party_type, party_name, contract_template",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "party_name",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
|
||||
@@ -46,19 +46,6 @@ class Contract(Document):
|
||||
status: DF.Literal["Unsigned", "Active", "Inactive"]
|
||||
# end: auto-generated types
|
||||
|
||||
def autoname(self):
|
||||
name = self.party_name
|
||||
|
||||
if self.contract_template:
|
||||
name += f" - {self.contract_template} Agreement"
|
||||
|
||||
# If identical, append contract name with the next number in the iteration
|
||||
if frappe.db.exists("Contract", name):
|
||||
count = len(frappe.get_all("Contract", filters={"name": ["like", f"%{name}%"]}))
|
||||
name = f"{name} - {count}"
|
||||
|
||||
self.name = _(name)
|
||||
|
||||
def validate(self):
|
||||
self.set_missing_values()
|
||||
self.validate_dates()
|
||||
|
||||
@@ -4,18 +4,19 @@
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Opportunity",
|
||||
"dynamic_filters_json": "[[\"Opportunity\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Opportunity\",\"company\",\"=\",null]]",
|
||||
"filters_json": "[]",
|
||||
"function": "Count",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "Open Opportunity",
|
||||
"modified": "2024-12-05 12:00:00.000000",
|
||||
"modified": "2025-06-24 11:10:17.468713",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Open Opportunity",
|
||||
"owner": "Administrator",
|
||||
"show_full_number": 0,
|
||||
"show_percentage_stats": 1,
|
||||
"stats_time_interval": "Daily",
|
||||
"type": "Document Type"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ def validate_webhooks_request(doctype, hmac_key, secret_key="secret"):
|
||||
def innerfn(fn):
|
||||
settings = frappe.get_doc(doctype)
|
||||
|
||||
if frappe.request and settings and settings.get(secret_key) and not frappe.flags.in_test:
|
||||
if frappe.request and settings and settings.get(secret_key) and not frappe.in_test:
|
||||
sig = base64.b64encode(
|
||||
hmac.new(
|
||||
settings.get(secret_key).encode("utf8"), frappe.request.data, hashlib.sha256
|
||||
|
||||
3216
erpnext/locale/ar.po
3216
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
3216
erpnext/locale/bs.po
3216
erpnext/locale/bs.po
File diff suppressed because it is too large
Load Diff
61544
erpnext/locale/cs.po
Normal file
61544
erpnext/locale/cs.po
Normal file
File diff suppressed because it is too large
Load Diff
3286
erpnext/locale/de.po
3286
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
3216
erpnext/locale/eo.po
3216
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
3216
erpnext/locale/es.po
3216
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
3220
erpnext/locale/fa.po
3220
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
3216
erpnext/locale/fr.po
3216
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
3210
erpnext/locale/hr.po
3210
erpnext/locale/hr.po
File diff suppressed because it is too large
Load Diff
3214
erpnext/locale/hu.po
3214
erpnext/locale/hu.po
File diff suppressed because it is too large
Load Diff
102087
erpnext/locale/it.po
102087
erpnext/locale/it.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
102085
erpnext/locale/nl.po
102085
erpnext/locale/nl.po
File diff suppressed because it is too large
Load Diff
3210
erpnext/locale/pl.po
3210
erpnext/locale/pl.po
File diff suppressed because it is too large
Load Diff
3210
erpnext/locale/pt.po
3210
erpnext/locale/pt.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
3214
erpnext/locale/ru.po
3214
erpnext/locale/ru.po
File diff suppressed because it is too large
Load Diff
61544
erpnext/locale/sr.po
Normal file
61544
erpnext/locale/sr.po
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
3366
erpnext/locale/sv.po
3366
erpnext/locale/sv.po
File diff suppressed because it is too large
Load Diff
3210
erpnext/locale/th.po
3210
erpnext/locale/th.po
File diff suppressed because it is too large
Load Diff
3218
erpnext/locale/tr.po
3218
erpnext/locale/tr.po
File diff suppressed because it is too large
Load Diff
102087
erpnext/locale/vi.po
102087
erpnext/locale/vi.po
File diff suppressed because it is too large
Load Diff
3216
erpnext/locale/zh.po
3216
erpnext/locale/zh.po
File diff suppressed because it is too large
Load Diff
@@ -134,6 +134,7 @@
|
||||
"fieldname": "quantity",
|
||||
"fieldtype": "Float",
|
||||
"label": "Quantity",
|
||||
"non_negative": 1,
|
||||
"oldfieldname": "quantity",
|
||||
"oldfieldtype": "Currency",
|
||||
"reqd": 1
|
||||
@@ -663,7 +664,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-03 16:24:47.518411",
|
||||
"modified": "2025-06-16 16:13:22.497695",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM",
|
||||
@@ -696,10 +697,11 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "item, item_name",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1302,7 +1302,7 @@ def validate_bom_no(item, bom_no):
|
||||
if not bom.is_active:
|
||||
frappe.throw(_("BOM {0} must be active").format(bom_no))
|
||||
if bom.docstatus != 1:
|
||||
if not getattr(frappe.flags, "in_test", False):
|
||||
if not frappe.in_test:
|
||||
frappe.throw(_("BOM {0} must be submitted").format(bom_no))
|
||||
if item:
|
||||
rm_item_exists = False
|
||||
|
||||
@@ -108,7 +108,7 @@ class BOMUpdateLog(Document):
|
||||
doc=self,
|
||||
boms=boms,
|
||||
timeout=40000,
|
||||
now=frappe.flags.in_test,
|
||||
now=frappe.in_test,
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
else:
|
||||
@@ -116,7 +116,7 @@ class BOMUpdateLog(Document):
|
||||
method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.process_boms_cost_level_wise",
|
||||
queue="long",
|
||||
update_doc=self,
|
||||
now=frappe.flags.in_test,
|
||||
now=frappe.in_test,
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
|
||||
@@ -128,7 +128,7 @@ def run_replace_bom_job(
|
||||
try:
|
||||
doc.db_set("status", "In Progress")
|
||||
|
||||
if not frappe.flags.in_test:
|
||||
if not frappe.in_test:
|
||||
frappe.db.commit()
|
||||
|
||||
frappe.db.auto_commit_on_many_writes = 1
|
||||
@@ -141,7 +141,7 @@ def run_replace_bom_job(
|
||||
finally:
|
||||
frappe.db.auto_commit_on_many_writes = 0
|
||||
|
||||
if not frappe.flags.in_test:
|
||||
if not frappe.in_test:
|
||||
frappe.db.commit() # nosemgrep
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ def queue_bom_cost_jobs(current_boms_list: list[str], update_doc: "BOMUpdateLog"
|
||||
bom_list=boms_to_process,
|
||||
batch_name=batch_row.name,
|
||||
queue="long",
|
||||
now=frappe.flags.in_test,
|
||||
now=frappe.in_test,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ def update_cost_in_level(doc: "BOMUpdateLog", bom_list: list[str], batch_name: i
|
||||
except Exception:
|
||||
handle_exception(doc)
|
||||
finally:
|
||||
if not frappe.flags.in_test:
|
||||
if not frappe.in_test:
|
||||
frappe.db.commit() # nosemgrep
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ def update_cost_in_boms(bom_list: list[str]) -> None:
|
||||
bom_doc.calculate_cost(save_updates=True, update_hour_rate=True)
|
||||
bom_doc.db_update()
|
||||
|
||||
if (index % 50 == 0) and not frappe.flags.in_test:
|
||||
if (index % 50 == 0) and not frappe.in_test:
|
||||
frappe.db.commit() # nosemgrep
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ def set_values_in_log(log_name: str, values: dict[str, Any], commit: bool = Fals
|
||||
query = query.set(key, value)
|
||||
query.run()
|
||||
|
||||
if commit and not frappe.flags.in_test:
|
||||
if commit and not frappe.in_test:
|
||||
frappe.db.commit() # nosemgrep
|
||||
|
||||
|
||||
|
||||
@@ -45,8 +45,7 @@ frappe.ui.form.on("Job Card", {
|
||||
|
||||
setup_stock_entry(frm) {
|
||||
if (
|
||||
frm.doc.manufactured_qty &&
|
||||
frm.doc.finished_good &&
|
||||
frm.doc.track_semi_finished_goods &&
|
||||
frm.doc.docstatus === 1 &&
|
||||
!frm.doc.is_subcontracted &&
|
||||
flt(frm.doc.for_quantity) + flt(frm.doc.process_loss_qty) > flt(frm.doc.manufactured_qty)
|
||||
@@ -252,7 +251,6 @@ frappe.ui.form.on("Job Card", {
|
||||
fieldtype: "Float",
|
||||
label: __("Process Loss Quantity"),
|
||||
fieldname: "process_loss_qty",
|
||||
reqd: 1,
|
||||
onchange() {
|
||||
let doc = frm.job_completion_dialog;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user