mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-29 22:08:35 +00:00
Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9a9224ec4 | ||
|
|
b08de1f1e5 | ||
|
|
e46be25e9f | ||
|
|
570c67bb34 | ||
|
|
e71b066eec | ||
|
|
e9a26b5086 | ||
|
|
872c86e223 | ||
|
|
d54938fa64 | ||
|
|
4555c323af | ||
|
|
1d0edf1b9a | ||
|
|
2b6f2c2f9c | ||
|
|
00ba64baae | ||
|
|
3dc128881c | ||
|
|
d40c36a4b1 | ||
|
|
7b50f67b55 | ||
|
|
b40445fe44 | ||
|
|
77121f2a41 | ||
|
|
a80de9bd01 | ||
|
|
be605adbc1 | ||
|
|
29323cb0b1 | ||
|
|
17324ec45b | ||
|
|
f5d05b969b | ||
|
|
c788106011 | ||
|
|
8b56b7ba0e | ||
|
|
2035fac494 | ||
|
|
e5c9e7abdc | ||
|
|
b4f2b6cab2 | ||
|
|
373476042e | ||
|
|
c066880978 | ||
|
|
7a1def07e9 | ||
|
|
0051950afb | ||
|
|
97279c7e26 | ||
|
|
0be5ba6bac | ||
|
|
a67b489d17 | ||
|
|
3df7a28476 | ||
|
|
694ebb53ec | ||
|
|
9643720858 | ||
|
|
bcd72a7fec | ||
|
|
768425ebf1 | ||
|
|
5d031c0a04 | ||
|
|
476054f684 | ||
|
|
570ef45e46 | ||
|
|
2462be0e61 | ||
|
|
d3631860db | ||
|
|
3b734f4d5d | ||
|
|
f3307b3ca9 | ||
|
|
82e1221dc9 | ||
|
|
4d055d374a | ||
|
|
8e3fbab94a | ||
|
|
6f9954bb62 | ||
|
|
3f53af8b1f | ||
|
|
9469889bd5 | ||
|
|
6e61ee8d70 | ||
|
|
dcf076aad6 | ||
|
|
edd18fd650 | ||
|
|
62209348a4 | ||
|
|
537225494c | ||
|
|
bbb3181c6e | ||
|
|
20b14395e3 | ||
|
|
6ac699d3bb | ||
|
|
e6e5591088 | ||
|
|
635c51acf7 | ||
|
|
e605675e11 | ||
|
|
bf58393fda | ||
|
|
88ce356d62 | ||
|
|
396feadace | ||
|
|
c0dab55fcc | ||
|
|
cb47745d8c | ||
|
|
f4b827cb3d | ||
|
|
dc9ae20db8 | ||
|
|
6cb42ab8b1 | ||
|
|
35e06045bd | ||
|
|
d47aa4917a | ||
|
|
6569fa2b9f | ||
|
|
6c37acc180 | ||
|
|
42c121a750 | ||
|
|
1e027364e3 | ||
|
|
d2fee32eb3 | ||
|
|
21912402c0 | ||
|
|
43b355eaf6 | ||
|
|
175aac4156 | ||
|
|
30650f298b | ||
|
|
3110ab1c57 | ||
|
|
40110d83c9 | ||
|
|
dbc831e008 | ||
|
|
686437bd54 | ||
|
|
58d5f39e0a | ||
|
|
c7dbedbfdc | ||
|
|
8e21af0a63 | ||
|
|
87e498cd7d | ||
|
|
f8aa4c730c | ||
|
|
a335838691 | ||
|
|
1f075d4bbf | ||
|
|
7f441864d6 | ||
|
|
2b28b7e694 | ||
|
|
56d9cbabbf | ||
|
|
4481efec17 | ||
|
|
84a1a51023 | ||
|
|
98f45221e6 | ||
|
|
846e0a9f06 | ||
|
|
6185507614 | ||
|
|
d051407126 | ||
|
|
3d91e021a3 | ||
|
|
a6310351fd |
1
.github/workflows/patch.yml
vendored
1
.github/workflows/patch.yml
vendored
@@ -134,6 +134,7 @@ jobs:
|
||||
|
||||
# Resetup env and install apps
|
||||
pgrep honcho | xargs kill
|
||||
sleep 10
|
||||
rm -rf ~/frappe-bench/env
|
||||
bench -v setup env --python python$2
|
||||
bench pip install -e ./apps/erpnext
|
||||
|
||||
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "16.23.0"
|
||||
__version__ = "16.25.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -10,7 +10,7 @@ frappe.ui.form.on("Accounts Settings", {
|
||||
},
|
||||
};
|
||||
});
|
||||
if (!frm.naming_controller) frm.naming_controller = new erpnext.NamingSeriesController(frm);
|
||||
if (!frm.naming_controller) frm.naming_controller = new frappe.ui.NamingSeriesController(frm);
|
||||
|
||||
frm.naming_controller.render_table("transaction_naming_html", get_transactions(frm));
|
||||
},
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"period_closing_settings_section",
|
||||
"ignore_account_closing_balance",
|
||||
"use_legacy_controller_for_pcv",
|
||||
"pcv_job_timeout",
|
||||
"column_break_25",
|
||||
"reports_tab",
|
||||
"remarks_section",
|
||||
@@ -611,6 +612,14 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Use legacy controller for Period Closing Voucher"
|
||||
},
|
||||
{
|
||||
"default": "3600",
|
||||
"depends_on": "eval: !doc.use_legacy_controller_for_pcv",
|
||||
"description": "Timeout (in seconds) for each background job enqueued by Process Period Closing Voucher",
|
||||
"fieldname": "pcv_job_timeout",
|
||||
"fieldtype": "Int",
|
||||
"label": "PCV Job Timeout (seconds)"
|
||||
},
|
||||
{
|
||||
"description": "Users with this role will be notified if the asset depreciation gets failed",
|
||||
"fieldname": "role_to_notify_on_depreciation_failure",
|
||||
@@ -748,7 +757,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-06-15 18:26:50.778723",
|
||||
"modified": "2026-06-24 12:59:41.868865",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -90,6 +90,7 @@ class AccountsSettings(Document):
|
||||
make_payment_via_journal_entry: DF.Check
|
||||
merge_similar_account_heads: DF.Check
|
||||
over_billing_allowance: DF.Currency
|
||||
pcv_job_timeout: DF.Int
|
||||
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"]
|
||||
receivable_payable_remarks_length: DF.Int
|
||||
reconciliation_queue_size: DF.Int
|
||||
|
||||
@@ -22,11 +22,13 @@ class TestAdvancePaymentLedgerEntry(ERPNextTestSuite, AccountsTestMixin):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_usd_receivable_account()
|
||||
self.create_usd_payable_account()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.supplier = "_Test Supplier"
|
||||
self.item = "_Test Item"
|
||||
self.cash = "Cash - _TC"
|
||||
self.debtors_usd = "_Test Receivable USD - _TC"
|
||||
self.creditors_usd = "_Test Payable USD - _TC"
|
||||
|
||||
def create_sales_order(self, qty=1, rate=100, currency="INR", do_not_submit=False):
|
||||
"""
|
||||
|
||||
@@ -9,6 +9,13 @@ cur_frm.add_fetch("bank", "swift_number", "swift_number");
|
||||
|
||||
frappe.ui.form.on("Bank Guarantee", {
|
||||
setup: function (frm) {
|
||||
frm.set_query("reference_doctype", function () {
|
||||
return {
|
||||
filters: {
|
||||
name: ["in", ["Sales Order", "Purchase Order"]],
|
||||
},
|
||||
};
|
||||
});
|
||||
frm.set_query("bank_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"autoname": "ACC-BG-.YYYY.-.#####",
|
||||
"creation": "2016-12-17 10:43:35.731631",
|
||||
"doctype": "DocType",
|
||||
@@ -50,8 +51,7 @@
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Link",
|
||||
"label": "Reference Document Type",
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_docname",
|
||||
@@ -60,14 +60,14 @@
|
||||
"options": "reference_doctype"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.bg_type == \"Receiving\"",
|
||||
"depends_on": "eval: doc.reference_doctype == \"Sales Order\"",
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"label": "Customer",
|
||||
"options": "Customer"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.bg_type == \"Providing\"",
|
||||
"depends_on": "eval: doc.reference_doctype == \"Purchase Order\"",
|
||||
"fieldname": "supplier",
|
||||
"fieldtype": "Link",
|
||||
"label": "Supplier",
|
||||
@@ -218,10 +218,11 @@
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-29 11:52:33.550847",
|
||||
"modified": "2026-05-25 18:12:10.768835",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Guarantee",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
@@ -1078,7 +1078,7 @@ def reconcile_vouchers(bank_transaction_name: str | int, vouchers: str, is_new_v
|
||||
@frappe.whitelist()
|
||||
def get_linked_payments(
|
||||
bank_transaction_name: str | int,
|
||||
document_types: list[str] | None = None,
|
||||
document_types: str | list[str] | None = None,
|
||||
from_date: str | date | None = None,
|
||||
to_date: str | date | None = None,
|
||||
filter_by_reference_date: bool | None = None,
|
||||
|
||||
@@ -17,9 +17,10 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.bank = "HDFC - _TC"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
bank_dt = qb.DocType("Bank")
|
||||
qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
|
||||
self.create_bank_account()
|
||||
|
||||
@@ -26,9 +26,9 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestBankStatementImportLog(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.bank = "HDFC - _TC"
|
||||
bank_dt = qb.DocType("Bank")
|
||||
qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
|
||||
self.create_bank_account()
|
||||
|
||||
@@ -11,9 +11,11 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestBankTransactionRule(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.bank = "HDFC - _TC"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cash = "Cash - _TC"
|
||||
bank_dt = qb.DocType("Bank")
|
||||
qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
|
||||
self.create_bank_account()
|
||||
|
||||
@@ -136,6 +136,9 @@ function set_total_budget_amount(frm) {
|
||||
function toggle_distribution_fields(frm) {
|
||||
const grid = frm.fields_dict.budget_distribution.grid;
|
||||
|
||||
frm.set_df_property("budget_distribution", "cannot_add_rows", true);
|
||||
frm.set_df_property("budget_distribution", "cannot_delete_rows", true);
|
||||
|
||||
["amount", "percent"].forEach((field) => {
|
||||
grid.update_docfield_property(field, "read_only", frm.doc.distribute_equally);
|
||||
});
|
||||
|
||||
@@ -159,9 +159,9 @@ class Budget(Document):
|
||||
frappe.throw(_("Account {0} does not belong to company {1}").format(self.account, self.company))
|
||||
elif account_details.report_type != "Profit and Loss":
|
||||
frappe.throw(
|
||||
_("Budget cannot be assigned against {0}, as it's not an Income or Expense account").format(
|
||||
self.account
|
||||
)
|
||||
_(
|
||||
"Budget cannot be assigned against {0}, as its Root Type is not of Income or Expense"
|
||||
).format(self.account)
|
||||
)
|
||||
|
||||
def set_null_value(self):
|
||||
@@ -355,8 +355,8 @@ class Budget(Document):
|
||||
if self.should_regenerate_budget_distribution():
|
||||
return
|
||||
|
||||
total_amount = sum(d.amount for d in self.budget_distribution)
|
||||
total_percent = sum(d.percent for d in self.budget_distribution)
|
||||
total_amount = sum(flt(d.amount) for d in self.budget_distribution)
|
||||
total_percent = sum(flt(d.percent) for d in self.budget_distribution)
|
||||
|
||||
if flt(abs(total_amount - self.budget_amount), 2) > 0.10:
|
||||
frappe.throw(
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Start Date",
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
@@ -25,26 +26,29 @@
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "End Date",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount"
|
||||
"label": "Amount",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "percent",
|
||||
"fieldtype": "Percent",
|
||||
"in_list_view": 1,
|
||||
"label": "Percent"
|
||||
"label": "Percent",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-03 13:18:28.398198",
|
||||
"modified": "2026-06-18 11:23:17.669733",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Budget Distribution",
|
||||
|
||||
@@ -15,12 +15,12 @@ class BudgetDistribution(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
amount: DF.Currency
|
||||
end_date: DF.Date | None
|
||||
end_date: DF.Date
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
percent: DF.Percent
|
||||
start_date: DF.Date | None
|
||||
start_date: DF.Date
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
||||
@@ -75,7 +75,10 @@ def validate_company(company):
|
||||
|
||||
@frappe.whitelist()
|
||||
def import_coa(file_name, company):
|
||||
frappe.only_for("Accounts Manager")
|
||||
|
||||
# delete existing data for accounts
|
||||
frappe.has_permission("Company", "write", company, throw=True)
|
||||
unset_existing_data(company)
|
||||
|
||||
# create accounts
|
||||
@@ -451,6 +454,7 @@ def unset_existing_data(company):
|
||||
fieldnames = get_linked_fields("Account").get("Company", {}).get("fieldname", [])
|
||||
linked = [{"fieldname": name} for name in fieldnames]
|
||||
update_values = {d.get("fieldname"): "" for d in linked}
|
||||
|
||||
frappe.db.set_value("Company", company, update_values, update_values)
|
||||
|
||||
# remove accounts data from various doctypes
|
||||
@@ -462,8 +466,7 @@ def unset_existing_data(company):
|
||||
"Sales Taxes and Charges Template",
|
||||
"Purchase Taxes and Charges Template",
|
||||
]:
|
||||
dt = frappe.qb.DocType(doctype)
|
||||
frappe.qb.from_(dt).where(dt.company == company).delete().run()
|
||||
frappe.get_query(doctype, delete=True, filters={"company": company}, ignore_permissions=False).run()
|
||||
|
||||
|
||||
def set_default_accounts(company):
|
||||
|
||||
@@ -616,6 +616,10 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
|
||||
def get_account_details(
|
||||
company, posting_date, account, party_type=None, party=None, rounding_loss_allowance: float | None = None
|
||||
):
|
||||
if not account:
|
||||
return
|
||||
frappe.has_permission("Account", doc=account, throw=True)
|
||||
|
||||
if not (company and posting_date):
|
||||
frappe.throw(_("Company and Posting Date is mandatory"))
|
||||
|
||||
|
||||
@@ -15,11 +15,11 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_usd_receivable_account()
|
||||
self.create_item()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
self.company = "_Test Company"
|
||||
self.item = "_Test Item"
|
||||
self.customer = "_Test Customer"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.debtors_usd = "_Test Receivable USD - _TC"
|
||||
self.set_system_and_company_settings()
|
||||
|
||||
def set_system_and_company_settings(self):
|
||||
|
||||
@@ -19,6 +19,7 @@ from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger
|
||||
validate_docs_for_voucher_types,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import JournalTaxWithholding
|
||||
from erpnext.accounts.general_ledger import validate_opening_entry_against_pcv
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
@@ -131,6 +132,9 @@ class JournalEntry(AccountsController):
|
||||
if not self.is_opening:
|
||||
self.is_opening = "No"
|
||||
|
||||
if self.is_opening == "Yes":
|
||||
validate_opening_entry_against_pcv(self.company)
|
||||
|
||||
self.clearance_date = None
|
||||
|
||||
self.validate_party()
|
||||
|
||||
@@ -12,10 +12,11 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestLedgerHealth(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.income_account = "Sales - _TC"
|
||||
self.configure_monitoring_tool()
|
||||
self.clear_old_entries()
|
||||
|
||||
def configure_monitoring_tool(self):
|
||||
monitor_settings = frappe.get_doc("Ledger Health Monitor")
|
||||
|
||||
@@ -74,29 +74,31 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
|
||||
},
|
||||
|
||||
setup_company_filters: function (frm) {
|
||||
frm.set_query("cost_center", "invoices", function (doc, cdt, cdn) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
frm.events.apply_company_query_filter(frm, "cost_center", "invoices", { is_group: 0 });
|
||||
frm.events.apply_company_query_filter(frm, "project", "invoices");
|
||||
frm.events.apply_company_query_filter(frm, "project");
|
||||
frm.events.apply_company_query_filter(frm, "cost_center", undefined, { is_group: 0 });
|
||||
frm.events.apply_company_query_filter(frm, "temporary_opening_account", "invoices", {
|
||||
account_type: "Temporary",
|
||||
is_group: 0,
|
||||
});
|
||||
},
|
||||
|
||||
frm.set_query("cost_center", function (doc) {
|
||||
apply_company_query_filter: function (frm, field_name, child_doctype = null, filters = {}) {
|
||||
const query = function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
...filters,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
frm.set_query("temporary_opening_account", "invoices", function (doc, cdt, cdn) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
if (child_doctype) {
|
||||
frm.set_query(field_name, child_doctype, query);
|
||||
} else {
|
||||
frm.set_query(field_name, query);
|
||||
}
|
||||
},
|
||||
|
||||
company: function (frm) {
|
||||
@@ -120,11 +122,6 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
|
||||
},
|
||||
|
||||
invoice_type: function (frm) {
|
||||
$.each(frm.doc.invoices, (idx, row) => {
|
||||
row.party_type = frm.doc.invoice_type == "Sales" ? "Customer" : "Supplier";
|
||||
frappe.model.set_value(row.doctype, row.name, "party", "");
|
||||
frappe.model.set_value(row.doctype, row.name, "party_name", "");
|
||||
});
|
||||
frm.clear_table("invoices");
|
||||
frm.refresh_fields();
|
||||
frm.trigger("update_party_labels");
|
||||
@@ -219,7 +216,19 @@ frappe.ui.form.on("Opening Invoice Creation Tool Item", {
|
||||
});
|
||||
},
|
||||
|
||||
invoices_add: (frm) => {
|
||||
invoices_add: (frm, cdt, cdn) => {
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
const field_copy = [];
|
||||
|
||||
["project", "cost_center"].forEach((fieldname) => {
|
||||
if (frm.doc[fieldname]) {
|
||||
frappe.model.set_value(cdt, cdn, fieldname, frm.doc[fieldname]);
|
||||
} else {
|
||||
field_copy.push(fieldname);
|
||||
}
|
||||
});
|
||||
|
||||
frm.script_manager.copy_from_first_row("invoices", row, field_copy);
|
||||
frm.trigger("update_invoice_table");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -133,6 +133,17 @@ class OpeningInvoiceCreationTool(Document):
|
||||
if not row.get(scrub(d)):
|
||||
frappe.throw(mandatory_error_msg.format(row.idx, d, self.invoice_type))
|
||||
|
||||
self.validate_temporary_opening_account(row)
|
||||
|
||||
def validate_temporary_opening_account(self, row):
|
||||
account_type = frappe.get_cached_value("Account", row.temporary_opening_account, "account_type")
|
||||
if account_type != "Temporary":
|
||||
frappe.throw(
|
||||
_("Row #{0}: {1} account is not of type {2}").format(
|
||||
row.idx, row.temporary_opening_account, "Temporary"
|
||||
)
|
||||
)
|
||||
|
||||
def get_invoices(self):
|
||||
invoices = []
|
||||
for row in self.invoices:
|
||||
@@ -203,6 +214,7 @@ class OpeningInvoiceCreationTool(Document):
|
||||
"description": row.item_name or "Opening Invoice Item",
|
||||
income_expense_account_field: row.temporary_opening_account,
|
||||
"cost_center": cost_center,
|
||||
"project": row.get("project") or self.get("project"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
|
||||
get_temporary_opening_account,
|
||||
)
|
||||
from erpnext.projects.doctype.project.test_project import make_project
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
@@ -14,21 +16,26 @@ class TestOpeningInvoiceCreationTool(ERPNextTestSuite):
|
||||
self,
|
||||
invoice_type="Sales",
|
||||
company=None,
|
||||
party_1=None,
|
||||
party_2=None,
|
||||
invoice_number=None,
|
||||
invoices=None,
|
||||
project=None,
|
||||
cost_center=None,
|
||||
department=None,
|
||||
return_doc=False,
|
||||
):
|
||||
doc = frappe.get_single("Opening Invoice Creation Tool")
|
||||
args = get_opening_invoice_creation_dict(
|
||||
invoice_type=invoice_type,
|
||||
company=company,
|
||||
party_1=party_1,
|
||||
party_2=party_2,
|
||||
invoice_number=invoice_number,
|
||||
invoices=invoices,
|
||||
project=project,
|
||||
cost_center=cost_center,
|
||||
department=department,
|
||||
)
|
||||
doc.update(args)
|
||||
|
||||
if return_doc:
|
||||
return doc
|
||||
|
||||
return doc.make_invoices()
|
||||
|
||||
def test_opening_sales_invoice_creation(self):
|
||||
@@ -37,8 +44,8 @@ class TestOpeningInvoiceCreationTool(ERPNextTestSuite):
|
||||
self.assertEqual(len(invoices), 2)
|
||||
expected_value = {
|
||||
"keys": ["customer", "outstanding_amount", "status"],
|
||||
0: ["_Test Customer", 300, "Overdue"],
|
||||
1: ["_Test Customer 1", 250, "Overdue"],
|
||||
0: ["_Test Customer", 200, "Overdue"],
|
||||
1: ["_Test Customer 1", 200, "Overdue"],
|
||||
}
|
||||
self.check_expected_values(invoices, expected_value)
|
||||
|
||||
@@ -55,48 +62,34 @@ class TestOpeningInvoiceCreationTool(ERPNextTestSuite):
|
||||
for field_idx, field in enumerate(expected_value["keys"]):
|
||||
self.assertEqual(si.get(field, ""), expected_value[invoice_idx][field_idx])
|
||||
|
||||
def test_opening_invoice_requires_temporary_account_type(self):
|
||||
doc = self.make_invoices(company="_Test Opening Invoice Company", return_doc=True)
|
||||
doc.invoices[0].temporary_opening_account = "Sales - _TOIC"
|
||||
self.assertRaises(frappe.ValidationError, doc.make_invoices)
|
||||
|
||||
def test_opening_purchase_invoice_creation(self):
|
||||
invoices = self.make_invoices(invoice_type="Purchase", company="_Test Opening Invoice Company")
|
||||
|
||||
self.assertEqual(len(invoices), 2)
|
||||
expected_value = {
|
||||
"keys": ["supplier", "outstanding_amount", "status"],
|
||||
0: ["_Test Supplier", 300, "Overdue"],
|
||||
1: ["_Test Supplier 1", 250, "Overdue"],
|
||||
0: ["_Test Supplier", 200, "Overdue"],
|
||||
1: ["_Test Supplier 1", 200, "Overdue"],
|
||||
}
|
||||
self.check_expected_values(invoices, expected_value, "Purchase")
|
||||
|
||||
def test_opening_sales_invoice_creation_with_missing_debit_account(self):
|
||||
company = "_Test Opening Invoice Company"
|
||||
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
|
||||
|
||||
old_default_receivable_account = frappe.db.get_value("Company", company, "default_receivable_account")
|
||||
frappe.db.set_value("Company", company, "default_receivable_account", "")
|
||||
old_default_receivable_account = frappe.db.get_value(
|
||||
"Company", "_Test Opening Invoice Company", "default_receivable_account"
|
||||
)
|
||||
frappe.db.set_value("Company", "_Test Opening Invoice Company", "default_receivable_account", "")
|
||||
|
||||
if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"):
|
||||
cc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Cost Center",
|
||||
"cost_center_name": "_Test Opening Invoice Company",
|
||||
"is_group": 1,
|
||||
"company": "_Test Opening Invoice Company",
|
||||
}
|
||||
)
|
||||
cc.insert(ignore_mandatory=True)
|
||||
cc2 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Cost Center",
|
||||
"cost_center_name": "Main",
|
||||
"is_group": 0,
|
||||
"company": "_Test Opening Invoice Company",
|
||||
"parent_cost_center": cc.name,
|
||||
}
|
||||
)
|
||||
cc2.insert()
|
||||
|
||||
frappe.db.set_value("Company", company, "cost_center", "Main - _TOIC")
|
||||
|
||||
self.make_invoices(company="_Test Opening Invoice Company", party_1=party_1, party_2=party_2)
|
||||
self.make_invoices(
|
||||
company="_Test Opening Invoice Company",
|
||||
invoices=[{"party": party_1}, {"party": party_2}],
|
||||
)
|
||||
|
||||
# Check if missing debit account error raised
|
||||
error_log = frappe.db.exists(
|
||||
@@ -106,71 +99,107 @@ class TestOpeningInvoiceCreationTool(ERPNextTestSuite):
|
||||
self.assertTrue(error_log)
|
||||
|
||||
# teardown
|
||||
frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
|
||||
|
||||
def test_renaming_of_invoice_using_invoice_number_field(self):
|
||||
company = "_Test Opening Invoice Company"
|
||||
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
|
||||
self.make_invoices(
|
||||
company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11"
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
"_Test Opening Invoice Company",
|
||||
"default_receivable_account",
|
||||
old_default_receivable_account,
|
||||
)
|
||||
|
||||
sales_inv1 = frappe.get_all("Sales Invoice", filters={"customer": "Customer A"})[0].get("name")
|
||||
sales_inv2 = frappe.get_all("Sales Invoice", filters={"customer": "Customer B"})[0].get("name")
|
||||
self.assertEqual(sales_inv1, "TEST-NEW-INV-11")
|
||||
def test_renaming_of_invoice_using_invoice_number_field(self):
|
||||
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
|
||||
invoices = self.make_invoices(
|
||||
company="_Test Opening Invoice Company",
|
||||
invoices=[
|
||||
{"party": party_1, "invoice_number": "TEST-NEW-INV-11"},
|
||||
{"party": party_2},
|
||||
],
|
||||
)
|
||||
|
||||
# teardown
|
||||
for inv in [sales_inv1, sales_inv2]:
|
||||
doc = frappe.get_doc("Sales Invoice", inv)
|
||||
doc.cancel()
|
||||
self.assertEqual(invoices[0], "TEST-NEW-INV-11")
|
||||
|
||||
def test_opening_invoice_with_accounting_dimension(self):
|
||||
invoices = self.make_invoices(
|
||||
invoice_type="Sales", company="_Test Opening Invoice Company", department="Sales - _TOIC"
|
||||
)
|
||||
|
||||
expected_value = {
|
||||
"keys": ["customer", "outstanding_amount", "status", "department"],
|
||||
0: ["_Test Customer", 300, "Overdue", "Sales - _TOIC"],
|
||||
1: ["_Test Customer 1", 250, "Overdue", "Sales - _TOIC"],
|
||||
}
|
||||
self.check_expected_values(invoices, expected_value, invoice_type="Sales")
|
||||
for invoice in invoices:
|
||||
self.assertEqual(frappe.db.get_value("Sales Invoice", invoice, "department"), "Sales - _TOIC")
|
||||
|
||||
def test_opening_entry_project_linking(self):
|
||||
doc = self.make_invoices(
|
||||
company="_Test Opening Invoice Company", invoice_type="Sales", return_doc=True
|
||||
)
|
||||
project_1 = make_project(
|
||||
{"project_name": "Test Opening Invoice projecty 01", "company": "_Test Opening Invoice Company"}
|
||||
)
|
||||
project_2 = make_project(
|
||||
{"project_name": "Test Opening Invoice projecty 02", "company": "_Test Opening Invoice Company"}
|
||||
)
|
||||
doc.invoices[0].project = project_1.name
|
||||
doc.invoices[1].project = project_2.name
|
||||
invoices = doc.make_invoices()
|
||||
sales_invoice_1 = frappe.get_doc("Sales Invoice", invoices[0])
|
||||
sales_invoice_2 = frappe.get_doc("Sales Invoice", invoices[1])
|
||||
|
||||
self.assertEqual(sales_invoice_1.items[0].project, project_1.name)
|
||||
self.assertEqual(sales_invoice_2.items[0].project, project_2.name)
|
||||
|
||||
|
||||
def get_opening_invoice_creation_dict(**args):
|
||||
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
|
||||
company = args.get("company", "_Test Company")
|
||||
default_invoices = []
|
||||
default_invoice_rows = [
|
||||
{
|
||||
"qty": 1.0,
|
||||
"outstanding_amount": 200,
|
||||
"party": f"_Test {party}",
|
||||
"item_name": "Opening Item",
|
||||
"due_date": add_days(today(), -10),
|
||||
"posting_date": add_days(today(), -15),
|
||||
"temporary_opening_account": get_temporary_opening_account(company),
|
||||
},
|
||||
{
|
||||
"qty": 1.0,
|
||||
"outstanding_amount": 200,
|
||||
"party": f"_Test {party} 1",
|
||||
"item_name": "Opening Item",
|
||||
"due_date": add_days(today(), -10),
|
||||
"posting_date": add_days(today(), -15),
|
||||
"temporary_opening_account": get_temporary_opening_account(company),
|
||||
},
|
||||
]
|
||||
|
||||
for row in args.get("invoices") or default_invoice_rows:
|
||||
default_invoices.append(
|
||||
{
|
||||
"qty": row.get("qty") or 1.0,
|
||||
"outstanding_amount": row.get("outstanding_amount") or 200,
|
||||
"party": row.get("party") or f"_Test {party}",
|
||||
"item_name": row.get("item_name") or "Opening Item",
|
||||
"due_date": row.get("due_date") or add_days(today(), -10),
|
||||
"posting_date": row.get("posting_date") or add_days(today(), -15),
|
||||
"temporary_opening_account": row.get("temporary_opening_account")
|
||||
or get_temporary_opening_account(company),
|
||||
"invoice_number": row.get("invoice_number"),
|
||||
"project": row.get("project"),
|
||||
"cost_center": row.get("cost_center"),
|
||||
}
|
||||
)
|
||||
|
||||
invoice_dict = frappe._dict(
|
||||
{
|
||||
"company": company,
|
||||
"invoice_type": args.get("invoice_type", "Sales"),
|
||||
"invoices": [
|
||||
{
|
||||
"qty": 1.0,
|
||||
"outstanding_amount": 300,
|
||||
"party": args.get("party_1") or f"_Test {party}",
|
||||
"item_name": "Opening Item",
|
||||
"due_date": "2016-09-10",
|
||||
"posting_date": "2016-09-05",
|
||||
"temporary_opening_account": get_temporary_opening_account(company),
|
||||
"invoice_number": args.get("invoice_number"),
|
||||
},
|
||||
{
|
||||
"qty": 2.0,
|
||||
"outstanding_amount": 250,
|
||||
"party": args.get("party_2") or f"_Test {party} 1",
|
||||
"item_name": "Opening Item",
|
||||
"due_date": "2016-09-10",
|
||||
"posting_date": "2016-09-05",
|
||||
"temporary_opening_account": get_temporary_opening_account(company),
|
||||
"invoice_number": None,
|
||||
},
|
||||
],
|
||||
"project": args.get("project"),
|
||||
"cost_center": args.get("cost_center"),
|
||||
"invoices": default_invoices,
|
||||
}
|
||||
)
|
||||
|
||||
invoice_dict.update(args)
|
||||
invoice_dict.invoices = default_invoices
|
||||
return invoice_dict
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"qty",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break"
|
||||
"dimension_col_break",
|
||||
"project"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -125,11 +126,17 @@
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Party Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-20 02:11:42.023575",
|
||||
"modified": "2026-04-29 17:08:15.617047",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Opening Invoice Creation Tool Item",
|
||||
|
||||
@@ -26,6 +26,7 @@ class OpeningInvoiceCreationToolItem(Document):
|
||||
party_name: DF.Data | None
|
||||
party_type: DF.Link | None
|
||||
posting_date: DF.Date | None
|
||||
project: DF.Link | None
|
||||
qty: DF.Data | None
|
||||
supplier_invoice_date: DF.Date | None
|
||||
temporary_opening_account: DF.Link | None
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
{
|
||||
"fieldname": "advance_account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Advance Account",
|
||||
"options": "Account"
|
||||
}
|
||||
@@ -36,14 +37,15 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:08.489183",
|
||||
"modified": "2026-05-27 14:19:00.888437",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Party Account",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -754,17 +754,21 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.set_paid_amount_based_on_received_amount = true;
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
|
||||
if (frm.doc.base_received_amount && frm.doc.source_exchange_rate) {
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
if (frm.doc.paid_amount && frm.doc.source_exchange_rate) {
|
||||
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
|
||||
// target exchange rate should always be same as source if both account currencies is same
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate);
|
||||
frm.set_value("received_amount", frm.doc.paid_amount);
|
||||
} else {
|
||||
frm.set_value(
|
||||
"paid_amount",
|
||||
flt(frm.doc.base_paid_amount) / flt(frm.doc.source_exchange_rate)
|
||||
);
|
||||
const target_rate =
|
||||
flt(frm.doc.target_exchange_rate) ||
|
||||
(company_currency == frm.doc.paid_to_account_currency ? 1 : 0);
|
||||
if (target_rate) {
|
||||
frm.set_value("received_amount", flt(frm.doc.base_received_amount) / target_rate);
|
||||
}
|
||||
}
|
||||
|
||||
// set_unallocated_amount is called by below method,
|
||||
@@ -780,18 +784,23 @@ frappe.ui.form.on("Payment Entry", {
|
||||
target_exchange_rate: function (frm) {
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
|
||||
if (frm.doc.base_paid_amount && frm.doc.target_exchange_rate) {
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
if (
|
||||
!frm.doc.source_exchange_rate &&
|
||||
frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency
|
||||
) {
|
||||
if (frm.doc.received_amount && frm.doc.target_exchange_rate) {
|
||||
frm.set_value(
|
||||
"base_received_amount",
|
||||
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate)
|
||||
);
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
|
||||
frm.set_value("paid_amount", frm.doc.received_amount);
|
||||
} else {
|
||||
frm.set_value(
|
||||
"received_amount",
|
||||
flt(frm.doc.base_received_amount) / flt(frm.doc.target_exchange_rate)
|
||||
);
|
||||
const source_rate =
|
||||
flt(frm.doc.source_exchange_rate) ||
|
||||
(company_currency == frm.doc.paid_from_account_currency ? 1 : 0);
|
||||
if (source_rate) {
|
||||
frm.set_value("paid_amount", flt(frm.doc.base_paid_amount) / source_rate);
|
||||
}
|
||||
}
|
||||
|
||||
// set_unallocated_amount is called by below method,
|
||||
|
||||
@@ -1206,9 +1206,9 @@ class PaymentEntry(AccountsController):
|
||||
continue
|
||||
|
||||
if tax.add_deduct_tax == "Add":
|
||||
included_taxes += tax.base_tax_amount
|
||||
included_taxes += flt(tax.base_tax_amount)
|
||||
else:
|
||||
included_taxes -= tax.base_tax_amount
|
||||
included_taxes -= flt(tax.base_tax_amount)
|
||||
|
||||
return included_taxes
|
||||
|
||||
|
||||
@@ -1113,6 +1113,27 @@ class TestPaymentEntry(ERPNextTestSuite):
|
||||
|
||||
self.assertEqual(gl_entries, expected_gl_entries)
|
||||
|
||||
def test_payment_entry_with_inclusive_tax(self):
|
||||
# inclusive tax built server-side: base_tax_amount is None until apply_taxes()
|
||||
payment_entry = create_payment_entry(paid_amount=1180)
|
||||
payment_entry.append(
|
||||
"taxes",
|
||||
{
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"charge_type": "On Paid Amount",
|
||||
"rate": 18,
|
||||
"included_in_paid_amount": 1,
|
||||
"add_deduct_tax": "Add",
|
||||
"description": "Service Tax",
|
||||
},
|
||||
)
|
||||
payment_entry.save()
|
||||
payment_entry.submit()
|
||||
|
||||
# 1180 incl 18% => 1000 base + 180 tax
|
||||
self.assertEqual(flt(payment_entry.total_taxes_and_charges, 2), 180.0)
|
||||
self.assertEqual(flt(payment_entry.unallocated_amount, 2), 1000.0)
|
||||
|
||||
def test_payment_entry_against_onhold_purchase_invoice(self):
|
||||
pi = make_purchase_invoice()
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ class TestPaymentLedgerEntry(ERPNextTestSuite):
|
||||
self.create_company()
|
||||
self.create_item()
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
|
||||
def create_company(self):
|
||||
company_name = "_Test Payment Ledger"
|
||||
|
||||
@@ -95,6 +95,8 @@ def start_pcv_processing(docname: str):
|
||||
frappe.has_permission("Process Payment Reconciliation", "write", doc=docname, throw=True)
|
||||
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running")
|
||||
|
||||
timeout = frappe.db.get_single_value("Accounts Settings", "pcv_job_timeout") or 3600
|
||||
|
||||
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
|
||||
if normal_balances := (
|
||||
qb.from_(ppcvd)
|
||||
@@ -121,7 +123,7 @@ def start_pcv_processing(docname: str):
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date",
|
||||
queue="long",
|
||||
timeout="3600",
|
||||
timeout=timeout,
|
||||
is_async=True,
|
||||
enqueue_after_commit=True,
|
||||
docname=docname,
|
||||
@@ -247,6 +249,8 @@ def get_gle_for_closing_account(pcv, dimension_balance, dimensions):
|
||||
|
||||
@frappe.whitelist()
|
||||
def schedule_next_date(docname: str):
|
||||
timeout = frappe.db.get_single_value("Accounts Settings", "pcv_job_timeout") or 3600
|
||||
|
||||
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
|
||||
if to_process := (
|
||||
qb.from_(ppcvd)
|
||||
@@ -272,7 +276,7 @@ def schedule_next_date(docname: str):
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date",
|
||||
queue="long",
|
||||
timeout="3600",
|
||||
timeout=timeout,
|
||||
is_async=True,
|
||||
enqueue_after_commit=True,
|
||||
docname=docname,
|
||||
@@ -302,7 +306,7 @@ def schedule_next_date(docname: str):
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.summarize_and_post_ledger_entries",
|
||||
queue="long",
|
||||
timeout="3600",
|
||||
timeout=timeout,
|
||||
is_async=True,
|
||||
job_name=job_name,
|
||||
enqueue_after_commit=True,
|
||||
|
||||
@@ -21,10 +21,8 @@ class TestProcessStatementOfAccounts(ERPNextTestSuite, AccountsTestMixin):
|
||||
letterhead.is_default = 0
|
||||
letterhead.save()
|
||||
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.company = "_Test Company"
|
||||
self.create_customer(customer_name="Other Customer")
|
||||
self.clear_old_entries()
|
||||
self.si = create_sales_invoice()
|
||||
create_sales_invoice(customer="Other Customer")
|
||||
|
||||
|
||||
@@ -1433,6 +1433,10 @@ class PurchaseInvoice(BuyingController):
|
||||
# tax table gl entries
|
||||
valuation_tax = {}
|
||||
|
||||
# Amount of each valuation charge actually capitalized into stock/asset valuation, keyed by
|
||||
# tax row name - a non-stock item's share of a spread-across-all-items charge is excluded.
|
||||
capitalized_valuation_tax = self.get_capitalized_valuation_tax()
|
||||
|
||||
for tax in self.get("taxes"):
|
||||
amount, base_amount = self.get_tax_amounts(tax, None)
|
||||
if tax.category in ("Total", "Valuation and Total") and flt(base_amount):
|
||||
@@ -1469,8 +1473,7 @@ class PurchaseInvoice(BuyingController):
|
||||
tax.idx, _(tax.category)
|
||||
)
|
||||
)
|
||||
valuation_tax.setdefault(tax.name, 0)
|
||||
valuation_tax[tax.name] += (tax.add_deduct_tax == "Add" and 1 or -1) * flt(base_amount)
|
||||
valuation_tax[tax.name] = capitalized_valuation_tax.get(tax.name, 0.0)
|
||||
|
||||
if self.is_opening == "No" and self.negative_expense_to_be_booked and valuation_tax:
|
||||
# credit valuation tax amount in "Expenses Included In Valuation"
|
||||
|
||||
@@ -3008,6 +3008,14 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
|
||||
party_link.delete()
|
||||
|
||||
def test_purchase_invoice_cancellation_post_account_freezing_date(self):
|
||||
pi = make_purchase_invoice()
|
||||
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", add_days(getdate(), 1))
|
||||
try:
|
||||
self.assertRaises(frappe.ValidationError, pi.cancel)
|
||||
finally:
|
||||
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", None)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -11,13 +11,16 @@
|
||||
"add_deduct_tax",
|
||||
"charge_type",
|
||||
"row_id",
|
||||
"included_in_print_rate",
|
||||
"included_in_paid_amount",
|
||||
"col_break1",
|
||||
"account_head",
|
||||
"description",
|
||||
"section_break_mvae",
|
||||
"is_tax_withholding_account",
|
||||
"set_by_item_tax_template",
|
||||
"allocate_full_amount_to_stock_items",
|
||||
"column_break_odzz",
|
||||
"included_in_print_rate",
|
||||
"included_in_paid_amount",
|
||||
"section_break_10",
|
||||
"rate",
|
||||
"accounting_dimensions_section",
|
||||
@@ -78,6 +81,15 @@
|
||||
"oldfieldname": "row_id",
|
||||
"oldfieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:doc.charge_type=='Actual' && ['Valuation', 'Valuation and Total'].includes(doc.category)",
|
||||
"description": "If checked, the entire amount (e.g. Freight) is allocated to the valuation of stock & asset items only. If unchecked, the amount is distributed across all items and the portion belonging to non-stock items is not added to valuation.",
|
||||
"fieldname": "allocate_full_amount_to_stock_items",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allocate Full Amount to Stock Items",
|
||||
"show_description_on_click": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If checked, the tax amount will be considered as already included in the Print Rate / Print Amount",
|
||||
@@ -272,13 +284,21 @@
|
||||
"label": "Don't Recompute Tax",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_mvae",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_odzz",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-24 18:22:56.886010",
|
||||
"modified": "2026-06-21 17:08:57.096729",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Taxes and Charges",
|
||||
|
||||
@@ -17,6 +17,7 @@ class PurchaseTaxesandCharges(Document):
|
||||
account_currency: DF.Link | None
|
||||
account_head: DF.Link
|
||||
add_deduct_tax: DF.Literal["Add", "Deduct"]
|
||||
allocate_full_amount_to_stock_items: DF.Check
|
||||
base_net_amount: DF.Currency
|
||||
base_tax_amount: DF.Currency
|
||||
base_tax_amount_after_discount_amount: DF.Currency
|
||||
|
||||
@@ -460,8 +460,8 @@ class SalesInvoice(SellingController):
|
||||
validate_account_head(item.idx, item.income_account, self.company, _("Income"))
|
||||
|
||||
def before_save(self):
|
||||
self.set_account_for_mode_of_payment()
|
||||
self.set_paid_amount()
|
||||
self.set_account_for_mode_of_payment()
|
||||
|
||||
def before_submit(self):
|
||||
self.add_remarks()
|
||||
@@ -900,6 +900,13 @@ class SalesInvoice(SellingController):
|
||||
def set_paid_amount(self):
|
||||
paid_amount = 0.0
|
||||
base_paid_amount = 0.0
|
||||
|
||||
if not cint(self.is_pos) and self.is_return:
|
||||
self.set("payments", [])
|
||||
self.paid_amount = paid_amount
|
||||
self.base_paid_amount = base_paid_amount
|
||||
return
|
||||
|
||||
for data in self.payments:
|
||||
data.base_amount = flt(data.amount * self.conversion_rate, self.precision("base_paid_amount"))
|
||||
paid_amount += data.amount
|
||||
|
||||
@@ -16,12 +16,14 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestUnreconcilePayment(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_supplier()
|
||||
self.create_usd_receivable_account()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.supplier = "_Test Supplier"
|
||||
self.item = "_Test Item"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.cash = "Cash - _TC"
|
||||
self.debtors_usd = "_Test Receivable USD - _TC"
|
||||
|
||||
def create_sales_invoice(self, do_not_submit=False):
|
||||
si = create_sales_invoice(
|
||||
@@ -372,7 +374,6 @@ class TestUnreconcilePayment(ERPNextTestSuite, AccountsTestMixin):
|
||||
self.assertEqual(so.advance_paid, 0)
|
||||
|
||||
def test_06_unreconcile_advance_from_payment_entry(self):
|
||||
self.enable_advance_as_liability()
|
||||
so1 = self.create_sales_order()
|
||||
so2 = self.create_sales_order()
|
||||
|
||||
@@ -423,7 +424,11 @@ class TestUnreconcilePayment(ERPNextTestSuite, AccountsTestMixin):
|
||||
self.disable_advance_as_liability()
|
||||
|
||||
def test_07_adv_from_so_to_invoice(self):
|
||||
self.enable_advance_as_liability()
|
||||
frappe.db.set_value("Company", self.company, "book_advance_payments_in_separate_party_account", True)
|
||||
frappe.db.set_value(
|
||||
"Company", self.company, "default_advance_received_account", "Advance Received - _TC"
|
||||
)
|
||||
|
||||
so = self.create_sales_order()
|
||||
pe = self.create_payment_entry()
|
||||
pe.paid_amount = 1000
|
||||
|
||||
@@ -716,7 +716,7 @@ def make_reverse_gl_entries(
|
||||
partial_cancel=partial_cancel,
|
||||
)
|
||||
validate_accounting_period(gl_entries)
|
||||
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
|
||||
check_freezing_date(gl_entries[0]["posting_date"], gl_entries[0]["company"], adv_adj)
|
||||
|
||||
is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries)
|
||||
|
||||
@@ -821,13 +821,24 @@ def check_freezing_date(posting_date, company, adv_adj=False):
|
||||
)
|
||||
|
||||
|
||||
def validate_against_pcv(is_opening, posting_date, company):
|
||||
if is_opening and frappe.db.exists("Period Closing Voucher", {"docstatus": 1, "company": company}):
|
||||
def validate_opening_entry_against_pcv(company):
|
||||
if frappe.db.exists("Period Closing Voucher", {"docstatus": 1, "company": company}):
|
||||
frappe.throw(
|
||||
_("Opening Entry can not be created after Period Closing Voucher is created."),
|
||||
_(
|
||||
"A Period Closing Voucher is already submitted and an Opening Entry can no longer be created. {0} to learn more."
|
||||
).format(
|
||||
'<a href="https://docs.frappe.io/erpnext/period-closing-voucher#14-pcv-and-opening-entries" target="_blank" rel="noopener">'
|
||||
+ _("Read the docs")
|
||||
+ "</a>"
|
||||
),
|
||||
title=_("Invalid Opening Entry"),
|
||||
)
|
||||
|
||||
|
||||
def validate_against_pcv(is_opening, posting_date, company):
|
||||
if is_opening:
|
||||
validate_opening_entry_against_pcv(company)
|
||||
|
||||
last_pcv_date = frappe.db.get_value(
|
||||
"Period Closing Voucher", {"docstatus": 1, "company": company}, [{"MAX": "period_end_date"}]
|
||||
)
|
||||
|
||||
@@ -9,11 +9,10 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestAccountsPayable(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.create_supplier(currency="USD", supplier_name="Test Supplier2")
|
||||
self.create_usd_payable_account()
|
||||
self.company = "_Test Company"
|
||||
self.item = "_Test Item"
|
||||
self.supplier = "_Test Supplier 2"
|
||||
self.creditors_usd = "_Test Payable USD - _TC"
|
||||
|
||||
def test_accounts_payable_for_foreign_currency_supplier(self):
|
||||
pi = self.create_purchase_invoice(do_not_submit=True)
|
||||
|
||||
@@ -12,11 +12,17 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.create_usd_receivable_account()
|
||||
self.clear_old_entries()
|
||||
self.company = "_Test Company"
|
||||
self.company_abbr = "_TC"
|
||||
self.customer = "_Test Customer"
|
||||
self.item = "_Test Item"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.warehouse = "Stores - _TC"
|
||||
self.income_account = "Sales - _TC"
|
||||
self.expense_account = "Cost of Goods Sold - _TC"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cash = "Cash - _TC"
|
||||
self.debtors_usd = "_Test Receivable USD - _TC"
|
||||
|
||||
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False, **args):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -11,10 +11,11 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.item = "_Test Item"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
|
||||
def test_01_receivable_summary_output(self):
|
||||
"""
|
||||
|
||||
@@ -84,7 +84,13 @@ def build_budget_map(budget_records, filters):
|
||||
budget_distributions = get_budget_distributions(budget)
|
||||
|
||||
for row in budget_distributions:
|
||||
if not row.start_date or not row.end_date:
|
||||
continue
|
||||
|
||||
months = get_months_in_range(row.start_date, row.end_date)
|
||||
if not months:
|
||||
continue
|
||||
|
||||
monthly_budget = flt(row.amount) / len(months)
|
||||
|
||||
for month_date in months:
|
||||
|
||||
@@ -12,10 +12,12 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestCustomerLedgerSummary(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.item = "_Test Item"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.cash = "Cash - _TC"
|
||||
|
||||
def create_sales_invoice(self, do_not_submit=False, **args):
|
||||
si = create_sales_invoice(
|
||||
|
||||
@@ -61,11 +61,16 @@ class TestDeferredRevenueAndExpense(ERPNextTestSuite, AccountsTestMixin):
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer("_Test Customer")
|
||||
self.create_supplier("_Test Furniture Supplier")
|
||||
self.company = "_Test Company"
|
||||
self.company_abbr = "_TC"
|
||||
self.customer = "_Test Customer"
|
||||
self.supplier = "_Test Supplier"
|
||||
self.warehouse = "Stores - _TC"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.income_account = "Sales - _TC"
|
||||
self.expense_account = "Cost of Goods Sold - _TC"
|
||||
self.setup_deferred_accounts_and_items()
|
||||
self.clear_old_entries()
|
||||
|
||||
@ERPNextTestSuite.change_settings("Accounts Settings", {"book_deferred_entries_based_on": "Months"})
|
||||
def test_deferred_revenue(self):
|
||||
|
||||
@@ -12,7 +12,13 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestGeneralAndPaymentLedger(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.company = "_Test Company"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.expense_account = "Cost of Goods Sold - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.income_account = "Sales - _TC"
|
||||
self.warehouse = "Stores - _TC"
|
||||
self.creditors = "Creditors - _TC"
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self):
|
||||
|
||||
@@ -14,7 +14,6 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
class TestGeneralLedger(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.company = "_Test Company"
|
||||
self.clear_old_entries()
|
||||
|
||||
def clear_old_entries(self):
|
||||
doctype_list = [
|
||||
|
||||
@@ -18,8 +18,6 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
self.create_item()
|
||||
self.create_bundle()
|
||||
self.create_customer()
|
||||
self.create_sales_invoice()
|
||||
self.clear_old_entries()
|
||||
|
||||
def create_company(self):
|
||||
company_name = "_Test Gross Profit"
|
||||
|
||||
@@ -9,9 +9,9 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestItemWisePurchaseRegister(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_supplier()
|
||||
self.create_item()
|
||||
self.company = "_Test Company"
|
||||
self.supplier = "_Test Supplier"
|
||||
self.item = "_Test Item"
|
||||
|
||||
def create_purchase_invoice(self, do_not_submit=False):
|
||||
pi = make_purchase_invoice(
|
||||
|
||||
@@ -9,9 +9,11 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.item = "_Test Item"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
|
||||
def create_sales_invoice(self, item=None, taxes=None, do_not_submit=False):
|
||||
si = create_sales_invoice(
|
||||
|
||||
@@ -14,9 +14,11 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestProfitAndLossStatement(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.item = "_Test Item"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
|
||||
def create_sales_invoice(self, qty=1, rate=150, no_payment_schedule=False, do_not_submit=False):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -10,9 +10,13 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.company = "_Test Company"
|
||||
self.customer = "_Test Customer"
|
||||
self.item = "_Test Item"
|
||||
self.debit_to = "Debtors - _TC"
|
||||
self.cost_center = "Main - _TC"
|
||||
self.income_account = "Sales - _TC"
|
||||
self.cash = "Cash - _TC"
|
||||
self.create_child_cost_center()
|
||||
|
||||
def create_child_cost_center(self):
|
||||
|
||||
@@ -9,10 +9,9 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestSupplierLedgerSummary(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_supplier()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
self.company = "_Test Company"
|
||||
self.supplier = "_Test Supplier"
|
||||
self.item = "_Test Item"
|
||||
|
||||
def create_purchase_invoice(self, do_not_submit=False):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -20,8 +20,7 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.clear_old_entries()
|
||||
self.company = "_Test Company"
|
||||
create_records()
|
||||
|
||||
def test_tax_withholding_for_customers(self):
|
||||
|
||||
@@ -146,7 +146,6 @@ def get_appropriate_company(filters):
|
||||
return company
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_invoiced_item_gross_margin(sales_invoice=None, item_code=None, company=None, with_item_data=False):
|
||||
from erpnext.accounts.report.gross_profit.gross_profit import GrossProfitGenerator
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
frappe.ui.form.on("Buying Settings", {
|
||||
refresh(frm) {
|
||||
if (!frm.naming_controller) frm.naming_controller = new erpnext.NamingSeriesController(frm);
|
||||
if (!frm.naming_controller) frm.naming_controller = new frappe.ui.NamingSeriesController(frm);
|
||||
|
||||
const display = frm.doc.supp_master_name === "Naming Series";
|
||||
frm.set_df_property("naming_series_details", "hidden", !display);
|
||||
|
||||
@@ -68,6 +68,31 @@ frappe.ui.form.on("Supplier", {
|
||||
});
|
||||
|
||||
frm.make_methods = {
|
||||
"Purchase Order": () =>
|
||||
frappe.model.with_doctype("Purchase Order", function () {
|
||||
const po = frappe.model.get_new_doc("Purchase Order");
|
||||
po.supplier = frm.doc.name;
|
||||
frappe.set_route("Form", "Purchase Order", po.name);
|
||||
}),
|
||||
"Purchase Invoice": () =>
|
||||
frappe.model.with_doctype("Purchase Invoice", function () {
|
||||
const pi = frappe.model.get_new_doc("Purchase Invoice");
|
||||
pi.supplier = frm.doc.name;
|
||||
frappe.set_route("Form", "Purchase Invoice", pi.name);
|
||||
}),
|
||||
"Request for Quotation": () =>
|
||||
frappe.model.with_doctype("Request for Quotation", function () {
|
||||
const rfq = frappe.model.get_new_doc("Request for Quotation");
|
||||
const row = frappe.model.add_child(rfq, "suppliers");
|
||||
row.supplier = frm.doc.name;
|
||||
frappe.set_route("Form", "Request for Quotation", rfq.name);
|
||||
}),
|
||||
"Supplier Quotation": () =>
|
||||
frappe.model.with_doctype("Supplier Quotation", function () {
|
||||
const sq = frappe.model.get_new_doc("Supplier Quotation");
|
||||
sq.supplier = frm.doc.name;
|
||||
frappe.set_route("Form", "Supplier Quotation", sq.name);
|
||||
}),
|
||||
"Bank Account": () => erpnext.utils.make_bank_account(frm.doc.doctype, frm.doc.name),
|
||||
"Pricing Rule": () => frm.trigger("make_pricing_rule"),
|
||||
};
|
||||
@@ -117,6 +142,20 @@ frappe.ui.form.on("Supplier", {
|
||||
__("View")
|
||||
);
|
||||
|
||||
for (const doctype in frm.make_methods) {
|
||||
frm.add_custom_button(__(doctype), frm.make_methods[doctype], __("Create"));
|
||||
}
|
||||
|
||||
if (frm.doc.supplier_group) {
|
||||
frm.add_custom_button(
|
||||
__("Get Supplier Group Details"),
|
||||
function () {
|
||||
frm.trigger("get_supplier_group_details");
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
cint(frappe.defaults.get_default("enable_common_party_accounting")) &&
|
||||
frappe.model.can_create("Party Link")
|
||||
@@ -173,6 +212,8 @@ frappe.ui.form.on("Supplier", {
|
||||
frm.toggle_reqd("represents_company", true);
|
||||
} else {
|
||||
frm.toggle_reqd("represents_company", false);
|
||||
frm.set_value("represents_company", "");
|
||||
frm.set_value("companies", []);
|
||||
}
|
||||
},
|
||||
show_party_link_dialog: function (frm) {
|
||||
|
||||
@@ -11,72 +11,76 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"naming_series",
|
||||
"supplier_type",
|
||||
"supplier_name",
|
||||
"supplier_type",
|
||||
"alias",
|
||||
"gender",
|
||||
"column_break0",
|
||||
"supplier_group",
|
||||
"country",
|
||||
"is_transporter",
|
||||
"image",
|
||||
"defaults_section",
|
||||
"default_currency",
|
||||
"default_bank_account",
|
||||
"column_break_10",
|
||||
"default_price_list",
|
||||
"column_break2",
|
||||
"supplier_details",
|
||||
"column_break_30",
|
||||
"website",
|
||||
"language",
|
||||
"customer_numbers",
|
||||
"payment_terms",
|
||||
"contact_and_address_tab",
|
||||
"address_contacts",
|
||||
"address_html",
|
||||
"column_break1",
|
||||
"contact_html",
|
||||
"primary_address_and_contact_detail_section",
|
||||
"column_break_44",
|
||||
"supplier_primary_address",
|
||||
"primary_address",
|
||||
"column_break_mglr",
|
||||
"supplier_primary_contact",
|
||||
"mobile_no",
|
||||
"email_id",
|
||||
"tax_tab",
|
||||
"tax_id",
|
||||
"tax_category",
|
||||
"column_break_27",
|
||||
"tax_withholding_category",
|
||||
"tax_withholding_group",
|
||||
"accounting_tab",
|
||||
"payment_terms",
|
||||
"default_accounts_section",
|
||||
"accounts",
|
||||
"internal_supplier_section",
|
||||
"is_internal_supplier",
|
||||
"represents_company",
|
||||
"column_break_16",
|
||||
"section_break_pgad",
|
||||
"companies",
|
||||
"tax_tab",
|
||||
"taxation_section",
|
||||
"tax_id",
|
||||
"tax_category",
|
||||
"column_break_27",
|
||||
"tax_withholding_category",
|
||||
"tax_withholding_group",
|
||||
"settings_tab",
|
||||
"invoice_settings_section",
|
||||
"is_transporter",
|
||||
"allow_purchase_invoice_creation_without_purchase_order",
|
||||
"allow_purchase_invoice_creation_without_purchase_receipt",
|
||||
"column_break_54",
|
||||
"disabled",
|
||||
"rfq_and_purchase_order_settings_section",
|
||||
"is_frozen",
|
||||
"block_supplier_section",
|
||||
"on_hold",
|
||||
"hold_type",
|
||||
"release_date",
|
||||
"rfq_and_purchase_order_settings_section",
|
||||
"warn_rfqs",
|
||||
"prevent_rfqs",
|
||||
"column_break_oxjw",
|
||||
"warn_pos",
|
||||
"prevent_pos",
|
||||
"block_supplier_section",
|
||||
"on_hold",
|
||||
"hold_type",
|
||||
"column_break_59",
|
||||
"release_date",
|
||||
"portal_users_tab",
|
||||
"portal_users",
|
||||
"more_info_tab",
|
||||
"column_break2",
|
||||
"website",
|
||||
"language",
|
||||
"column_break_30",
|
||||
"supplier_details",
|
||||
"section_break_jqla",
|
||||
"customer_numbers",
|
||||
"dashboard_tab"
|
||||
],
|
||||
"fields": [
|
||||
@@ -101,6 +105,13 @@
|
||||
"oldfieldtype": "Data",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "alias",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
"label": "Alias",
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "country",
|
||||
"fieldtype": "Link",
|
||||
@@ -110,21 +121,24 @@
|
||||
{
|
||||
"fieldname": "default_bank_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Company Bank Account",
|
||||
"label": "Company Bank Account",
|
||||
"options": "Bank Account"
|
||||
},
|
||||
{
|
||||
"description": "Supplier's tax identification number (e.g. PAN, VAT, GST)",
|
||||
"fieldname": "tax_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Tax ID"
|
||||
},
|
||||
{
|
||||
"description": "Determines which tax rules apply to this supplier",
|
||||
"fieldname": "tax_category",
|
||||
"fieldtype": "Link",
|
||||
"label": "Tax Category",
|
||||
"options": "Tax Category"
|
||||
},
|
||||
{
|
||||
"description": "TDS / withholding tax category applied when paying this supplier",
|
||||
"fieldname": "tax_withholding_category",
|
||||
"fieldtype": "Link",
|
||||
"label": "Tax Withholding Category",
|
||||
@@ -132,15 +146,18 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enable to make this supplier selectable as a transporter on Delivery Notes and Stock Entries",
|
||||
"fieldname": "is_transporter",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Transporter"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Used for inter-company transactions",
|
||||
"fieldname": "is_internal_supplier",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Internal Supplier"
|
||||
"label": "Is Internal Supplier",
|
||||
"show_description_on_click": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "is_internal_supplier",
|
||||
@@ -192,6 +209,7 @@
|
||||
{
|
||||
"bold": 1,
|
||||
"default": "0",
|
||||
"description": "Disabled suppliers are hidden from selection in new transactions but remain in historical records",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
@@ -232,7 +250,7 @@
|
||||
"depends_on": "represents_company",
|
||||
"fieldname": "companies",
|
||||
"fieldtype": "Table",
|
||||
"label": "Allowed To Transact With",
|
||||
"label": "Allowed to transact with",
|
||||
"options": "Allowed To Transact With"
|
||||
},
|
||||
{
|
||||
@@ -258,21 +276,24 @@
|
||||
{
|
||||
"fieldname": "payment_terms",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Payment Terms Template",
|
||||
"label": "Payment Terms Template",
|
||||
"options": "Payment Terms Template"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "When enabled, transactions with this supplier will be blocked based on the Hold Type below",
|
||||
"fieldname": "on_hold",
|
||||
"fieldtype": "Check",
|
||||
"label": "Block Supplier"
|
||||
"label": "Block Supplier",
|
||||
"show_description_on_click": 1
|
||||
},
|
||||
{
|
||||
"default": "All",
|
||||
"depends_on": "eval:doc.on_hold",
|
||||
"fieldname": "hold_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Hold Type",
|
||||
"options": "\nAll\nInvoices\nPayments"
|
||||
"options": "All\nInvoices\nPayments"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.on_hold",
|
||||
@@ -307,14 +328,13 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"description": "Mention if non-standard payable account",
|
||||
"description": "Override the default payable / advance accounts on a per-company basis. Leave blank to use each company's defaults from Company settings.",
|
||||
"fieldname": "accounts",
|
||||
"fieldtype": "Table",
|
||||
"label": "Accounts",
|
||||
"label": "Per-Company Accounts",
|
||||
"options": "Party Account"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "supplier_details",
|
||||
"fieldname": "column_break2",
|
||||
"fieldtype": "Section Break",
|
||||
@@ -329,7 +349,7 @@
|
||||
"oldfieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"description": "Statutory info and other general information about your Supplier",
|
||||
"description": "General information about your Supplier",
|
||||
"fieldname": "supplier_details",
|
||||
"fieldtype": "Text",
|
||||
"label": "Supplier Details",
|
||||
@@ -342,6 +362,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Frozen suppliers block ledger entries until unfrozen. Use this to temporarily lock accounting activity without disabling the supplier.",
|
||||
"fieldname": "is_frozen",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Frozen"
|
||||
@@ -350,13 +371,13 @@
|
||||
"default": "0",
|
||||
"fieldname": "allow_purchase_invoice_creation_without_purchase_order",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Purchase Invoice Creation Without Purchase Order"
|
||||
"label": "Allow purchase invoice creation without purchase order"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_purchase_invoice_creation_without_purchase_receipt",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Purchase Invoice Creation Without Purchase Receipt"
|
||||
"label": "Allow purchase invoice creation without purchase receipt"
|
||||
},
|
||||
{
|
||||
"fieldname": "primary_address_and_contact_detail_section",
|
||||
@@ -367,7 +388,7 @@
|
||||
"description": "Reselect, if the chosen contact is edited after save",
|
||||
"fieldname": "supplier_primary_contact",
|
||||
"fieldtype": "Link",
|
||||
"label": "Supplier Primary Contact",
|
||||
"label": "Primary Contact",
|
||||
"no_copy": 1,
|
||||
"options": "Contact"
|
||||
},
|
||||
@@ -382,17 +403,13 @@
|
||||
"fetch_from": "supplier_primary_contact.email_id",
|
||||
"fieldname": "email_id",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "Email Id",
|
||||
"label": "Email ID",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_44",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "primary_address",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Primary Address",
|
||||
"label": "Primary Address Preview",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -400,7 +417,7 @@
|
||||
"description": "Reselect, if the chosen address is edited after save",
|
||||
"fieldname": "supplier_primary_address",
|
||||
"fieldtype": "Link",
|
||||
"label": "Supplier Primary Address",
|
||||
"label": "Primary Address",
|
||||
"no_copy": 1,
|
||||
"options": "Address"
|
||||
},
|
||||
@@ -436,10 +453,11 @@
|
||||
"label": "Tax"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "is_internal_supplier",
|
||||
"fieldname": "internal_supplier_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Internal Supplier Accounting"
|
||||
"hide_border": 1,
|
||||
"label": "Internal Supplier Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_16",
|
||||
@@ -458,10 +476,6 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Block Supplier"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_59",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "default_accounts_section",
|
||||
"fieldtype": "Section Break",
|
||||
@@ -483,12 +497,14 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"description": "Account / customer numbers assigned to your companies by this supplier (for reconciliation on their statements)",
|
||||
"fieldname": "customer_numbers",
|
||||
"fieldtype": "Table",
|
||||
"label": "Customer Numbers",
|
||||
"options": "Customer Number At Supplier"
|
||||
},
|
||||
{
|
||||
"description": "Used to pick the correct rate row inside the Tax Withholding Category for this supplier (e.g. Company vs Individual rates)",
|
||||
"fieldname": "tax_withholding_group",
|
||||
"fieldtype": "Link",
|
||||
"label": "Tax Withholding Group",
|
||||
@@ -504,11 +520,34 @@
|
||||
{
|
||||
"fieldname": "rfq_and_purchase_order_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 1,
|
||||
"label": "RFQ and Purchase Order Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_oxjw",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "taxation_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Tax Identification"
|
||||
},
|
||||
{
|
||||
"fieldname": "invoice_settings_section",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "more_info_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "More Info"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_jqla",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_pgad",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -522,7 +561,7 @@
|
||||
"link_fieldname": "party"
|
||||
}
|
||||
],
|
||||
"modified": "2026-05-29 16:52:59.441272",
|
||||
"modified": "2026-06-22 12:23:09.241125",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier",
|
||||
@@ -582,7 +621,7 @@
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "supplier_group",
|
||||
"search_fields": "supplier_group, alias",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
|
||||
@@ -39,6 +39,7 @@ class Supplier(TransactionBase):
|
||||
from erpnext.utilities.doctype.portal_user.portal_user import PortalUser
|
||||
|
||||
accounts: DF.Table[PartyAccount]
|
||||
alias: DF.Data | None
|
||||
allow_purchase_invoice_creation_without_purchase_order: DF.Check
|
||||
allow_purchase_invoice_creation_without_purchase_receipt: DF.Check
|
||||
companies: DF.Table[AllowedToTransactWith]
|
||||
@@ -50,7 +51,7 @@ class Supplier(TransactionBase):
|
||||
disabled: DF.Check
|
||||
email_id: DF.ReadOnly | None
|
||||
gender: DF.Link | None
|
||||
hold_type: DF.Literal["", "All", "Invoices", "Payments"]
|
||||
hold_type: DF.Literal["All", "Invoices", "Payments"]
|
||||
image: DF.AttachImage | None
|
||||
is_frozen: DF.Check
|
||||
is_internal_supplier: DF.Check
|
||||
@@ -88,7 +89,6 @@ class Supplier(TransactionBase):
|
||||
|
||||
def before_save(self):
|
||||
if not self.on_hold:
|
||||
self.hold_type = ""
|
||||
self.release_date = ""
|
||||
elif self.on_hold and not self.hold_type:
|
||||
self.hold_type = "All"
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
frappe.listview_settings["Supplier"] = {
|
||||
add_fields: ["supplier_name", "supplier_group", "image", "on_hold"],
|
||||
add_fields: ["supplier_name", "supplier_group", "image", "on_hold", "disabled", "is_frozen"],
|
||||
get_indicator: function (doc) {
|
||||
if (cint(doc.on_hold)) {
|
||||
return [__("On Hold"), "red"];
|
||||
if (cint(doc.disabled)) {
|
||||
return [__("Disabled"), "gray", "disabled,=,1"];
|
||||
} else if (cint(doc.on_hold)) {
|
||||
return [__("On Hold"), "red", "on_hold,=,1"];
|
||||
} else if (cint(doc.is_frozen)) {
|
||||
return [__("Frozen"), "orange", "is_frozen,=,1"];
|
||||
} else {
|
||||
return [__("Active"), "green", "disabled,=,0|on_hold,=,0|is_frozen,=,0"];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -414,39 +414,29 @@ class BuyingController(SubcontractingController):
|
||||
stock_and_asset_items = []
|
||||
stock_and_asset_items = self.get_stock_items() + self.get_asset_items()
|
||||
|
||||
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
|
||||
last_item_idx = 1
|
||||
for d in self.get("items"):
|
||||
if d.item_code:
|
||||
stock_and_asset_items_qty += flt(d.qty)
|
||||
stock_and_asset_items_amount += flt(d.base_net_amount)
|
||||
(
|
||||
tax_accounts,
|
||||
total_valuation_amount,
|
||||
all_item_charges,
|
||||
stock_item_charges,
|
||||
) = self.get_tax_details()
|
||||
|
||||
last_item_idx = d.idx
|
||||
# Pre-compute each item's share of the "Actual" valuation charges (keyed by row idx).
|
||||
actual_charge_per_item = self.distribute_actual_tax_amount(
|
||||
stock_and_asset_items, all_item_charges, stock_item_charges
|
||||
)
|
||||
|
||||
tax_accounts, total_valuation_amount, total_actual_tax_amount = self.get_tax_details()
|
||||
remaining_amount = total_actual_tax_amount
|
||||
last_item_idx = max((d.idx for d in self.get("items")), default=1)
|
||||
|
||||
for i, item in enumerate(self.get("items")):
|
||||
if item.item_code and (item.qty or item.get("rejected_qty")):
|
||||
item_tax_amount, actual_tax_amount = 0.0, 0.0
|
||||
if i == (last_item_idx - 1):
|
||||
# dump any rounding remainder of the On Net Total valuation on the last item
|
||||
item_tax_amount = total_valuation_amount
|
||||
actual_tax_amount = remaining_amount
|
||||
else:
|
||||
# calculate item tax amount
|
||||
item_tax_amount = self.get_item_tax_amount(item, tax_accounts)
|
||||
total_valuation_amount -= item_tax_amount
|
||||
|
||||
if total_actual_tax_amount:
|
||||
actual_tax_amount = self.get_item_actual_tax_amount(
|
||||
item,
|
||||
total_actual_tax_amount,
|
||||
stock_and_asset_items_amount,
|
||||
stock_and_asset_items_qty,
|
||||
)
|
||||
|
||||
remaining_amount -= actual_tax_amount
|
||||
|
||||
# This code is required here to calculate the correct valuation for stock items
|
||||
if item.item_code not in stock_and_asset_items:
|
||||
item.valuation_rate = 0.0
|
||||
@@ -454,7 +444,8 @@ class BuyingController(SubcontractingController):
|
||||
|
||||
# Item tax amount is the total tax amount applied on that item and actual tax type amount
|
||||
item.item_tax_amount = flt(
|
||||
item_tax_amount + actual_tax_amount, self.precision("item_tax_amount", item)
|
||||
item_tax_amount + actual_charge_per_item.get(item.idx, 0.0),
|
||||
self.precision("item_tax_amount", item),
|
||||
)
|
||||
|
||||
self.round_floats_in(item)
|
||||
@@ -503,7 +494,11 @@ class BuyingController(SubcontractingController):
|
||||
def get_tax_details(self):
|
||||
tax_accounts = []
|
||||
total_valuation_amount = 0.0
|
||||
total_actual_tax_amount = 0.0
|
||||
# Per-row "Actual" valuation charge amounts, kept separate (not pooled) so each can be
|
||||
# distributed individually - this keeps the per-item item_tax_amount in lockstep with the
|
||||
# per-tax-row amount capitalized in the GL (see get_capitalized_valuation_tax).
|
||||
all_item_charges = []
|
||||
stock_item_charges = []
|
||||
|
||||
for d in self.get("taxes"):
|
||||
if d.category not in ["Valuation", "Valuation and Total"]:
|
||||
@@ -516,10 +511,13 @@ class BuyingController(SubcontractingController):
|
||||
if d.charge_type == "On Net Total":
|
||||
total_valuation_amount += amount
|
||||
tax_accounts.append(d.account_head)
|
||||
elif d.charge_type == "Actual" and d.get("allocate_full_amount_to_stock_items"):
|
||||
# Capitalize the full amount onto stock/asset items only (e.g. Freight)
|
||||
stock_item_charges.append(amount)
|
||||
else:
|
||||
total_actual_tax_amount += amount
|
||||
all_item_charges.append(amount)
|
||||
|
||||
return tax_accounts, total_valuation_amount, total_actual_tax_amount
|
||||
return tax_accounts, total_valuation_amount, all_item_charges, stock_item_charges
|
||||
|
||||
def get_item_tax_amount(self, item, tax_accounts):
|
||||
item_tax_amount = 0.0
|
||||
@@ -540,16 +538,81 @@ class BuyingController(SubcontractingController):
|
||||
|
||||
return item_tax_amount
|
||||
|
||||
def get_item_actual_tax_amount(
|
||||
self, item, actual_tax_amount, stock_and_asset_items_amount, stock_and_asset_items_qty
|
||||
):
|
||||
item_proportion = (
|
||||
flt(item.base_net_amount) / stock_and_asset_items_amount
|
||||
if stock_and_asset_items_amount
|
||||
else flt(item.qty) / stock_and_asset_items_qty
|
||||
def distribute_actual_tax_amount(self, stock_and_asset_items, all_item_charges, stock_item_charges):
|
||||
"""Distribute "Actual" valuation charges to each item, keyed by row idx.
|
||||
|
||||
Each charge is spread individually (not pooled together) so the resulting per-item
|
||||
item_tax_amount decomposes exactly into the per-tax-row amount capitalized in the GL
|
||||
(see get_capitalized_valuation_tax) - pooling first and spreading the aggregate can drift
|
||||
by rounding for multiple charges over unevenly valued items. A charge in `all_item_charges`
|
||||
is spread across every item by net amount; a non-stock item's share is computed but never
|
||||
capitalized (e.g. a genuine tax). A charge in `stock_item_charges` (flagged
|
||||
`allocate_full_amount_to_stock_items`) is spread across stock/asset items only, so the whole
|
||||
charge is capitalized (e.g. Freight).
|
||||
"""
|
||||
all_items = [d for d in self.get("items") if d.item_code]
|
||||
stock_items = [d for d in all_items if d.item_code in stock_and_asset_items]
|
||||
|
||||
charge_per_item = {}
|
||||
for charge in all_item_charges:
|
||||
self._spread_charge_over_items(charge_per_item, charge, all_items)
|
||||
for charge in stock_item_charges:
|
||||
self._spread_charge_over_items(charge_per_item, charge, stock_items)
|
||||
return charge_per_item
|
||||
|
||||
def _spread_charge_over_items(self, charge_per_item, total_charge, items):
|
||||
"""Add each item's proportional share of `total_charge` into `charge_per_item`.
|
||||
Proportion is by net amount (falling back to qty); any rounding remainder is assigned
|
||||
to the last item in the group."""
|
||||
if not total_charge or not items:
|
||||
return
|
||||
|
||||
total_amount = sum(flt(d.base_net_amount) for d in items)
|
||||
total_qty = sum(flt(d.qty) for d in items)
|
||||
|
||||
# Nothing to proportion against (all rows have zero amount and zero qty)
|
||||
if not total_amount and not total_qty:
|
||||
return
|
||||
|
||||
remaining = total_charge
|
||||
for d in items[:-1]:
|
||||
proportion = flt(d.base_net_amount) / total_amount if total_amount else flt(d.qty) / total_qty
|
||||
charge = flt(proportion * total_charge, self.precision("item_tax_amount", d))
|
||||
charge_per_item[d.idx] = charge_per_item.get(d.idx, 0.0) + charge
|
||||
remaining -= charge
|
||||
|
||||
last = items[-1]
|
||||
charge_per_item[last.idx] = charge_per_item.get(last.idx, 0.0) + flt(
|
||||
remaining, self.precision("item_tax_amount", last)
|
||||
)
|
||||
|
||||
return flt(item_proportion * actual_tax_amount, self.precision("item_tax_amount", item))
|
||||
def get_capitalized_valuation_tax(self):
|
||||
stock_and_asset_items = self.get_stock_items() + self.get_asset_items()
|
||||
all_items = [d for d in self.get("items") if d.item_code]
|
||||
stock_item_idx = {d.idx for d in all_items if d.item_code in stock_and_asset_items}
|
||||
|
||||
capitalized = {}
|
||||
for tax in self.get("taxes"):
|
||||
if tax.category not in ("Valuation", "Valuation and Total"):
|
||||
continue
|
||||
|
||||
amount = flt(tax.base_tax_amount_after_discount_amount) * (
|
||||
-1 if tax.get("add_deduct_tax") == "Deduct" else 1
|
||||
)
|
||||
if not amount:
|
||||
continue
|
||||
|
||||
if tax.charge_type == "Actual" and not tax.get("allocate_full_amount_to_stock_items"):
|
||||
# Spread across all items; only the stock/asset items' share is capitalized.
|
||||
charge_per_item = {}
|
||||
self._spread_charge_over_items(charge_per_item, amount, all_items)
|
||||
amount = sum(
|
||||
charge for item_idx, charge in charge_per_item.items() if item_idx in stock_item_idx
|
||||
)
|
||||
|
||||
capitalized[tax.name] = amount
|
||||
|
||||
return capitalized
|
||||
|
||||
def set_incoming_rate(self):
|
||||
"""
|
||||
|
||||
@@ -7,6 +7,7 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Case
|
||||
from frappe.utils import cstr, flt
|
||||
|
||||
from erpnext.utilities.product import get_item_codes_by_attributes
|
||||
@@ -129,6 +130,53 @@ def validate_is_incremental(numeric_attribute, attribute, value, item):
|
||||
)
|
||||
|
||||
|
||||
def get_attribute_value_renames(item_attribute):
|
||||
"""Return old to new attribute value mappings for renamed Item Attribute Value rows."""
|
||||
if item_attribute.numeric_values:
|
||||
return {}
|
||||
|
||||
db_value = item_attribute.get_doc_before_save()
|
||||
if not db_value:
|
||||
return {}
|
||||
|
||||
old_values = {d.name: d.attribute_value for d in db_value.item_attribute_values}
|
||||
renames = {}
|
||||
|
||||
for row in item_attribute.item_attribute_values:
|
||||
if row.name in old_values and old_values[row.name] != row.attribute_value:
|
||||
renames[old_values[row.name]] = row.attribute_value
|
||||
|
||||
return renames
|
||||
|
||||
|
||||
def update_variant_attribute_values(item_attribute):
|
||||
"""Propagate renamed Item Attribute Values to Item Variant Attribute on variant items."""
|
||||
value_map = get_attribute_value_renames(item_attribute)
|
||||
if not value_map:
|
||||
return
|
||||
|
||||
item_variant_table = frappe.qb.DocType("Item Variant Attribute")
|
||||
item_table = frappe.qb.DocType("Item")
|
||||
attribute_value = item_variant_table.attribute_value
|
||||
attribute_value_case = Case()
|
||||
|
||||
for old_value, new_value in value_map.items():
|
||||
attribute_value_case = attribute_value_case.when(attribute_value == old_value, new_value)
|
||||
|
||||
(
|
||||
frappe.qb.update(item_variant_table)
|
||||
.join(item_table)
|
||||
.on(item_table.name == item_variant_table.parent)
|
||||
.set(attribute_value, attribute_value_case.else_(attribute_value))
|
||||
.where(item_table.variant_of.isnotnull())
|
||||
.where(item_table.variant_of != "")
|
||||
.where(item_variant_table.attribute == item_attribute.name)
|
||||
.where(attribute_value.isin(list(value_map)))
|
||||
).run()
|
||||
|
||||
frappe.flags.attribute_values = None
|
||||
|
||||
|
||||
def validate_item_attribute_value(attributes_list, attribute, attribute_value, item, from_variant=True):
|
||||
allow_rename_attribute_value = frappe.db.get_single_value(
|
||||
"Item Variant Settings", "allow_rename_attribute_value"
|
||||
|
||||
@@ -216,11 +216,8 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
|
||||
group = "Customer Group" if filters.get("customer") else "Supplier Group"
|
||||
item_rules_list = frappe.get_all(
|
||||
"Party Specific Item",
|
||||
filters={
|
||||
"party": ["!=", party],
|
||||
"party_type": party_type,
|
||||
},
|
||||
fields=["restrict_based_on", "based_on_value"],
|
||||
filters={"party_type": party_type},
|
||||
fields=["party", "restrict_based_on", "based_on_value"],
|
||||
)
|
||||
|
||||
party_group_rules_list = frappe.get_all(
|
||||
@@ -229,21 +226,30 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
|
||||
fields=["party as party_group", "restrict_based_on", "based_on_value"],
|
||||
)
|
||||
current_party_group = frappe.get_value(party_type, party, frappe.scrub(group))
|
||||
|
||||
restricted_items = defaultdict(set)
|
||||
allowed_items = defaultdict(set)
|
||||
|
||||
for rule in item_rules_list:
|
||||
restrict_based_on = "name" if rule.restrict_based_on == "Item" else rule.restrict_based_on
|
||||
|
||||
if rule.party == party:
|
||||
allowed_items[restrict_based_on].add(rule.based_on_value)
|
||||
else:
|
||||
restricted_items[restrict_based_on].add(rule.based_on_value)
|
||||
|
||||
for rule in party_group_rules_list:
|
||||
if current_party_group != rule.party_group:
|
||||
item_rules_list.append(rule)
|
||||
restrict_based_on = "name" if rule.restrict_based_on == "Item" else rule.restrict_based_on
|
||||
|
||||
filters_dict = {}
|
||||
for rule in item_rules_list:
|
||||
if rule["restrict_based_on"] == "Item":
|
||||
rule["restrict_based_on"] = "name"
|
||||
filters_dict[rule.restrict_based_on] = []
|
||||
if current_party_group == rule.party_group:
|
||||
allowed_items[restrict_based_on].add(rule.based_on_value)
|
||||
else:
|
||||
restricted_items[restrict_based_on].add(rule.based_on_value)
|
||||
|
||||
for rule in item_rules_list:
|
||||
filters_dict[rule.restrict_based_on].append(rule.based_on_value)
|
||||
|
||||
for filter in filters_dict:
|
||||
filters[scrub(filter)] = ["not in", filters_dict[filter]]
|
||||
for field, restricted_values in restricted_items.items():
|
||||
values_to_exclude = restricted_values - allowed_items[field]
|
||||
if values_to_exclude:
|
||||
filters[scrub(field)] = ["not in", list(values_to_exclude)]
|
||||
|
||||
if filters.get("customer"):
|
||||
del filters["customer"]
|
||||
|
||||
@@ -445,6 +445,8 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
doc.pricing_rules = []
|
||||
doc.return_against = source.name
|
||||
doc.set_warehouse = ""
|
||||
if doctype == "Sales Invoice":
|
||||
doc.is_debit_note = 0
|
||||
if doctype == "Sales Invoice" or doctype == "POS Invoice":
|
||||
doc.is_pos = source.is_pos
|
||||
|
||||
|
||||
@@ -186,7 +186,8 @@ class StatusUpdater(Document):
|
||||
"""
|
||||
|
||||
def on_discard(self):
|
||||
self.db_set("status", "Cancelled")
|
||||
if self.meta.has_field("status"):
|
||||
self.db_set("status", "Cancelled")
|
||||
|
||||
def update_prevdoc_status(self):
|
||||
self.update_qty()
|
||||
|
||||
@@ -22,6 +22,14 @@ from erpnext.controllers.sales_and_purchase_return import (
|
||||
filter_serial_batches,
|
||||
make_serial_batch_bundle_for_return,
|
||||
)
|
||||
|
||||
# Re-exported for backward compatibility; canonical home is erpnext.exceptions.
|
||||
from erpnext.exceptions import (
|
||||
BatchExpiredError,
|
||||
QualityInspectionNotSubmittedError,
|
||||
QualityInspectionRejectedError,
|
||||
QualityInspectionRequiredError,
|
||||
)
|
||||
from erpnext.setup.doctype.brand.brand import get_brand_defaults
|
||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
@@ -37,22 +45,6 @@ from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle impor
|
||||
from erpnext.stock.stock_ledger import get_items_to_be_repost
|
||||
|
||||
|
||||
class QualityInspectionRequiredError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class QualityInspectionRejectedError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class QualityInspectionNotSubmittedError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class BatchExpiredError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class StockController(AccountsController):
|
||||
def validate(self):
|
||||
super().validate()
|
||||
@@ -2163,7 +2155,7 @@ def check_item_quality_inspection(doctype: str, docstatus: str | int, items: str
|
||||
|
||||
inspection_fieldname = inspection_fieldname_map.get(doctype)
|
||||
if inspection_fieldname is None:
|
||||
return []
|
||||
return items if doctype == "Stock Entry" else []
|
||||
|
||||
allow_after_transaction = cint(docstatus) == 1 and frappe.get_single_value(
|
||||
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
|
||||
|
||||
@@ -743,7 +743,14 @@ class SubcontractingInwardController:
|
||||
"name": ["in", list(data.keys())],
|
||||
"docstatus": 1,
|
||||
},
|
||||
fields=["rate", "name", "required_qty", "received_qty"],
|
||||
fields=[
|
||||
"rate",
|
||||
"name",
|
||||
"required_qty",
|
||||
"received_qty",
|
||||
"returned_qty",
|
||||
"consumed_qty",
|
||||
],
|
||||
)
|
||||
|
||||
doc_updates = {}
|
||||
@@ -751,13 +758,17 @@ class SubcontractingInwardController:
|
||||
current_qty = flt(data[d.name].transfer_qty) * (1 if self._action == "submit" else -1)
|
||||
current_rate = flt(data[d.name].rate)
|
||||
|
||||
# Calculate weighted average rate
|
||||
old_total = d.rate * d.received_qty
|
||||
# Weighted average rate must be computed on the on-hand balance
|
||||
balance_qty = d.received_qty - d.returned_qty - d.consumed_qty
|
||||
old_total = d.rate * balance_qty
|
||||
current_total = current_rate * current_qty
|
||||
|
||||
new_balance_qty = balance_qty + current_qty
|
||||
d.received_qty = d.received_qty + current_qty
|
||||
d.rate = (
|
||||
flt((old_total + current_total) / d.received_qty, precision) if d.received_qty else 0.0
|
||||
flt((old_total + current_total) / new_balance_qty, precision)
|
||||
if new_balance_qty > 0
|
||||
else 0.0
|
||||
)
|
||||
|
||||
if not d.required_qty and not d.received_qty:
|
||||
|
||||
@@ -32,11 +32,12 @@ from erpnext.utilities.regional import temporary_flag
|
||||
class calculate_taxes_and_totals:
|
||||
def __init__(self, doc: Document):
|
||||
self.doc = doc
|
||||
frappe.flags.round_off_applicable_accounts = []
|
||||
frappe.flags.round_off_applicable_accounts = (
|
||||
get_round_off_applicable_accounts(self.doc.company, [], self.doc) or []
|
||||
)
|
||||
frappe.flags.round_row_wise_tax = frappe.get_single_value("Accounts Settings", "round_row_wise_tax")
|
||||
|
||||
self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items")
|
||||
get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)
|
||||
self.calculate()
|
||||
|
||||
def filter_rows(self):
|
||||
@@ -1240,14 +1241,16 @@ def get_itemised_tax_breakup_html(doc):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_round_off_applicable_accounts(company, account_list):
|
||||
def get_round_off_applicable_accounts(
|
||||
company: str, account_list: list | str, doc: str | dict | Document | None = None
|
||||
):
|
||||
# required to set correct region
|
||||
with temporary_flag("company", company):
|
||||
return get_regional_round_off_accounts(company, account_list)
|
||||
return get_regional_round_off_accounts(company, account_list, doc)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def get_regional_round_off_accounts(company, account_list):
|
||||
def get_regional_round_off_accounts(company, account_list, doc=None):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals
|
||||
@@ -6,6 +8,28 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestTaxesAndTotals(ERPNextTestSuite):
|
||||
def test_regional_round_off_accounts(self):
|
||||
"""
|
||||
Regional overrides cannot extend the list in-place — the return
|
||||
value must be assigned back to frappe.flags.round_off_applicable_accounts.
|
||||
"""
|
||||
test_account = "_Test Round Off Account"
|
||||
|
||||
def mock_regional(company, account_list: list, doc=None) -> list:
|
||||
# Simulates a regional override
|
||||
account_list.extend([test_account])
|
||||
return account_list
|
||||
|
||||
so = make_sales_order(do_not_save=True)
|
||||
|
||||
with patch(
|
||||
"erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts",
|
||||
mock_regional,
|
||||
):
|
||||
calculate_taxes_and_totals(so)
|
||||
|
||||
self.assertIn(test_account, frappe.flags.round_off_applicable_accounts)
|
||||
|
||||
def test_disabling_rounded_total_resets_base_fields(self):
|
||||
"""Disabling rounded total should also clear base rounded values."""
|
||||
so = make_sales_order(do_not_save=True)
|
||||
|
||||
@@ -20,7 +20,11 @@
|
||||
"section_break_13",
|
||||
"carry_forward_communication_and_comments",
|
||||
"column_break_junk",
|
||||
"update_timestamp_on_new_communication"
|
||||
"update_timestamp_on_new_communication",
|
||||
"frappe_crm_section",
|
||||
"enable_frappe_crm_data_synchronization",
|
||||
"column_break_jbzj",
|
||||
"allowed_users"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -105,6 +109,30 @@
|
||||
"fieldname": "enable_opportunity_creation_from_contact_us",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Opportunity Creation from Contact Us"
|
||||
},
|
||||
{
|
||||
"fieldname": "frappe_crm_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Frappe CRM"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_jbzj",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.enable_frappe_crm_data_synchronization === 1;",
|
||||
"fieldname": "allowed_users",
|
||||
"fieldtype": "Table MultiSelect",
|
||||
"label": "Allowed Users",
|
||||
"options": "Frappe CRM Allowed User",
|
||||
"permlevel": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enable_frappe_crm_data_synchronization",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Frappe CRM Data Synchronization",
|
||||
"permlevel": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -112,7 +140,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-06-11 23:09:49.750381",
|
||||
"modified": "2026-06-22 01:26:13.474915",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "CRM Settings",
|
||||
@@ -146,6 +174,16 @@
|
||||
"role": "Sales Master Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"permlevel": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields, delete_custom_fields
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
@@ -15,12 +16,16 @@ class CRMSettings(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.crm.doctype.frappe_crm_allowed_user.frappe_crm_allowed_user import FrappeCRMAllowedUser
|
||||
|
||||
allow_lead_duplication_based_on_emails: DF.Check
|
||||
allowed_users: DF.TableMultiSelect[FrappeCRMAllowedUser]
|
||||
auto_creation_of_contact: DF.Check
|
||||
campaign_naming_by: DF.Literal["Campaign Name", "Naming Series"]
|
||||
carry_forward_communication_and_comments: DF.Check
|
||||
close_opportunity_after_days: DF.Int
|
||||
default_valid_till: DF.Data | None
|
||||
enable_frappe_crm_data_synchronization: DF.Check
|
||||
enable_opportunity_creation_from_contact_us: DF.Check
|
||||
update_timestamp_on_new_communication: DF.Check
|
||||
# end: auto-generated types
|
||||
@@ -28,6 +33,7 @@ class CRMSettings(Document):
|
||||
def validate(self):
|
||||
frappe.db.set_default("campaign_naming_by", self.get("campaign_naming_by", ""))
|
||||
self.validate_enable_opportunity_creation_from_contact_us()
|
||||
self.validate_allowed_users()
|
||||
|
||||
def validate_enable_opportunity_creation_from_contact_us(self):
|
||||
contact_disabled = frappe.get_single_value("Contact Us Settings", "is_disabled")
|
||||
@@ -38,3 +44,43 @@ class CRMSettings(Document):
|
||||
"Cannot enable Opportunity creation from Contact Us because the Contact Us form is disabled."
|
||||
)
|
||||
)
|
||||
|
||||
def validate_allowed_users(self):
|
||||
if self.enable_frappe_crm_data_synchronization and not self.allowed_users:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please add atleast one user on Allowed Users to allow Data Synchronization from Frappe CRM site."
|
||||
)
|
||||
)
|
||||
|
||||
def before_save(self):
|
||||
self.clear_allowed_users()
|
||||
|
||||
def on_update(self):
|
||||
self.custom_fields_for_frappe_crm_data_sync()
|
||||
|
||||
def clear_allowed_users(self):
|
||||
if not self.enable_frappe_crm_data_synchronization:
|
||||
self.allowed_users = []
|
||||
|
||||
def custom_fields_for_frappe_crm_data_sync(self):
|
||||
custom_fields = {
|
||||
"Quotation": [
|
||||
{
|
||||
"fieldname": "crm_deal",
|
||||
"fieldtype": "Data",
|
||||
"label": "Frappe CRM Deal",
|
||||
"insert_after": "party_name",
|
||||
}
|
||||
],
|
||||
"Customer": [
|
||||
{
|
||||
"fieldname": "crm_deal",
|
||||
"fieldtype": "Data",
|
||||
"label": "Frappe CRM Deal",
|
||||
"insert_after": "prospect_name",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
create_custom_fields(custom_fields, ignore_validate=True)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"allow_rename": 1,
|
||||
"creation": "2026-06-22 00:47:12.265968",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"user"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "User",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-06-22 01:49:54.586410",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Frappe CRM Allowed User",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"rows_threshold_for_grid_search": 20,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class FrappeCRMAllowedUser(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
|
||||
user: DF.Link
|
||||
# end: auto-generated types
|
||||
|
||||
_DOCTYPE_NAME = "Frappe CRM Allowed User"
|
||||
@@ -2,35 +2,12 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_custom_fields_for_frappe_crm():
|
||||
frappe.only_for("System Manager")
|
||||
custom_fields = {
|
||||
"Quotation": [
|
||||
{
|
||||
"fieldname": "crm_deal",
|
||||
"fieldtype": "Data",
|
||||
"label": "Frappe CRM Deal",
|
||||
"insert_after": "party_name",
|
||||
}
|
||||
],
|
||||
"Customer": [
|
||||
{
|
||||
"fieldname": "crm_deal",
|
||||
"fieldtype": "Data",
|
||||
"label": "Frappe CRM Deal",
|
||||
"insert_after": "prospect_name",
|
||||
}
|
||||
],
|
||||
}
|
||||
create_custom_fields(custom_fields, ignore_validate=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_prospect_against_crm_deal():
|
||||
validate_frappe_crm_sync()
|
||||
|
||||
doc = frappe.form_dict
|
||||
prospect = frappe.new_doc("Prospect")
|
||||
prospect.company_name = doc.organization or doc.lead_name
|
||||
@@ -161,6 +138,8 @@ CUSTOMER_ALLOWED_FIELDS = {
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_customer(customer_data=None):
|
||||
validate_frappe_crm_sync()
|
||||
|
||||
if not customer_data:
|
||||
customer_data = frappe.form_dict
|
||||
|
||||
@@ -181,3 +160,21 @@ def create_customer(customer_data=None):
|
||||
except Exception:
|
||||
frappe.log_error(frappe.get_traceback(), "Error while creating customer against Frappe CRM Deal")
|
||||
pass
|
||||
|
||||
|
||||
def validate_frappe_crm_sync():
|
||||
CRMSettings = frappe.get_single("CRM Settings")
|
||||
if not CRMSettings.enable_frappe_crm_data_synchronization:
|
||||
frappe.throw(
|
||||
_("Frappe CRM data synchronization is not enabled on ERPNext. Contact System Manager of ERPNext.")
|
||||
)
|
||||
|
||||
allowed_users = [d.user for d in CRMSettings.allowed_users]
|
||||
|
||||
if frappe.session.user not in allowed_users:
|
||||
frappe.throw(
|
||||
_(
|
||||
"User not allowed to synchronize data from Frappe CRM on ERPNext. Contact System Manager of ERPNext."
|
||||
),
|
||||
exc=frappe.PermissionError,
|
||||
)
|
||||
|
||||
@@ -28,3 +28,20 @@ class MandatoryAccountDimensionError(frappe.ValidationError):
|
||||
|
||||
class ReportingCurrencyExchangeNotFoundError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
# stock
|
||||
class QualityInspectionRequiredError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class QualityInspectionRejectedError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class QualityInspectionNotSubmittedError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class BatchExpiredError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
3822
erpnext/locale/af.po
3822
erpnext/locale/af.po
File diff suppressed because it is too large
Load Diff
3824
erpnext/locale/ar.po
3824
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
3914
erpnext/locale/bs.po
3914
erpnext/locale/bs.po
File diff suppressed because it is too large
Load Diff
3818
erpnext/locale/cs.po
3818
erpnext/locale/cs.po
File diff suppressed because it is too large
Load Diff
3818
erpnext/locale/da.po
3818
erpnext/locale/da.po
File diff suppressed because it is too large
Load Diff
3830
erpnext/locale/de.po
3830
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
3888
erpnext/locale/eo.po
3888
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
3822
erpnext/locale/es.po
3822
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
3876
erpnext/locale/fa.po
3876
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
3822
erpnext/locale/fi.po
3822
erpnext/locale/fi.po
File diff suppressed because it is too large
Load Diff
3822
erpnext/locale/fr.po
3822
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
3820
erpnext/locale/hi.po
3820
erpnext/locale/hi.po
File diff suppressed because it is too large
Load Diff
4052
erpnext/locale/hr.po
4052
erpnext/locale/hr.po
File diff suppressed because it is too large
Load Diff
3818
erpnext/locale/hu.po
3818
erpnext/locale/hu.po
File diff suppressed because it is too large
Load Diff
3822
erpnext/locale/id.po
3822
erpnext/locale/id.po
File diff suppressed because it is too large
Load Diff
3818
erpnext/locale/it.po
3818
erpnext/locale/it.po
File diff suppressed because it is too large
Load Diff
3824
erpnext/locale/ko.po
3824
erpnext/locale/ko.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
3818
erpnext/locale/my.po
3818
erpnext/locale/my.po
File diff suppressed because it is too large
Load Diff
3818
erpnext/locale/nb.po
3818
erpnext/locale/nb.po
File diff suppressed because it is too large
Load Diff
3824
erpnext/locale/nl.po
3824
erpnext/locale/nl.po
File diff suppressed because it is too large
Load Diff
3818
erpnext/locale/pl.po
3818
erpnext/locale/pl.po
File diff suppressed because it is too large
Load Diff
3818
erpnext/locale/pt.po
3818
erpnext/locale/pt.po
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user