mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-25 14:27:05 +00:00
Compare commits
41 Commits
version-15
...
v15.121.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26f0687834 | ||
|
|
f2cfe30596 | ||
|
|
df8b7f9648 | ||
|
|
0deed27cfe | ||
|
|
fdc48277a8 | ||
|
|
8635652c90 | ||
|
|
1a0bf0bf6c | ||
|
|
57266ab474 | ||
|
|
945e825bee | ||
|
|
5d60bd9f54 | ||
|
|
0526834071 | ||
|
|
88f32f1d1b | ||
|
|
d707cb1e0e | ||
|
|
ac5b4d185b | ||
|
|
be176617cc | ||
|
|
942a1ddb2c | ||
|
|
866688931b | ||
|
|
f0d1887e6e | ||
|
|
7098602dcc | ||
|
|
32b56ac505 | ||
|
|
9f1bdba9a7 | ||
|
|
c630226846 | ||
|
|
d5b49cd66e | ||
|
|
b9c9b76f5b | ||
|
|
da0e3b5882 | ||
|
|
3d4198494b | ||
|
|
e6b4799b1a | ||
|
|
49eb2366cd | ||
|
|
fb2a4e5f98 | ||
|
|
0ce7917648 | ||
|
|
41038979ec | ||
|
|
88b6779062 | ||
|
|
b5f784612d | ||
|
|
457424f7a4 | ||
|
|
25ee3695f0 | ||
|
|
ff205da810 | ||
|
|
2980171007 | ||
|
|
c6c4815e8d | ||
|
|
82a85818c2 | ||
|
|
57a2be6b56 | ||
|
|
47f54a4725 |
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.112.0"
|
||||
__version__ = "15.121.3"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -49,10 +49,6 @@ class ERPNextAddress(Address):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_shipping_address(company, address=None):
|
||||
# `select`, not `read`: the roles that fill in these transactions hold no Company `read` row.
|
||||
# doc= so the named company is evaluated and User Permissions apply.
|
||||
frappe.has_permission("Company", ptype="select", doc=company, throw=True)
|
||||
|
||||
filters = [
|
||||
["Dynamic Link", "link_doctype", "=", "Company"],
|
||||
["Dynamic Link", "link_name", "=", company],
|
||||
|
||||
@@ -25,7 +25,6 @@ def get(
|
||||
):
|
||||
if chart_name:
|
||||
chart = frappe.get_doc("Dashboard Chart", chart_name)
|
||||
chart.check_permission("read")
|
||||
else:
|
||||
chart = frappe._dict(frappe.parse_json(chart))
|
||||
timespan = chart.timespan
|
||||
@@ -54,9 +53,6 @@ def get(
|
||||
)
|
||||
)
|
||||
|
||||
# authorise the account itself, as get_balance_on() does; doc= brings User Permissions with it
|
||||
frappe.has_permission("Account", doc=account, throw=True)
|
||||
|
||||
if not to_date:
|
||||
to_date = nowdate()
|
||||
if not from_date:
|
||||
|
||||
@@ -191,7 +191,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2024-08-19 15:19:11.095045",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account",
|
||||
@@ -244,46 +244,6 @@
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Quality Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "account_number",
|
||||
|
||||
@@ -502,19 +502,12 @@ class Account(NestedSet):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_parent_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
return frappe.get_list(
|
||||
"Account",
|
||||
filters=[
|
||||
["is_group", "=", 1],
|
||||
["docstatus", "!=", 2],
|
||||
["company", "=", filters["company"]],
|
||||
[searchfield, "like", f"%{txt}%"],
|
||||
],
|
||||
fields=["name"],
|
||||
order_by="name",
|
||||
limit_start=start,
|
||||
limit_page_length=page_len,
|
||||
as_list=True,
|
||||
return frappe.db.sql(
|
||||
"""select name from tabAccount
|
||||
where is_group = 1 and docstatus != 2 and company = {}
|
||||
and {} like {} order by name limit {} offset {}""".format("%s", searchfield, "%s", "%s", "%s"),
|
||||
(filters["company"], "%%%s%%" % txt, page_len, start),
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -120,8 +120,6 @@ def identify_is_group(child):
|
||||
def get_chart(chart_template, existing_company=None):
|
||||
chart = {}
|
||||
if existing_company:
|
||||
frappe.has_permission("Company", doc=existing_company, throw=True)
|
||||
|
||||
return get_account_tree_from_existing_company(existing_company)
|
||||
|
||||
elif chart_template == "Standard":
|
||||
|
||||
@@ -207,11 +207,8 @@ def delete_accounting_dimension(doc):
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def disable_dimension(doc):
|
||||
# toggle_disabling rewrites a Custom Field site-wide, so demand the write that configures dimensions
|
||||
frappe.has_permission("Accounting Dimension", "write", throw=True)
|
||||
|
||||
if frappe.flags.in_test:
|
||||
toggle_disabling(doc=doc)
|
||||
else:
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2020-07-17 14:00:13.105433",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank",
|
||||
@@ -118,14 +118,6 @@
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Accounts User",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
||||
@@ -254,7 +254,7 @@
|
||||
"link_fieldname": "default_bank_account"
|
||||
}
|
||||
],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2026-01-20 00:46:16.633364",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Account",
|
||||
@@ -284,22 +284,6 @@
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales User",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -60,9 +60,6 @@ def get_voucher_details(bank_guarantee_type: str, reference_name: str):
|
||||
if not isinstance(reference_name, str):
|
||||
raise TypeError("reference_name must be a string")
|
||||
|
||||
# the form is the boundary, not the referenced order: an order guard would break one of the two roles
|
||||
frappe.has_permission("Bank Guarantee", throw=True)
|
||||
|
||||
fields_to_fetch = ["grand_total"]
|
||||
|
||||
if bank_guarantee_type == "Receiving":
|
||||
|
||||
@@ -68,7 +68,6 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
frappe.msgprint(__("Please select Bank Account"));
|
||||
return;
|
||||
}
|
||||
frm.events.validate_dates(frm);
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.auto_reconcile_vouchers",
|
||||
args: {
|
||||
@@ -83,7 +82,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
});
|
||||
|
||||
frm.add_custom_button(__("Get Unreconciled Entries"), function () {
|
||||
return frm.trigger("make_reconciliation_tool");
|
||||
frm.trigger("make_reconciliation_tool");
|
||||
});
|
||||
frm.change_custom_button_type(__("Get Unreconciled Entries"), null, "primary");
|
||||
},
|
||||
@@ -102,24 +101,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
frm.trigger("get_account_opening_balance");
|
||||
},
|
||||
|
||||
validate_dates(frm) {
|
||||
const from_date = frm.doc.filter_by_reference_date
|
||||
? frm.doc.from_reference_date
|
||||
: frm.doc.bank_statement_from_date;
|
||||
const to_date = frm.doc.filter_by_reference_date
|
||||
? frm.doc.to_reference_date
|
||||
: frm.doc.bank_statement_to_date;
|
||||
if (from_date && to_date && from_date > to_date) {
|
||||
frappe.throw(
|
||||
frm.doc.filter_by_reference_date
|
||||
? __("From Reference Date cannot be greater than To Reference Date")
|
||||
: __("From Date cannot be greater than To Date")
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
make_reconciliation_tool(frm) {
|
||||
frm.events.validate_dates(frm);
|
||||
frm.get_field("reconciliation_tool_cards").$wrapper.empty();
|
||||
if (frm.doc.company && frm.doc.bank_account && frm.doc.bank_statement_to_date) {
|
||||
frm.trigger("get_cleared_balance").then(() => {
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
|
||||
|
||||
import json
|
||||
from datetime import date
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, create_batch, flt, getdate
|
||||
from frappe.utils import cint, create_batch, flt
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
|
||||
@@ -48,12 +47,8 @@ class BankReconciliationTool(Document):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_bank_transactions(
|
||||
bank_account: str, from_date: str | date | None = None, to_date: str | date | None = None
|
||||
):
|
||||
def get_bank_transactions(bank_account, from_date=None, to_date=None):
|
||||
# returns bank transactions for a bank account
|
||||
validate_date_range(from_date, to_date)
|
||||
|
||||
filters = []
|
||||
filters.append(["bank_account", "=", bank_account])
|
||||
filters.append(["docstatus", "=", 1])
|
||||
@@ -377,14 +372,13 @@ def create_payment_entry_bts(
|
||||
|
||||
@frappe.whitelist()
|
||||
def auto_reconcile_vouchers(
|
||||
bank_account: str,
|
||||
from_date: str | date | None = None,
|
||||
to_date: str | date | None = None,
|
||||
filter_by_reference_date: bool | None = None,
|
||||
from_reference_date: str | date | None = None,
|
||||
to_reference_date: str | date | None = None,
|
||||
bank_account,
|
||||
from_date=None,
|
||||
to_date=None,
|
||||
filter_by_reference_date=None,
|
||||
from_reference_date=None,
|
||||
to_reference_date=None,
|
||||
):
|
||||
validate_date_range(from_date, to_date, filter_by_reference_date, from_reference_date, to_reference_date)
|
||||
bank_transactions = get_bank_transactions(bank_account)
|
||||
|
||||
if len(bank_transactions) > 10:
|
||||
@@ -494,16 +488,15 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_linked_payments(
|
||||
bank_transaction_name: str,
|
||||
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,
|
||||
from_reference_date: str | date | None = None,
|
||||
to_reference_date: str | date | None = None,
|
||||
bank_transaction_name,
|
||||
document_types=None,
|
||||
from_date=None,
|
||||
to_date=None,
|
||||
filter_by_reference_date=None,
|
||||
from_reference_date=None,
|
||||
to_reference_date=None,
|
||||
):
|
||||
# get all matching payments for a bank transaction
|
||||
validate_date_range(from_date, to_date, filter_by_reference_date, from_reference_date, to_reference_date)
|
||||
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
|
||||
bank_account = frappe.db.get_values(
|
||||
"Bank Account", transaction.bank_account, ["account", "company"], as_dict=True
|
||||
@@ -523,23 +516,6 @@ def get_linked_payments(
|
||||
return subtract_allocations(gl_account, matching)
|
||||
|
||||
|
||||
def validate_date_range(
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date=False,
|
||||
from_reference_date=None,
|
||||
to_reference_date=None,
|
||||
):
|
||||
if cint(filter_by_reference_date):
|
||||
from_date, to_date = from_reference_date, to_reference_date
|
||||
message = _("From Reference Date cannot be greater than To Reference Date")
|
||||
else:
|
||||
message = _("From Date cannot be greater than To Date")
|
||||
|
||||
if from_date and to_date and getdate(from_date) > getdate(to_date):
|
||||
frappe.throw(message)
|
||||
|
||||
|
||||
def subtract_allocations(gl_account, vouchers):
|
||||
"Look up & subtract any existing Bank Transaction allocations"
|
||||
copied = []
|
||||
|
||||
@@ -10,7 +10,6 @@ from frappe.utils import add_days, today
|
||||
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
|
||||
auto_reconcile_vouchers,
|
||||
get_bank_transactions,
|
||||
get_linked_payments,
|
||||
)
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
@@ -99,42 +98,3 @@ class TestBankReconciliationTool(AccountsTestMixin, FrappeTestCase):
|
||||
# assert API output post reconciliation
|
||||
transactions = get_bank_transactions(self.bank_account, from_date, to_date)
|
||||
self.assertEqual(len(transactions), 0)
|
||||
|
||||
def test_rejects_reversed_date_ranges(self):
|
||||
from_date, to_date = today(), add_days(today(), -1)
|
||||
with self.assertRaisesRegex(frappe.ValidationError, "From Date cannot be greater than To Date"):
|
||||
get_bank_transactions(self.bank_account, from_date, to_date)
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
frappe.ValidationError, "From Reference Date cannot be greater than To Reference Date"
|
||||
):
|
||||
auto_reconcile_vouchers(
|
||||
self.bank_account,
|
||||
filter_by_reference_date=True,
|
||||
from_reference_date=from_date,
|
||||
to_reference_date=to_date,
|
||||
)
|
||||
|
||||
transaction = (
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Bank Transaction",
|
||||
"date": today(),
|
||||
"deposit": 100,
|
||||
"bank_account": self.bank_account,
|
||||
"currency": "INR",
|
||||
}
|
||||
)
|
||||
.insert()
|
||||
.submit()
|
||||
)
|
||||
with self.assertRaisesRegex(
|
||||
frappe.ValidationError, "From Reference Date cannot be greater than To Reference Date"
|
||||
):
|
||||
get_linked_payments(
|
||||
transaction.name,
|
||||
["payment_entry"],
|
||||
filter_by_reference_date=True,
|
||||
from_reference_date=from_date,
|
||||
to_reference_date=to_date,
|
||||
)
|
||||
|
||||
@@ -7,7 +7,6 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
is_company_account: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -296,10 +296,6 @@ def get_import_logs(docname: str):
|
||||
|
||||
@frappe.whitelist()
|
||||
def upload_bank_statement(**args):
|
||||
# `create`: the sole caller is the Bank Reconciliation Tool's upload button, which routes
|
||||
# straight into a new Bank Statement Import form.
|
||||
frappe.has_permission("Bank Statement Import", "create", throw=True)
|
||||
|
||||
args = frappe._dict(args)
|
||||
bsi = frappe.new_doc("Bank Statement Import")
|
||||
|
||||
|
||||
@@ -11,9 +11,6 @@ from frappe.utils.dateutils import parse_date
|
||||
|
||||
@frappe.whitelist()
|
||||
def upload_bank_statement():
|
||||
# parsing a statement is the first step of creating Bank Transactions from it
|
||||
frappe.has_permission("Bank Transaction", "create", throw=True)
|
||||
|
||||
if getattr(frappe, "uploaded_file", None):
|
||||
with open(frappe.uploaded_file, "rb") as upfile:
|
||||
fcontent = upfile.read()
|
||||
@@ -39,11 +36,6 @@ def upload_bank_statement():
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_bank_entries(columns, data, bank_account):
|
||||
# insert()/submit() enforce this per document, but only after the row loop has written an
|
||||
# Error Log for every rejected row -- so check once up front.
|
||||
frappe.has_permission("Bank Transaction", "create", throw=True)
|
||||
frappe.has_permission("Bank Account", doc=bank_account, throw=True)
|
||||
|
||||
header_map = get_header_mapping(columns, bank_account)
|
||||
|
||||
success = 0
|
||||
|
||||
@@ -8,7 +8,6 @@ from functools import reduce
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.core.doctype.file.utils import find_file_by_url
|
||||
from frappe.desk.form.linked_with import get_linked_fields
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, cstr
|
||||
@@ -59,8 +58,6 @@ def validate_columns(data):
|
||||
|
||||
@frappe.whitelist()
|
||||
def validate_company(company):
|
||||
frappe.has_permission("Chart of Accounts Importer", throw=True)
|
||||
|
||||
parent_company, allow_account_creation_against_child_company = frappe.get_cached_value(
|
||||
"Company", company, ["parent_company", "allow_account_creation_against_child_company"]
|
||||
)
|
||||
@@ -113,11 +110,7 @@ def import_coa(file_name, company):
|
||||
|
||||
|
||||
def get_file(file_name):
|
||||
# look the file up through find_file_by_url, which returns it only when the caller may download it
|
||||
file_doc = find_file_by_url(file_name)
|
||||
if not file_doc:
|
||||
raise frappe.PermissionError
|
||||
|
||||
file_doc = frappe.get_doc("File", {"file_url": file_name})
|
||||
parts = file_doc.get_extension()
|
||||
extension = parts[1]
|
||||
extension = extension.lstrip(".")
|
||||
@@ -183,7 +176,6 @@ def generate_data_from_excel(file_doc, extension, as_dict=False):
|
||||
@frappe.whitelist()
|
||||
def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0):
|
||||
"""called by tree view (to fetch node's children)"""
|
||||
frappe.has_permission("Chart of Accounts Importer", throw=True)
|
||||
|
||||
file_doc, extension = get_file(file_name)
|
||||
parent = None if parent == _("All Accounts") else parent
|
||||
@@ -332,8 +324,6 @@ def build_response_as_excel(writer):
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_template(file_type, template_type, company):
|
||||
frappe.has_permission("Chart of Accounts Importer", throw=True)
|
||||
|
||||
writer = get_template(template_type, company)
|
||||
|
||||
if file_type == "CSV":
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2024-04-24 10:55:54.083042",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Cost Center",
|
||||
@@ -172,54 +172,6 @@
|
||||
"role": "Employee",
|
||||
"select": 1,
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Projects Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Projects User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Quality Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "parent_cost_center, is_group",
|
||||
|
||||
@@ -5,10 +5,7 @@ frappe.ui.form.on("Coupon Code", {
|
||||
setup: function (frm) {
|
||||
frm.set_query("pricing_rule", function () {
|
||||
return {
|
||||
filters: {
|
||||
coupon_code_based: 1,
|
||||
disable: 0,
|
||||
},
|
||||
filters: [["Pricing Rule", "coupon_code_based", "=", "1"]],
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2019-10-19 14:48:14.602481",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Coupon Code",
|
||||
@@ -168,14 +168,6 @@
|
||||
"role": "Website Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -41,23 +41,7 @@ class CouponCode(Document):
|
||||
self.coupon_code = frappe.generate_hash()[:10].upper()
|
||||
|
||||
def validate(self):
|
||||
self.validate_from_to_dates("valid_from", "valid_upto")
|
||||
self.validate_pricing_rule()
|
||||
|
||||
if self.coupon_type == "Gift Card":
|
||||
self.maximum_use = 1
|
||||
if not self.customer:
|
||||
frappe.throw(_("Please select the customer."))
|
||||
|
||||
def validate_pricing_rule(self):
|
||||
if not self.pricing_rule or self.get("from_external_ecomm_platform"):
|
||||
return
|
||||
|
||||
# Allow existing coupons to be updated after their pricing rule is disabled.
|
||||
if not (
|
||||
self.has_value_changed("pricing_rule") or self.has_value_changed("from_external_ecomm_platform")
|
||||
):
|
||||
return
|
||||
|
||||
if frappe.db.get_value("Pricing Rule", self.pricing_rule, "disable"):
|
||||
frappe.throw(_("Pricing Rule {0} is disabled").format(frappe.bold(self.pricing_rule)))
|
||||
|
||||
@@ -115,46 +115,8 @@ class TestCouponCode(unittest.TestCase):
|
||||
test_create_test_data()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def test_disabled_pricing_rule_validation(self):
|
||||
coupon = frappe.get_doc("Coupon Code", "SAVE30")
|
||||
rule = frappe.get_doc("Pricing Rule", coupon.pricing_rule)
|
||||
rule.disable = 1
|
||||
rule.save()
|
||||
|
||||
with self.subTest("new coupon cannot select a disabled rule"):
|
||||
new_coupon = frappe.copy_doc(coupon)
|
||||
new_coupon.coupon_name = "Festival Savings"
|
||||
new_coupon.coupon_code = "FESTSAVE"
|
||||
with self.assertRaisesRegex(frappe.ValidationError, "is disabled"):
|
||||
new_coupon.insert()
|
||||
|
||||
with self.subTest("existing coupon can retain a disabled rule"):
|
||||
coupon.description = "Offer paused"
|
||||
coupon.save()
|
||||
coupon.reload()
|
||||
self.assertEqual(coupon.description, "Offer paused")
|
||||
self.assertEqual(coupon.pricing_rule, rule.name)
|
||||
|
||||
with self.subTest("existing coupon cannot switch to a disabled rule"):
|
||||
disabled_rule = frappe.copy_doc(rule)
|
||||
disabled_rule.insert()
|
||||
coupon.reload()
|
||||
coupon.pricing_rule = disabled_rule.name
|
||||
with self.assertRaisesRegex(frappe.ValidationError, "is disabled"):
|
||||
coupon.save()
|
||||
coupon.reload()
|
||||
self.assertEqual(coupon.pricing_rule, rule.name)
|
||||
|
||||
def test_cannot_save_coupon_with_reversed_validity_dates(self):
|
||||
coupon = frappe.get_doc("Coupon Code", "SAVE30")
|
||||
coupon.valid_from = "2026-09-17"
|
||||
coupon.valid_upto = "2026-09-02"
|
||||
with self.assertRaises(frappe.exceptions.InvalidDates):
|
||||
coupon.save()
|
||||
|
||||
def test_sales_order_with_coupon_code(self):
|
||||
frappe.db.set_value("Coupon Code", "SAVE30", "used", 0)
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2018-05-09 14:55:01.394387",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Finance Book",
|
||||
@@ -119,18 +119,6 @@
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 0
|
||||
},
|
||||
{
|
||||
"role": "HR Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Quality Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"icon": "fa fa-calendar",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2024-05-27 17:29:55.560840",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Fiscal Year",
|
||||
@@ -131,10 +131,6 @@
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Auditor"
|
||||
},
|
||||
{
|
||||
"role": "Sales Master Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"show_name_in_global_search": 1,
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2023-07-09 18:11:23.105589",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Item Tax Template",
|
||||
@@ -95,54 +95,6 @@
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Item Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Stock User",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"show_name_in_global_search": 1,
|
||||
|
||||
@@ -1479,10 +1479,6 @@ class JournalEntryTaxWithholding:
|
||||
def get_default_bank_cash_account(company, account_type=None, mode_of_payment=None, account=None):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
|
||||
|
||||
# `select`, not `read`: also runs server-side from get_payment_entry, and Auditor/HR User/
|
||||
# Desk User hold only the select row on Company. doc= for User Permissions.
|
||||
frappe.has_permission("Company", ptype="select", doc=company, throw=True)
|
||||
|
||||
if mode_of_payment:
|
||||
account = get_bank_cash_account(mode_of_payment, company).get("account")
|
||||
|
||||
@@ -1511,10 +1507,6 @@ def get_default_bank_cash_account(company, account_type=None, mode_of_payment=No
|
||||
account = account_list[0].name
|
||||
|
||||
if account:
|
||||
# `account` is caller-supplied; get_balance_on() checks it only on the branch that reads a
|
||||
# balance, and `fetch_balance` is caller-supplied too.
|
||||
frappe.has_permission("Account", doc=account, throw=True)
|
||||
|
||||
account_details = frappe.get_cached_value(
|
||||
"Account", account, ["account_currency", "account_type"], as_dict=1
|
||||
)
|
||||
@@ -1700,39 +1692,29 @@ def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
|
||||
if not frappe.db.has_column("Journal Entry", searchfield):
|
||||
return []
|
||||
|
||||
account = filters.get("account")
|
||||
party = filters.get("party")
|
||||
|
||||
# each names one value. A list would be read as a filter operator below and widen the search
|
||||
# past what the caller named.
|
||||
for value in (account, party):
|
||||
if value and not isinstance(value, str):
|
||||
frappe.throw(_("Invalid filter"), frappe.PermissionError)
|
||||
|
||||
# get_list applies the permission query conditions; the child-table filter resolves the check to `read`
|
||||
je_filters = [
|
||||
["docstatus", "=", 1],
|
||||
[searchfield, "like", f"%{txt}%"],
|
||||
["Journal Entry Account", "account", "=", account],
|
||||
["Journal Entry Account", "reference_type", "is", "not set"],
|
||||
]
|
||||
je_filters.append(
|
||||
["Journal Entry Account", "party", "=", party]
|
||||
if party
|
||||
else ["Journal Entry Account", "party", "is", "not set"]
|
||||
)
|
||||
|
||||
return frappe.get_list(
|
||||
"Journal Entry",
|
||||
filters=je_filters,
|
||||
fields=["name", "posting_date", "user_remark"],
|
||||
order_by="name desc",
|
||||
limit_start=start,
|
||||
limit_page_length=page_len,
|
||||
as_list=True,
|
||||
# one row per entry, not per matching account row. group_by rather than distinct: frappe
|
||||
# drops ORDER BY from a distinct query on postgres, which would lose the ordering above.
|
||||
group_by="name",
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT jv.name, jv.posting_date, jv.user_remark
|
||||
FROM `tabJournal Entry` jv, `tabJournal Entry Account` jv_detail
|
||||
WHERE jv_detail.parent = jv.name
|
||||
AND jv_detail.account = %(account)s
|
||||
AND IFNULL(jv_detail.party, '') = %(party)s
|
||||
AND (
|
||||
jv_detail.reference_type IS NULL
|
||||
OR jv_detail.reference_type = ''
|
||||
)
|
||||
AND jv.docstatus = 1
|
||||
AND jv.`{searchfield}` LIKE %(txt)s
|
||||
ORDER BY jv.name DESC
|
||||
LIMIT %(limit)s offset %(offset)s
|
||||
""",
|
||||
dict(
|
||||
account=filters.get("account"),
|
||||
party=cstr(filters.get("party")),
|
||||
txt=f"%{txt}%",
|
||||
offset=start,
|
||||
limit=page_len,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -1937,10 +1919,6 @@ def make_reverse_journal_entry(source_name: str, target_doc: str | dict | Docume
|
||||
|
||||
def post_process(source, target):
|
||||
target.reversal_of = source.name
|
||||
target.naming_series = source.naming_series
|
||||
if source.voucher_type == "Bank Entry":
|
||||
target.cheque_no = source.cheque_no
|
||||
target.cheque_date = source.cheque_date
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Journal Entry",
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2019-05-26 09:11:46.120251",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Loyalty Program",
|
||||
@@ -169,14 +169,6 @@
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales User",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
||||
@@ -86,8 +86,6 @@ def get_loyalty_program_details_with_points(
|
||||
include_expired_entry=False,
|
||||
current_transaction_amount=0,
|
||||
):
|
||||
frappe.has_permission("Customer", doc=customer, throw=True)
|
||||
|
||||
lp_details = get_loyalty_program_details(customer, loyalty_program, company=company, silent=silent)
|
||||
loyalty_program = frappe.get_doc("Loyalty Program", loyalty_program)
|
||||
lp_details.update(
|
||||
@@ -118,10 +116,6 @@ def get_loyalty_program_details(
|
||||
silent=False,
|
||||
include_expired_entry=False,
|
||||
):
|
||||
# authorise the customer, not the programme: a Loyalty Program check is read-only to System
|
||||
# Manager and would deny every role that fills in the two calling forms.
|
||||
frappe.has_permission("Customer", doc=customer, throw=True)
|
||||
|
||||
lp_details = frappe._dict()
|
||||
|
||||
if not loyalty_program:
|
||||
@@ -143,13 +137,6 @@ def get_loyalty_program_details(
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_redeemption_factor(loyalty_program=None, customer=None):
|
||||
# both call sites send only `loyalty_program`, so the calling form is the boundary; the customer branch stays guarded
|
||||
if not (frappe.has_permission("Sales Invoice") or frappe.has_permission("POS Invoice")):
|
||||
frappe.throw(_("Not permitted"), frappe.PermissionError)
|
||||
|
||||
if customer:
|
||||
frappe.has_permission("Customer", doc=customer, throw=True)
|
||||
|
||||
customer_loyalty_program = None
|
||||
if not loyalty_program:
|
||||
customer_loyalty_program = frappe.db.get_value("Customer", customer, "loyalty_program")
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2020-09-18 17:57:23.835236",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Mode of Payment",
|
||||
@@ -68,30 +68,6 @@
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Maintenance Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales User",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"icon": "fa fa-bar-chart",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2024-01-30 13:57:55.802744",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Monthly Distribution",
|
||||
@@ -69,10 +69,6 @@
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Sales Master Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -57,56 +57,26 @@ class PaymentOrder(Document):
|
||||
frappe.db.set_value(self.payment_order_type, d.get(ref_doc_field), ref_field, status)
|
||||
|
||||
|
||||
def _readable_payment_order(filters):
|
||||
"""Authorise the parent before reading its rows.
|
||||
|
||||
A child table carries no permissions of its own, so a read of it has to be authorised on the
|
||||
Payment Order the rows belong to.
|
||||
"""
|
||||
parent = filters.get("parent")
|
||||
if not parent or not frappe.db.exists("Payment Order", parent):
|
||||
return None
|
||||
|
||||
ptype = "select" if frappe.only_has_select_perm("Payment Order") else "read"
|
||||
frappe.has_permission("Payment Order", ptype, doc=parent, throw=True)
|
||||
return parent
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_mop_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
if not _readable_payment_order(filters):
|
||||
return []
|
||||
|
||||
return frappe.get_all(
|
||||
"Payment Order Reference",
|
||||
filters={"parent": filters.get("parent"), "mode_of_payment": ["like", f"%{txt}%"]},
|
||||
fields=["mode_of_payment"],
|
||||
limit_start=start,
|
||||
limit_page_length=page_len,
|
||||
order_by="", # match the original query (no ORDER BY); avoid get_all's default sort
|
||||
as_list=True,
|
||||
return frappe.db.sql(
|
||||
""" select mode_of_payment from `tabPayment Order Reference`
|
||||
where parent = %(parent)s and mode_of_payment like %(txt)s
|
||||
limit %(page_len)s offset %(start)s""",
|
||||
{"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_supplier_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
if not _readable_payment_order(filters):
|
||||
return []
|
||||
|
||||
return frappe.get_all(
|
||||
"Payment Order Reference",
|
||||
filters={
|
||||
"parent": filters.get("parent"),
|
||||
"supplier": ["like", f"%{txt}%"],
|
||||
"payment_reference": ["is", "not set"],
|
||||
},
|
||||
fields=["supplier"],
|
||||
limit_start=start,
|
||||
limit_page_length=page_len,
|
||||
order_by="", # match the original query (no ORDER BY); avoid get_all's default sort
|
||||
as_list=True,
|
||||
return frappe.db.sql(
|
||||
""" select supplier from `tabPayment Order Reference`
|
||||
where parent = %(parent)s and supplier like %(txt)s and
|
||||
(payment_reference is null or payment_reference='')
|
||||
limit %(page_len)s offset %(start)s""",
|
||||
{"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -455,13 +455,6 @@ class PaymentReconciliation(Document):
|
||||
default_exchange_gain_loss_account = frappe.get_cached_value(
|
||||
"Company", self.company, "exchange_gain_loss_account"
|
||||
)
|
||||
account_currency = frappe.get_cached_value(
|
||||
"Account", self.receivable_payable_account, "account_currency"
|
||||
)
|
||||
allocated_amount_precision = get_field_precision(
|
||||
frappe.get_meta("Payment Reconciliation Allocation").get_field("allocated_amount"),
|
||||
currency=account_currency,
|
||||
)
|
||||
|
||||
entries = []
|
||||
for pay in args.get("payments"):
|
||||
@@ -469,17 +462,11 @@ class PaymentReconciliation(Document):
|
||||
for inv in args.get("invoices"):
|
||||
if pay.get("amount") >= inv.get("outstanding_amount"):
|
||||
res = self.get_allocated_entry(pay, inv, inv["outstanding_amount"])
|
||||
pay["amount"] = flt(
|
||||
flt(pay.get("amount")) - flt(inv.get("outstanding_amount")),
|
||||
allocated_amount_precision,
|
||||
)
|
||||
pay["amount"] = flt(pay.get("amount")) - flt(inv.get("outstanding_amount"))
|
||||
inv["outstanding_amount"] = 0
|
||||
else:
|
||||
res = self.get_allocated_entry(pay, inv, pay["amount"])
|
||||
inv["outstanding_amount"] = flt(
|
||||
flt(inv.get("outstanding_amount")) - flt(pay.get("amount")),
|
||||
allocated_amount_precision,
|
||||
)
|
||||
inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount"))
|
||||
pay["amount"] = 0
|
||||
|
||||
inv["exchange_rate"] = invoice_exchange_map.get(inv.get("invoice_number"))
|
||||
|
||||
@@ -1484,75 +1484,6 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
# Should not raise frappe.exceptions.ValidationError: Payment Entry has been modified after you pulled it. Please pull it again.
|
||||
pr.reconcile()
|
||||
|
||||
@change_settings("System Settings", {"currency_precision": 2})
|
||||
def test_allocate_entries_rounds_running_balance_to_currency_precision(self):
|
||||
pr = frappe.new_doc("Payment Reconciliation")
|
||||
pr.company = self.company
|
||||
pr.party_type = "Customer"
|
||||
pr.party = self.customer
|
||||
pr.receivable_payable_account = self.debit_to
|
||||
pr.set("invoices", [{"invoice_number": "INV-1"}])
|
||||
pr.set("payments", [{"reference_name": "PAY-1"}])
|
||||
|
||||
invoices = [
|
||||
{
|
||||
"invoice_type": "Sales Invoice",
|
||||
"invoice_number": "INV-1",
|
||||
"outstanding_amount": 17592.415,
|
||||
"currency": "INR",
|
||||
},
|
||||
]
|
||||
payments = [
|
||||
{
|
||||
"reference_type": "Payment Entry",
|
||||
"reference_name": "PAY-1",
|
||||
"amount": 18230,
|
||||
"currency": "INR",
|
||||
}
|
||||
]
|
||||
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
self.assertEqual(payments[0]["amount"], flt(637.585, 2))
|
||||
|
||||
@change_settings("System Settings", {"currency_precision": "", "use_number_format_from_currency": 1})
|
||||
def test_allocate_entries_rounds_running_balance_to_account_currency_precision(self):
|
||||
account_currency = frappe.get_cached_value("Account", self.debit_to, "account_currency")
|
||||
original_number_format = frappe.db.get_value("Currency", account_currency, "number_format")
|
||||
frappe.db.set_value("Currency", account_currency, "number_format", "#,###.###")
|
||||
self.addCleanup(
|
||||
frappe.db.set_value, "Currency", account_currency, "number_format", original_number_format
|
||||
)
|
||||
|
||||
pr = frappe.new_doc("Payment Reconciliation")
|
||||
pr.company = self.company
|
||||
pr.party_type = "Customer"
|
||||
pr.party = self.customer
|
||||
pr.receivable_payable_account = self.debit_to
|
||||
pr.set("invoices", [{"invoice_number": "INV-1"}])
|
||||
pr.set("payments", [{"reference_name": "PAY-1"}])
|
||||
|
||||
invoices = [
|
||||
{
|
||||
"invoice_type": "Sales Invoice",
|
||||
"invoice_number": "INV-1",
|
||||
"outstanding_amount": 17592.415,
|
||||
"currency": account_currency,
|
||||
},
|
||||
]
|
||||
payments = [
|
||||
{
|
||||
"reference_type": "Payment Entry",
|
||||
"reference_name": "PAY-1",
|
||||
"amount": 18230,
|
||||
"currency": account_currency,
|
||||
}
|
||||
]
|
||||
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
self.assertEqual(payments[0]["amount"], flt(637.585, 3))
|
||||
|
||||
def test_reverse_payment_against_payment_for_supplier(self):
|
||||
"""
|
||||
Reconcile a payment against a reverse payment, for a supplier.
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2021-02-15 20:30:56.256403",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Term",
|
||||
@@ -157,30 +157,6 @@
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales User",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
||||
@@ -147,22 +147,12 @@ class POSClosingEntry(StatusUpdater):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_cashiers(doctype, txt, searchfield, start, page_len, filters):
|
||||
pos_profile = filters.get("parent")
|
||||
if not pos_profile or not frappe.db.exists("POS Profile", pos_profile):
|
||||
return []
|
||||
|
||||
ptype = "select" if frappe.only_has_select_perm("POS Profile") else "read"
|
||||
frappe.has_permission("POS Profile", ptype, doc=pos_profile, throw=True)
|
||||
|
||||
cashiers_list = frappe.get_all("POS Profile User", filters=filters, fields=["user"], as_list=1)
|
||||
return [c for c in cashiers_list]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_pos_invoices(start, end, pos_profile, user):
|
||||
frappe.has_permission("POS Profile", doc=pos_profile, throw=True)
|
||||
frappe.has_permission("POS Invoice", throw=True)
|
||||
|
||||
data = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
|
||||
@@ -1572,7 +1572,7 @@
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2026-08-12 12:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
@@ -1615,14 +1615,6 @@
|
||||
"permlevel": 1,
|
||||
"read": 1,
|
||||
"role": "All"
|
||||
},
|
||||
{
|
||||
"role": "Sales Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales User",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -801,21 +801,6 @@ class POSInvoice(SalesInvoice):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_stock_availability(item_code, warehouse):
|
||||
# POS Profile is the only boundary that fits: Item/Bin `read` exclude Accounts Manager, Item
|
||||
# `select` is granted to every desk user, and POS Invoice `read` is granted to `All`.
|
||||
# select-or-read: the shipped rows give Sales Manager only `select`.
|
||||
ptype = "select" if frappe.only_has_select_perm("POS Profile") else "read"
|
||||
frappe.has_permission("POS Profile", ptype, throw=True)
|
||||
|
||||
# the caller picks the warehouse when allow_warehouse_change is set; costs nobody who has no
|
||||
# Warehouse User Permission
|
||||
from frappe.permissions import get_allowed_docs_for_doctype, get_user_permissions
|
||||
|
||||
if warehouse_permissions := get_user_permissions(frappe.session.user).get("Warehouse"):
|
||||
allowed_warehouses = get_allowed_docs_for_doctype(warehouse_permissions, "POS Invoice")
|
||||
if allowed_warehouses and warehouse not in allowed_warehouses:
|
||||
frappe.throw(_("Not permitted for {0}").format(warehouse), frappe.PermissionError)
|
||||
|
||||
if frappe.db.get_value("Item", item_code, "is_stock_item"):
|
||||
is_stock_item = True
|
||||
bin_qty = get_bin_qty(item_code, warehouse)
|
||||
|
||||
@@ -217,8 +217,6 @@ class POSInvoiceMergeLog(Document):
|
||||
|
||||
loyalty_amount_sum, loyalty_points_sum, idx = 0, 0, 1
|
||||
|
||||
reversed_rows = get_reversed_rows([doc.return_against for doc in data if doc.is_return])
|
||||
|
||||
for doc in data:
|
||||
map_doc(doc, invoice, table_map={"doctype": invoice.doctype})
|
||||
|
||||
@@ -241,13 +239,9 @@ class POSInvoiceMergeLog(Document):
|
||||
si_item.pos_invoice = doc.name
|
||||
si_item.pos_invoice_item = item.name
|
||||
if doc.is_return:
|
||||
reversed_row = reversed_rows.get(item.pos_invoice_item) or frappe._dict()
|
||||
si_item.sales_invoice_item = reversed_row.get("name")
|
||||
# quote the rate of the row being reversed: rounding an invoice-level discount
|
||||
# can leave a return's net rate a minor unit above the sale's, and
|
||||
# validate_returned_items refuses a return priced above its original
|
||||
if si_item.sales_invoice_item:
|
||||
si_item.rate = reversed_row.rate
|
||||
si_item.sales_invoice_item = get_sales_invoice_item(
|
||||
doc.return_against, item.pos_invoice_item
|
||||
)
|
||||
if item.serial_and_batch_bundle:
|
||||
si_item.serial_and_batch_bundle = item.serial_and_batch_bundle
|
||||
items.append(si_item)
|
||||
@@ -445,29 +439,6 @@ def update_item_wise_tax_detail(consolidate_tax_row, tax_row):
|
||||
consolidate_tax_row.item_wise_tax_detail = json.dumps(consolidated_tax_detail, separators=(",", ":"))
|
||||
|
||||
|
||||
def get_reversed_rows(return_against):
|
||||
"""Rows of the consolidated sales these returns reverse, keyed by the POS invoice row."""
|
||||
if not return_against:
|
||||
return {}
|
||||
|
||||
sales_invoice = DocType("Sales Invoice")
|
||||
sales_invoice_item = DocType("Sales Invoice Item")
|
||||
|
||||
rows = (
|
||||
frappe.qb.from_(sales_invoice)
|
||||
.from_(sales_invoice_item)
|
||||
.select(sales_invoice_item.name, sales_invoice_item.rate, sales_invoice_item.pos_invoice_item)
|
||||
.where(
|
||||
(sales_invoice.name == sales_invoice_item.parent)
|
||||
& (sales_invoice.is_return == 0)
|
||||
& (sales_invoice_item.pos_invoice.isin(return_against))
|
||||
& (sales_invoice.docstatus == 1)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
return {row.pos_invoice_item: row for row in rows}
|
||||
|
||||
|
||||
def get_all_unconsolidated_invoices():
|
||||
filters = {
|
||||
"consolidated_invoice": ["in", ["", None]],
|
||||
@@ -712,3 +683,27 @@ def get_error_message(message) -> str:
|
||||
return message["message"]
|
||||
except Exception:
|
||||
return str(message)
|
||||
|
||||
|
||||
def get_sales_invoice_item(return_against_pos_invoice, pos_invoice_item):
|
||||
try:
|
||||
SalesInvoice = DocType("Sales Invoice")
|
||||
SalesInvoiceItem = DocType("Sales Invoice Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(SalesInvoice)
|
||||
.from_(SalesInvoiceItem)
|
||||
.select(SalesInvoiceItem.name)
|
||||
.where(
|
||||
(SalesInvoice.name == SalesInvoiceItem.parent)
|
||||
& (SalesInvoice.is_return == 0)
|
||||
& (SalesInvoiceItem.pos_invoice == return_against_pos_invoice)
|
||||
& (SalesInvoiceItem.pos_invoice_item == pos_invoice_item)
|
||||
& (SalesInvoice.docstatus == 1)
|
||||
)
|
||||
)
|
||||
|
||||
result = query.run(as_dict=True)
|
||||
return result[0].name if result else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
|
||||
import json
|
||||
import unittest
|
||||
from contextlib import contextmanager
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import change_settings
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
|
||||
@@ -21,67 +19,6 @@ from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
|
||||
@contextmanager
|
||||
def rounding_method(method):
|
||||
"""System Settings is also cached on frappe.local, so that copy has to go as well."""
|
||||
previous = frappe.db.get_single_value("System Settings", "rounding_method")
|
||||
try:
|
||||
frappe.db.set_single_value("System Settings", "rounding_method", method)
|
||||
frappe.local.system_settings = None
|
||||
yield
|
||||
finally:
|
||||
frappe.db.set_single_value("System Settings", "rounding_method", previous)
|
||||
frappe.local.system_settings = None
|
||||
|
||||
|
||||
def sell_over_the_counter(lines, discount_percentage=0):
|
||||
item_code, qty, rate = lines[0]
|
||||
sale = create_pos_invoice(item_code=item_code, qty=qty, rate=rate, do_not_save=True)
|
||||
for item_code, qty, rate in lines[1:]:
|
||||
sale.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": item_code,
|
||||
"qty": qty,
|
||||
"rate": rate,
|
||||
"price_list_rate": rate,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"income_account": "Sales - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
},
|
||||
)
|
||||
|
||||
if discount_percentage:
|
||||
sale.apply_discount_on = "Net Total"
|
||||
sale.additional_discount_percentage = discount_percentage
|
||||
|
||||
sale.run_method("calculate_taxes_and_totals")
|
||||
payable = sale.rounded_total or sale.grand_total
|
||||
sale.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": payable})
|
||||
sale.paid_amount = sale.base_paid_amount = payable
|
||||
sale.insert()
|
||||
sale.submit()
|
||||
return sale
|
||||
|
||||
|
||||
def refund_over_the_counter(sale, qty=None):
|
||||
"""Hand back every line of `sale`, `qty` of each when fewer units come back."""
|
||||
note = make_sales_return(sale.name)
|
||||
if qty is not None:
|
||||
for item in note.items:
|
||||
item.qty = qty
|
||||
|
||||
note.run_method("calculate_taxes_and_totals")
|
||||
refundable = note.rounded_total or note.grand_total
|
||||
note.payments[0].amount = refundable
|
||||
for spare in note.payments[1:]:
|
||||
spare.amount = 0
|
||||
note.paid_amount = note.base_paid_amount = refundable
|
||||
note.insert()
|
||||
note.submit()
|
||||
return note
|
||||
|
||||
|
||||
class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
def test_consolidated_invoice_creation(self):
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
@@ -573,92 +510,3 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
@change_settings("Selling Settings", {"allow_multiple_items": 1})
|
||||
def test_consolidating_returns_priced_off_a_rounded_invoice_discount(self):
|
||||
"""A return works out its own share of an invoice-level discount, so rounding can leave
|
||||
it a minor unit above the sale's, and validate_returned_items then refuses it.
|
||||
|
||||
Every shape that reaches a consolidated credit note goes through one consolidation:
|
||||
a split landing on a half minor unit, the same item on two rows so the rows can only
|
||||
be paired through sales_invoice_item, fewer units coming back than went out, and — as
|
||||
a control — a sale with no invoice-level discount to split at all.
|
||||
"""
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
try:
|
||||
for item_code in ("_Test Item", "_Test Item 2"):
|
||||
make_stock_entry(to_warehouse="_Test Warehouse - _TC", item_code=item_code, rate=100, qty=40)
|
||||
init_user_and_profile()
|
||||
|
||||
with rounding_method("Banker's Rounding (legacy)"):
|
||||
tied = sell_over_the_counter(
|
||||
[("_Test Item", 1, 42.86), ("_Test Item 2", 1, 57.14)], discount_percentage=25
|
||||
)
|
||||
repeated = sell_over_the_counter(
|
||||
[("_Test Item", 1, 42.86), ("_Test Item", 1, 57.14)], discount_percentage=25
|
||||
)
|
||||
oversold = sell_over_the_counter(
|
||||
[("_Test Item", 3, 42.86), ("_Test Item 2", 3, 57.14)], discount_percentage=25
|
||||
)
|
||||
undiscounted = sell_over_the_counter([("_Test Item", 1, 42.86), ("_Test Item 2", 1, 57.14)])
|
||||
|
||||
# the sale and the return really do round the split apart
|
||||
self.assertEqual(
|
||||
{item.item_code: item.net_rate for item in tied.items},
|
||||
{"_Test Item": 32.15, "_Test Item 2": 42.85},
|
||||
)
|
||||
returns = [
|
||||
refund_over_the_counter(tied),
|
||||
refund_over_the_counter(repeated),
|
||||
refund_over_the_counter(oversold, qty=-1),
|
||||
refund_over_the_counter(undiscounted),
|
||||
]
|
||||
self.assertEqual(
|
||||
{item.item_code: item.net_rate for item in returns[0].items},
|
||||
{"_Test Item": 32.14, "_Test Item 2": 42.86},
|
||||
)
|
||||
|
||||
consolidate_pos_invoices()
|
||||
|
||||
for pos_invoice in [tied, repeated, oversold, undiscounted, *returns]:
|
||||
pos_invoice.load_from_db()
|
||||
self.assertTrue(
|
||||
frappe.db.exists("Sales Invoice", pos_invoice.consolidated_invoice),
|
||||
f"{pos_invoice.name} was not consolidated",
|
||||
)
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"Sales Invoice", pos_invoice.consolidated_invoice, "outstanding_amount"
|
||||
),
|
||||
0,
|
||||
)
|
||||
|
||||
for note in returns:
|
||||
# no returned row may be priced above the row it reverses
|
||||
for row in frappe.get_all(
|
||||
"Sales Invoice Item",
|
||||
filters={"parent": note.consolidated_invoice},
|
||||
fields=["item_code", "rate", "sales_invoice_item"],
|
||||
):
|
||||
self.assertTrue(row.sales_invoice_item, f"{row.item_code} lost its link to the sale")
|
||||
sold_rate = frappe.db.get_value("Sales Invoice Item", row.sales_invoice_item, "rate")
|
||||
self.assertLessEqual(row.rate, sold_rate)
|
||||
|
||||
# returns for one customer land on a single credit note, which still adds up to
|
||||
# everything handed back over the counter
|
||||
refunded = {}
|
||||
for note in returns:
|
||||
refunded[note.consolidated_invoice] = refunded.get(note.consolidated_invoice, 0) + flt(
|
||||
note.grand_total
|
||||
)
|
||||
for consolidated_name, handed_back in refunded.items():
|
||||
self.assertEqual(
|
||||
flt(frappe.db.get_value("Sales Invoice", consolidated_name, "grand_total"), 2),
|
||||
flt(handed_back, 2),
|
||||
)
|
||||
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
@@ -422,7 +422,7 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2026-02-22 04:17:03.308876",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
@@ -446,10 +446,6 @@
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Sales Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -12,29 +12,6 @@ from frappe import _, throw
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
# the transactions the pricing engine is called for, from transaction.js and the POS
|
||||
PRICING_TRANSACTION_DOCTYPES = frozenset(
|
||||
{
|
||||
"Quotation",
|
||||
"Sales Order",
|
||||
"Delivery Note",
|
||||
"Sales Invoice",
|
||||
"POS Invoice",
|
||||
"Supplier Quotation",
|
||||
"Purchase Order",
|
||||
"Purchase Receipt",
|
||||
"Purchase Invoice",
|
||||
"Material Request",
|
||||
# these three also extend a controller that calls the pricing engine: BOM and BOM Creator
|
||||
# through TransactionController, Request for Quotation through BuyingController
|
||||
"BOM",
|
||||
"BOM Creator",
|
||||
"Request for Quotation",
|
||||
# no client sends this one, but set_transaction_type below still branches on it
|
||||
"Opportunity",
|
||||
}
|
||||
)
|
||||
|
||||
apply_on_dict = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"}
|
||||
|
||||
other_fields = ["other_item_code", "other_item_group", "other_brand"]
|
||||
@@ -387,18 +364,6 @@ def apply_pricing_rule(args, doc=None):
|
||||
|
||||
args = frappe._dict(args)
|
||||
|
||||
# an allow-list, not a type check: `doctype` is caller-chosen, and any doctype the caller can
|
||||
# read would otherwise satisfy the has_permission below while the pricing engine still ran
|
||||
transaction_doctype = args.get("doctype")
|
||||
if transaction_doctype not in PRICING_TRANSACTION_DOCTYPES:
|
||||
frappe.throw(_("Invalid doctype"), frappe.PermissionError)
|
||||
|
||||
transaction_name = args.get("name")
|
||||
if not isinstance(transaction_name, str) or not frappe.db.exists(transaction_doctype, transaction_name):
|
||||
transaction_name = None
|
||||
|
||||
frappe.has_permission(transaction_doctype, doc=transaction_name, throw=True)
|
||||
|
||||
set_transaction_type(args)
|
||||
|
||||
# list of dictionaries
|
||||
@@ -424,7 +389,6 @@ def apply_pricing_rule(args, doc=None):
|
||||
for item in item_list:
|
||||
args_copy = copy.deepcopy(args)
|
||||
args_copy.update(item)
|
||||
set_transaction_type(args_copy)
|
||||
data = get_pricing_rule_for_item(args_copy, doc=doc)
|
||||
out.append(data)
|
||||
|
||||
@@ -769,18 +733,14 @@ def make_pricing_rule(doctype, docname):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_item_uoms(doctype, txt, searchfield, start, page_len, filters):
|
||||
if filters.get("apply_on") == "Item Code":
|
||||
item_filters = [["name", "=", filters.get("value")]]
|
||||
else:
|
||||
item_filters = [[frappe.scrub(filters.get("apply_on")), "=", filters.get("value")]]
|
||||
|
||||
items = frappe.get_list("Item", filters=item_filters, pluck="name")
|
||||
if not items:
|
||||
return []
|
||||
items = [filters.get("value")]
|
||||
if filters.get("apply_on") != "Item Code":
|
||||
field = frappe.scrub(filters.get("apply_on"))
|
||||
items = [d.name for d in frappe.db.get_all("Item", filters={field: filters.get("value")})]
|
||||
|
||||
return frappe.get_all(
|
||||
"UOM Conversion Detail",
|
||||
filters={"parent": ("in", items), "parenttype": "Item", "uom": ("like", f"{txt}%")},
|
||||
filters={"parent": ("in", items), "uom": ("like", f"{txt}%")},
|
||||
fields=["distinct uom"],
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
@@ -130,17 +130,14 @@ def initialize_parallel_threads(docname: str):
|
||||
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed")
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def start_pcv_processing(docname: str):
|
||||
# checked before the status is read, not inside the branch: otherwise an unentitled caller
|
||||
# learns the document's status from whether this returns or throws
|
||||
frappe.has_permission("Process Period Closing Voucher", "write", doc=docname, throw=True)
|
||||
|
||||
if frappe.db.get_value("Process Period Closing Voucher", docname, "status") in ["Queued", "Running"]:
|
||||
frappe.has_permission("Process Period Closing Voucher", "write", doc=docname, throw=True)
|
||||
initialize_parallel_threads(docname)
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def pause_pcv_processing(docname: str):
|
||||
frappe.has_permission("Process Period Closing Voucher", ptype="write", doc=docname, throw=True)
|
||||
|
||||
@@ -157,7 +154,7 @@ def pause_pcv_processing(docname: str):
|
||||
qb.update(ppcvd).set(ppcvd.status, "Paused").where(ppcvd.name.isin(queued_dates)).run()
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def cancel_pcv_processing(docname: str):
|
||||
frappe.has_permission("Process Period Closing Voucher", ptype="cancel", doc=docname, throw=True)
|
||||
|
||||
@@ -173,7 +170,7 @@ def cancel_pcv_processing(docname: str):
|
||||
qb.update(ppcvd).set(ppcvd.status, "Cancelled").where(ppcvd.name.isin(queued_dates)).run()
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def resume_pcv_processing(docname: str):
|
||||
frappe.has_permission("Process Period Closing Voucher", ptype="write", doc=docname, throw=True)
|
||||
|
||||
@@ -258,11 +255,8 @@ def get_gle_for_closing_account(pcv, dimension_balance, dimensions):
|
||||
return gl_entry
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def schedule_next_date(docname: str):
|
||||
# marks a row Running and enqueues a long job, so it needs the same write check as the sibling controls
|
||||
frappe.has_permission("Process Period Closing Voucher", ptype="write", doc=docname, throw=True)
|
||||
|
||||
timeout = frappe.db.get_single_value("Accounts Settings", "pcv_job_timeout") or 3600
|
||||
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
|
||||
|
||||
|
||||
@@ -293,13 +293,6 @@ def get_html(doc, filters, entry, col, res, ageing):
|
||||
from frappe.www.printview import get_letter_head
|
||||
|
||||
letter_head = get_letter_head(doc, 0)
|
||||
# render letter head content as a template so its Jinja resolves against the doc
|
||||
if letter_head.get("content"):
|
||||
# nosemgrep: frappe-semgrep-rules.rules.security.frappe-ssti
|
||||
letter_head["content"] = frappe.render_template(letter_head["content"], {"doc": doc})
|
||||
if letter_head.get("footer"):
|
||||
# nosemgrep: frappe-semgrep-rules.rules.security.frappe-ssti
|
||||
letter_head["footer"] = frappe.render_template(letter_head["footer"], {"doc": doc})
|
||||
html = frappe.render_template(
|
||||
template_path,
|
||||
{
|
||||
|
||||
@@ -1664,7 +1664,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2026-08-12 12:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
@@ -1717,18 +1717,6 @@
|
||||
"read": 1,
|
||||
"role": "Accounts Manager",
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Quality Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
"icon": "fa fa-money",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2024-01-30 13:08:09.537242",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Taxes and Charges Template",
|
||||
@@ -104,22 +104,6 @@
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Purchase User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Accounts User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"show_title_field_in_link": 1,
|
||||
|
||||
@@ -524,10 +524,6 @@ def validate_docs_for_voucher_types(doc_voucher_types):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_repost_allowed_types(doctype, txt, searchfield, start, page_len, filters):
|
||||
# `filters` goes straight to db.get_all, so reaching this needs gating rather than typing.
|
||||
# Reached only from this doctype's own form, which is System-Manager-only.
|
||||
frappe.has_permission("Repost Accounting Ledger", throw=True)
|
||||
|
||||
filters = {"allowed": True}
|
||||
|
||||
if txt:
|
||||
|
||||
@@ -494,7 +494,6 @@ class SalesInvoice(SellingController):
|
||||
self.validate_standalone_serial_nos_customer()
|
||||
self.update_stock_reservation_entries()
|
||||
self.update_stock_ledger()
|
||||
self.validate_produced_serial_nos_against_reservation()
|
||||
|
||||
self.process_asset_depreciation()
|
||||
|
||||
@@ -2591,28 +2590,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_received_items(reference_name: str, doctype: str, reference_fieldname: str):
|
||||
# The only two targets this resolves a reference field for. Stating them rejects a caller
|
||||
# supplied doctype that would otherwise be filtered on a column it does not have.
|
||||
reference_fields = {
|
||||
"Purchase Invoice": ("inter_company_invoice_reference", "Sales Invoice", "sales_invoice_item"),
|
||||
"Purchase Order": ("inter_company_order_reference", "Sales Order", "sales_order_item"),
|
||||
}
|
||||
if doctype not in reference_fields:
|
||||
frappe.throw(_("Invalid doctype {0}").format(doctype), frappe.PermissionError)
|
||||
|
||||
reference_field, source_doctype, expected_fieldname = reference_fields[doctype]
|
||||
|
||||
# the source document decides access, not the targets: those belong to the counterpart company
|
||||
# and the caller legitimately may not read them. doc= for User Permissions.
|
||||
frappe.has_permission(source_doctype, doc=reference_name, throw=True)
|
||||
|
||||
# `reference_fieldname` becomes a selected column and the result key, so it has to be this
|
||||
# target's own reference field: any other item-table column would be returned from unauthorised rows.
|
||||
if reference_fieldname != expected_fieldname:
|
||||
frappe.throw(
|
||||
_("{0} is not a valid reference field for {1}").format(reference_fieldname, doctype),
|
||||
frappe.ValidationError,
|
||||
)
|
||||
reference_field = "inter_company_invoice_reference"
|
||||
if doctype == "Purchase Order":
|
||||
reference_field = "inter_company_order_reference"
|
||||
|
||||
filters = {
|
||||
reference_field: reference_name,
|
||||
|
||||
@@ -47,7 +47,7 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
|
||||
create_stock_reconciliation,
|
||||
)
|
||||
from erpnext.stock.get_item_details import get_item_tax_map
|
||||
from erpnext.stock.utils import _get_incoming_rate, get_stock_balance
|
||||
from erpnext.stock.utils import get_incoming_rate, get_stock_balance
|
||||
|
||||
|
||||
class TestSalesInvoice(FrappeTestCase):
|
||||
@@ -2943,7 +2943,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
rate = 0.0
|
||||
for d in si.get("items"):
|
||||
rate = _get_incoming_rate(
|
||||
rate = get_incoming_rate(
|
||||
{
|
||||
"item_code": d.item_code,
|
||||
"warehouse": d.warehouse,
|
||||
@@ -5042,34 +5042,6 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
frappe.db.set_value("Company", "_Test Company 1", "cost_center", cost_center)
|
||||
|
||||
@change_settings("Stock Settings", {"enable_stock_reservation": 1})
|
||||
def test_update_stock_restricted_to_reserved_produced_serial_nos(self):
|
||||
from erpnext.selling.doctype.sales_order.sales_order import (
|
||||
make_sales_invoice as make_si_from_so,
|
||||
)
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import (
|
||||
make_so_with_reserved_produced_serial_no,
|
||||
)
|
||||
|
||||
so, reserved, unreserved = make_so_with_reserved_produced_serial_no()
|
||||
|
||||
def make_si(serial_no):
|
||||
si = make_si_from_so(so.name)
|
||||
si.update_stock = 1
|
||||
si.items[0].warehouse = so.items[0].warehouse
|
||||
si.items[0].use_serial_batch_fields = 1
|
||||
si.items[0].serial_no = serial_no
|
||||
return si.save()
|
||||
|
||||
frappe.db.savepoint("unreserved_serial_no")
|
||||
si = make_si(unreserved[0])
|
||||
self.assertRaises(frappe.ValidationError, si.submit)
|
||||
frappe.db.rollback(save_point="unreserved_serial_no")
|
||||
|
||||
si = make_si(reserved[0])
|
||||
si.submit()
|
||||
self.assertEqual(get_serial_nos_from_bundle(si.items[0].serial_and_batch_bundle), reserved)
|
||||
|
||||
|
||||
def make_item_for_si(item_code, properties=None):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
"icon": "fa fa-money",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2024-01-30 13:07:28.801104",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Taxes and Charges Template",
|
||||
@@ -113,26 +113,6 @@
|
||||
"role": "Sales Master Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"show_title_field_in_link": 1,
|
||||
|
||||
@@ -9,15 +9,10 @@ frappe.ui.form.on("Shipping Rule", {
|
||||
},
|
||||
|
||||
company: function (frm) {
|
||||
if (frm.previous_company !== frm.doc.company) {
|
||||
frm.previous_company = frm.doc.company;
|
||||
frm.set_value("account", "");
|
||||
}
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
frm.previous_company = frm.doc.company;
|
||||
frm.set_query("account", function () {
|
||||
return {
|
||||
filters: {
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
"icon": "fa fa-truck",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2026-07-22 14:53:27.315435",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Shipping Rule",
|
||||
@@ -195,38 +195,6 @@
|
||||
"set_user_permissions": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -52,23 +52,10 @@ class ShippingRule(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_account_company()
|
||||
self.validate_from_to_values()
|
||||
self.sort_shipping_rule_conditions()
|
||||
self.validate_overlapping_shipping_rule_conditions()
|
||||
|
||||
def validate_account_company(self):
|
||||
if not self.company or not self.account:
|
||||
return
|
||||
|
||||
if frappe.get_cached_value("Account", self.account, "company") != self.company:
|
||||
throw(
|
||||
_("Shipping Account {0} does not belong to Company {1}").format(
|
||||
frappe.bold(self.account), frappe.bold(self.company)
|
||||
),
|
||||
title=_("Invalid Shipping Account"),
|
||||
)
|
||||
|
||||
def validate_from_to_values(self):
|
||||
if self.calculate_based_on == "Fixed":
|
||||
if self.conditions:
|
||||
|
||||
@@ -15,39 +15,6 @@ test_records = frappe.get_test_records("Shipping Rule")
|
||||
|
||||
|
||||
class TestShippingRule(unittest.TestCase):
|
||||
def test_account_company_on_insert(self):
|
||||
for rule_type in ("Selling", "Buying"):
|
||||
with self.subTest(shipping_rule_type=rule_type):
|
||||
shipping_rule = frappe.copy_doc(test_records[0])
|
||||
shipping_rule.label = f"{rule_type} Delivery"
|
||||
shipping_rule.shipping_rule_type = rule_type
|
||||
shipping_rule.company = "_Test Company 1"
|
||||
shipping_rule.cost_center = None
|
||||
with self.assertRaisesRegex(frappe.ValidationError, "does not belong to Company"):
|
||||
shipping_rule.insert()
|
||||
|
||||
def test_account_company_on_update(self):
|
||||
shipping_rule = frappe.copy_doc(test_records[0])
|
||||
shipping_rule.label = "Standard Delivery"
|
||||
shipping_rule.insert()
|
||||
shipping_rule.company = "_Test Company 1"
|
||||
shipping_rule.cost_center = None
|
||||
with self.assertRaisesRegex(frappe.ValidationError, "does not belong to Company"):
|
||||
shipping_rule.save()
|
||||
|
||||
shipping_rule.reload()
|
||||
shipping_rule.company = "_Test Company 1"
|
||||
shipping_rule.account = "_Test Account Shipping Charges - _TC1"
|
||||
shipping_rule.cost_center = None
|
||||
shipping_rule.save()
|
||||
shipping_rule.reload()
|
||||
self.assertEqual(shipping_rule.company, "_Test Company 1")
|
||||
self.assertEqual(shipping_rule.account, "_Test Account Shipping Charges - _TC1")
|
||||
|
||||
shipping_rule.account = "_Test Account Shipping Charges - _TC"
|
||||
with self.assertRaisesRegex(frappe.ValidationError, "does not belong to Company"):
|
||||
shipping_rule.save()
|
||||
|
||||
def test_from_greater_than_to(self):
|
||||
shipping_rule = frappe.copy_doc(test_records[0])
|
||||
shipping_rule.name = test_records[0].get("name")
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2022-01-18 21:13:41.161017",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Category",
|
||||
@@ -68,62 +68,6 @@
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Item Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Stock User",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
||||
@@ -145,12 +145,6 @@ def get_party_details(party, party_type, args=None):
|
||||
out = {}
|
||||
billing_address, shipping_address = None, None
|
||||
if args:
|
||||
# each of these names a single Address. A dict is read as a filter instead, and `get_doc`
|
||||
# would resolve it to whichever Address happens to match, so only a plain name is accepted
|
||||
for fieldname in ("billing_address", "shipping_address"):
|
||||
if args.get(fieldname) and not isinstance(args.get(fieldname), str):
|
||||
frappe.throw(_("Invalid address"), frappe.PermissionError)
|
||||
|
||||
if args.get("billing_address"):
|
||||
billing_address = frappe.get_doc("Address", args.get("billing_address"))
|
||||
if args.get("shipping_address"):
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2025-07-30 07:13:51.785735",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withholding Category",
|
||||
@@ -144,26 +144,6 @@
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Item Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales User",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -106,8 +106,6 @@ def get_linked_payments_for_doc(
|
||||
company: str | None = None, doctype: str | None = None, docname: str | None = None
|
||||
) -> list:
|
||||
if company and doctype and docname:
|
||||
frappe.has_permission(doctype, doc=docname, throw=True)
|
||||
|
||||
_dt = doctype
|
||||
_dn = docname
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
import frappe
|
||||
from frappe import _, msgprint, qb, scrub
|
||||
from frappe.contacts.doctype.address.address import get_company_address, get_default_address
|
||||
from frappe.core.doctype.user_permission.user_permission import get_user_permissions
|
||||
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.permissions import get_allowed_docs_for_doctype
|
||||
from frappe.query_builder.functions import Abs, Date, Sum
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
@@ -157,7 +156,7 @@ def _get_party_details(
|
||||
)
|
||||
set_contact_details(party_details, party, party_type)
|
||||
set_other_values(party_details, party, party_type)
|
||||
set_price_list(party_details, party, party_type, price_list, pos_profile, doctype)
|
||||
set_price_list(party_details, party, party_type, price_list, pos_profile)
|
||||
|
||||
tax_template = set_taxes(
|
||||
party.name,
|
||||
@@ -379,33 +378,13 @@ def get_default_price_list(party):
|
||||
return frappe.get_cached_value("Customer Group", party.customer_group, "default_price_list")
|
||||
|
||||
|
||||
def get_permitted_price_lists(doctype=None):
|
||||
permissions = sorted(
|
||||
get_user_permissions().get("Price List", []), key=lambda p: p.get("is_default"), reverse=True
|
||||
)
|
||||
|
||||
# a permission applicable for another doctype doesn't restrict this transaction
|
||||
return get_allowed_docs_for_doctype(permissions, doctype)
|
||||
|
||||
|
||||
def get_usable_price_list(price_lists, party_doctype):
|
||||
transaction_side = "selling" if party_doctype == "Customer" else "buying"
|
||||
|
||||
for price_list in price_lists:
|
||||
details = frappe.get_cached_value(
|
||||
"Price List", price_list, ["enabled", transaction_side], as_dict=True
|
||||
)
|
||||
if details.enabled and details[transaction_side]:
|
||||
return price_list
|
||||
|
||||
|
||||
def set_price_list(party_details, party, party_type, given_price_list, pos=None, doctype=None):
|
||||
def set_price_list(party_details, party, party_type, given_price_list, pos=None):
|
||||
# price list
|
||||
permitted_price_lists = get_permitted_price_lists(doctype)
|
||||
price_list = get_permitted_documents("Price List")
|
||||
|
||||
# if there is only one permitted document based on user permissions, set it
|
||||
if len(permitted_price_lists) == 1:
|
||||
price_list = get_usable_price_list(permitted_price_lists, party.doctype)
|
||||
if price_list and len(price_list) == 1:
|
||||
price_list = price_list[0]
|
||||
elif pos and party_type == "Customer":
|
||||
customer_price_list = frappe.get_value("Customer", party.name, "default_price_list")
|
||||
|
||||
@@ -417,10 +396,6 @@ def set_price_list(party_details, party, party_type, given_price_list, pos=None,
|
||||
else:
|
||||
price_list = get_default_price_list(party) or given_price_list
|
||||
|
||||
# don't set a price list the user has no permission for, the transaction can't be saved with it
|
||||
if price_list and permitted_price_lists and price_list not in permitted_price_lists:
|
||||
price_list = get_usable_price_list(permitted_price_lists, party.doctype)
|
||||
|
||||
if price_list:
|
||||
party_details.price_list_currency = frappe.db.get_value(
|
||||
"Price List", price_list, "currency", cache=True
|
||||
|
||||
@@ -151,7 +151,7 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
},
|
||||
{
|
||||
fieldname: "for_revaluation_journals",
|
||||
label: __("Include Revaluation Journals"),
|
||||
label: __("Revaluation Journals"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -113,7 +113,7 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
||||
},
|
||||
{
|
||||
fieldname: "for_revaluation_journals",
|
||||
label: __("Include Revaluation Journals"),
|
||||
label: __("Revaluation Journals"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -178,7 +178,7 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
},
|
||||
{
|
||||
fieldname: "for_revaluation_journals",
|
||||
label: __("Include Revaluation Journals"),
|
||||
label: __("Revaluation Journals"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -141,7 +141,7 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
},
|
||||
{
|
||||
fieldname: "for_revaluation_journals",
|
||||
label: __("Include Revaluation Journals"),
|
||||
label: __("Revaluation Journals"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -16,7 +16,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
)
|
||||
from erpnext.accounts.report.financial_statements import get_cost_centers_with_children
|
||||
from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition
|
||||
from erpnext.stock.utils import _get_incoming_rate
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -732,32 +732,23 @@ class GrossProfitGenerator:
|
||||
)
|
||||
|
||||
def get_returned_invoice_items(self):
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
si_item = frappe.qb.DocType("Sales Invoice Item")
|
||||
query = (
|
||||
frappe.qb.from_(si)
|
||||
.inner_join(si_item)
|
||||
.on(si.name == si_item.parent)
|
||||
.select(
|
||||
si.name,
|
||||
si_item.item_code,
|
||||
si_item.sales_invoice_item,
|
||||
si_item.stock_qty.as_("qty"),
|
||||
si_item.base_net_amount.as_("base_amount"),
|
||||
si.return_against,
|
||||
)
|
||||
.where(
|
||||
(si.docstatus == 1)
|
||||
& (si.is_return == 1)
|
||||
& si.posting_date.between(self.filters.from_date, self.filters.to_date)
|
||||
)
|
||||
returned_invoices = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
si.name, si_item.item_code, si_item.sales_invoice_item,
|
||||
si_item.stock_qty as qty, si_item.base_net_amount as base_amount, si.return_against
|
||||
from
|
||||
`tabSales Invoice` si, `tabSales Invoice Item` si_item
|
||||
where
|
||||
si.name = si_item.parent
|
||||
and si.docstatus = 1
|
||||
and si.is_return = 1
|
||||
and si.posting_date between %(from_date)s and %(to_date)s
|
||||
""",
|
||||
{"from_date": self.filters.from_date, "to_date": self.filters.to_date},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if self.filters.company:
|
||||
query = query.where(si.company == self.filters.company)
|
||||
|
||||
returned_invoices = query.run(as_dict=1)
|
||||
|
||||
self.returned_invoices = frappe._dict()
|
||||
self.legacy_returned_invoices = frappe._dict()
|
||||
for inv in returned_invoices:
|
||||
@@ -973,7 +964,7 @@ class GrossProfitGenerator:
|
||||
if row.serial_and_batch_bundle:
|
||||
args.update({"serial_and_batch_bundle": row.serial_and_batch_bundle})
|
||||
|
||||
average_buying_rate = _get_incoming_rate(args)
|
||||
average_buying_rate = get_incoming_rate(args)
|
||||
self.average_buying_rate[key] = flt(average_buying_rate)
|
||||
|
||||
return self.average_buying_rate[key]
|
||||
@@ -1356,4 +1347,7 @@ class GrossProfitGenerator:
|
||||
).setdefault(d.parent_item, []).append(d)
|
||||
|
||||
def load_non_stock_items(self):
|
||||
self.non_stock_items = frappe.get_all("Item", filters={"is_stock_item": 0}, pluck="name")
|
||||
self.non_stock_items = frappe.db.sql_list(
|
||||
"""select name from tabItem
|
||||
where is_stock_item=0"""
|
||||
)
|
||||
|
||||
@@ -406,15 +406,7 @@ def get_invoices(filters, additional_query_columns):
|
||||
if filters.get("supplier"):
|
||||
query = query.where(pi.supplier == filters.supplier)
|
||||
if filters.get("supplier_group"):
|
||||
# read the group from the supplier master, to match the Supplier Group column
|
||||
supplier = frappe.qb.DocType("Supplier")
|
||||
query = query.where(
|
||||
pi.supplier.isin(
|
||||
frappe.qb.from_(supplier)
|
||||
.select(supplier.name)
|
||||
.where(supplier.supplier_group == filters.supplier_group)
|
||||
)
|
||||
)
|
||||
query = query.where(pi.supplier_group == filters.supplier_group)
|
||||
|
||||
query = get_conditions(filters, query, "Purchase Invoice")
|
||||
|
||||
|
||||
@@ -97,23 +97,6 @@ class TestPurchaseRegister(FrappeTestCase):
|
||||
self.assertEqual(first_row.credit, 600)
|
||||
self.assertEqual(first_row.balance, 500)
|
||||
|
||||
def test_supplier_group_filter_uses_supplier_master(self):
|
||||
# invoices created before the supplier_group field existed have it blank
|
||||
pi = make_purchase_invoice()
|
||||
pi.db_set("supplier_group", None, update_modified=False)
|
||||
supplier_group = frappe.db.get_value("Supplier", pi.supplier, "supplier_group")
|
||||
|
||||
filters = frappe._dict(
|
||||
company="_Test Company 6",
|
||||
from_date=add_months(today(), -1),
|
||||
to_date=today(),
|
||||
supplier_group=supplier_group,
|
||||
)
|
||||
rows = [frappe._dict(row) for row in execute(filters)[1] if row.get("voucher_no") == pi.name]
|
||||
|
||||
self.assertEqual(len(rows), 1)
|
||||
self.assertEqual(rows[0].supplier_group, supplier_group)
|
||||
|
||||
|
||||
def make_purchase_invoice():
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||
from erpnext.accounts.party import get_default_price_list, set_price_list
|
||||
from erpnext.accounts.party import get_default_price_list
|
||||
|
||||
|
||||
class PartyTestCase(FrappeTestCase):
|
||||
@@ -13,170 +12,3 @@ class PartyTestCase(FrappeTestCase):
|
||||
customer.save()
|
||||
price_list = get_default_price_list(customer)
|
||||
assert price_list is None
|
||||
|
||||
def test_fallback_should_not_pick_an_unpermitted_price_list(self):
|
||||
permitted_default = self.create_price_list(enabled=1)
|
||||
permitted_other = self.create_price_list(enabled=1)
|
||||
user = self.create_user_with_price_list_permissions([permitted_default, permitted_other])
|
||||
customer = self.create_customer()
|
||||
|
||||
party_details = frappe._dict()
|
||||
with self.set_user(user):
|
||||
set_price_list(
|
||||
party_details, customer, "Customer", self.create_price_list(enabled=1), doctype="Sales Order"
|
||||
)
|
||||
|
||||
self.assertEqual(party_details.selling_price_list, permitted_default)
|
||||
|
||||
def test_permitted_given_price_list_should_be_kept(self):
|
||||
permitted_default = self.create_price_list(enabled=1)
|
||||
permitted_other = self.create_price_list(enabled=1)
|
||||
user = self.create_user_with_price_list_permissions([permitted_default, permitted_other])
|
||||
customer = self.create_customer()
|
||||
|
||||
party_details = frappe._dict()
|
||||
with self.set_user(user):
|
||||
set_price_list(party_details, customer, "Customer", permitted_other, doctype="Sales Order")
|
||||
|
||||
self.assertEqual(party_details.selling_price_list, permitted_other)
|
||||
|
||||
def test_permission_for_another_doctype_should_not_apply(self):
|
||||
permitted = [self.create_price_list(enabled=1), self.create_price_list(enabled=1)]
|
||||
user = self.create_user_with_price_list_permissions(permitted, applicable_for="Quotation")
|
||||
customer = self.create_customer()
|
||||
given_price_list = self.create_price_list(enabled=1)
|
||||
|
||||
party_details = frappe._dict()
|
||||
with self.set_user(user):
|
||||
set_price_list(party_details, customer, "Customer", given_price_list, doctype="Sales Order")
|
||||
|
||||
self.assertEqual(party_details.selling_price_list, given_price_list)
|
||||
|
||||
def test_a_single_permitted_price_list_should_fit_the_transaction(self):
|
||||
buying_price_list = self.create_price_list(enabled=1, selling=0, buying=1)
|
||||
user = self.create_user_with_price_list_permissions([buying_price_list])
|
||||
customer = self.create_customer()
|
||||
given_price_list = self.create_price_list(enabled=1)
|
||||
|
||||
party_details = frappe._dict()
|
||||
with self.set_user(user):
|
||||
set_price_list(party_details, customer, "Customer", given_price_list, doctype="Sales Order")
|
||||
|
||||
self.assertIsNone(party_details.selling_price_list)
|
||||
|
||||
def test_buying_transaction_should_not_take_a_selling_price_list(self):
|
||||
permitted = [self.create_price_list(enabled=1) for _ in range(2)]
|
||||
user = self.create_user_with_price_list_permissions(permitted)
|
||||
supplier_price_list = self.create_price_list(enabled=1, selling=0, buying=1)
|
||||
supplier = self.create_supplier(default_price_list=supplier_price_list)
|
||||
|
||||
party_details = frappe._dict()
|
||||
with self.set_user(user):
|
||||
set_price_list(party_details, supplier, "Supplier", None, doctype="Purchase Order")
|
||||
|
||||
self.assertIsNone(party_details.buying_price_list)
|
||||
|
||||
def test_permission_for_another_doctype_should_not_apply_without_a_doctype(self):
|
||||
permitted = [self.create_price_list(enabled=1), self.create_price_list(enabled=1)]
|
||||
user = self.create_user_with_price_list_permissions(permitted, applicable_for="Quotation")
|
||||
customer = self.create_customer()
|
||||
given_price_list = self.create_price_list(enabled=1)
|
||||
|
||||
party_details = frappe._dict()
|
||||
with self.set_user(user):
|
||||
set_price_list(party_details, customer, "Customer", given_price_list)
|
||||
|
||||
self.assertEqual(party_details.selling_price_list, given_price_list)
|
||||
|
||||
def test_pos_price_list_should_be_kept(self):
|
||||
permitted = [self.create_price_list(enabled=1), self.create_price_list(enabled=1)]
|
||||
user = self.create_user_with_price_list_permissions(permitted)
|
||||
pos_price_list = self.create_price_list(enabled=1)
|
||||
pos_profile = make_pos_profile(selling_price_list=pos_price_list)
|
||||
customer = self.create_customer()
|
||||
|
||||
party_details = frappe._dict()
|
||||
with self.set_user(user):
|
||||
set_price_list(
|
||||
party_details, customer, "Customer", None, pos=pos_profile.name, doctype="POS Invoice"
|
||||
)
|
||||
|
||||
self.assertEqual(party_details.selling_price_list, pos_price_list)
|
||||
|
||||
def test_disabled_permitted_price_lists_should_clear_the_price_list(self):
|
||||
permitted = [self.create_price_list(enabled=0), self.create_price_list(enabled=0)]
|
||||
user = self.create_user_with_price_list_permissions(permitted)
|
||||
customer = self.create_customer()
|
||||
given_price_list = self.create_price_list(enabled=1)
|
||||
|
||||
party_details = frappe._dict()
|
||||
with self.set_user(user):
|
||||
set_price_list(party_details, customer, "Customer", given_price_list, doctype="Sales Order")
|
||||
|
||||
self.assertIsNone(party_details.selling_price_list)
|
||||
|
||||
def create_user_with_price_list_permissions(self, price_lists, applicable_for=None):
|
||||
user = frappe.get_doc(
|
||||
{
|
||||
"doctype": "User",
|
||||
"email": f"{frappe.generate_hash(length=10)}@example.com",
|
||||
"first_name": "Price List Test",
|
||||
"send_welcome_email": 0,
|
||||
"roles": [{"role": "Sales User"}],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
for idx, price_list in enumerate(price_lists):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "User Permission",
|
||||
"user": user.name,
|
||||
"allow": "Price List",
|
||||
"for_value": price_list,
|
||||
"is_default": int(idx == 0),
|
||||
"apply_to_all_doctypes": int(not applicable_for),
|
||||
"applicable_for": applicable_for,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
frappe.clear_cache(user=user.name)
|
||||
self.addCleanup(frappe.clear_cache, user=user.name)
|
||||
|
||||
return user.name
|
||||
|
||||
def create_price_list(self, enabled, selling=1, buying=0):
|
||||
price_list = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Price List",
|
||||
"price_list_name": frappe.generate_hash(length=10),
|
||||
"currency": "INR",
|
||||
"selling": selling,
|
||||
"buying": buying,
|
||||
"enabled": enabled,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
return price_list.name
|
||||
|
||||
def create_supplier(self, **values):
|
||||
return frappe.get_doc(
|
||||
{
|
||||
"doctype": "Supplier",
|
||||
"supplier_name": frappe.generate_hash(length=10),
|
||||
**values,
|
||||
}
|
||||
).insert(ignore_permissions=True, ignore_mandatory=True)
|
||||
|
||||
def create_customer(self, **values):
|
||||
customer = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_name": frappe.generate_hash(length=10),
|
||||
"customer_group": "Individual",
|
||||
**values,
|
||||
}
|
||||
).insert(ignore_permissions=True, ignore_mandatory=True)
|
||||
customer.customer_group = None
|
||||
customer.save()
|
||||
|
||||
return customer
|
||||
|
||||
@@ -597,7 +597,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2026-09-23 12:00:00.000000",
|
||||
"modified": "2026-03-13 12:15:25.734623",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
@@ -633,38 +633,6 @@
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Purchase User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Stock User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "System Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -1211,22 +1211,6 @@ def is_cwip_accounting_enabled(asset_category):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_asset_value_after_depreciation(asset_name, finance_book=None):
|
||||
# one of the three calling forms is the boundary; Asset itself excludes the roles holding Asset Value Adjustment write
|
||||
if not any(
|
||||
frappe.has_permission(dt, "write")
|
||||
for dt in ("Asset Value Adjustment", "Asset Capitalization", "Asset Repair")
|
||||
):
|
||||
frappe.throw(_("Not permitted"), frappe.PermissionError)
|
||||
|
||||
# select-or-read: these roles hold `select` on Asset, which does not satisfy a `read` check.
|
||||
# Guard only here -- the in-process callers use _get_asset_value_after_depreciation() below.
|
||||
ptype = "select" if frappe.only_has_select_perm("Asset") else "read"
|
||||
frappe.has_permission("Asset", ptype, doc=asset_name, throw=True)
|
||||
|
||||
return _get_asset_value_after_depreciation(asset_name, finance_book)
|
||||
|
||||
|
||||
def _get_asset_value_after_depreciation(asset_name, finance_book=None):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
if not asset.calculate_depreciation:
|
||||
return flt(asset.value_after_depreciation)
|
||||
@@ -1236,8 +1220,6 @@ def _get_asset_value_after_depreciation(asset_name, finance_book=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def has_active_capitalization(asset):
|
||||
frappe.has_permission("Asset", doc=asset, throw=True)
|
||||
|
||||
active_capitalizations = frappe.db.count(
|
||||
"Asset Capitalization", filters={"target_asset": asset, "docstatus": 1}
|
||||
)
|
||||
@@ -1246,15 +1228,6 @@ def has_active_capitalization(asset):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
|
||||
# `doctype` is caller-supplied and reaches frappe.get_doc(), so without this list any document
|
||||
# with an `items` table could be read for its valuation rates
|
||||
if doctype not in ("Purchase Receipt", "Purchase Invoice"):
|
||||
frappe.throw(_("Invalid document type"), frappe.PermissionError)
|
||||
|
||||
# the Asset form is the boundary: Quality Manager writes Assets but reads neither Purchase
|
||||
# Receipt nor Purchase Invoice, so the purchase document cannot be it
|
||||
frappe.has_permission("Asset", "write", throw=True)
|
||||
|
||||
purchase_doc = frappe.get_doc(doctype, purchase_doc_name)
|
||||
matching_items = [item for item in purchase_doc.items if item.item_code == item_code]
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from frappe import _
|
||||
from frappe.utils import cint, flt, get_link_to_form
|
||||
|
||||
import erpnext
|
||||
from erpnext.assets.doctype.asset.asset import _get_asset_value_after_depreciation
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
depreciate_asset,
|
||||
get_gl_entries_on_asset_disposal,
|
||||
@@ -31,7 +31,7 @@ from erpnext.stock.get_item_details import (
|
||||
get_item_warehouse,
|
||||
)
|
||||
from erpnext.stock.stock_ledger import get_previous_sle
|
||||
from erpnext.stock.utils import _get_incoming_rate
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
|
||||
force_fields = [
|
||||
"target_item_name",
|
||||
@@ -205,7 +205,7 @@ class AssetCapitalization(StockController):
|
||||
cumulative_qty += flt(d.stock_qty)
|
||||
args = self.get_args_for_incoming_rate(d)
|
||||
args["qty"] = -1 * cumulative_qty
|
||||
cumulative_rate = flt(_get_incoming_rate(args, raise_error_if_no_rate=False))
|
||||
cumulative_rate = flt(get_incoming_rate(args, raise_error_if_no_rate=False))
|
||||
cumulative_value = cumulative_rate * cumulative_qty
|
||||
|
||||
row_value = cumulative_value - prev_cumulative_value
|
||||
@@ -358,8 +358,6 @@ class AssetCapitalization(StockController):
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_warehouse_details(self):
|
||||
self.check_permission("write")
|
||||
|
||||
for d in self.get("stock_items"):
|
||||
if d.item_code and d.warehouse:
|
||||
args = self.get_args_for_incoming_rate(d)
|
||||
@@ -370,13 +368,11 @@ class AssetCapitalization(StockController):
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_asset_values(self):
|
||||
self.check_permission("write")
|
||||
|
||||
for d in self.get("asset_items"):
|
||||
if d.asset:
|
||||
finance_book = d.get("finance_book") or self.get("finance_book")
|
||||
d.current_asset_value = flt(
|
||||
_get_asset_value_after_depreciation(d.asset, finance_book=finance_book)
|
||||
get_asset_value_after_depreciation(d.asset, finance_book=finance_book)
|
||||
)
|
||||
d.asset_value = get_value_after_depreciation_on_disposal_date(
|
||||
d.asset, self.posting_date, finance_book=finance_book
|
||||
@@ -700,15 +696,8 @@ class AssetCapitalization(StockController):
|
||||
)
|
||||
|
||||
|
||||
def check_capitalization_access():
|
||||
"""Every lookup in this file feeds the Asset Capitalization form, so that form is the boundary."""
|
||||
frappe.has_permission("Asset Capitalization", throw=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_target_item_details(item_code=None, company=None):
|
||||
check_capitalization_access()
|
||||
|
||||
out = frappe._dict()
|
||||
|
||||
# Get Item Details
|
||||
@@ -746,8 +735,6 @@ def get_target_item_details(item_code=None, company=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_target_asset_details(asset=None, company=None):
|
||||
check_capitalization_access()
|
||||
|
||||
out = frappe._dict()
|
||||
|
||||
# Get Asset Details
|
||||
@@ -842,25 +829,17 @@ def get_warehouse_details(args):
|
||||
frappe.has_permission("Stock Ledger Entry", throw=True)
|
||||
out = {
|
||||
"actual_qty": get_previous_sle(args).get("qty_after_transaction") or 0,
|
||||
"valuation_rate": _get_incoming_rate(args, raise_error_if_no_rate=False),
|
||||
"valuation_rate": get_incoming_rate(args, raise_error_if_no_rate=False),
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_consumed_asset_details(args):
|
||||
check_capitalization_access()
|
||||
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
args = frappe._dict(args)
|
||||
|
||||
# and the Asset the caller named: its depreciation values are returned through the unguarded
|
||||
# _get_asset_value_after_depreciation. select-or-read, as in the asset.py wrapper.
|
||||
if args.get("asset"):
|
||||
ptype = "select" if frappe.only_has_select_perm("Asset") else "read"
|
||||
frappe.has_permission("Asset", ptype, doc=args.get("asset"), throw=True)
|
||||
out = frappe._dict()
|
||||
|
||||
asset_details = frappe._dict()
|
||||
@@ -877,7 +856,7 @@ def get_consumed_asset_details(args):
|
||||
|
||||
if args.asset:
|
||||
out.current_asset_value = flt(
|
||||
_get_asset_value_after_depreciation(args.asset, finance_book=args.finance_book)
|
||||
get_asset_value_after_depreciation(args.asset, finance_book=args.finance_book)
|
||||
)
|
||||
out.asset_value = get_value_after_depreciation_on_disposal_date(
|
||||
args.asset, args.posting_date, finance_book=args.finance_book
|
||||
@@ -906,8 +885,6 @@ def get_consumed_asset_details(args):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_service_item_details(args):
|
||||
check_capitalization_access()
|
||||
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
@@ -936,8 +913,6 @@ def get_service_item_details(args):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_items_tagged_to_wip_composite_asset(params):
|
||||
check_capitalization_access()
|
||||
|
||||
if isinstance(params, str):
|
||||
params = json.loads(params)
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2025-05-13 15:33:03.791814",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Category",
|
||||
@@ -116,10 +116,6 @@
|
||||
"role": "Quality Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Item Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2021-01-22 15:09:03.347345",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Maintenance Team",
|
||||
@@ -78,10 +78,6 @@
|
||||
"role": "Manufacturing User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Quality Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2023-11-29 04:04:24.272872",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Shift Factor",
|
||||
@@ -66,10 +66,6 @@
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Quality Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -10,7 +10,7 @@ from frappe.utils import cstr, flt, formatdate, get_link_to_form, getdate
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.asset import _get_asset_value_after_depreciation
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
|
||||
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
|
||||
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
|
||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||
@@ -81,7 +81,7 @@ class AssetValueAdjustment(Document):
|
||||
|
||||
def set_current_asset_value(self):
|
||||
if not self.current_asset_value and self.asset:
|
||||
self.current_asset_value = _get_asset_value_after_depreciation(self.asset, self.finance_book)
|
||||
self.current_asset_value = get_asset_value_after_depreciation(self.asset, self.finance_book)
|
||||
|
||||
def make_depreciation_entry(self):
|
||||
asset = frappe.get_doc("Asset", self.asset)
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
],
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2023-08-29 12:49:33.290527",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Location",
|
||||
@@ -219,10 +219,6 @@
|
||||
"role": "Agriculture User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Quality Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
||||
@@ -15,7 +15,7 @@ from erpnext.accounts.report.financial_statements import (
|
||||
validate_fiscal_year,
|
||||
)
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.assets.doctype.asset.asset import _get_asset_value_after_depreciation
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
|
||||
@@ -699,19 +699,14 @@ def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=
|
||||
return item_last_purchase_rate
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def close_or_unclose_purchase_orders(names, status):
|
||||
frappe.has_permission("Purchase Order", "write", throw=True)
|
||||
if not frappe.has_permission("Purchase Order", "write"):
|
||||
frappe.throw(_("Not permitted"), frappe.PermissionError)
|
||||
|
||||
names = json.loads(names)
|
||||
for name in names:
|
||||
if not isinstance(name, str):
|
||||
frappe.throw(_("Invalid name"), frappe.PermissionError)
|
||||
|
||||
# the check above is doctype level and never consults User Permissions, so on its own it lets
|
||||
# a caller restricted to one company close another company's orders
|
||||
po = frappe.get_doc("Purchase Order", name)
|
||||
po.check_permission("submit")
|
||||
if po.docstatus == 1:
|
||||
if status == "Closed":
|
||||
if po.status not in ("Cancelled", "Closed") and (
|
||||
|
||||
@@ -11,6 +11,7 @@ from frappe.core.doctype.communication.email import make
|
||||
from frappe.desk.form.load import get_attachments
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder import Order
|
||||
from frappe.utils import get_url
|
||||
from frappe.utils.print_format import download_pdf
|
||||
from frappe.utils.user import get_user_fullname
|
||||
@@ -682,34 +683,32 @@ def get_supplier_tag():
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filters):
|
||||
rfq_filters = [
|
||||
["docstatus", "=", 1],
|
||||
["company", "=", filters.get("company")],
|
||||
]
|
||||
rfq = frappe.qb.DocType("Request for Quotation")
|
||||
rfq_supplier = frappe.qb.DocType("Request for Quotation Supplier")
|
||||
|
||||
if frappe.has_permission("Request for Quotation", "read"):
|
||||
rfq_filters.append(["Request for Quotation Supplier", "supplier", "=", filters.get("supplier")])
|
||||
else:
|
||||
parents = frappe.get_all(
|
||||
"Request for Quotation Supplier",
|
||||
filters={"supplier": filters.get("supplier"), "parenttype": "Request for Quotation"},
|
||||
pluck="parent",
|
||||
distinct=True,
|
||||
query = (
|
||||
frappe.qb.from_(rfq)
|
||||
.from_(rfq_supplier)
|
||||
.select(rfq.name)
|
||||
.distinct()
|
||||
.select(rfq.transaction_date, rfq.company)
|
||||
.where(
|
||||
(rfq.name == rfq_supplier.parent)
|
||||
& (rfq_supplier.supplier == filters.get("supplier"))
|
||||
& (rfq.docstatus == 1)
|
||||
& (rfq.company == filters.get("company"))
|
||||
)
|
||||
rfq_filters.append(["name", "in", parents or [""]])
|
||||
.orderby(rfq.transaction_date, order=Order.asc)
|
||||
.limit(page_len)
|
||||
.offset(start)
|
||||
)
|
||||
|
||||
if txt:
|
||||
rfq_filters.append(["name", "like", f"%{txt}%"])
|
||||
query = query.where(rfq.name.like(f"%%{txt}%%"))
|
||||
|
||||
if filters.get("transaction_date"):
|
||||
rfq_filters.append(["transaction_date", "=", filters.get("transaction_date")])
|
||||
query = query.where(rfq.transaction_date == filters.get("transaction_date"))
|
||||
|
||||
return frappe.get_list(
|
||||
"Request for Quotation",
|
||||
filters=rfq_filters,
|
||||
fields=["name", "transaction_date", "company"],
|
||||
group_by="name",
|
||||
order_by="transaction_date asc",
|
||||
limit_start=start,
|
||||
limit_page_length=page_len,
|
||||
)
|
||||
rfq_data = query.run(as_dict=1)
|
||||
|
||||
return rfq_data
|
||||
|
||||
@@ -491,7 +491,7 @@
|
||||
"link_fieldname": "party"
|
||||
}
|
||||
],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2026-05-29 16:52:59.441272",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier",
|
||||
@@ -547,30 +547,6 @@
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Delivery Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Delivery User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Quality Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales Master Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Website Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
||||
@@ -233,15 +233,6 @@ class Supplier(TransactionBase):
|
||||
def get_supplier_primary(doctype, txt, searchfield, start, page_len, filters):
|
||||
supplier = filters.get("supplier")
|
||||
type = filters.get("type")
|
||||
|
||||
# `type` is caller-supplied and was interpolated into qb.DocType(), so any doctype could be
|
||||
# joined to Dynamic Link and read. The two pickers send only these values.
|
||||
if type not in ("Contact", "Address"):
|
||||
frappe.throw(_("Invalid type"), frappe.PermissionError)
|
||||
|
||||
# authorise the party, not Contact/Address: the `if_owner` row on Address would empty the picker rather than error
|
||||
frappe.has_permission("Supplier", doc=supplier, throw=True)
|
||||
|
||||
type_doctype = frappe.qb.DocType(type)
|
||||
dynamic_link = frappe.qb.DocType("Dynamic Link")
|
||||
|
||||
|
||||
@@ -939,7 +939,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2026-08-12 12:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier Quotation",
|
||||
@@ -998,14 +998,6 @@
|
||||
"read": 1,
|
||||
"role": "Purchase Manager",
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance Manager",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -55,7 +55,8 @@ class SupplierScorecard(Document):
|
||||
self.update_standing()
|
||||
|
||||
def on_update(self):
|
||||
if make_all_scorecards(self.name) > 0:
|
||||
score = make_all_scorecards(self.name)
|
||||
if score > 0:
|
||||
self.save()
|
||||
|
||||
def validate_standings(self):
|
||||
@@ -197,7 +198,7 @@ def refresh_scorecards():
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_all_scorecards(docname: str):
|
||||
def make_all_scorecards(docname):
|
||||
sc = frappe.get_doc("Supplier Scorecard", docname)
|
||||
supplier = frappe.get_doc("Supplier", sc.supplier)
|
||||
supplier.check_permission("write")
|
||||
@@ -212,18 +213,25 @@ def make_all_scorecards(docname: str):
|
||||
|
||||
while (start_date < todays) and (end_date <= todays):
|
||||
# check to make sure there is no scorecard period already created
|
||||
# (inclusive bounds: a single-day period — supplier created on a month's
|
||||
# last day — must match its own window, else it is re-created every run)
|
||||
scorecards = frappe.get_all(
|
||||
"Supplier Scorecard Period",
|
||||
fields=["name"],
|
||||
filters={
|
||||
"scorecard": docname,
|
||||
"docstatus": 1,
|
||||
"start_date": ["<=", end_date],
|
||||
"end_date": [">=", start_date],
|
||||
},
|
||||
order_by="end_date desc",
|
||||
scorecards = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
scp.name
|
||||
FROM
|
||||
`tabSupplier Scorecard Period` scp
|
||||
WHERE
|
||||
scp.scorecard = %(sc)s
|
||||
AND scp.docstatus = 1
|
||||
AND (
|
||||
(scp.start_date > %(end_date)s
|
||||
AND scp.end_date < %(start_date)s)
|
||||
OR
|
||||
(scp.start_date < %(end_date)s
|
||||
AND scp.end_date > %(start_date)s))
|
||||
ORDER BY
|
||||
scp.end_date DESC""",
|
||||
{"sc": docname, "start_date": start_date, "end_date": end_date},
|
||||
as_dict=1,
|
||||
)
|
||||
if len(scorecards) == 0:
|
||||
period_card = make_supplier_scorecard(docname, None)
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_months, get_last_day, getdate
|
||||
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
|
||||
|
||||
class TestSupplierScorecard(FrappeTestCase):
|
||||
@@ -21,32 +18,6 @@ class TestSupplierScorecard(FrappeTestCase):
|
||||
d.weight = 0
|
||||
self.assertRaises(frappe.ValidationError, my_doc.insert)
|
||||
|
||||
def test_no_recursion_for_supplier_created_on_month_end(self):
|
||||
make_supplier_scorecard() # ensures the "Delivery" criteria master exists
|
||||
|
||||
supplier = create_supplier(supplier_name="_Test Month End Scorecard Supplier")
|
||||
month_end = get_last_day(add_months(getdate(), -1))
|
||||
frappe.db.set_value("Supplier", supplier.name, "creation", month_end, update_modified=False)
|
||||
|
||||
scorecard = frappe.get_doc(valid_scorecard[0])
|
||||
scorecard.supplier = supplier.name
|
||||
scorecard.name = supplier.name
|
||||
scorecard.insert()
|
||||
|
||||
periods = frappe.get_all(
|
||||
"Supplier Scorecard Period",
|
||||
filters={"scorecard": scorecard.name},
|
||||
fields=["start_date", "end_date"],
|
||||
)
|
||||
self.assertEqual(len(periods), 1)
|
||||
self.assertEqual(periods[0].start_date, month_end)
|
||||
self.assertEqual(periods[0].end_date, month_end)
|
||||
|
||||
# saving again must not re-create the single-day period or recurse
|
||||
frappe.get_doc("Supplier Scorecard", scorecard.name).save()
|
||||
periods = frappe.get_all("Supplier Scorecard Period", filters={"scorecard": scorecard.name})
|
||||
self.assertEqual(len(periods), 1)
|
||||
|
||||
|
||||
def make_supplier_scorecard():
|
||||
my_doc = frappe.get_doc(valid_scorecard[0])
|
||||
|
||||
@@ -43,9 +43,7 @@ def update_last_purchase_rate(doc, is_submit) -> None:
|
||||
frappe.throw(_("UOM Conversion factor is required in row {0}").format(d.idx))
|
||||
|
||||
# update last purchsae rate
|
||||
frappe.db.set_value(
|
||||
"Item", d.item_code, "last_purchase_rate", flt(last_purchase_rate), update_modified=False
|
||||
)
|
||||
frappe.db.set_value("Item", d.item_code, "last_purchase_rate", flt(last_purchase_rate))
|
||||
|
||||
|
||||
def validate_for_items(doc) -> None:
|
||||
@@ -120,35 +118,7 @@ def check_on_hold_or_closed_status(doctype, docname) -> None:
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_linked_material_requests(items):
|
||||
try:
|
||||
items = frappe.parse_json(items)
|
||||
except (TypeError, ValueError):
|
||||
frappe.throw(_("Items must be a list of Item codes"))
|
||||
|
||||
if isinstance(items, str):
|
||||
items = [items]
|
||||
|
||||
if not isinstance(items, list | tuple) or any(not isinstance(item, str) for item in items):
|
||||
frappe.throw(_("Items must be a list of Item codes"))
|
||||
|
||||
# get_list, not the raw query below on its own: it applies the caller's Material Request
|
||||
# permission and their User Permissions, so the loop can only ever return permitted documents
|
||||
permitted_material_requests = frappe.get_list(
|
||||
"Material Request",
|
||||
filters=[
|
||||
["material_request_type", "=", "Purchase"],
|
||||
["docstatus", "=", 1],
|
||||
["status", "!=", "Stopped"],
|
||||
["per_ordered", "<", 99.99],
|
||||
["Material Request Item", "item_code", "in", items],
|
||||
],
|
||||
pluck="name",
|
||||
distinct=True,
|
||||
)
|
||||
|
||||
if not permitted_material_requests:
|
||||
return []
|
||||
|
||||
items = json.loads(items)
|
||||
mr_list = []
|
||||
for item in items:
|
||||
material_request = frappe.db.sql(
|
||||
@@ -159,13 +129,12 @@ def get_linked_material_requests(items):
|
||||
FROM `tabMaterial Request` mr, `tabMaterial Request Item` mr_item
|
||||
WHERE mr.name = mr_item.parent
|
||||
AND mr_item.item_code = %(item)s
|
||||
AND mr.name in %(permitted)s
|
||||
AND mr.material_request_type = 'Purchase'
|
||||
AND mr.per_ordered < 99.99
|
||||
AND mr.docstatus = 1
|
||||
AND mr.status != 'Stopped'
|
||||
ORDER BY mr_item.item_code ASC""",
|
||||
{"item": item, "permitted": permitted_material_requests},
|
||||
{"item": item},
|
||||
as_dict=1,
|
||||
)
|
||||
if material_request:
|
||||
|
||||
@@ -387,26 +387,12 @@ class AccountsController(TransactionBase):
|
||||
return any(item.delivered_by_supplier for item in items)
|
||||
|
||||
def validate_price_list(self):
|
||||
if self.get("selling_price_list"):
|
||||
price_list_field, transaction_side = "selling_price_list", "selling"
|
||||
else:
|
||||
price_list_field, transaction_side = "buying_price_list", "buying"
|
||||
|
||||
price_list_field = "selling_price_list" if self.get("selling_price_list") else "buying_price_list"
|
||||
price_list = self.get(price_list_field)
|
||||
if not price_list:
|
||||
if not price_list or frappe.db.get_value("Price List", price_list, "enabled"):
|
||||
return
|
||||
|
||||
details = (
|
||||
frappe.db.get_value("Price List", price_list, ["enabled", transaction_side], as_dict=True)
|
||||
or frappe._dict()
|
||||
)
|
||||
|
||||
# An internal transfer carries the price list of the outward document into the inward one.
|
||||
fits_transaction = details.get(transaction_side) or self.is_internal_transfer()
|
||||
if details.enabled and fits_transaction:
|
||||
return
|
||||
|
||||
# Returns retain a submitted voucher's pricing even if its price list no longer fits.
|
||||
# Returns retain a submitted voucher's pricing even if its price list is now disabled.
|
||||
if (
|
||||
self.get("is_return")
|
||||
and self.get("return_against")
|
||||
@@ -417,20 +403,9 @@ class AccountsController(TransactionBase):
|
||||
):
|
||||
return
|
||||
|
||||
if not details.enabled:
|
||||
frappe.throw(
|
||||
_("Price List {0} is disabled").format(get_link_to_form("Price List", price_list)),
|
||||
title=_("Disabled Price List"),
|
||||
)
|
||||
|
||||
if transaction_side == "selling":
|
||||
message = _("Price List {0} cannot be used on a selling transaction")
|
||||
else:
|
||||
message = _("Price List {0} cannot be used on a buying transaction")
|
||||
|
||||
frappe.throw(
|
||||
message.format(get_link_to_form("Price List", price_list)),
|
||||
title=_("Invalid Price List"),
|
||||
_("Price List {0} is disabled").format(get_link_to_form("Price List", price_list)),
|
||||
title=_("Disabled Price List"),
|
||||
)
|
||||
|
||||
def set_default_letter_head(self):
|
||||
@@ -3224,22 +3199,11 @@ def get_tax_rate(account_head):
|
||||
return frappe.get_cached_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True)
|
||||
|
||||
|
||||
# the only doctypes a `taxes_and_charges` Link points at; `master_doctype` is caller-supplied and reaches get_doc()
|
||||
TAX_MASTER_DOCTYPES = ("Sales Taxes and Charges Template", "Purchase Taxes and Charges Template")
|
||||
|
||||
|
||||
def validate_tax_master(master_doctype):
|
||||
if master_doctype not in TAX_MASTER_DOCTYPES:
|
||||
frappe.throw(_("Invalid tax master doctype"), frappe.PermissionError)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_default_taxes_and_charges(master_doctype, tax_template=None, company=None):
|
||||
if not company:
|
||||
return {}
|
||||
|
||||
validate_tax_master(master_doctype)
|
||||
|
||||
if tax_template and company:
|
||||
tax_template_company = frappe.get_cached_value(master_doctype, tax_template, "company")
|
||||
if tax_template_company == company:
|
||||
@@ -3257,9 +3221,6 @@ def get_default_taxes_and_charges(master_doctype, tax_template=None, company=Non
|
||||
def get_taxes_and_charges(master_doctype, master_name):
|
||||
if not master_name:
|
||||
return
|
||||
|
||||
validate_tax_master(master_doctype)
|
||||
|
||||
from frappe.model import child_table_fields, default_fields
|
||||
|
||||
tax_master = frappe.get_doc(master_doctype, master_name)
|
||||
|
||||
@@ -17,7 +17,7 @@ from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
||||
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
||||
from erpnext.controllers.subcontracting_controller import SubcontractingController
|
||||
from erpnext.stock.get_item_details import get_conversion_factor
|
||||
from erpnext.stock.utils import _get_incoming_rate
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
|
||||
|
||||
class QtyMismatchError(ValidationError):
|
||||
@@ -136,7 +136,7 @@ class BuyingController(SubcontractingController):
|
||||
for row in self.items:
|
||||
if row.rate <= 0:
|
||||
# override the rate with valuation rate
|
||||
row.rate = _get_incoming_rate(
|
||||
row.rate = get_incoming_rate(
|
||||
{
|
||||
"item_code": row.item_code,
|
||||
"warehouse": row.warehouse,
|
||||
@@ -366,7 +366,7 @@ class BuyingController(SubcontractingController):
|
||||
|
||||
net_rate = item.base_net_amount
|
||||
if item.sales_incoming_rate: # for internal transfer
|
||||
net_rate = self.get_internal_transfer_qty(item) * item.sales_incoming_rate
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
|
||||
if (
|
||||
not net_rate
|
||||
@@ -459,7 +459,7 @@ class BuyingController(SubcontractingController):
|
||||
if not posting_time:
|
||||
posting_time = nowtime()
|
||||
|
||||
outgoing_rate = _get_incoming_rate(
|
||||
outgoing_rate = get_incoming_rate(
|
||||
{
|
||||
"item_code": d.item_code,
|
||||
"warehouse": d.get("from_warehouse"),
|
||||
@@ -572,26 +572,6 @@ class BuyingController(SubcontractingController):
|
||||
if status in ("Closed", "On Hold"):
|
||||
frappe.throw(_("{0} {1} is {2}").format(ref_doctype, d.get(ref_fieldname), status))
|
||||
|
||||
def get_internal_transfer_qty(self, row) -> float:
|
||||
if flt(row.qty) or not self.is_internal_receipt():
|
||||
return flt(row.qty)
|
||||
|
||||
return flt(row.rejected_qty)
|
||||
|
||||
def is_internal_receipt(self) -> bool:
|
||||
return self.doctype == "Purchase Receipt" and self.is_internal_transfer()
|
||||
|
||||
def get_source_warehouse_qty(self, row, accepted_qty):
|
||||
if not (self.is_internal_receipt() and flt(row.rejected_qty)):
|
||||
return accepted_qty
|
||||
|
||||
if row.get("serial_and_batch_bundle") or row.get("rejected_serial_and_batch_bundle"):
|
||||
return accepted_qty
|
||||
|
||||
rejected_qty = flt(flt(row.rejected_qty) * flt(row.conversion_factor), row.precision("stock_qty"))
|
||||
|
||||
return flt(accepted_qty + rejected_qty, row.precision("stock_qty"))
|
||||
|
||||
def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_voucher=False):
|
||||
self.update_ordered_and_reserved_qty()
|
||||
|
||||
@@ -604,9 +584,8 @@ class BuyingController(SubcontractingController):
|
||||
|
||||
if d.warehouse:
|
||||
pr_qty = flt(flt(d.qty) * flt(d.conversion_factor), d.precision("stock_qty"))
|
||||
source_qty = self.get_source_warehouse_qty(d, pr_qty)
|
||||
|
||||
if pr_qty or source_qty:
|
||||
if pr_qty:
|
||||
if d.from_warehouse and (
|
||||
(not cint(self.is_return) and self.docstatus == 1)
|
||||
or (cint(self.is_return) and self.docstatus == 2)
|
||||
@@ -622,7 +601,7 @@ class BuyingController(SubcontractingController):
|
||||
from_warehouse_sle = self.get_sl_entries(
|
||||
d,
|
||||
{
|
||||
"actual_qty": -1 * source_qty,
|
||||
"actual_qty": -1 * pr_qty,
|
||||
"warehouse": d.from_warehouse,
|
||||
"outgoing_rate": d.rate,
|
||||
"recalculate_rate": 1,
|
||||
@@ -695,7 +674,7 @@ class BuyingController(SubcontractingController):
|
||||
from_warehouse_sle = self.get_sl_entries(
|
||||
d,
|
||||
{
|
||||
"actual_qty": -1 * source_qty,
|
||||
"actual_qty": -1 * pr_qty,
|
||||
"warehouse": d.from_warehouse,
|
||||
"recalculate_rate": 1,
|
||||
"serial_and_batch_bundle": (
|
||||
|
||||
@@ -34,10 +34,6 @@ def get_variant(template, args=None, variant=None, manufacturer=None, manufactur
|
||||
:param item: Template Item
|
||||
:param args: A dictionary with "Attribute" as key and "Attribute Value" as value
|
||||
"""
|
||||
# the template Item is the boundary and `read` is loser-free: the roles that cannot read Item
|
||||
# cannot open the calling form either
|
||||
frappe.has_permission("Item", doc=template, throw=True)
|
||||
|
||||
item_template = frappe.get_doc("Item", template)
|
||||
|
||||
if item_template.variant_based_on == "Manufacturer" and manufacturer:
|
||||
@@ -313,11 +309,6 @@ def find_variant(template, args, variant_item_code=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_variant(item, args, use_template_image=False):
|
||||
# same right its sibling enqueue_multiple_variant_creation requires, plus record-level read on
|
||||
# the template it copies from
|
||||
frappe.has_permission("Item", ptype="create", throw=True)
|
||||
frappe.has_permission("Item", doc=item, throw=True)
|
||||
|
||||
use_template_image = frappe.parse_json(use_template_image)
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
@@ -343,7 +334,7 @@ def create_variant(item, args, use_template_image=False):
|
||||
return variant
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def enqueue_multiple_variant_creation(item, args, use_template_image=False):
|
||||
frappe.has_permission("Item", ptype="create", throw=True)
|
||||
use_template_image = frappe.parse_json(use_template_image)
|
||||
@@ -536,10 +527,6 @@ def make_variant_item_code(template_item_code, template_item_name, variant):
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_variant_doc_for_quick_entry(template, args):
|
||||
# Delegates to get_variant and create_variant below, which carry their own checks; this one
|
||||
# fails fast rather than relying on that delegation.
|
||||
frappe.has_permission("Item", doc=template, throw=True)
|
||||
|
||||
variant_based_on = frappe.db.get_value("Item", template, "variant_based_on")
|
||||
args = json.loads(args)
|
||||
if variant_based_on == "Manufacturer":
|
||||
|
||||
@@ -6,7 +6,7 @@ import json
|
||||
from collections import OrderedDict, defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, scrub
|
||||
from frappe import qb, scrub
|
||||
from frappe.desk.reportview import get_filters_cond, get_match_cond
|
||||
from frappe.permissions import has_permission
|
||||
from frappe.query_builder import Criterion, CustomFunction
|
||||
@@ -310,61 +310,43 @@ def get_project_name(
|
||||
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict | None = None
|
||||
):
|
||||
proj = qb.DocType("Project")
|
||||
meta = frappe.get_meta(doctype)
|
||||
qb_filter_and_conditions = []
|
||||
qb_filter_or_conditions = []
|
||||
ifelse = CustomFunction("IF", ["condition", "then", "else"])
|
||||
|
||||
list_filters = [["status", "not in", ["Completed", "Cancelled", "On hold"]]]
|
||||
|
||||
if filters:
|
||||
if filters.get("customer"):
|
||||
# an `in` containing "" renders as `ifnull(customer,'') in (...)`: this customer, or none
|
||||
list_filters.append(["customer", "in", [filters.get("customer"), ""]])
|
||||
qb_filter_and_conditions.append(
|
||||
(proj.customer == filters.get("customer")) | (proj.customer.isnull()) | (proj.customer == "")
|
||||
)
|
||||
|
||||
if filters.get("company"):
|
||||
list_filters.append(["company", "=", filters.get("company")])
|
||||
qb_filter_and_conditions.append(proj.company == filters.get("company"))
|
||||
|
||||
# don't consider 'customer' and 'status' fields for pattern search, as they must be exactly matched
|
||||
# permlevel fields go too: get_list refuses to filter on one, which would fail the whole call
|
||||
searchfields = [
|
||||
x
|
||||
for x in meta.get_search_fields()
|
||||
if x not in ["customer", "status"] and not (meta.get_field(x) and meta.get_field(x).permlevel)
|
||||
]
|
||||
qb_filter_and_conditions.append(proj.status.notin(["Completed", "Cancelled", "On hold"]))
|
||||
|
||||
q = qb.from_(proj)
|
||||
|
||||
fields = get_fields(doctype, ["name", "project_name"])
|
||||
for x in fields:
|
||||
q = q.select(proj[x])
|
||||
|
||||
# get_list applies the doctype check and the caller's record-level conditions
|
||||
if not txt:
|
||||
# no search term means no relevance ordering, so the whole query is expressible here and
|
||||
# stays paginated in SQL rather than materialising every permitted name
|
||||
return frappe.get_list(
|
||||
"Project",
|
||||
filters=list_filters,
|
||||
fields=fields,
|
||||
order_by="idx desc, name",
|
||||
limit_start=start,
|
||||
limit_page_length=page_len,
|
||||
as_list=True,
|
||||
)
|
||||
# don't consider 'customer' and 'status' fields for pattern search, as they must be exactly matched
|
||||
searchfields = [
|
||||
x for x in frappe.get_meta(doctype).get_search_fields() if x not in ["customer", "status"]
|
||||
]
|
||||
|
||||
# with a search term, resolve the (already LIKE-narrowed) permitted names and rank them below:
|
||||
# the relevance ordering is a CASE expression, which `order_by` rejects
|
||||
permitted = frappe.get_list(
|
||||
"Project",
|
||||
filters=list_filters,
|
||||
or_filters=[[x, "like", f"%{txt}%"] for x in searchfields],
|
||||
pluck="name",
|
||||
order_by="",
|
||||
limit_page_length=0,
|
||||
)
|
||||
# pattern search
|
||||
if txt:
|
||||
for x in searchfields:
|
||||
qb_filter_or_conditions.append(proj[x].like(f"%{txt}%"))
|
||||
|
||||
if not permitted:
|
||||
return []
|
||||
q = q.where(Criterion.all(qb_filter_and_conditions)).where(Criterion.any(qb_filter_or_conditions))
|
||||
|
||||
q = qb.from_(proj).select(*[proj[fieldname] for fieldname in fields]).where(proj.name.isin(permitted))
|
||||
|
||||
# project_name containing search string 'txt' will be given higher precedence
|
||||
q = q.orderby(ifelse(Locate(txt, proj.project_name) > 0, Locate(txt, proj.project_name), 99999))
|
||||
# ordering
|
||||
if txt:
|
||||
# project_name containing search string 'txt' will be given higher precedence
|
||||
q = q.orderby(ifelse(Locate(txt, proj.project_name) > 0, Locate(txt, proj.project_name), 99999))
|
||||
q = q.orderby(proj.idx, order=Order.desc).orderby(proj.name)
|
||||
|
||||
if page_len:
|
||||
@@ -627,31 +609,27 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters):
|
||||
bo_filters = [
|
||||
["docstatus", "=", 1],
|
||||
["blanket_order_type", "=", filters.get("blanket_order_type")],
|
||||
["company", "=", filters.get("company")],
|
||||
]
|
||||
bo = frappe.qb.DocType("Blanket Order")
|
||||
bo_item = frappe.qb.DocType("Blanket Order Item")
|
||||
|
||||
if frappe.has_permission("Blanket Order", "read"):
|
||||
bo_filters.append(["Blanket Order Item", "item_code", "=", filters.get("item")])
|
||||
else:
|
||||
parents = frappe.get_all(
|
||||
"Blanket Order Item",
|
||||
filters={"item_code": filters.get("item"), "parenttype": "Blanket Order"},
|
||||
pluck="parent",
|
||||
distinct=True,
|
||||
blanket_orders = (
|
||||
frappe.qb.from_(bo)
|
||||
.from_(bo_item)
|
||||
.select(bo.name)
|
||||
.distinct()
|
||||
.select(bo.blanket_order_type, bo.to_date)
|
||||
.where(
|
||||
(bo_item.parent == bo.name)
|
||||
& (bo_item.item_code == filters.get("item"))
|
||||
& (bo.blanket_order_type == filters.get("blanket_order_type"))
|
||||
& (bo.company == filters.get("company"))
|
||||
& (bo.docstatus == 1)
|
||||
)
|
||||
bo_filters.append(["name", "in", parents or [""]])
|
||||
|
||||
return frappe.get_list(
|
||||
"Blanket Order",
|
||||
filters=bo_filters,
|
||||
fields=["name", "blanket_order_type", "to_date"],
|
||||
group_by="name",
|
||||
as_list=True,
|
||||
.run()
|
||||
)
|
||||
|
||||
return blanket_orders
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
@@ -832,22 +810,15 @@ def get_doctype_wise_filters(filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_batch_numbers(doctype, txt, searchfield, start, page_len, filters):
|
||||
# get_list applies the select check and the caller's record-level conditions together
|
||||
batch_filters = [["disabled", "=", 0], ["name", "like", f"%{txt}%"]]
|
||||
query = """select batch_id from `tabBatch`
|
||||
where disabled = 0
|
||||
and (expiry_date >= CURRENT_DATE or expiry_date IS NULL)
|
||||
and name like {txt}""".format(txt=frappe.db.escape(f"%{txt}%"))
|
||||
|
||||
if filters and filters.get("item"):
|
||||
batch_filters.append(["item", "=", filters.get("item")])
|
||||
query += " and item = {item}".format(item=frappe.db.escape(filters.get("item")))
|
||||
|
||||
return frappe.get_list(
|
||||
"Batch",
|
||||
filters=batch_filters,
|
||||
or_filters=[["expiry_date", "is", "not set"], ["expiry_date", ">=", today()]],
|
||||
fields=["batch_id"],
|
||||
order_by="batch_id",
|
||||
limit_start=start,
|
||||
limit_page_length=page_len,
|
||||
as_list=True,
|
||||
)
|
||||
return frappe.db.sql(query, filters)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -872,69 +843,35 @@ def item_manufacturer_query(doctype, txt, searchfield, start, page_len, filters)
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_purchase_receipts(doctype, txt, searchfield, start, page_len, filters):
|
||||
pr_filters = [["docstatus", "=", 1], ["name", "like", f"%{txt}%"]]
|
||||
query = """
|
||||
select pr.name
|
||||
from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pritem
|
||||
where pr.docstatus = 1 and pritem.parent = pr.name
|
||||
and pr.name like {txt}""".format(txt=frappe.db.escape(f"%{txt}%"))
|
||||
|
||||
if filters and filters.get("item_code"):
|
||||
if frappe.has_permission("Purchase Receipt", "read"):
|
||||
# one indexed join, deduped by group_by below
|
||||
pr_filters.append(["Purchase Receipt Item", "item_code", "=", filters.get("item_code")])
|
||||
else:
|
||||
# a select-only caller may use this picker but may not filter on a child table, so resolve
|
||||
# the parents separately rather than losing the filter to a PermissionError
|
||||
parents = frappe.get_all(
|
||||
"Purchase Receipt Item",
|
||||
filters={"item_code": filters.get("item_code"), "parenttype": "Purchase Receipt"},
|
||||
pluck="parent",
|
||||
distinct=True,
|
||||
)
|
||||
pr_filters.append(["name", "in", parents or [""]])
|
||||
query += " and pritem.item_code = {item_code}".format(
|
||||
item_code=frappe.db.escape(filters.get("item_code"))
|
||||
)
|
||||
|
||||
# group_by, not distinct: it dedupes the child join just the same, and frappe drops ORDER BY
|
||||
# from a distinct query on Postgres
|
||||
return frappe.get_list(
|
||||
"Purchase Receipt",
|
||||
filters=pr_filters,
|
||||
fields=["name"],
|
||||
group_by="name",
|
||||
order_by="name",
|
||||
limit_start=start,
|
||||
limit_page_length=page_len,
|
||||
as_list=True,
|
||||
)
|
||||
return frappe.db.sql(query, filters)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters):
|
||||
pi_filters = [["docstatus", "=", 1], ["name", "like", f"%{txt}%"]]
|
||||
query = """
|
||||
select pi.name
|
||||
from `tabPurchase Invoice` pi, `tabPurchase Invoice Item` piitem
|
||||
where pi.docstatus = 1 and piitem.parent = pi.name
|
||||
and pi.name like {txt}""".format(txt=frappe.db.escape(f"%{txt}%"))
|
||||
|
||||
if filters and filters.get("item_code"):
|
||||
if frappe.has_permission("Purchase Invoice", "read"):
|
||||
# one indexed join, deduped by group_by below
|
||||
pi_filters.append(["Purchase Invoice Item", "item_code", "=", filters.get("item_code")])
|
||||
else:
|
||||
# a select-only caller may use this picker but may not filter on a child table, so resolve
|
||||
# the parents separately rather than losing the filter to a PermissionError
|
||||
parents = frappe.get_all(
|
||||
"Purchase Invoice Item",
|
||||
filters={"item_code": filters.get("item_code"), "parenttype": "Purchase Invoice"},
|
||||
pluck="parent",
|
||||
distinct=True,
|
||||
)
|
||||
pi_filters.append(["name", "in", parents or [""]])
|
||||
query += " and piitem.item_code = {item_code}".format(
|
||||
item_code=frappe.db.escape(filters.get("item_code"))
|
||||
)
|
||||
|
||||
# group_by, not distinct: it dedupes the child join just the same, and frappe drops ORDER BY
|
||||
# from a distinct query on Postgres
|
||||
return frappe.get_list(
|
||||
"Purchase Invoice",
|
||||
filters=pi_filters,
|
||||
fields=["name"],
|
||||
group_by="name",
|
||||
order_by="name",
|
||||
limit_start=start,
|
||||
limit_page_length=page_len,
|
||||
as_list=True,
|
||||
)
|
||||
return frappe.db.sql(query, filters)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -1015,28 +952,9 @@ def get_fields(doctype, fields=None):
|
||||
def get_payment_terms_for_references(doctype, txt, searchfield, start, page_len, filters) -> list:
|
||||
terms = []
|
||||
if filters:
|
||||
reference = filters.get("reference")
|
||||
if not reference:
|
||||
return terms
|
||||
|
||||
# only a plain name names one document: a filter operator (["like", "%"], ["!=", ""]) would
|
||||
# widen this past the document the caller named, and past the one being authorised below
|
||||
if not isinstance(reference, str):
|
||||
frappe.throw(_("Invalid reference"), frappe.PermissionError)
|
||||
|
||||
# Payment Schedule is a child table with no permissions of its own, so its parent decides access.
|
||||
# Prefer the caller's reference type: the lookup cannot tell same-named parents of two types apart.
|
||||
parenttype = filters.get("reference_doctype") or frappe.db.get_value(
|
||||
"Payment Schedule", {"parent": reference}, "parenttype"
|
||||
)
|
||||
if not parenttype:
|
||||
return terms
|
||||
|
||||
frappe.has_permission(parenttype, doc=reference, throw=True)
|
||||
|
||||
terms = frappe.db.get_all(
|
||||
"Payment Schedule",
|
||||
filters={"parent": reference, "parenttype": parenttype},
|
||||
filters={"parent": filters.get("reference")},
|
||||
fields=["payment_term"],
|
||||
limit=page_len,
|
||||
as_list=1,
|
||||
@@ -1047,30 +965,6 @@ def get_payment_terms_for_references(doctype, txt, searchfield, start, page_len,
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_filtered_child_rows(doctype, txt, searchfield, start, page_len, filters) -> list:
|
||||
parent = filters.get("parent") if filters else None
|
||||
|
||||
# a plain name, never a filter operator: ["like", "%"] here would span parents, and only one
|
||||
# of them would be the document authorised below
|
||||
if not parent or not isinstance(parent, str):
|
||||
frappe.throw(_("Parent document is required to search child rows"), frappe.PermissionError)
|
||||
|
||||
# `doctype` is caller supplied, so it has to be a child table before it is worth checking:
|
||||
# any other doctype would put the caller's filters on a table this query never meant to read
|
||||
if not frappe.get_meta(doctype).istable:
|
||||
frappe.throw(_("{0} is not a child table").format(doctype), frappe.PermissionError)
|
||||
|
||||
# child tables carry no permissions of their own, so the parent decides access. Read the parent
|
||||
# type off the rows, not off `filters`, so the document authorised is the one returned.
|
||||
parenttype = frappe.db.get_value(doctype, {"parent": parent}, "parenttype")
|
||||
|
||||
if not parenttype or not frappe.db.exists(parenttype, parent):
|
||||
return []
|
||||
|
||||
frappe.has_permission(doctype, parent_doctype=parenttype, throw=True)
|
||||
|
||||
# and on the parent record itself, so that User Permissions still apply
|
||||
frappe.has_permission(parenttype, doc=parent, throw=True)
|
||||
|
||||
table = frappe.qb.DocType(doctype)
|
||||
query = (
|
||||
frappe.get_query(table, filters=filters)
|
||||
@@ -1095,11 +989,7 @@ def get_filtered_child_rows(doctype, txt, searchfield, start, page_len, filters)
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_item_uom_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
if frappe.db.get_single_value("Stock Settings", "allow_uom_with_conversion_rate_defined_in_item"):
|
||||
item_code = filters.get("item_code")
|
||||
if not item_code or not frappe.get_list("Item", filters=[["name", "=", item_code]], pluck="name"):
|
||||
return []
|
||||
|
||||
query_filters = {"parent": item_code, "parenttype": "Item"}
|
||||
query_filters = {"parent": filters.get("item_code")}
|
||||
|
||||
if txt:
|
||||
query_filters["uom"] = ["like", f"%{txt}%"]
|
||||
@@ -1114,7 +1004,7 @@ def get_item_uom_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
return frappe.get_list(
|
||||
return frappe.get_all(
|
||||
"UOM",
|
||||
filters={"name": ["like", f"%{txt}%"], "enabled": 1},
|
||||
fields=["name"],
|
||||
|
||||
@@ -11,7 +11,7 @@ from frappe.utils import cint, flt, format_datetime, get_datetime
|
||||
import erpnext
|
||||
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
|
||||
from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
|
||||
from erpnext.stock.utils import _get_incoming_rate, get_combine_datetime, get_valuation_method, getdate
|
||||
from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_valuation_method, getdate
|
||||
|
||||
|
||||
class StockOverReturnError(frappe.ValidationError):
|
||||
@@ -736,7 +736,7 @@ def get_rate_for_return(
|
||||
rate = frappe.db.get_value(f"{voucher_type} Item", voucher_detail_no, "incoming_rate")
|
||||
|
||||
if not rate and sle:
|
||||
rate = _get_incoming_rate(
|
||||
rate = get_incoming_rate(
|
||||
{
|
||||
"item_code": sle.item_code,
|
||||
"warehouse": sle.warehouse,
|
||||
@@ -1224,23 +1224,8 @@ def get_available_serial_nos(serial_nos, warehouse):
|
||||
)
|
||||
|
||||
|
||||
# the only doctypes this endpoint is called for; it reaches get_value()/get_all() as the doctype itself
|
||||
RETURNABLE_INVOICE_DOCTYPES = ("Sales Invoice", "POS Invoice")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_payment_data(invoice):
|
||||
# `invoice` may be either a Sales Invoice or a POS Invoice — both share the Sales Invoice
|
||||
# Payment child table — so resolve which one it is before authorising rather than guessing.
|
||||
parenttype = frappe.db.get_value("Sales Invoice Payment", {"parent": invoice}, "parenttype")
|
||||
if not parenttype:
|
||||
return []
|
||||
|
||||
if parenttype not in RETURNABLE_INVOICE_DOCTYPES:
|
||||
frappe.throw(_("Invalid document type"), frappe.PermissionError)
|
||||
|
||||
frappe.has_permission(parenttype, doc=invoice, throw=True)
|
||||
|
||||
payment = frappe.db.get_all("Sales Invoice Payment", {"parent": invoice}, ["mode_of_payment", "amount"])
|
||||
return payment
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from erpnext.controllers.sales_and_purchase_return import get_rate_for_return, i
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.stock.doctype.item.item import set_item_default
|
||||
from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor
|
||||
from erpnext.stock.utils import _get_incoming_rate, get_combine_datetime, get_valuation_method
|
||||
from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_valuation_method
|
||||
|
||||
|
||||
class SellingController(StockController):
|
||||
@@ -565,7 +565,7 @@ class SellingController(StockController):
|
||||
or self.is_internal_transfer()
|
||||
or (get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return"))
|
||||
):
|
||||
d.incoming_rate = _get_incoming_rate(
|
||||
d.incoming_rate = get_incoming_rate(
|
||||
{
|
||||
"item_code": d.item_code,
|
||||
"warehouse": d.warehouse,
|
||||
@@ -896,77 +896,6 @@ class SellingController(StockController):
|
||||
title=_("Not Allowed"),
|
||||
)
|
||||
|
||||
def validate_produced_serial_nos_against_reservation(self):
|
||||
"""Restrict delivery to the serial nos reserved for a Sales Order Item with ensure delivery by serial no."""
|
||||
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||
get_sre_reserved_serial_nos_for_voucher_detail_nos,
|
||||
)
|
||||
|
||||
if self.is_return or not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"):
|
||||
return
|
||||
|
||||
so_field = "sales_order" if self.doctype == "Sales Invoice" else "against_sales_order"
|
||||
rows = [d for d in self.items if d.get(so_field) and d.so_detail]
|
||||
if not rows:
|
||||
return
|
||||
|
||||
flagged_so_details = frappe.get_all(
|
||||
"Sales Order Item",
|
||||
filters={
|
||||
"name": ("in", [d.so_detail for d in rows]),
|
||||
"ensure_delivery_based_on_produced_serial_no": 1,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
rows = [d for d in rows if d.so_detail in flagged_so_details]
|
||||
if not rows:
|
||||
return
|
||||
|
||||
reserved_serial_nos = get_sre_reserved_serial_nos_for_voucher_detail_nos(
|
||||
"Sales Order", flagged_so_details
|
||||
)
|
||||
bundle_map = dict(
|
||||
frappe.get_all(
|
||||
rows[0].doctype,
|
||||
filters={"name": ("in", [d.name for d in rows])},
|
||||
fields=["name", "serial_and_batch_bundle"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
bundle_serial_nos = frappe._dict()
|
||||
if bundles := [b for b in bundle_map.values() if b]:
|
||||
for entry in frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": ("in", bundles), "serial_no": ("is", "set")},
|
||||
fields=["parent", "serial_no"],
|
||||
):
|
||||
bundle_serial_nos.setdefault(entry.parent, []).append(entry.serial_no)
|
||||
|
||||
for row in rows:
|
||||
if not reserved_serial_nos.get(row.so_detail):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Delivery of Item {1} is ensured by produced Serial No, but no Serial No is reserved against Sales Order {2}. Reserve the produced Serial Nos from the Sales Order."
|
||||
).format(row.idx, frappe.bold(row.item_code), frappe.bold(row.get(so_field))),
|
||||
title=_("Serial No Not Reserved"),
|
||||
)
|
||||
|
||||
bundle = bundle_map.get(row.name)
|
||||
serial_nos = bundle_serial_nos.get(bundle, []) if bundle else get_serial_nos(row.serial_no)
|
||||
if invalid_serial_nos := [
|
||||
sn for sn in serial_nos if sn not in reserved_serial_nos[row.so_detail]
|
||||
]:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Serial No {1} is not reserved against Sales Order {2}. Deliver only the Serial Nos produced and reserved for it."
|
||||
).format(
|
||||
row.idx, frappe.bold(", ".join(invalid_serial_nos)), frappe.bold(row.get(so_field))
|
||||
),
|
||||
title=_("Serial No Not Reserved"),
|
||||
)
|
||||
|
||||
def update_stock_reservation_entries(self) -> None:
|
||||
"""Updates Delivered Qty in Stock Reservation Entries."""
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle impor
|
||||
)
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation, get_serial_nos_from_bundle
|
||||
from erpnext.stock.utils import _get_incoming_rate
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
|
||||
|
||||
class SubcontractingController(StockController):
|
||||
@@ -82,7 +82,7 @@ class SubcontractingController(StockController):
|
||||
}
|
||||
)
|
||||
|
||||
rate = _get_incoming_rate(kwargs)
|
||||
rate = get_incoming_rate(kwargs)
|
||||
precision = frappe.get_precision("Subcontracting Receipt Supplied Item", "rate")
|
||||
if flt(rate, precision) != flt(row.rate, precision):
|
||||
row.rate = rate
|
||||
@@ -764,7 +764,7 @@ class SubcontractingController(StockController):
|
||||
args["batch_no"] = rm_obj.batch_no
|
||||
args["serial_no"] = rm_obj.serial_no
|
||||
|
||||
rm_obj.rate = _get_incoming_rate(args)
|
||||
rm_obj.rate = get_incoming_rate(args)
|
||||
|
||||
def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
|
||||
key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field))
|
||||
@@ -1148,7 +1148,7 @@ class SubcontractingController(StockController):
|
||||
and reset_outgoing_rate
|
||||
and frappe.get_cached_value("Item", item.rm_item_code, "is_stock_item")
|
||||
):
|
||||
rate = _get_incoming_rate(
|
||||
rate = get_incoming_rate(
|
||||
{
|
||||
"item_code": item.rm_item_code,
|
||||
"warehouse": self.supplier_warehouse,
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
|
||||
class TestPriceListValidation(FrappeTestCase):
|
||||
def create_price_list(self, selling=0, buying=0, enabled=1):
|
||||
return (
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Price List",
|
||||
"price_list_name": frappe.generate_hash(length=10),
|
||||
"currency": "INR",
|
||||
"selling": selling,
|
||||
"buying": buying,
|
||||
"enabled": enabled,
|
||||
}
|
||||
)
|
||||
.insert()
|
||||
.name
|
||||
)
|
||||
|
||||
def test_selling_transaction_should_reject_a_buying_price_list(self):
|
||||
invoice = create_sales_invoice(do_not_save=1)
|
||||
invoice.selling_price_list = self.create_price_list(buying=1)
|
||||
|
||||
with self.assertRaisesRegex(frappe.ValidationError, "selling transaction"):
|
||||
invoice.save()
|
||||
|
||||
def test_buying_transaction_should_reject_a_selling_price_list(self):
|
||||
invoice = make_purchase_invoice(do_not_save=1)
|
||||
invoice.buying_price_list = self.create_price_list(selling=1)
|
||||
|
||||
with self.assertRaisesRegex(frappe.ValidationError, "buying transaction"):
|
||||
invoice.save()
|
||||
|
||||
def test_a_price_list_for_both_sides_should_be_accepted(self):
|
||||
price_list = self.create_price_list(selling=1, buying=1)
|
||||
|
||||
invoice = create_sales_invoice(do_not_save=1)
|
||||
invoice.selling_price_list = price_list
|
||||
invoice.save()
|
||||
|
||||
self.assertEqual(invoice.selling_price_list, price_list)
|
||||
|
||||
def test_a_missing_price_list_should_report_rather_than_crash(self):
|
||||
invoice = create_sales_invoice(do_not_save=1)
|
||||
invoice.selling_price_list = frappe.generate_hash(length=10)
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
invoice.validate_price_list()
|
||||
|
||||
def test_internal_transfer_should_keep_the_outward_price_list(self):
|
||||
"""The inward document of an internal transfer takes the price list of the outward one, which
|
||||
is flagged for the opposite side."""
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
|
||||
make_purchase_receipt,
|
||||
prepare_data_for_internal_transfer,
|
||||
)
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
company = "_Test Company with perpetual inventory"
|
||||
selling_only = self.create_price_list(selling=1)
|
||||
make_purchase_receipt(warehouse="Stores - TCP1", company=company)
|
||||
|
||||
delivery_note = create_delivery_note(
|
||||
company=company,
|
||||
customer="_Test Internal Customer 2",
|
||||
cost_center="Main - TCP1",
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
warehouse="Stores - TCP1",
|
||||
target_warehouse=create_warehouse("_Test Transit For Price List", company=company),
|
||||
do_not_submit=1,
|
||||
)
|
||||
delivery_note.selling_price_list = selling_only
|
||||
delivery_note.save()
|
||||
delivery_note.submit()
|
||||
|
||||
receipt = make_inter_company_purchase_receipt(delivery_note.name)
|
||||
receipt.items[0].warehouse = "Stores - TCP1"
|
||||
receipt.save()
|
||||
|
||||
self.assertEqual(receipt.buying_price_list, selling_only)
|
||||
|
||||
def test_disabled_price_list_should_still_report_as_disabled(self):
|
||||
invoice = create_sales_invoice(do_not_save=1)
|
||||
invoice.selling_price_list = self.create_price_list(selling=1, enabled=0)
|
||||
|
||||
with self.assertRaisesRegex(frappe.ValidationError, "is disabled"):
|
||||
invoice.save()
|
||||
@@ -517,7 +517,7 @@
|
||||
"idx": 5,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2025-06-26 11:02:01.158901",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Lead",
|
||||
@@ -574,10 +574,6 @@
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales User"
|
||||
},
|
||||
{
|
||||
"role": "Support Team",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -474,14 +474,6 @@ def get_lead_details(lead, posting_date=None, company=None, doctype=None):
|
||||
def make_lead_from_communication(communication: str, ignore_communication_links: bool = False):
|
||||
"""raise a issue from email"""
|
||||
|
||||
# `communication` is caller-supplied. Communication grants read to `All` only for the owner and
|
||||
# carries a has_permission hook, so doc= is what decides access.
|
||||
frappe.has_permission("Communication", doc=communication, throw=True)
|
||||
|
||||
# the insert path checks `create` itself, but the path that reuses an existing Lead required
|
||||
# nothing, so it returned a Lead name to callers with no access to Leads at all
|
||||
frappe.has_permission("Lead", ptype="create", throw=True)
|
||||
|
||||
doc = frappe.get_doc("Communication", communication)
|
||||
lead_name = None
|
||||
if doc.sender:
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2026-09-16 12:00:00.000000",
|
||||
"modified": "2025-12-17 12:09:34.687368",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Market Segment",
|
||||
@@ -36,10 +36,6 @@
|
||||
"role": "Sales Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Sales Master Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
||||
@@ -553,14 +553,8 @@ def make_opportunity_from_communication(
|
||||
):
|
||||
from erpnext.crm.doctype.lead.lead import make_lead_from_communication
|
||||
|
||||
# `communication` is caller-supplied. Communication grants read to `All` only for the owner and
|
||||
# carries a has_permission hook, so doc= is what decides access.
|
||||
frappe.has_permission("Communication", doc=communication, throw=True)
|
||||
|
||||
doc = frappe.get_doc("Communication", communication)
|
||||
|
||||
# make_lead_from_communication() carries its own check, but it is skipped entirely when the
|
||||
# email already references a Lead, so this cannot rely on it.
|
||||
lead = doc.reference_name if doc.reference_doctype == "Lead" else None
|
||||
if not lead:
|
||||
lead = make_lead_from_communication(communication, ignore_communication_links=True)
|
||||
|
||||
@@ -150,10 +150,6 @@ def link_open_events(ref_doctype, ref_docname, doc):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_open_activities(ref_doctype, ref_docname):
|
||||
# both arguments are caller-supplied and the ToDo/Event rows are read with get_all, so the
|
||||
# referenced document decides who may see its activities. doc= applies User Permissions.
|
||||
frappe.has_permission(ref_doctype, doc=ref_docname, throw=True)
|
||||
|
||||
tasks = get_open_todos(ref_doctype, ref_docname)
|
||||
events = get_open_events(ref_doctype, ref_docname)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import frappe
|
||||
@@ -79,48 +78,8 @@ class CodeList(Document):
|
||||
self.url = getattr(root.find(".//Identification/LocationUri"), "text", None)
|
||||
|
||||
|
||||
def _version_key(version: str | None) -> list:
|
||||
"""Natural sort key for the version formats publishers use: integers and ISO dates.
|
||||
|
||||
Orders 3 < 10 (which a lexical sort gets wrong) and 2020-01-01 < 2020-11-05.
|
||||
"""
|
||||
return [int(p) if p.isdigit() else p for p in re.split(r"(\d+)", version or "")]
|
||||
|
||||
|
||||
@frappe.request_cache
|
||||
def resolve_code_list(code_list: str) -> str | None:
|
||||
"""Return the Code List for a document name or a canonical URI.
|
||||
|
||||
Code Lists are named after their CanonicalVersionUri, so one canonical URI can
|
||||
map to several documents, one per version. An exact document name takes
|
||||
precedence, which lets a caller request a specific version; a canonical URI
|
||||
resolves to the latest version available.
|
||||
"""
|
||||
if frappe.db.exists("Code List", code_list):
|
||||
return code_list
|
||||
|
||||
candidates = frappe.get_all(
|
||||
"Code List",
|
||||
filters={"canonical_uri": code_list},
|
||||
fields=["name", "version"],
|
||||
)
|
||||
if not candidates:
|
||||
return None
|
||||
|
||||
# ponytail: assumes one publisher sticks to one version format. An integer and an
|
||||
# ISO date under the same canonical URI compare numerically (3 < 2020), so the date
|
||||
# would win; import the genericode ValidityDate and sort on that if it ever happens.
|
||||
return max(candidates, key=lambda cl: _version_key(cl.version)).name
|
||||
|
||||
|
||||
def get_codes_for(code_list: str, doctype: str, name: str) -> tuple[str]:
|
||||
"""Return the common code for a given record.
|
||||
|
||||
`code_list` may be a Code List name or a canonical URI (latest version wins).
|
||||
"""
|
||||
if not (code_list := resolve_code_list(code_list)):
|
||||
return ()
|
||||
|
||||
"""Return the common code for a given record"""
|
||||
CommonCode = frappe.qb.DocType("Common Code")
|
||||
DynamicLink = frappe.qb.DocType("Dynamic Link")
|
||||
|
||||
@@ -142,13 +101,7 @@ def get_codes_for(code_list: str, doctype: str, name: str) -> tuple[str]:
|
||||
|
||||
|
||||
def get_docnames_for(code_list: str, doctype: str, code: str) -> tuple[str]:
|
||||
"""Return the record name for a given common code.
|
||||
|
||||
`code_list` may be a Code List name or a canonical URI (latest version wins).
|
||||
"""
|
||||
if not (code_list := resolve_code_list(code_list)):
|
||||
return ()
|
||||
|
||||
"""Return the record name for a given common code"""
|
||||
CommonCode = frappe.qb.DocType("Common Code")
|
||||
DynamicLink = frappe.qb.DocType("Dynamic Link")
|
||||
|
||||
@@ -170,12 +123,6 @@ def get_docnames_for(code_list: str, doctype: str, code: str) -> tuple[str]:
|
||||
|
||||
|
||||
def get_default_code(code_list: str) -> str | None:
|
||||
"""Return the default common code for a given code list.
|
||||
|
||||
`code_list` may be a Code List name or a canonical URI (latest version wins).
|
||||
"""
|
||||
if not (code_list := resolve_code_list(code_list)):
|
||||
return None
|
||||
|
||||
"""Return the default common code for a given code list"""
|
||||
code_id = frappe.db.get_value("Code List", code_list, "default_common_code")
|
||||
return frappe.db.get_value("Common Code", code_id, "common_code") if code_id else None
|
||||
|
||||
@@ -20,11 +20,8 @@ class CodeListSelectionMismatchError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def import_genericode():
|
||||
# check before save(), which only runs after the XML is fetched and parsed; denies exactly who save() would, sooner
|
||||
frappe.has_permission("Code List", "create", throw=True)
|
||||
|
||||
try:
|
||||
content, file_name = get_uploaded_genericode_file()
|
||||
|
||||
@@ -164,7 +161,7 @@ def parse_genericode_content(content: bytes):
|
||||
return etree.fromstring(content, parser=parser)
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def process_genericode_import(
|
||||
code_list_name: str,
|
||||
file_name: str,
|
||||
@@ -175,11 +172,6 @@ def process_genericode_import(
|
||||
):
|
||||
from erpnext.edi.doctype.common_code.common_code import import_genericode
|
||||
|
||||
# Same reasoning as above: common_code.save() enforces this per document, but only after the
|
||||
# file has been read and its XML parsed and queried.
|
||||
frappe.has_permission("Common Code", "create", throw=True)
|
||||
frappe.has_permission("Code List", doc=code_list_name, throw=True)
|
||||
|
||||
column_map = {"code": code_column, "title": title_column, "description": description_column}
|
||||
|
||||
return import_genericode(code_list_name, file_name, column_map, json.loads(filters) if filters else None)
|
||||
|
||||
@@ -1,83 +1,9 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.edi.doctype.code_list.code_list import (
|
||||
_version_key,
|
||||
get_codes_for,
|
||||
get_default_code,
|
||||
get_docnames_for,
|
||||
resolve_code_list,
|
||||
)
|
||||
|
||||
CANONICAL_URI = "urn:test:erpnext:codeliste:resolve"
|
||||
OLD_VERSION = f"{CANONICAL_URI}:3"
|
||||
NEW_VERSION = f"{CANONICAL_URI}:10"
|
||||
UNKNOWN_URI = "urn:test:erpnext:codeliste:missing"
|
||||
|
||||
|
||||
class TestCodeList(FrappeTestCase):
|
||||
def setUp(self):
|
||||
"""Create two versions of one code list. FrappeTestCase rolls back once per class, so the inserts are guarded."""
|
||||
for name, version in ((OLD_VERSION, "3"), (NEW_VERSION, "10")):
|
||||
if not frappe.db.exists("Code List", name):
|
||||
frappe.get_doc(
|
||||
doctype="Code List",
|
||||
name=name,
|
||||
title=name,
|
||||
canonical_uri=CANONICAL_URI,
|
||||
version=version,
|
||||
).insert()
|
||||
|
||||
default_code = frappe.get_doc(
|
||||
doctype="Common Code",
|
||||
title="Test Default",
|
||||
common_code="XYZ",
|
||||
code_list=NEW_VERSION,
|
||||
).insert()
|
||||
frappe.db.set_value("Code List", NEW_VERSION, "default_common_code", default_code.name)
|
||||
|
||||
# resolution is request-cached, so fixtures must not be masked by earlier lookups
|
||||
frappe.local.request_cache.clear()
|
||||
|
||||
def test_version_key_orders_integers_and_iso_dates(self):
|
||||
"""Integer and ISO date versions must both order correctly, unlike a lexical sort."""
|
||||
self.assertEqual(sorted(["10", "3", None, "9"], key=_version_key), [None, "3", "9", "10"])
|
||||
self.assertEqual(
|
||||
sorted(["2020-11-05", "2019-12-31", "2020-01-01"], key=_version_key),
|
||||
["2019-12-31", "2020-01-01", "2020-11-05"],
|
||||
)
|
||||
|
||||
def test_canonical_uri_resolves_to_latest_version(self):
|
||||
self.assertEqual(resolve_code_list(CANONICAL_URI), NEW_VERSION)
|
||||
|
||||
def test_name_resolves_to_itself(self):
|
||||
"""Passing a version-specific name must return that version, not the latest one."""
|
||||
self.assertEqual(resolve_code_list(OLD_VERSION), OLD_VERSION)
|
||||
|
||||
def test_name_takes_precedence_over_canonical_uri(self):
|
||||
"""A document named like a canonical URI must not redirect to another version."""
|
||||
frappe.get_doc(
|
||||
doctype="Code List",
|
||||
name=CANONICAL_URI,
|
||||
title=CANONICAL_URI,
|
||||
canonical_uri=CANONICAL_URI,
|
||||
version="1",
|
||||
).insert()
|
||||
frappe.local.request_cache.clear()
|
||||
|
||||
self.assertEqual(resolve_code_list(CANONICAL_URI), CANONICAL_URI)
|
||||
|
||||
def test_unknown_uri_resolves_to_none(self):
|
||||
self.assertIsNone(resolve_code_list(UNKNOWN_URI))
|
||||
|
||||
def test_lookups_are_empty_for_unknown_code_list(self):
|
||||
"""An unresolved code list must not fall through to an unfiltered query."""
|
||||
self.assertEqual(get_codes_for(UNKNOWN_URI, "UOM", "Nos"), ())
|
||||
self.assertEqual(get_docnames_for(UNKNOWN_URI, "UOM", "XYZ"), ())
|
||||
self.assertIsNone(get_default_code(UNKNOWN_URI))
|
||||
|
||||
def test_default_code_follows_latest_version(self):
|
||||
self.assertEqual(get_default_code(CANONICAL_URI), "XYZ")
|
||||
pass
|
||||
|
||||
@@ -37,12 +37,8 @@ class PlaidSettings(Document):
|
||||
return plaid.get_link_token()
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def get_plaid_configuration():
|
||||
# Returns plaid_env and a freshly minted Plaid link_token. Plaid Settings is a System-Manager-only
|
||||
# single doctype and every caller reaches this from its own form, so that is the boundary.
|
||||
frappe.has_permission("Plaid Settings", throw=True)
|
||||
|
||||
if frappe.db.get_single_value("Plaid Settings", "enabled"):
|
||||
plaid_settings = frappe.get_single("Plaid Settings")
|
||||
return {
|
||||
@@ -56,8 +52,6 @@ def get_plaid_configuration():
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_institution(token, response):
|
||||
frappe.has_permission("Plaid Settings", throw=True)
|
||||
|
||||
response = json.loads(response)
|
||||
|
||||
plaid = PlaidConnector()
|
||||
@@ -86,8 +80,6 @@ def add_institution(token, response):
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_bank_accounts(response, bank, company):
|
||||
frappe.has_permission("Plaid Settings", throw=True)
|
||||
|
||||
try:
|
||||
response = json.loads(response)
|
||||
except TypeError:
|
||||
@@ -327,10 +319,8 @@ def automatic_synchronization():
|
||||
enqueue_synchronization()
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def enqueue_synchronization():
|
||||
frappe.has_permission("Plaid Settings", throw=True)
|
||||
|
||||
plaid_accounts = frappe.get_all(
|
||||
"Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"]
|
||||
)
|
||||
@@ -343,12 +333,8 @@ def enqueue_synchronization():
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def get_link_token_for_update(access_token):
|
||||
# `access_token` is caller-supplied and is used to mint a link token at Plaid, so this creates
|
||||
# state at the provider even though it writes nothing here.
|
||||
frappe.has_permission("Plaid Settings", throw=True)
|
||||
|
||||
plaid = PlaidConnector(access_token)
|
||||
return plaid.get_link_token(update_mode=True)
|
||||
|
||||
@@ -367,10 +353,8 @@ def get_company(bank_account_name):
|
||||
frappe.throw(_("Could not detect the Company for updating Bank Accounts"))
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def update_bank_account_ids(response):
|
||||
frappe.has_permission("Plaid Settings", throw=True)
|
||||
|
||||
data = json.loads(response)
|
||||
institution_name = data["institution"]["name"]
|
||||
bank = frappe.get_doc("Bank", institution_name).as_dict()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user