mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-14 15:11:52 +00:00
Merge branch 'version-15-hotfix' into mergify/bp/version-15-hotfix/pr-55627
This commit is contained in:
10
.greptile/config.json
Normal file
10
.greptile/config.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"disabledLabels": [
|
||||
"conflicts"
|
||||
],
|
||||
"context": {
|
||||
"repos": [
|
||||
"frappe/frappe"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.108.3"
|
||||
__version__ = "15.112.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -579,10 +579,12 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
||||
@frappe.whitelist()
|
||||
def merge_account(old, new):
|
||||
_ensure_idle_system()
|
||||
# Validate properties before merging
|
||||
new_account = frappe.get_cached_doc("Account", new)
|
||||
old_account = frappe.get_cached_doc("Account", old)
|
||||
|
||||
new_account.check_permission("write")
|
||||
old_account.check_permission("write")
|
||||
|
||||
if not new_account:
|
||||
throw(_("Account {0} does not exist").format(new))
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
"acc_frozen_upto",
|
||||
"ignore_account_closing_balance",
|
||||
"use_legacy_controller_for_pcv",
|
||||
"pcv_job_timeout",
|
||||
"column_break_25",
|
||||
"frozen_accounts_modifier",
|
||||
"tab_break_dpet",
|
||||
@@ -651,6 +652,14 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Party Balance"
|
||||
},
|
||||
{
|
||||
"default": "3600",
|
||||
"depends_on": "eval: !doc.use_legacy_controller_for_pcv",
|
||||
"description": "Timeout (in seconds) for each background job enqueued by Process Period Closing Voucher",
|
||||
"fieldname": "pcv_job_timeout",
|
||||
"fieldtype": "Int",
|
||||
"label": "PCV Job Timeout (seconds)"
|
||||
},
|
||||
{
|
||||
"default": "30, 60, 90, 120",
|
||||
"fieldname": "default_ageing_range",
|
||||
@@ -663,7 +672,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-05-18 12:16:33.679345",
|
||||
"modified": "2026-06-24 12:59:41.868865",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -60,6 +60,7 @@ class AccountsSettings(Document):
|
||||
merge_similar_account_heads: DF.Check
|
||||
over_billing_allowance: DF.Currency
|
||||
post_change_gl_entries: DF.Check
|
||||
pcv_job_timeout: DF.Int
|
||||
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"]
|
||||
receivable_payable_remarks_length: DF.Int
|
||||
reconciliation_queue_size: DF.Int
|
||||
|
||||
@@ -90,7 +90,14 @@ class BankClearance(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_clearance_date(self):
|
||||
clearance_date_updated = False
|
||||
payment_docs = []
|
||||
for d in self.get("payment_entries"):
|
||||
if d.payment_document not in payment_docs:
|
||||
payment_docs.append(d.payment_document)
|
||||
|
||||
for doctype in payment_docs:
|
||||
frappe.has_permission(doctype, "write", throw=True)
|
||||
|
||||
for d in self.get("payment_entries"):
|
||||
if d.clearance_date:
|
||||
if not d.payment_document:
|
||||
|
||||
@@ -9,6 +9,13 @@ cur_frm.add_fetch("bank", "swift_number", "swift_number");
|
||||
|
||||
frappe.ui.form.on("Bank Guarantee", {
|
||||
setup: function (frm) {
|
||||
frm.set_query("reference_doctype", function () {
|
||||
return {
|
||||
filters: {
|
||||
name: ["in", ["Sales Order", "Purchase Order"]],
|
||||
},
|
||||
};
|
||||
});
|
||||
frm.set_query("bank_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"autoname": "ACC-BG-.YYYY.-.#####",
|
||||
"creation": "2016-12-17 10:43:35.731631",
|
||||
"doctype": "DocType",
|
||||
@@ -50,8 +51,7 @@
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Link",
|
||||
"label": "Reference Document Type",
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_docname",
|
||||
@@ -60,14 +60,14 @@
|
||||
"options": "reference_doctype"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.bg_type == \"Receiving\"",
|
||||
"depends_on": "eval: doc.reference_doctype == \"Sales Order\"",
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"label": "Customer",
|
||||
"options": "Customer"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.bg_type == \"Providing\"",
|
||||
"depends_on": "eval: doc.reference_doctype == \"Purchase Order\"",
|
||||
"fieldname": "supplier",
|
||||
"fieldtype": "Link",
|
||||
"label": "Supplier",
|
||||
@@ -217,11 +217,11 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-09-26 00:38:17.584694",
|
||||
"modified": "2026-05-25 18:12:10.768835",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Guarantee",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
@@ -121,7 +121,7 @@ class BisectAccountingStatements(Document):
|
||||
|
||||
cur_node.save()
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
def build_tree(self):
|
||||
frappe.db.delete("Bisect Nodes")
|
||||
|
||||
|
||||
@@ -103,8 +103,8 @@ class Budget(Document):
|
||||
elif account_details.report_type != "Profit and Loss":
|
||||
frappe.throw(
|
||||
_(
|
||||
"Budget cannot be assigned against {0}, as it's not an Income or Expense account"
|
||||
).format(d.account)
|
||||
"Budget cannot be assigned against {0}, as its Root Type is not of Income or Expense"
|
||||
).format(self.account)
|
||||
)
|
||||
|
||||
if d.account in account_list:
|
||||
@@ -425,11 +425,11 @@ def get_ordered_amount(args):
|
||||
|
||||
|
||||
def get_other_condition(args, for_doc):
|
||||
condition = "expense_account = '%s'" % (args.expense_account)
|
||||
condition = f"expense_account = {frappe.db.escape(args.expense_account)}"
|
||||
budget_against_field = args.get("budget_against_field")
|
||||
|
||||
if budget_against_field and args.get(budget_against_field):
|
||||
condition += f" and child.{budget_against_field} = '{args.get(budget_against_field)}'"
|
||||
condition += f" and child.{budget_against_field} = {frappe.db.escape(args.get(budget_against_field))}"
|
||||
|
||||
if args.get("fiscal_year"):
|
||||
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
|
||||
@@ -437,8 +437,7 @@ def get_other_condition(args, for_doc):
|
||||
"Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"]
|
||||
)
|
||||
|
||||
condition += f""" and parent.{date_field}
|
||||
between '{start_date}' and '{end_date}' """
|
||||
condition += f" and parent.{date_field} between {frappe.db.escape(str(start_date))} and {frappe.db.escape(str(end_date))}"
|
||||
|
||||
return condition
|
||||
|
||||
|
||||
@@ -6,12 +6,14 @@ frappe.provide("erpnext.cheque_print");
|
||||
frappe.ui.form.on("Cheque Print Template", {
|
||||
refresh: function (frm) {
|
||||
if (!frm.doc.__islocal) {
|
||||
frm.add_custom_button(
|
||||
frm.doc.has_print_format ? __("Update Print Format") : __("Create Print Format"),
|
||||
function () {
|
||||
erpnext.cheque_print.view_cheque_print(frm);
|
||||
}
|
||||
).addClass("btn-primary");
|
||||
if (frappe.user.has_role("System Manager")) {
|
||||
frm.add_custom_button(
|
||||
frm.doc.has_print_format ? __("Update Print Format") : __("Create Print Format"),
|
||||
function () {
|
||||
erpnext.cheque_print.view_cheque_print(frm);
|
||||
}
|
||||
).addClass("btn-primary");
|
||||
}
|
||||
|
||||
$(frm.fields_dict.cheque_print_preview.wrapper).empty();
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -48,6 +48,8 @@ class ChequePrintTemplate(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_or_update_cheque_print_format(template_name):
|
||||
frappe.only_for("System Manager")
|
||||
|
||||
if not frappe.db.exists("Print Format", template_name):
|
||||
cheque_print = frappe.new_doc("Print Format")
|
||||
cheque_print.update(
|
||||
|
||||
@@ -11,22 +11,28 @@ frappe.ui.form.on("Currency Exchange Settings", {
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r && r.message) {
|
||||
let result = [],
|
||||
params = {};
|
||||
if (frm.doc.service_provider == "exchangerate.host") {
|
||||
let result = ["result"];
|
||||
let params = {
|
||||
result = ["result"];
|
||||
params = {
|
||||
date: "{transaction_date}",
|
||||
from: "{from_currency}",
|
||||
to: "{to_currency}",
|
||||
};
|
||||
add_param(frm, r.message, params, result);
|
||||
} else if (["frankfurter.app", "frankfurter.dev"].includes(frm.doc.service_provider)) {
|
||||
let result = ["rates", "{to_currency}"];
|
||||
let params = {
|
||||
result = ["rates", "{to_currency}"];
|
||||
params = {
|
||||
base: "{from_currency}",
|
||||
symbols: "{to_currency}",
|
||||
};
|
||||
add_param(frm, r.message, params, result);
|
||||
} else if (frm.doc.service_provider == "frankfurter.dev - v2") {
|
||||
result = ["rate"];
|
||||
params = {
|
||||
date: "{transaction_date}",
|
||||
};
|
||||
}
|
||||
add_param(frm, r.message, params, result);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
"fieldname": "service_provider",
|
||||
"fieldtype": "Select",
|
||||
"label": "Service Provider",
|
||||
"options": "frankfurter.dev\nexchangerate.host\nCustom",
|
||||
"options": "frankfurter.dev\nexchangerate.host\nfrankfurter.dev - v2\nCustom",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -104,7 +104,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-25 13:03:41.896424",
|
||||
"modified": "2026-06-15 11:25:55.873110",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Currency Exchange Settings",
|
||||
@@ -121,24 +121,11 @@
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document):
|
||||
disabled: DF.Check
|
||||
req_params: DF.Table[CurrencyExchangeSettingsDetails]
|
||||
result_key: DF.Table[CurrencyExchangeSettingsResult]
|
||||
service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "Custom"]
|
||||
service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "frankfurter.dev - v2", "Custom"]
|
||||
url: DF.Data | None
|
||||
use_http: DF.Check
|
||||
# end: auto-generated types
|
||||
@@ -70,6 +70,14 @@ class CurrencyExchangeSettings(Document):
|
||||
self.append("req_params", {"key": "base", "value": "{from_currency}"})
|
||||
self.append("req_params", {"key": "symbols", "value": "{to_currency}"})
|
||||
|
||||
elif self.service_provider == "frankfurter.dev - v2":
|
||||
self.set("result_key", [])
|
||||
self.set("req_params", [])
|
||||
|
||||
self.api_endpoint = get_api_endpoint(self.service_provider, self.use_http)
|
||||
self.append("result_key", {"key": "rate"})
|
||||
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
|
||||
|
||||
def validate_parameters(self):
|
||||
params = {}
|
||||
for row in self.req_params:
|
||||
@@ -105,13 +113,20 @@ class CurrencyExchangeSettings(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
|
||||
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev", "frankfurter.app"]:
|
||||
if service_provider and service_provider in [
|
||||
"exchangerate.host",
|
||||
"frankfurter.dev",
|
||||
"frankfurter.app",
|
||||
"frankfurter.dev - v2",
|
||||
]:
|
||||
if service_provider == "exchangerate.host":
|
||||
api = "api.exchangerate.host/convert"
|
||||
elif service_provider == "frankfurter.app":
|
||||
api = "api.frankfurter.app/{transaction_date}"
|
||||
elif service_provider == "frankfurter.dev":
|
||||
api = "api.frankfurter.dev/v1/{transaction_date}"
|
||||
elif service_provider == "frankfurter.dev - v2":
|
||||
api = "api.frankfurter.dev/v2/rate/{from_currency}/{to_currency}"
|
||||
|
||||
protocol = "https://"
|
||||
if use_http:
|
||||
|
||||
@@ -619,6 +619,10 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
|
||||
def get_account_details(
|
||||
company, posting_date, account, party_type=None, party=None, rounding_loss_allowance: float | None = None
|
||||
):
|
||||
if not account:
|
||||
return
|
||||
frappe.has_permission("Account", doc=account, throw=True)
|
||||
|
||||
if not (company and posting_date):
|
||||
frappe.throw(_("Company and Posting Date is mandatory"))
|
||||
|
||||
|
||||
@@ -1184,7 +1184,11 @@ class JournalEntry(AccountsController):
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
def get_values(self):
|
||||
cond = f" and outstanding_amount <= {self.write_off_amount}" if flt(self.write_off_amount) > 0 else ""
|
||||
cond = (
|
||||
f" and outstanding_amount <= {flt(self.write_off_amount)}"
|
||||
if flt(self.write_off_amount) > 0
|
||||
else ""
|
||||
)
|
||||
|
||||
if self.write_off_based_on == "Accounts Receivable":
|
||||
return frappe.db.sql(
|
||||
|
||||
@@ -769,17 +769,21 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.set_paid_amount_based_on_received_amount = true;
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
|
||||
if (frm.doc.base_received_amount && frm.doc.source_exchange_rate) {
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
if (frm.doc.paid_amount && frm.doc.source_exchange_rate) {
|
||||
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
|
||||
// target exchange rate should always be same as source if both account currencies is same
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate);
|
||||
frm.set_value("received_amount", frm.doc.paid_amount);
|
||||
} else {
|
||||
frm.set_value(
|
||||
"paid_amount",
|
||||
flt(frm.doc.base_paid_amount) / flt(frm.doc.source_exchange_rate)
|
||||
);
|
||||
const target_rate =
|
||||
flt(frm.doc.target_exchange_rate) ||
|
||||
(company_currency == frm.doc.paid_to_account_currency ? 1 : 0);
|
||||
if (target_rate) {
|
||||
frm.set_value("received_amount", flt(frm.doc.base_received_amount) / target_rate);
|
||||
}
|
||||
}
|
||||
|
||||
// set_unallocated_amount is called by below method,
|
||||
@@ -795,18 +799,23 @@ frappe.ui.form.on("Payment Entry", {
|
||||
target_exchange_rate: function (frm) {
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
|
||||
if (frm.doc.base_paid_amount && frm.doc.target_exchange_rate) {
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
if (
|
||||
!frm.doc.source_exchange_rate &&
|
||||
frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency
|
||||
) {
|
||||
if (frm.doc.received_amount && frm.doc.target_exchange_rate) {
|
||||
frm.set_value(
|
||||
"base_received_amount",
|
||||
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate)
|
||||
);
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
|
||||
frm.set_value("paid_amount", frm.doc.received_amount);
|
||||
} else {
|
||||
frm.set_value(
|
||||
"received_amount",
|
||||
flt(frm.doc.base_received_amount) / flt(frm.doc.target_exchange_rate)
|
||||
);
|
||||
const source_rate =
|
||||
flt(frm.doc.source_exchange_rate) ||
|
||||
(company_currency == frm.doc.paid_from_account_currency ? 1 : 0);
|
||||
if (source_rate) {
|
||||
frm.set_value("paid_amount", flt(frm.doc.base_paid_amount) / source_rate);
|
||||
}
|
||||
}
|
||||
|
||||
// set_unallocated_amount is called by below method,
|
||||
|
||||
@@ -1197,9 +1197,9 @@ class PaymentEntry(AccountsController):
|
||||
continue
|
||||
|
||||
if tax.add_deduct_tax == "Add":
|
||||
included_taxes += tax.base_tax_amount
|
||||
included_taxes += flt(tax.base_tax_amount)
|
||||
else:
|
||||
included_taxes -= tax.base_tax_amount
|
||||
included_taxes -= flt(tax.base_tax_amount)
|
||||
|
||||
return included_taxes
|
||||
|
||||
@@ -2279,6 +2279,9 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
if args.get("party_type") == "Member":
|
||||
return
|
||||
|
||||
if args.get("party_type") and args.get("party"):
|
||||
frappe.has_permission(args["party_type"], "read", args["party"], throw=True)
|
||||
|
||||
if not args.get("get_outstanding_invoices") and not args.get("get_orders_to_be_billed"):
|
||||
args["get_outstanding_invoices"] = True
|
||||
|
||||
@@ -2788,7 +2791,8 @@ def get_reference_details(
|
||||
):
|
||||
total_amount = outstanding_amount = exchange_rate = account = None
|
||||
|
||||
ref_doc = frappe.get_doc(reference_doctype, reference_name)
|
||||
frappe.has_permission(reference_doctype, "read", reference_name, throw=True)
|
||||
ref_doc = frappe.get_lazy_doc(reference_doctype, reference_name)
|
||||
company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency(ref_doc.company)
|
||||
|
||||
# Only applies for Reverse Payment Entries
|
||||
@@ -3034,7 +3038,7 @@ def get_payment_entry(
|
||||
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
|
||||
)
|
||||
|
||||
pe.set_exchange_rate(ref_doc=doc)
|
||||
pe.set_exchange_rate()
|
||||
pe.set_amounts()
|
||||
|
||||
# If PE is created from PR directly, then no need to find open PRs for the references
|
||||
|
||||
@@ -537,6 +537,8 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
si.submit()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700)
|
||||
pe.source_exchange_rate = 50
|
||||
pe.set_amounts()
|
||||
pe.reference_no = si.name
|
||||
pe.reference_date = nowdate()
|
||||
|
||||
@@ -612,6 +614,8 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
pe = get_payment_entry(
|
||||
"Sales Invoice", si.name, party_amount=20, bank_account="_Test Bank - _TC", bank_amount=900
|
||||
)
|
||||
pe.source_exchange_rate = 50
|
||||
pe.set_amounts()
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = "2016-01-01"
|
||||
|
||||
@@ -1114,6 +1118,27 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
|
||||
self.assertEqual(gl_entries, expected_gl_entries)
|
||||
|
||||
def test_payment_entry_with_inclusive_tax(self):
|
||||
# inclusive tax built server-side: base_tax_amount is None until apply_taxes()
|
||||
payment_entry = create_payment_entry(paid_amount=1180)
|
||||
payment_entry.append(
|
||||
"taxes",
|
||||
{
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"charge_type": "On Paid Amount",
|
||||
"rate": 18,
|
||||
"included_in_paid_amount": 1,
|
||||
"add_deduct_tax": "Add",
|
||||
"description": "Service Tax",
|
||||
},
|
||||
)
|
||||
payment_entry.save()
|
||||
payment_entry.submit()
|
||||
|
||||
# 1180 incl 18% => 1000 base + 180 tax
|
||||
self.assertEqual(flt(payment_entry.total_taxes_and_charges, 2), 180.0)
|
||||
self.assertEqual(flt(payment_entry.unallocated_amount, 2), 1000.0)
|
||||
|
||||
def test_payment_entry_against_onhold_purchase_invoice(self):
|
||||
pi = make_purchase_invoice()
|
||||
|
||||
|
||||
@@ -11,11 +11,12 @@ from erpnext import get_company_currency
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.doctype.bank_account.bank_account import get_party_bank_account
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
get_payment_entry,
|
||||
)
|
||||
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
||||
from erpnext.accounts.party import get_party_account, get_party_bank_account
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import get_account_currency, get_currency_precision
|
||||
from erpnext.utilities import payment_app_import_guard
|
||||
|
||||
|
||||
@@ -195,7 +195,12 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
pe = pr.set_as_paid()
|
||||
pe = pr.create_payment_entry(submit=False)
|
||||
pe.source_exchange_rate = 50
|
||||
pe.target_exchange_rate = 50
|
||||
pe.set_amounts()
|
||||
pe.insert(ignore_permissions=True)
|
||||
pe.submit()
|
||||
|
||||
expected_gle = dict(
|
||||
(d[0], d)
|
||||
@@ -281,7 +286,12 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
pr = make_payment_request(dt=po_doc.doctype, dn=po_doc.name, recipient_id="nabin@erpnext.com")
|
||||
pr = frappe.get_doc(pr).save().submit()
|
||||
|
||||
pe = pr.create_payment_entry()
|
||||
pe = pr.create_payment_entry(submit=False)
|
||||
pe.target_exchange_rate = 80
|
||||
pe.paid_amount = 800
|
||||
pe.set_amounts()
|
||||
pe.insert(ignore_permissions=True)
|
||||
pe.submit()
|
||||
self.assertEqual(pe.base_paid_amount, 800)
|
||||
self.assertEqual(pe.paid_amount, 800)
|
||||
self.assertEqual(pe.base_received_amount, 800)
|
||||
|
||||
@@ -308,32 +308,3 @@ def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
)
|
||||
|
||||
return pos_profile
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_default_profile(pos_profile, company):
|
||||
modified = now()
|
||||
user = frappe.session.user
|
||||
|
||||
if pos_profile and company:
|
||||
frappe.db.sql(
|
||||
""" update `tabPOS Profile User` pfu, `tabPOS Profile` pf
|
||||
set
|
||||
pfu.default = 0, pf.modified = %s, pf.modified_by = %s
|
||||
where
|
||||
pfu.user = %s and pf.name = pfu.parent and pf.company = %s
|
||||
and pfu.default = 1""",
|
||||
(modified, user, user, company),
|
||||
auto_commit=1,
|
||||
)
|
||||
|
||||
frappe.db.sql(
|
||||
""" update `tabPOS Profile User` pfu, `tabPOS Profile` pf
|
||||
set
|
||||
pfu.default = 1, pf.modified = %s, pf.modified_by = %s
|
||||
where
|
||||
pfu.user = %s and pf.name = pfu.parent and pf.company = %s and pf.name = %s
|
||||
""",
|
||||
(modified, user, user, company, pos_profile),
|
||||
auto_commit=1,
|
||||
)
|
||||
|
||||
@@ -128,6 +128,7 @@ def is_job_running(job_name: str) -> bool:
|
||||
@frappe.whitelist()
|
||||
def pause_job_for_doc(docname: str | None = None):
|
||||
if docname:
|
||||
frappe.has_permission("Process Payment Reconciliation", "write", doc=docname, throw=True)
|
||||
frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Paused")
|
||||
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": docname})
|
||||
if log:
|
||||
@@ -142,6 +143,8 @@ def trigger_job_for_doc(docname: str | None = None):
|
||||
if not docname:
|
||||
return
|
||||
|
||||
frappe.has_permission("Process Payment Reconciliation", "write", doc=docname, throw=True)
|
||||
|
||||
if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
|
||||
frappe.throw(
|
||||
_("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format(
|
||||
|
||||
@@ -89,8 +89,11 @@ class ProcessPeriodClosingVoucher(Document):
|
||||
@frappe.whitelist()
|
||||
def start_pcv_processing(docname: str):
|
||||
if frappe.db.get_value("Process Period Closing Voucher", docname, "status") in ["Queued", "Running"]:
|
||||
frappe.has_permission("Process Payment Reconciliation", "write", doc=docname, throw=True)
|
||||
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running")
|
||||
|
||||
timeout = frappe.db.get_single_value("Accounts Settings", "pcv_job_timeout") or 3600
|
||||
|
||||
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
|
||||
if normal_balances := (
|
||||
qb.from_(ppcvd)
|
||||
@@ -117,7 +120,7 @@ def start_pcv_processing(docname: str):
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date",
|
||||
queue="long",
|
||||
timeout="3600",
|
||||
timeout=timeout,
|
||||
is_async=True,
|
||||
enqueue_after_commit=True,
|
||||
docname=docname,
|
||||
@@ -243,6 +246,8 @@ def get_gle_for_closing_account(pcv, dimension_balance, dimensions):
|
||||
|
||||
@frappe.whitelist()
|
||||
def schedule_next_date(docname: str):
|
||||
timeout = frappe.db.get_single_value("Accounts Settings", "pcv_job_timeout") or 3600
|
||||
|
||||
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
|
||||
if to_process := (
|
||||
qb.from_(ppcvd)
|
||||
@@ -268,7 +273,7 @@ def schedule_next_date(docname: str):
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date",
|
||||
queue="long",
|
||||
timeout="3600",
|
||||
timeout=timeout,
|
||||
is_async=True,
|
||||
enqueue_after_commit=True,
|
||||
docname=docname,
|
||||
@@ -298,7 +303,7 @@ def schedule_next_date(docname: str):
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.summarize_and_post_ledger_entries",
|
||||
queue="long",
|
||||
timeout="3600",
|
||||
timeout=timeout,
|
||||
is_async=True,
|
||||
job_name=job_name,
|
||||
enqueue_after_commit=True,
|
||||
|
||||
@@ -99,6 +99,7 @@ class ProcessStatementOfAccounts(Document):
|
||||
|
||||
validate_template(self.subject)
|
||||
validate_template(self.body)
|
||||
validate_template(self.pdf_name)
|
||||
|
||||
if not self.customers:
|
||||
frappe.throw(_("Customers not selected."))
|
||||
@@ -389,7 +390,6 @@ def get_context(customer, doc):
|
||||
return {
|
||||
"doc": template_doc,
|
||||
"customer": frappe.get_doc("Customer", customer),
|
||||
"frappe": frappe.utils,
|
||||
}
|
||||
|
||||
|
||||
@@ -492,6 +492,7 @@ def download_statements(document_name):
|
||||
@frappe.whitelist()
|
||||
def send_emails(document_name, from_scheduler=False, posting_date=None):
|
||||
doc = frappe.get_doc("Process Statement Of Accounts", document_name)
|
||||
doc.check_permission()
|
||||
report = get_report_pdf(doc, consolidated=False)
|
||||
|
||||
if report:
|
||||
@@ -548,6 +549,7 @@ def send_emails(document_name, from_scheduler=False, posting_date=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def send_auto_email():
|
||||
frappe.has_permission("Process Statement Of Accounts", throw=True)
|
||||
selected = frappe.get_list(
|
||||
"Process Statement Of Accounts",
|
||||
filters={"enable_auto_email": 1},
|
||||
|
||||
@@ -2086,6 +2086,7 @@ def make_stock_entry(source_name, target_doc=None):
|
||||
def change_release_date(name, release_date=None):
|
||||
if frappe.db.exists("Purchase Invoice", name):
|
||||
pi = frappe.get_doc("Purchase Invoice", name)
|
||||
pi.check_permission()
|
||||
pi.db_set("release_date", release_date)
|
||||
|
||||
|
||||
|
||||
@@ -154,12 +154,13 @@ class RepostAccountingLedger(Document):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def start_repost(account_repost_doc=str) -> None:
|
||||
def start_repost(account_repost_doc: str | None = None) -> None:
|
||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
|
||||
frappe.flags.through_repost_accounting_ledger = True
|
||||
if account_repost_doc:
|
||||
repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc)
|
||||
repost_doc.check_permission("write")
|
||||
|
||||
if repost_doc.docstatus == 1:
|
||||
# Prevent repost on invoices with deferred accounting
|
||||
|
||||
@@ -180,12 +180,31 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
: "Inter Company Purchase Invoice";
|
||||
|
||||
me.frm.add_custom_button(
|
||||
button_label,
|
||||
__(button_label),
|
||||
function () {
|
||||
me.make_inter_company_invoice();
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.get_received_items",
|
||||
args: {
|
||||
reference_name: me.frm.doc.name,
|
||||
doctype: "Purchase Invoice",
|
||||
reference_fieldname: "sales_invoice_item",
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.exc) return;
|
||||
const received_items = r.message || {};
|
||||
const has_pending_qty = me.frm.doc.items.some(
|
||||
(item) => flt(item.qty) - flt(received_items[item.name] || 0) > 0
|
||||
);
|
||||
if (!has_pending_qty) {
|
||||
me.frm.remove_custom_button(__(button_label), __("Create"));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,12 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
|
||||
get_party_tax_withholding_details,
|
||||
)
|
||||
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
|
||||
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
|
||||
from erpnext.accounts.party import (
|
||||
CROSS_PARTY_FIELD_NO_MAP,
|
||||
get_due_date,
|
||||
get_party_account,
|
||||
get_party_details,
|
||||
)
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
@@ -450,8 +455,8 @@ class SalesInvoice(SellingController):
|
||||
self.calculate_taxes_and_totals()
|
||||
|
||||
def before_save(self):
|
||||
self.set_account_for_mode_of_payment()
|
||||
self.set_paid_amount()
|
||||
self.set_account_for_mode_of_payment()
|
||||
|
||||
def before_submit(self):
|
||||
self.add_remarks()
|
||||
@@ -786,6 +791,13 @@ class SalesInvoice(SellingController):
|
||||
def set_paid_amount(self):
|
||||
paid_amount = 0.0
|
||||
base_paid_amount = 0.0
|
||||
|
||||
if not cint(self.is_pos) and self.is_return:
|
||||
self.set("payments", [])
|
||||
self.paid_amount = paid_amount
|
||||
self.base_paid_amount = base_paid_amount
|
||||
return
|
||||
|
||||
for data in self.payments:
|
||||
data.base_amount = flt(data.amount * self.conversion_rate, self.precision("base_paid_amount"))
|
||||
paid_amount += data.amount
|
||||
@@ -2526,7 +2538,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
"rate": "rate",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: doc.qty > 0,
|
||||
"condition": lambda doc: doc.qty - received_items.get(doc.name, 0.0) > 0,
|
||||
}
|
||||
|
||||
if doctype in ["Sales Invoice", "Sales Order"]:
|
||||
@@ -2559,18 +2571,25 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
"doctype": target_doctype,
|
||||
"postprocess": update_details,
|
||||
"set_target_warehouse": "set_from_warehouse",
|
||||
"field_no_map": ["taxes_and_charges", "set_warehouse", "shipping_address", "cost_center"],
|
||||
"field_no_map": [*CROSS_PARTY_FIELD_NO_MAP, "set_warehouse", "cost_center"],
|
||||
},
|
||||
doctype + " Item": item_field_map,
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
if not doclist.get("items"):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot create Intercompany {0}. All items in the source {1} have already been fully invoiced. "
|
||||
"Please check the existing linked {2}s."
|
||||
).format(target_doctype, doctype, target_doctype)
|
||||
)
|
||||
return doclist
|
||||
|
||||
|
||||
def get_received_items(reference_name, doctype, reference_fieldname):
|
||||
@frappe.whitelist()
|
||||
def get_received_items(reference_name: str, doctype: str, reference_fieldname: str):
|
||||
reference_field = "inter_company_invoice_reference"
|
||||
if doctype == "Purchase Order":
|
||||
reference_field = "inter_company_order_reference"
|
||||
@@ -2583,20 +2602,19 @@ def get_received_items(reference_name, doctype, reference_fieldname):
|
||||
target_doctypes = frappe.get_all(
|
||||
doctype,
|
||||
filters=filters,
|
||||
as_list=True,
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
received_items_map = {}
|
||||
if target_doctypes:
|
||||
target_doctypes = list(target_doctypes[0])
|
||||
|
||||
received_items_map = frappe._dict(
|
||||
frappe.get_all(
|
||||
received_items_data = frappe.get_all(
|
||||
doctype + " Item",
|
||||
filters={"parent": ("in", target_doctypes)},
|
||||
fields=[reference_fieldname, "qty"],
|
||||
as_list=1,
|
||||
)
|
||||
)
|
||||
for item in received_items_data:
|
||||
key = item.get(reference_fieldname)
|
||||
if key:
|
||||
received_items_map[key] = received_items_map.get(key, 0.0) + flt(item.qty)
|
||||
|
||||
return received_items_map
|
||||
|
||||
|
||||
@@ -1049,6 +1049,21 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(pos_return.get("payments")[0].amount, -500)
|
||||
self.assertEqual(pos_return.get("payments")[1].amount, -500)
|
||||
|
||||
def test_non_pos_return_clears_payment_rows(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
|
||||
si = create_sales_invoice(do_not_save=True)
|
||||
si.append("payments", {"mode_of_payment": "Cash", "amount": 100})
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
si_return = make_sales_return(si.name)
|
||||
si_return.insert()
|
||||
|
||||
self.assertEqual(si_return.is_pos, 0)
|
||||
self.assertEqual(si_return.get("payments"), [])
|
||||
self.assertEqual(si_return.paid_amount, 0)
|
||||
|
||||
def test_pos_change_amount(self):
|
||||
make_pos_profile(
|
||||
company="_Test Company with perpetual inventory",
|
||||
@@ -2690,6 +2705,95 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(target_doc.company, "_Test Company 1")
|
||||
self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
|
||||
|
||||
def test_restrict_inter_company_pi_when_sales_invoice_qty_fully_consumed(self):
|
||||
item_code_1 = "_Test IC Item 1"
|
||||
item_code_2 = "_Test IC Item 2"
|
||||
|
||||
create_item(item_code_1, is_stock_item=1)
|
||||
create_item(item_code_2, is_stock_item=1)
|
||||
|
||||
si = create_sales_invoice(
|
||||
company="Wind Power LLC",
|
||||
customer="_Test Internal Customer",
|
||||
item_code=item_code_1,
|
||||
debit_to="Debtors - WP",
|
||||
warehouse="Stores - WP",
|
||||
income_account="Sales - WP",
|
||||
expense_account="Cost of Goods Sold - WP",
|
||||
cost_center="Main - WP",
|
||||
currency="USD",
|
||||
qty=3,
|
||||
do_not_save=1,
|
||||
)
|
||||
si.selling_price_list = "_Test Price List Rest of the World"
|
||||
si.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": item_code_2,
|
||||
"item_name": item_code_2,
|
||||
"description": item_code_2,
|
||||
"warehouse": "Stores - WP",
|
||||
"qty": 2,
|
||||
"uom": "Nos",
|
||||
"stock_uom": "Nos",
|
||||
"rate": 100,
|
||||
"price_list_rate": 100,
|
||||
"income_account": "Sales - WP",
|
||||
"expense_account": "Cost of Goods Sold - WP",
|
||||
"cost_center": "Main - WP",
|
||||
"conversion_factor": 1,
|
||||
},
|
||||
)
|
||||
|
||||
si.submit()
|
||||
|
||||
target_doc = make_inter_company_transaction("Sales Invoice", si.name)
|
||||
|
||||
for item in target_doc.items:
|
||||
item.update(
|
||||
{
|
||||
"expense_account": "Cost of Goods Sold - _TC1",
|
||||
"cost_center": "Main - _TC1",
|
||||
}
|
||||
)
|
||||
|
||||
target_doc.submit()
|
||||
self.assertEqual(len(target_doc.items), 2)
|
||||
self.assertEqual([item.qty for item in target_doc.items], [3, 2])
|
||||
with self.assertRaisesRegex(
|
||||
frappe.ValidationError,
|
||||
"already been fully invoiced",
|
||||
):
|
||||
make_inter_company_transaction("Sales Invoice", si.name)
|
||||
|
||||
def test_inter_company_transaction_does_not_inherit_party_fields(self):
|
||||
"""
|
||||
Party-derived fields on SI (from Customer) must not leak into the mapped PI.
|
||||
"""
|
||||
si = create_sales_invoice(
|
||||
company="Wind Power LLC",
|
||||
customer="_Test Internal Customer",
|
||||
debit_to="Debtors - WP",
|
||||
warehouse="Stores - WP",
|
||||
income_account="Sales - WP",
|
||||
expense_account="Cost of Goods Sold - WP",
|
||||
cost_center="Main - WP",
|
||||
currency="USD",
|
||||
do_not_save=1,
|
||||
)
|
||||
si.selling_price_list = "_Test Price List Rest of the World"
|
||||
si.tax_category = "_Test Tax Category 1"
|
||||
si.language = "ar"
|
||||
si.payment_terms_template = "_Test Payment Term Template"
|
||||
si.submit()
|
||||
|
||||
pi = make_inter_company_transaction("Sales Invoice", si.name)
|
||||
|
||||
supplier = frappe.get_doc("Supplier", "_Test Internal Supplier")
|
||||
self.assertEqual(pi.tax_category or None, supplier.tax_category or None)
|
||||
self.assertEqual(pi.language or None, supplier.language or None)
|
||||
self.assertEqual(pi.payment_terms_template or None, supplier.payment_terms or None)
|
||||
|
||||
def test_inter_company_transaction_without_default_warehouse(self):
|
||||
"Check mapping (expense account) of inter company SI to PI in absence of default warehouse."
|
||||
# setup
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Purchase Invoice",
|
||||
"dynamic_filters_json": "[[\"Purchase Invoice\",\"company\",\"=\",\" frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Purchase Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]",
|
||||
"dynamic_filters_json": "[[\"Purchase Invoice\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Purchase Invoice\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]",
|
||||
"filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\"]]",
|
||||
"function": "Sum",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "Total Incoming Bills",
|
||||
"modified": "2024-11-20 19:08:37.043777",
|
||||
"modified": "2026-06-01 12:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Total Incoming Bills",
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Payment Entry",
|
||||
"dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\",false]]",
|
||||
"dynamic_filters_json": "[[\"Payment Entry\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Payment Entry\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]",
|
||||
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\"],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\"]]",
|
||||
"function": "Sum",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "Total Incoming Payment",
|
||||
"modified": "2020-07-22 13:06:20.237689",
|
||||
"modified": "2026-06-01 12:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Total Incoming Payment",
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Sales Invoice",
|
||||
"dynamic_filters_json": "[[\"Sales Invoice\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Sales Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]",
|
||||
"dynamic_filters_json": "[[\"Sales Invoice\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Sales Invoice\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]",
|
||||
"filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\"]]",
|
||||
"function": "Sum",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "Total Outgoing Bills",
|
||||
"modified": "2020-07-22 13:07:19.633101",
|
||||
"modified": "2026-06-01 12:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Total Outgoing Bills",
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Payment Entry",
|
||||
"dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\",false]]",
|
||||
"dynamic_filters_json": "[[\"Payment Entry\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Payment Entry\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]",
|
||||
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\"],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\"]]",
|
||||
"function": "Sum",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "Total Outgoing Payment",
|
||||
"modified": "2020-07-22 12:49:34.942896",
|
||||
"modified": "2026-06-01 12:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Total Outgoing Payment",
|
||||
|
||||
@@ -48,6 +48,25 @@ SALES_TRANSACTION_TYPES = {
|
||||
}
|
||||
TRANSACTION_TYPES = PURCHASE_TRANSACTION_TYPES | SALES_TRANSACTION_TYPES
|
||||
|
||||
# Party-derived fields that must NOT be auto-copied by `get_mapped_doc` when the
|
||||
# source and target documents belong to different parties (e.g. Sales Order →
|
||||
# Purchase Order or inter-company Sales Invoice → Purchase Invoice).
|
||||
CROSS_PARTY_FIELD_NO_MAP = [
|
||||
"tax_category",
|
||||
"tax_id",
|
||||
"tax_withholding_category",
|
||||
"taxes_and_charges",
|
||||
"address_display",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"contact_person",
|
||||
"shipping_address",
|
||||
"dispatch_address",
|
||||
"payment_terms_template",
|
||||
"language",
|
||||
]
|
||||
|
||||
|
||||
class DuplicatePartyAccountError(frappe.ValidationError):
|
||||
pass
|
||||
@@ -491,11 +510,6 @@ def get_party_advance_account(party_type, party, company):
|
||||
return account
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_party_bank_account(party_type, party):
|
||||
return frappe.db.get_value("Bank Account", {"party_type": party_type, "party": party, "is_default": 1})
|
||||
|
||||
|
||||
def get_party_account_currency(party_type, party, company):
|
||||
def generator():
|
||||
party_account = get_party_account(party_type, party, company)
|
||||
@@ -530,11 +544,19 @@ def get_party_gle_currency(party_type, party, company):
|
||||
|
||||
def get_party_gle_account(party_type, party, company):
|
||||
def generator():
|
||||
existing_gle_account = frappe.db.sql(
|
||||
"""select account from `tabGL Entry`
|
||||
where docstatus=1 and company=%(company)s and party_type=%(party_type)s and party=%(party)s
|
||||
limit 1""",
|
||||
{"company": company, "party_type": party_type, "party": party},
|
||||
gl = qb.DocType("GL Entry")
|
||||
existing_gle_account = (
|
||||
qb.from_(gl)
|
||||
.select(gl.account)
|
||||
.where(
|
||||
(gl.docstatus == 1)
|
||||
& (gl.company == company)
|
||||
& (gl.party_type == party_type)
|
||||
& (gl.party == party)
|
||||
& (gl.is_cancelled == 0)
|
||||
)
|
||||
.limit(1)
|
||||
.run()
|
||||
)
|
||||
|
||||
return existing_gle_account[0][0] if existing_gle_account else None
|
||||
@@ -900,6 +922,15 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
|
||||
if party_type == "Supplier":
|
||||
info["total_unpaid"] = -1 * info["total_unpaid"]
|
||||
|
||||
if info["total_unpaid"] < 0:
|
||||
info["balance_label"] = (
|
||||
"Total Advance Paid" if party_type == "Supplier" else "Total Advance Received"
|
||||
)
|
||||
info["balance_amount"] = abs(info["total_unpaid"])
|
||||
else:
|
||||
info["balance_label"] = "Total Unpaid"
|
||||
info["balance_amount"] = info["total_unpaid"]
|
||||
|
||||
company_wise_info.append(info)
|
||||
|
||||
return company_wise_info
|
||||
|
||||
@@ -922,8 +922,28 @@ class ReceivablePayableReport:
|
||||
if self.filters.project:
|
||||
self.qb_selection_filter.append(self.ple.project.isin(self.filters.project))
|
||||
|
||||
self.add_user_permission_filters()
|
||||
|
||||
self.add_accounting_dimensions_filters()
|
||||
|
||||
def add_user_permission_filters(self):
|
||||
# Party is a dynamic link, so match conditions cannot auto-apply Customer/Supplier user permissions
|
||||
from frappe.core.doctype.user_permission.user_permission import get_user_permissions
|
||||
from frappe.permissions import get_allowed_docs_for_doctype
|
||||
|
||||
user_permissions = get_user_permissions()
|
||||
if not user_permissions:
|
||||
return
|
||||
|
||||
for party_type in self.party_type:
|
||||
if party_type not in user_permissions:
|
||||
continue
|
||||
|
||||
allowed_parties = get_allowed_docs_for_doctype(user_permissions[party_type], party_type)
|
||||
self.qb_selection_filter.append(
|
||||
(self.ple.party_type != party_type) | self.ple.party.isin(allowed_parties or [""])
|
||||
)
|
||||
|
||||
def get_cost_center_conditions(self):
|
||||
cost_center_list = get_cost_centers_with_children(self.filters.cost_center)
|
||||
self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list))
|
||||
|
||||
@@ -1253,3 +1253,53 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
row = report[1][0]
|
||||
self.assertEqual([si.name, project.name, 60], [row.voucher_no, row.project, row.outstanding])
|
||||
|
||||
def test_accounts_receivable_respects_user_permissions(self):
|
||||
# Party is a dynamic link on Payment Ledger Entry, so user permissions on Customer
|
||||
# must be applied explicitly. The report should only show permitted customers.
|
||||
|
||||
# Running the report writes an access log that commits, so these invoices survive
|
||||
# tearDown's rollback. Delete and commit them so they don't leak into other tests.
|
||||
def remove_committed_entries():
|
||||
self.clear_old_entries()
|
||||
frappe.db.commit() # nosemgrep
|
||||
|
||||
self.addCleanup(remove_committed_entries)
|
||||
|
||||
original_customer = self.customer
|
||||
second_customer = "_Test AR Perm Customer"
|
||||
|
||||
# create_customer overrides self.customer, so build the restricted invoice first
|
||||
self.create_customer(customer_name=second_customer)
|
||||
self.create_sales_invoice(no_payment_schedule=True)
|
||||
|
||||
self.customer = original_customer
|
||||
allowed_invoice = self.create_sales_invoice(no_payment_schedule=True)
|
||||
|
||||
test_user = "test_ar_user_permission@example.com"
|
||||
if not frappe.db.exists("User", test_user):
|
||||
user = frappe.new_doc("User")
|
||||
user.email = test_user
|
||||
user.first_name = "AR Perm"
|
||||
user.append("roles", {"role": "Accounts User"})
|
||||
user.save()
|
||||
|
||||
frappe.permissions.add_user_permission("Customer", original_customer, test_user)
|
||||
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"party_type": "Customer",
|
||||
"report_date": today(),
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
|
||||
frappe.set_user(test_user)
|
||||
try:
|
||||
report = execute(filters)
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
parties = {row.party for row in report[1]}
|
||||
self.assertIn(original_customer, parties)
|
||||
self.assertNotIn(second_customer, parties)
|
||||
self.assertEqual(allowed_invoice.customer, original_customer)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import CustomFunction
|
||||
from frappe.utils import cint
|
||||
|
||||
|
||||
@@ -94,19 +95,35 @@ def get_data(filters):
|
||||
def get_sales_details(filters):
|
||||
item_details_map = {}
|
||||
|
||||
date_field = "s.transaction_date" if filters["based_on"] == "Sales Order" else "s.posting_date"
|
||||
if filters["based_on"] not in ("Sales Order", "Sales Invoice"):
|
||||
frappe.throw(_("Invalid value {0} for 'Based On'").format(filters["based_on"]))
|
||||
|
||||
sales_data = frappe.db.sql(
|
||||
"""
|
||||
select s.territory, s.customer, si.item_group, si.item_code, si.qty, {date_field} as last_order_date,
|
||||
DATEDIFF(CURRENT_DATE, {date_field}) as days_since_last_order
|
||||
from `tab{doctype}` s, `tab{doctype} Item` si
|
||||
where s.name = si.parent and s.docstatus = 1
|
||||
order by days_since_last_order """.format( # nosec
|
||||
date_field=date_field, doctype=filters["based_on"]
|
||||
),
|
||||
as_dict=1,
|
||||
)
|
||||
parent = frappe.qb.DocType(filters["based_on"])
|
||||
child_doctype = "Sales Order Item" if filters["based_on"] == "Sales Order" else "Sales Invoice Item"
|
||||
child = frappe.qb.DocType(child_doctype)
|
||||
|
||||
date_diff = CustomFunction("DATEDIFF", ["d1", "d2"])
|
||||
current_date = CustomFunction("CURRENT_DATE", [])
|
||||
|
||||
date_col = parent.transaction_date if filters["based_on"] == "Sales Order" else parent.posting_date
|
||||
days_since_last_order = date_diff(current_date(), date_col)
|
||||
|
||||
sales_data = (
|
||||
frappe.qb.from_(parent)
|
||||
.inner_join(child)
|
||||
.on(parent.name == child.parent)
|
||||
.select(
|
||||
parent.territory,
|
||||
parent.customer,
|
||||
child.item_group,
|
||||
child.item_code,
|
||||
child.qty,
|
||||
date_col.as_("last_order_date"),
|
||||
days_since_last_order.as_("days_since_last_order"),
|
||||
)
|
||||
.where(parent.docstatus == 1)
|
||||
.orderby(days_since_last_order)
|
||||
).run(as_dict=True)
|
||||
|
||||
for d in sales_data:
|
||||
item_details_map.setdefault((d.territory, d.item_code), d)
|
||||
|
||||
@@ -146,7 +146,6 @@ def get_appropriate_company(filters):
|
||||
return company
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_invoiced_item_gross_margin(sales_invoice=None, item_code=None, company=None, with_item_data=False):
|
||||
from erpnext.accounts.report.gross_profit.gross_profit import GrossProfitGenerator
|
||||
|
||||
|
||||
@@ -89,6 +89,8 @@ class TestUtils(unittest.TestCase):
|
||||
purchase_invoice.submit()
|
||||
|
||||
payment_entry = get_payment_entry(purchase_invoice.doctype, purchase_invoice.name)
|
||||
payment_entry.target_exchange_rate = 82.32
|
||||
payment_entry.set_amounts()
|
||||
payment_entry.paid_amount = 15725
|
||||
payment_entry.deductions = []
|
||||
payment_entry.save()
|
||||
|
||||
@@ -176,7 +176,6 @@ def validate_fiscal_year(date, fiscal_year, company, label="Date", doc=None):
|
||||
throw(_("{0} '{1}' not in Fiscal Year {2}").format(_(label), formatdate(date), fiscal_year))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_balance_on(
|
||||
account=None,
|
||||
date=None,
|
||||
@@ -278,6 +277,7 @@ def get_balance_on(
|
||||
)
|
||||
|
||||
if party_type and party:
|
||||
frappe.has_permission(party_type, "read", party, throw=True)
|
||||
cond.append(
|
||||
f"""gle.party_type = {frappe.db.escape(party_type)} and gle.party = {frappe.db.escape(party)} """
|
||||
)
|
||||
@@ -1386,6 +1386,7 @@ def update_cost_center(docname, cost_center_name, cost_center_number, company, m
|
||||
Renames the document by adding the number as a prefix to the current name and updates
|
||||
all transaction where it was present.
|
||||
"""
|
||||
frappe.has_permission("Cost Center", "write", doc=docname, throw=True)
|
||||
validate_field_number("Cost Center", docname, cost_center_number, company, "cost_center_number")
|
||||
|
||||
if cost_center_number:
|
||||
|
||||
@@ -31,7 +31,8 @@ class BulkTransactionLog(Document):
|
||||
log_detail = qb.DocType("Bulk Transaction Log Detail")
|
||||
|
||||
has_records = frappe.db.sql(
|
||||
f"select exists (select * from `tabBulk Transaction Log Detail` where date = '{self.name}');"
|
||||
"select exists (select * from `tabBulk Transaction Log Detail` where date = %s);",
|
||||
(self.name,),
|
||||
)[0][0]
|
||||
if not has_records:
|
||||
raise frappe.DoesNotExistError
|
||||
|
||||
@@ -662,7 +662,7 @@ class PurchaseOrder(BuyingController):
|
||||
|
||||
def update_subcontracting_order_status(self):
|
||||
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
|
||||
update_subcontracting_order_status as update_sco_status,
|
||||
set_subcontracting_order_status as update_sco_status,
|
||||
)
|
||||
|
||||
if self.is_subcontracted and not self.is_old_subcontracting_flow:
|
||||
|
||||
@@ -5,6 +5,7 @@ def get_data():
|
||||
return {
|
||||
"fieldname": "supplier",
|
||||
"non_standard_fieldnames": {"Payment Entry": "party", "Bank Account": "party"},
|
||||
"dynamic_links": {"party": ["Supplier", "party_type"]},
|
||||
"transactions": [
|
||||
{"label": _("Procurement"), "items": ["Request for Quotation", "Supplier Quotation"]},
|
||||
{"label": _("Orders"), "items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]},
|
||||
|
||||
@@ -30,11 +30,15 @@
|
||||
"stock_qty",
|
||||
"sec_break_price_list",
|
||||
"price_list_rate",
|
||||
"base_price_list_rate",
|
||||
"discount_and_margin_section",
|
||||
"margin_type",
|
||||
"margin_rate_or_amount",
|
||||
"rate_with_margin",
|
||||
"col_break_6",
|
||||
"discount_percentage",
|
||||
"discount_amount",
|
||||
"distributed_discount_amount",
|
||||
"col_break_price_list",
|
||||
"base_price_list_rate",
|
||||
"sec_break1",
|
||||
"rate",
|
||||
"amount",
|
||||
@@ -531,10 +535,6 @@
|
||||
"fieldname": "sec_break_price_list",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break_price_list",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "ad_sec_break",
|
||||
@@ -572,13 +572,48 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Distributed Discount Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"depends_on": "price_list_rate",
|
||||
"fieldname": "margin_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Margin Type",
|
||||
"options": "\nPercentage\nAmount",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.margin_type && doc.price_list_rate",
|
||||
"fieldname": "margin_rate_or_amount",
|
||||
"fieldtype": "Float",
|
||||
"label": "Margin Rate or Amount",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount",
|
||||
"fieldname": "discount_and_margin_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Discount and Margin"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount",
|
||||
"fieldname": "rate_with_margin",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rate With Margin",
|
||||
"options": "currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-02 06:22:18.864822",
|
||||
"modified": "2025-06-17 12:05:52.441645",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier Quotation Item",
|
||||
@@ -589,4 +624,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ class SupplierQuotationItem(Document):
|
||||
lead_time_days: DF.Int
|
||||
manufacturer: DF.Link | None
|
||||
manufacturer_part_no: DF.Data | None
|
||||
margin_rate_or_amount: DF.Float
|
||||
margin_type: DF.Literal["", "Percentage", "Amount"]
|
||||
material_request: DF.Link | None
|
||||
material_request_item: DF.Data | None
|
||||
net_amount: DF.Currency
|
||||
@@ -52,6 +54,7 @@ class SupplierQuotationItem(Document):
|
||||
project: DF.Link | None
|
||||
qty: DF.Float
|
||||
rate: DF.Currency
|
||||
rate_with_margin: DF.Currency
|
||||
request_for_quotation: DF.Link | None
|
||||
request_for_quotation_item: DF.Data | None
|
||||
sales_order: DF.Link | None
|
||||
|
||||
@@ -201,6 +201,7 @@ def refresh_scorecards():
|
||||
def make_all_scorecards(docname):
|
||||
sc = frappe.get_doc("Supplier Scorecard", docname)
|
||||
supplier = frappe.get_doc("Supplier", sc.supplier)
|
||||
supplier.check_permission("write")
|
||||
|
||||
start_date = getdate(supplier.creation)
|
||||
end_date = get_scorecard_date(sc.period, start_date)
|
||||
|
||||
@@ -297,7 +297,8 @@ def get_message():
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_default_supplier(item_code, supplier, company):
|
||||
def set_default_supplier(item_code: str, supplier: str, company: str):
|
||||
frappe.has_permission("Item", "write", doc=item_code, throw=True)
|
||||
frappe.db.set_value(
|
||||
"Item Default",
|
||||
{"parent": item_code, "company": company},
|
||||
|
||||
@@ -7,6 +7,7 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Case
|
||||
from frappe.utils import cstr, flt
|
||||
|
||||
from erpnext.utilities.product import get_item_codes_by_attributes
|
||||
@@ -129,6 +130,53 @@ def validate_is_incremental(numeric_attribute, attribute, value, item):
|
||||
)
|
||||
|
||||
|
||||
def get_attribute_value_renames(item_attribute):
|
||||
"""Return old to new attribute value mappings for renamed Item Attribute Value rows."""
|
||||
if item_attribute.numeric_values:
|
||||
return {}
|
||||
|
||||
db_value = item_attribute.get_doc_before_save()
|
||||
if not db_value:
|
||||
return {}
|
||||
|
||||
old_values = {d.name: d.attribute_value for d in db_value.item_attribute_values}
|
||||
renames = {}
|
||||
|
||||
for row in item_attribute.item_attribute_values:
|
||||
if row.name in old_values and old_values[row.name] != row.attribute_value:
|
||||
renames[old_values[row.name]] = row.attribute_value
|
||||
|
||||
return renames
|
||||
|
||||
|
||||
def update_variant_attribute_values(item_attribute):
|
||||
"""Propagate renamed Item Attribute Values to Item Variant Attribute on variant items."""
|
||||
value_map = get_attribute_value_renames(item_attribute)
|
||||
if not value_map:
|
||||
return
|
||||
|
||||
item_variant_table = frappe.qb.DocType("Item Variant Attribute")
|
||||
item_table = frappe.qb.DocType("Item")
|
||||
attribute_value = item_variant_table.attribute_value
|
||||
attribute_value_case = Case()
|
||||
|
||||
for old_value, new_value in value_map.items():
|
||||
attribute_value_case = attribute_value_case.when(attribute_value == old_value, new_value)
|
||||
|
||||
(
|
||||
frappe.qb.update(item_variant_table)
|
||||
.join(item_table)
|
||||
.on(item_table.name == item_variant_table.parent)
|
||||
.set(attribute_value, attribute_value_case.else_(attribute_value))
|
||||
.where(item_table.variant_of.isnotnull())
|
||||
.where(item_table.variant_of != "")
|
||||
.where(item_variant_table.attribute == item_attribute.name)
|
||||
.where(attribute_value.isin(list(value_map)))
|
||||
).run()
|
||||
|
||||
frappe.flags.attribute_values = None
|
||||
|
||||
|
||||
def validate_item_attribute_value(attributes_list, attribute, attribute_value, item, from_variant=True):
|
||||
allow_rename_attribute_value = frappe.db.get_single_value(
|
||||
"Item Variant Settings", "allow_rename_attribute_value"
|
||||
|
||||
@@ -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_valuation_method, getdate
|
||||
from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_valuation_method, getdate
|
||||
|
||||
|
||||
class StockOverReturnError(frappe.ValidationError):
|
||||
@@ -380,6 +380,8 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
doc.pricing_rules = []
|
||||
doc.return_against = source.name
|
||||
doc.set_warehouse = ""
|
||||
if doctype == "Sales Invoice":
|
||||
doc.is_debit_note = 0
|
||||
if doctype == "Sales Invoice" or doctype == "POS Invoice":
|
||||
doc.is_pos = source.is_pos
|
||||
|
||||
@@ -533,6 +535,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
target_doc.so_detail = source_doc.so_detail
|
||||
target_doc.expense_account = source_doc.expense_account
|
||||
target_doc.dn_detail = source_doc.name
|
||||
target_doc.cost_center = source_doc.cost_center
|
||||
if default_warehouse_for_sales_return:
|
||||
target_doc.warehouse = default_warehouse_for_sales_return
|
||||
elif doctype == "Sales Invoice" or doctype == "POS Invoice":
|
||||
@@ -1179,8 +1182,7 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f
|
||||
"batches": data.get("batches"),
|
||||
"serial_nos_valuation": data.get("serial_nos_valuation"),
|
||||
"batches_valuation": data.get("batches_valuation"),
|
||||
"posting_date": parent_doc.posting_date,
|
||||
"posting_time": parent_doc.posting_time,
|
||||
"posting_datetime": get_combine_datetime(parent_doc.posting_date, parent_doc.posting_time),
|
||||
"voucher_type": parent_doc.doctype,
|
||||
"voucher_no": parent_doc.name,
|
||||
"voucher_detail_no": child_doc.name,
|
||||
|
||||
@@ -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_valuation_method
|
||||
from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_valuation_method
|
||||
|
||||
|
||||
class SellingController(StockController):
|
||||
@@ -1084,8 +1084,7 @@ def get_serial_and_batch_bundle(child, parent, delivery_note_child=None):
|
||||
"voucher_type": parent.doctype,
|
||||
"voucher_no": parent.name if parent.docstatus < 2 else None,
|
||||
"voucher_detail_no": delivery_note_child.name if delivery_note_child else child.name,
|
||||
"posting_date": parent.posting_date,
|
||||
"posting_time": parent.posting_time,
|
||||
"posting_datetime": get_combine_datetime(parent.posting_date, parent.posting_time),
|
||||
"qty": child.qty,
|
||||
"type_of_transaction": "Outward" if child.qty > 0 and parent.docstatus < 2 else "Inward",
|
||||
"company": parent.company,
|
||||
|
||||
@@ -394,9 +394,9 @@ class StatusUpdater(Document):
|
||||
for args in self.status_updater:
|
||||
# condition to include current record (if submit or no if cancel)
|
||||
if self.docstatus == 1:
|
||||
args["cond"] = " or parent='%s'" % self.name.replace('"', '"')
|
||||
args["cond"] = " or parent=%s" % frappe.db.escape(self.name)
|
||||
else:
|
||||
args["cond"] = " and parent!='%s'" % self.name.replace('"', '"')
|
||||
args["cond"] = " and parent!=%s" % frappe.db.escape(self.name)
|
||||
|
||||
self._update_children(args, update_modified)
|
||||
|
||||
@@ -426,9 +426,10 @@ class StatusUpdater(Document):
|
||||
args["second_source_condition"] = frappe.db.sql(
|
||||
""" select ifnull((select sum({second_source_field})
|
||||
from `tab{second_source_dt}`
|
||||
where `{second_join_field}`='{detail_id}'
|
||||
where `{second_join_field}`=%(detail_id)s
|
||||
and (`tab{second_source_dt}`.docstatus=1)
|
||||
{second_source_extra_cond}), 0) """.format(**args)
|
||||
{second_source_extra_cond}), 0) """.format(**args),
|
||||
{"detail_id": args["detail_id"]},
|
||||
)[0][0]
|
||||
|
||||
if args["detail_id"]:
|
||||
@@ -439,9 +440,10 @@ class StatusUpdater(Document):
|
||||
frappe.db.sql(
|
||||
"""
|
||||
(select ifnull(sum({source_field}), 0)
|
||||
from `tab{source_dt}` where `{join_field}`='{detail_id}'
|
||||
from `tab{source_dt}` where `{join_field}`=%(detail_id)s
|
||||
and (docstatus=1 {cond}) {extra_cond})
|
||||
""".format(**args)
|
||||
""".format(**args),
|
||||
{"detail_id": args["detail_id"]},
|
||||
)[0][0]
|
||||
or 0.0
|
||||
)
|
||||
@@ -452,7 +454,8 @@ class StatusUpdater(Document):
|
||||
frappe.db.sql(
|
||||
"""update `tab{target_dt}`
|
||||
set {target_field} = {source_dt_value} {update_modified}
|
||||
where name='{detail_id}'""".format(**args)
|
||||
where name=%(detail_id)s""".format(**args),
|
||||
{"detail_id": args["detail_id"]},
|
||||
)
|
||||
|
||||
def _update_percent_field_in_targets(self, args, update_modified=True):
|
||||
|
||||
@@ -26,6 +26,7 @@ from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
|
||||
get_evaluated_inventory_dimension,
|
||||
)
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
combine_datetime,
|
||||
get_type_of_transaction,
|
||||
)
|
||||
from erpnext.stock.stock_ledger import get_items_to_be_repost
|
||||
@@ -282,8 +283,7 @@ class StockController(AccountsController):
|
||||
):
|
||||
bundle_details = {
|
||||
"item_code": row.get("rm_item_code") or row.item_code,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"posting_datetime": combine_datetime(self.posting_date, self.posting_time),
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"voucher_detail_no": row.name,
|
||||
@@ -1694,7 +1694,7 @@ def check_item_quality_inspection(doctype: str, docstatus: str | int, items: str
|
||||
|
||||
inspection_fieldname = inspection_fieldname_map.get(doctype)
|
||||
if inspection_fieldname is None:
|
||||
return []
|
||||
return items if doctype == "Stock Entry" else []
|
||||
|
||||
allow_after_transaction = cint(docstatus) == 1 and frappe.get_single_value(
|
||||
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
|
||||
|
||||
@@ -13,6 +13,7 @@ from frappe.utils import cint, flt, get_link_to_form
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
combine_datetime,
|
||||
get_auto_batch_nos,
|
||||
get_available_serial_nos,
|
||||
get_voucher_wise_serial_batch_from_bundle,
|
||||
@@ -570,8 +571,7 @@ class SubcontractingController(StockController):
|
||||
"qty": qty,
|
||||
"serial_nos": serial_nos,
|
||||
"batches": batches,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"posting_datetime": combine_datetime(self.posting_date, self.posting_time),
|
||||
"voucher_type": "Subcontracting Receipt",
|
||||
"do_not_submit": True,
|
||||
"type_of_transaction": "Outward" if qty > 0 else "Inward",
|
||||
|
||||
@@ -38,7 +38,9 @@ class calculate_taxes_and_totals:
|
||||
|
||||
self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items")
|
||||
|
||||
get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)
|
||||
get_round_off_applicable_accounts(
|
||||
self.doc.company, frappe.flags.round_off_applicable_accounts, self.doc
|
||||
)
|
||||
self.calculate()
|
||||
|
||||
def filter_rows(self):
|
||||
@@ -183,7 +185,7 @@ class calculate_taxes_and_totals:
|
||||
return
|
||||
|
||||
if not self.discount_amount_applied:
|
||||
do_not_round_fields = ["valuation_rate", "incoming_rate"]
|
||||
do_not_round_fields = ["valuation_rate", "incoming_rate", "sales_incoming_rate"]
|
||||
|
||||
for item in self.doc.items:
|
||||
self.doc.round_floats_in(item, do_not_round_fields=do_not_round_fields)
|
||||
@@ -1128,14 +1130,14 @@ def get_itemised_tax_breakup_html(doc):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_round_off_applicable_accounts(company, account_list):
|
||||
def get_round_off_applicable_accounts(company, account_list, doc=None):
|
||||
# required to set correct region
|
||||
with temporary_flag("company", company):
|
||||
return get_regional_round_off_accounts(company, account_list)
|
||||
return get_regional_round_off_accounts(company, account_list, doc)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def get_regional_round_off_accounts(company, account_list):
|
||||
def get_regional_round_off_accounts(company, account_list, doc=None):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
@@ -6,6 +8,28 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
|
||||
|
||||
|
||||
class TestTaxesAndTotals(FrappeTestCase):
|
||||
def test_regional_round_off_accounts(self):
|
||||
"""
|
||||
Regional overrides cannot extend the list in-place — the return
|
||||
value must be assigned back to frappe.flags.round_off_applicable_accounts.
|
||||
"""
|
||||
test_account = "_Test Round Off Account"
|
||||
|
||||
def mock_regional(company, account_list: list, doc=None) -> list:
|
||||
# Simulates a regional override
|
||||
account_list.extend([test_account])
|
||||
return account_list
|
||||
|
||||
so = make_sales_order(do_not_save=True)
|
||||
|
||||
with patch(
|
||||
"erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts",
|
||||
mock_regional,
|
||||
):
|
||||
calculate_taxes_and_totals(so)
|
||||
|
||||
self.assertIn(test_account, frappe.flags.round_off_applicable_accounts)
|
||||
|
||||
def test_disabling_rounded_total_resets_base_fields(self):
|
||||
"""Disabling rounded total should also clear base rounded values."""
|
||||
so = make_sales_order(do_not_save=True)
|
||||
|
||||
@@ -7,7 +7,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.modules.utils import get_module_app
|
||||
from frappe.utils import flt, has_common
|
||||
from frappe.utils import cint, flt, has_common
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
|
||||
|
||||
@@ -14,12 +14,17 @@
|
||||
"opportunity_section",
|
||||
"close_opportunity_after_days",
|
||||
"column_break_9",
|
||||
"enable_opportunity_creation_from_contact_us",
|
||||
"quotation_section",
|
||||
"default_valid_till",
|
||||
"section_break_13",
|
||||
"carry_forward_communication_and_comments",
|
||||
"column_break_junk",
|
||||
"update_timestamp_on_new_communication"
|
||||
"update_timestamp_on_new_communication",
|
||||
"frappe_crm_section",
|
||||
"enable_frappe_crm_data_synchronization",
|
||||
"column_break_jbzj",
|
||||
"allowed_users"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -98,13 +103,43 @@
|
||||
"fieldname": "update_timestamp_on_new_communication",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update timestamp on new communication"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enable_opportunity_creation_from_contact_us",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Opportunity Creation from Contact Us"
|
||||
},
|
||||
{
|
||||
"fieldname": "frappe_crm_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Frappe CRM"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_jbzj",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.enable_frappe_crm_data_synchronization === 1;",
|
||||
"fieldname": "allowed_users",
|
||||
"fieldtype": "Table MultiSelect",
|
||||
"label": "Allowed Users",
|
||||
"options": "Frappe CRM Allowed User",
|
||||
"permlevel": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enable_frappe_crm_data_synchronization",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Frappe CRM Data Synchronization",
|
||||
"permlevel": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-16 16:12:14.889455",
|
||||
"modified": "2026-06-22 01:26:13.474915",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "CRM Settings",
|
||||
@@ -138,10 +173,20 @@
|
||||
"role": "Sales Master Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"permlevel": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields, delete_custom_fields
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
@@ -14,14 +16,71 @@ class CRMSettings(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.crm.doctype.frappe_crm_allowed_user.frappe_crm_allowed_user import FrappeCRMAllowedUser
|
||||
|
||||
allow_lead_duplication_based_on_emails: DF.Check
|
||||
allowed_users: DF.TableMultiSelect[FrappeCRMAllowedUser]
|
||||
auto_creation_of_contact: DF.Check
|
||||
campaign_naming_by: DF.Literal["Campaign Name", "Naming Series"]
|
||||
carry_forward_communication_and_comments: DF.Check
|
||||
close_opportunity_after_days: DF.Int
|
||||
default_valid_till: DF.Data | None
|
||||
enable_frappe_crm_data_synchronization: DF.Check
|
||||
enable_opportunity_creation_from_contact_us: DF.Check
|
||||
update_timestamp_on_new_communication: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
frappe.db.set_default("campaign_naming_by", self.get("campaign_naming_by", ""))
|
||||
self.validate_enable_opportunity_creation_from_contact_us()
|
||||
self.validate_allowed_users()
|
||||
|
||||
def validate_enable_opportunity_creation_from_contact_us(self):
|
||||
contact_disabled = frappe.get_single_value("Contact Us Settings", "is_disabled")
|
||||
|
||||
if self.enable_opportunity_creation_from_contact_us and contact_disabled:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot enable Opportunity creation from Contact Us because the Contact Us form is disabled."
|
||||
)
|
||||
)
|
||||
|
||||
def validate_allowed_users(self):
|
||||
if self.enable_frappe_crm_data_synchronization and not self.allowed_users:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please add atleast one user on Allowed Users to allow Data Synchronization from Frappe CRM site."
|
||||
)
|
||||
)
|
||||
|
||||
def before_save(self):
|
||||
self.clear_allowed_users()
|
||||
|
||||
def on_update(self):
|
||||
self.custom_fields_for_frappe_crm_data_sync()
|
||||
|
||||
def clear_allowed_users(self):
|
||||
if not self.enable_frappe_crm_data_synchronization:
|
||||
self.allowed_users = []
|
||||
|
||||
def custom_fields_for_frappe_crm_data_sync(self):
|
||||
custom_fields = {
|
||||
"Quotation": [
|
||||
{
|
||||
"fieldname": "crm_deal",
|
||||
"fieldtype": "Data",
|
||||
"label": "Frappe CRM Deal",
|
||||
"insert_after": "party_name",
|
||||
}
|
||||
],
|
||||
"Customer": [
|
||||
{
|
||||
"fieldname": "crm_deal",
|
||||
"fieldtype": "Data",
|
||||
"label": "Frappe CRM Deal",
|
||||
"insert_after": "prospect_name",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
create_custom_fields(custom_fields, ignore_validate=True)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"allow_rename": 1,
|
||||
"creation": "2026-06-22 00:47:12.265968",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"user"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "User",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-06-22 01:49:54.586410",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Frappe CRM Allowed User",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"rows_threshold_for_grid_search": 20,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class FrappeCRMAllowedUser(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
user: DF.Link
|
||||
# end: auto-generated types
|
||||
|
||||
_DOCTYPE_NAME = "Frappe CRM Allowed User"
|
||||
@@ -9,7 +9,7 @@ from frappe.contacts.address_and_contact import (
|
||||
)
|
||||
from frappe.email.inbox import link_communication_to_document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_email_address
|
||||
from frappe.utils import comma_and, get_link_to_form, validate_email_address
|
||||
|
||||
from erpnext.accounts.party import set_taxes
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
@@ -171,9 +171,6 @@ class Lead(SellingController, CRMNote):
|
||||
if self.email_id == self.lead_owner:
|
||||
frappe.throw(_("Lead Owner cannot be same as the Lead Email Address"))
|
||||
|
||||
if self.is_new() or not self.image:
|
||||
self.image = has_gravatar(self.email_id)
|
||||
|
||||
def link_to_contact(self):
|
||||
# update contact links
|
||||
if self.contact_doc:
|
||||
@@ -471,7 +468,7 @@ def get_lead_details(lead, posting_date=None, company=None, doctype=None):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_lead_from_communication(communication, ignore_communication_links=False):
|
||||
def make_lead_from_communication(communication: str, ignore_communication_links: bool = False):
|
||||
"""raise a issue from email"""
|
||||
|
||||
doc = frappe.get_doc("Communication", communication)
|
||||
@@ -490,7 +487,6 @@ def make_lead_from_communication(communication, ignore_communication_links=False
|
||||
}
|
||||
)
|
||||
lead.flags.ignore_mandatory = True
|
||||
lead.flags.ignore_permissions = True
|
||||
lead.insert()
|
||||
|
||||
lead_name = lead.name
|
||||
@@ -523,7 +519,7 @@ def get_lead_with_phone_number(number):
|
||||
def add_lead_to_prospect(lead, prospect):
|
||||
prospect = frappe.get_doc("Prospect", prospect)
|
||||
prospect.append("leads", {"lead": lead})
|
||||
prospect.save(ignore_permissions=True)
|
||||
prospect.save()
|
||||
|
||||
carry_forward_communication_and_comments = frappe.db.get_single_value(
|
||||
"CRM Settings", "carry_forward_communication_and_comments"
|
||||
|
||||
@@ -522,7 +522,9 @@ def auto_close_opportunity():
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_opportunity_from_communication(communication, company, ignore_communication_links=False):
|
||||
def make_opportunity_from_communication(
|
||||
communication: str, company: str, ignore_communication_links: bool = False
|
||||
):
|
||||
from erpnext.crm.doctype.lead.lead import make_lead_from_communication
|
||||
|
||||
doc = frappe.get_doc("Communication", communication)
|
||||
@@ -540,7 +542,7 @@ def make_opportunity_from_communication(communication, company, ignore_communica
|
||||
"opportunity_from": opportunity_from,
|
||||
"party_name": lead,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
).insert()
|
||||
|
||||
link_communication_to_document(doc, "Opportunity", opportunity.name, ignore_communication_links)
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ def make_opportunity(source_name, target_doc=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_opportunities(prospect):
|
||||
return frappe.get_all(
|
||||
return frappe.get_list(
|
||||
"Opportunity",
|
||||
filters={"opportunity_from": "Prospect", "party_name": prospect},
|
||||
fields=[
|
||||
|
||||
@@ -2,50 +2,23 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_custom_fields_for_frappe_crm():
|
||||
frappe.only_for("System Manager")
|
||||
custom_fields = {
|
||||
"Quotation": [
|
||||
{
|
||||
"fieldname": "crm_deal",
|
||||
"fieldtype": "Data",
|
||||
"label": "Frappe CRM Deal",
|
||||
"insert_after": "party_name",
|
||||
}
|
||||
],
|
||||
"Customer": [
|
||||
{
|
||||
"fieldname": "crm_deal",
|
||||
"fieldtype": "Data",
|
||||
"label": "Frappe CRM Deal",
|
||||
"insert_after": "prospect_name",
|
||||
}
|
||||
],
|
||||
}
|
||||
create_custom_fields(custom_fields, ignore_validate=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_prospect_against_crm_deal():
|
||||
validate_frappe_crm_sync()
|
||||
|
||||
doc = frappe.form_dict
|
||||
prospect = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Prospect",
|
||||
"company_name": doc.organization or doc.lead_name,
|
||||
"no_of_employees": doc.no_of_employees,
|
||||
"prospect_owner": doc.deal_owner,
|
||||
"company": doc.erpnext_company,
|
||||
"crm_deal": doc.crm_deal,
|
||||
"territory": doc.territory,
|
||||
"industry": doc.industry,
|
||||
"website": doc.website,
|
||||
"annual_revenue": doc.annual_revenue,
|
||||
}
|
||||
)
|
||||
prospect = frappe.new_doc("Prospect")
|
||||
prospect.company_name = doc.organization or doc.lead_name
|
||||
prospect.no_of_employees = doc.no_of_employees
|
||||
prospect.prospect_owner = doc.deal_owner
|
||||
prospect.company = doc.erpnext_company
|
||||
prospect.crm_deal = doc.crm_deal
|
||||
prospect.territory = doc.territory
|
||||
prospect.industry = doc.industry
|
||||
prospect.website = doc.website
|
||||
prospect.annual_revenue = doc.annual_revenue
|
||||
|
||||
try:
|
||||
prospect_name = frappe.db.get_value("Prospect", {"company_name": prospect.company_name})
|
||||
@@ -151,17 +124,33 @@ def contact_exists(email, mobile_no):
|
||||
return False
|
||||
|
||||
|
||||
CUSTOMER_ALLOWED_FIELDS = {
|
||||
"customer_name",
|
||||
"customer_group",
|
||||
"customer_type",
|
||||
"territory",
|
||||
"default_currency",
|
||||
"industry",
|
||||
"website",
|
||||
"crm_deal",
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_customer(customer_data=None):
|
||||
validate_frappe_crm_sync()
|
||||
|
||||
if not customer_data:
|
||||
customer_data = frappe.form_dict
|
||||
|
||||
try:
|
||||
customer_name = frappe.db.exists("Customer", {"customer_name": customer_data.get("customer_name")})
|
||||
if not customer_name:
|
||||
customer = frappe.get_doc({"doctype": "Customer", **customer_data}).insert(
|
||||
ignore_permissions=True
|
||||
)
|
||||
customer = frappe.new_doc("Customer")
|
||||
for field in CUSTOMER_ALLOWED_FIELDS:
|
||||
if customer_data.get(field) is not None:
|
||||
customer.set(field, customer_data.get(field))
|
||||
customer.insert(ignore_permissions=True)
|
||||
customer_name = customer.name
|
||||
|
||||
contacts = json.loads(customer_data.get("contacts"))
|
||||
@@ -171,3 +160,21 @@ def create_customer(customer_data=None):
|
||||
except Exception:
|
||||
frappe.log_error(frappe.get_traceback(), "Error while creating customer against Frappe CRM Deal")
|
||||
pass
|
||||
|
||||
|
||||
def validate_frappe_crm_sync():
|
||||
CRMSettings = frappe.get_single("CRM Settings")
|
||||
if not CRMSettings.enable_frappe_crm_data_synchronization:
|
||||
frappe.throw(
|
||||
_("Frappe CRM data synchronization is not enabled on ERPNext. Contact System Manager of ERPNext.")
|
||||
)
|
||||
|
||||
allowed_users = [d.user for d in CRMSettings.allowed_users]
|
||||
|
||||
if frappe.session.user not in allowed_users:
|
||||
frappe.throw(
|
||||
_(
|
||||
"User not allowed to synchronize data from Frappe CRM on ERPNext. Contact System Manager of ERPNext."
|
||||
),
|
||||
exc=frappe.PermissionError,
|
||||
)
|
||||
|
||||
@@ -5,6 +5,11 @@ from frappe.utils import cstr, now, today
|
||||
from pypika import functions
|
||||
|
||||
|
||||
def disable_opportunity_creation_on_contact_us_disabled(doc, method):
|
||||
if doc.is_disabled:
|
||||
frappe.db.set_single_value("CRM Settings", "enable_opportunity_creation_from_contact_us", 0)
|
||||
|
||||
|
||||
def update_lead_phone_numbers(contact, method):
|
||||
if contact.phone_nos:
|
||||
contact_lead = contact.get_link_for("Lead")
|
||||
|
||||
@@ -355,6 +355,9 @@ doc_events = {
|
||||
"Event": {
|
||||
"after_insert": "erpnext.crm.utils.link_events_with_prospect",
|
||||
},
|
||||
"Contact Us Settings": {
|
||||
"on_update": "erpnext.crm.utils.disable_opportunity_creation_on_contact_us_disabled",
|
||||
},
|
||||
"Sales Invoice": {
|
||||
"on_submit": [
|
||||
"erpnext.regional.create_transaction_log",
|
||||
|
||||
@@ -75,6 +75,9 @@ frappe.ui.form.on("BOM", {
|
||||
|
||||
with_operations: function (frm) {
|
||||
frm.set_df_property("fg_based_operating_cost", "hidden", frm.doc.with_operations ? 1 : 0);
|
||||
if (frm.doc.routing && frm.doc.with_operations && !frm.doc.operations.length) {
|
||||
frm.trigger("routing");
|
||||
}
|
||||
},
|
||||
|
||||
fg_based_operating_cost: function (frm) {
|
||||
@@ -438,7 +441,7 @@ frappe.ui.form.on("BOM", {
|
||||
},
|
||||
|
||||
routing(frm) {
|
||||
if (frm.doc.routing && frm.doc.with_operations && !frm.doc.operations) {
|
||||
if (frm.doc.routing && frm.doc.with_operations && !frm.doc.operations.length) {
|
||||
frappe.call({
|
||||
doc: frm.doc,
|
||||
method: "get_routing",
|
||||
|
||||
@@ -152,6 +152,7 @@ class BOMCreator(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_boms(self):
|
||||
self.check_permission("submit")
|
||||
self.submit()
|
||||
|
||||
def set_rate_for_items(self):
|
||||
@@ -209,10 +210,14 @@ class BOMCreator(Document):
|
||||
frappe.throw(_("Please set {0} in BOM Creator {1}").format(_(label), self.name))
|
||||
|
||||
def on_submit(self):
|
||||
self.enqueue_create_boms()
|
||||
self.enqueue_bom_creation()
|
||||
|
||||
@frappe.whitelist()
|
||||
def enqueue_create_boms(self):
|
||||
self.check_permission("submit")
|
||||
self.enqueue_bom_creation()
|
||||
|
||||
def enqueue_bom_creation(self):
|
||||
frappe.enqueue(
|
||||
self.create_boms,
|
||||
queue="short",
|
||||
@@ -281,6 +286,21 @@ class BOMCreator(Document):
|
||||
|
||||
frappe.msgprint(_("BOMs creation failed"))
|
||||
|
||||
@frappe.whitelist()
|
||||
def edit_qty(self, docname: str, qty: float):
|
||||
if not frappe.db.exists("BOM Creator Item", {"name": docname, "parent": self.name}):
|
||||
frappe.throw(_("BOM Creator Item {0} does not exist").format(docname))
|
||||
|
||||
for row in self.items:
|
||||
if row.name == docname:
|
||||
row.qty = flt(qty)
|
||||
break
|
||||
|
||||
self.set_rate_for_items()
|
||||
self.save()
|
||||
|
||||
return self
|
||||
|
||||
def create_bom(self, row, production_item_wise_rm):
|
||||
bom_creator_item = row.name if row.name != self.name else ""
|
||||
if frappe.db.exists(
|
||||
@@ -336,18 +356,157 @@ class BOMCreator(Document):
|
||||
production_item_wise_rm[(row.item_code, row.name)].bom_no = bom.name
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_default_bom(self, item_code) -> str:
|
||||
def get_default_bom(self, item_code: str) -> str:
|
||||
self.check_permission("read")
|
||||
return frappe.get_cached_value("Item", item_code, "default_bom")
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_item(self, **kwargs):
|
||||
if isinstance(kwargs, str):
|
||||
kwargs = frappe.parse_json(kwargs)
|
||||
|
||||
if isinstance(kwargs, dict):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
item_info = get_item_details(kwargs.item_code)
|
||||
|
||||
parent_row_no = ""
|
||||
if kwargs.fg_reference_id and self.name != kwargs.fg_reference_id:
|
||||
parent_row_no = get_parent_row_no(self, kwargs.fg_reference_id)
|
||||
|
||||
kwargs.update(
|
||||
{
|
||||
"uom": item_info.stock_uom,
|
||||
"stock_uom": item_info.stock_uom,
|
||||
"conversion_factor": 1,
|
||||
}
|
||||
)
|
||||
|
||||
if parent_row_no:
|
||||
kwargs.update({"parent_row_no": parent_row_no})
|
||||
|
||||
for key in BOM_ITEM_FIELDS:
|
||||
if key not in kwargs:
|
||||
kwargs[key] = ""
|
||||
|
||||
self.append("items", kwargs)
|
||||
self.save()
|
||||
|
||||
return self
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_sub_assembly(self, **kwargs):
|
||||
if isinstance(kwargs, str):
|
||||
kwargs = frappe.parse_json(kwargs)
|
||||
|
||||
if isinstance(kwargs, dict):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
bom_item = frappe.parse_json(kwargs.bom_item)
|
||||
|
||||
name = kwargs.fg_reference_id
|
||||
parent_row_no = ""
|
||||
if not kwargs.convert_to_sub_assembly:
|
||||
item_info = get_item_details(bom_item.item_code)
|
||||
parent_row_no = get_parent_row_no(self, kwargs.fg_reference_id)
|
||||
|
||||
item_row = self.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": bom_item.item_code,
|
||||
"qty": bom_item.qty,
|
||||
"uom": item_info.stock_uom,
|
||||
"fg_item": kwargs.fg_item,
|
||||
"conversion_factor": 1,
|
||||
"parent_row_no": parent_row_no,
|
||||
"fg_reference_id": name,
|
||||
"stock_qty": bom_item.qty,
|
||||
"do_not_explode": 1,
|
||||
"is_expandable": 1,
|
||||
"stock_uom": item_info.stock_uom,
|
||||
"allow_alternative_item": kwargs.allow_alternative_item,
|
||||
},
|
||||
)
|
||||
|
||||
parent_row_no = item_row.idx
|
||||
name = ""
|
||||
else:
|
||||
parent_row_no = get_parent_row_no(self, kwargs.fg_reference_id)
|
||||
|
||||
for row in bom_item.get("items"):
|
||||
row = frappe._dict(row)
|
||||
item_info = get_item_details(row.item_code)
|
||||
self.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": row.item_code,
|
||||
"qty": row.qty,
|
||||
"fg_item": bom_item.item_code,
|
||||
"uom": item_info.stock_uom,
|
||||
"fg_reference_id": name,
|
||||
"parent_row_no": parent_row_no,
|
||||
"conversion_factor": 1,
|
||||
"do_not_explode": 1,
|
||||
"stock_qty": row.qty,
|
||||
"stock_uom": item_info.stock_uom,
|
||||
},
|
||||
)
|
||||
|
||||
self.save()
|
||||
|
||||
return self
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_node(self, **kwargs):
|
||||
if isinstance(kwargs, str):
|
||||
kwargs = frappe.parse_json(kwargs)
|
||||
|
||||
if isinstance(kwargs, dict):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
updated = False
|
||||
if kwargs.docname:
|
||||
row = next((row for row in self.items if row.name == kwargs.docname), None)
|
||||
if not row:
|
||||
frappe.throw(_("BOM Creator Item with name {0} does not exist").format(kwargs.docname))
|
||||
|
||||
row.delete()
|
||||
self.remove(row)
|
||||
updated = True
|
||||
|
||||
items = get_children(parent=kwargs.fg_item, parent_id=self.name)
|
||||
if items:
|
||||
for item in items:
|
||||
updated = True
|
||||
child_row = next((row for row in self.items if row.name == item.name), None)
|
||||
if child_row:
|
||||
child_row.delete()
|
||||
self.remove(child_row)
|
||||
|
||||
if item.expandable:
|
||||
self.delete_node(fg_item=item.value)
|
||||
|
||||
if updated:
|
||||
self.set_rate_for_items()
|
||||
self.save()
|
||||
|
||||
return self
|
||||
|
||||
return frappe._dict()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_children(doctype=None, parent=None, **kwargs):
|
||||
def get_children(doctype: str | None = None, parent: str | None = None, **kwargs):
|
||||
# by default get_children takes first parameter as doctype, so added in the function
|
||||
|
||||
if isinstance(kwargs, str):
|
||||
kwargs = frappe.parse_json(kwargs)
|
||||
|
||||
if isinstance(kwargs, dict):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
frappe.has_permission("BOM Creator", "read", doc=kwargs.parent_id, throw=True)
|
||||
|
||||
fields = [
|
||||
"item_code as value",
|
||||
"item_name as title",
|
||||
@@ -373,102 +532,6 @@ def get_children(doctype=None, parent=None, **kwargs):
|
||||
return frappe.get_all("BOM Creator Item", fields=fields, filters=query_filters, order_by="idx")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_item(**kwargs):
|
||||
if isinstance(kwargs, str):
|
||||
kwargs = frappe.parse_json(kwargs)
|
||||
|
||||
if isinstance(kwargs, dict):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
doc = frappe.get_doc("BOM Creator", kwargs.parent)
|
||||
item_info = get_item_details(kwargs.item_code)
|
||||
|
||||
parent_row_no = ""
|
||||
if kwargs.fg_reference_id and doc.name != kwargs.fg_reference_id:
|
||||
parent_row_no = get_parent_row_no(doc, kwargs.fg_reference_id)
|
||||
|
||||
kwargs.update(
|
||||
{
|
||||
"uom": item_info.stock_uom,
|
||||
"stock_uom": item_info.stock_uom,
|
||||
"conversion_factor": 1,
|
||||
}
|
||||
)
|
||||
|
||||
if parent_row_no:
|
||||
kwargs.update({"parent_row_no": parent_row_no})
|
||||
|
||||
doc.append("items", kwargs)
|
||||
doc.save()
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_sub_assembly(**kwargs):
|
||||
if isinstance(kwargs, str):
|
||||
kwargs = frappe.parse_json(kwargs)
|
||||
|
||||
if isinstance(kwargs, dict):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
doc = frappe.get_doc("BOM Creator", kwargs.parent)
|
||||
bom_item = frappe.parse_json(kwargs.bom_item)
|
||||
|
||||
name = kwargs.fg_reference_id
|
||||
parent_row_no = ""
|
||||
if not kwargs.convert_to_sub_assembly:
|
||||
item_info = get_item_details(bom_item.item_code)
|
||||
parent_row_no = get_parent_row_no(doc, kwargs.fg_reference_id)
|
||||
|
||||
item_row = doc.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": bom_item.item_code,
|
||||
"qty": bom_item.qty,
|
||||
"uom": item_info.stock_uom,
|
||||
"fg_item": kwargs.fg_item,
|
||||
"conversion_factor": 1,
|
||||
"parent_row_no": parent_row_no,
|
||||
"fg_reference_id": name,
|
||||
"stock_qty": bom_item.qty,
|
||||
"do_not_explode": 1,
|
||||
"is_expandable": 1,
|
||||
"stock_uom": item_info.stock_uom,
|
||||
"allow_alternative_item": kwargs.allow_alternative_item,
|
||||
},
|
||||
)
|
||||
|
||||
parent_row_no = item_row.idx
|
||||
name = ""
|
||||
else:
|
||||
parent_row_no = get_parent_row_no(doc, kwargs.fg_reference_id)
|
||||
|
||||
for row in bom_item.get("items"):
|
||||
row = frappe._dict(row)
|
||||
item_info = get_item_details(row.item_code)
|
||||
doc.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": row.item_code,
|
||||
"qty": row.qty,
|
||||
"fg_item": bom_item.item_code,
|
||||
"uom": item_info.stock_uom,
|
||||
"fg_reference_id": name,
|
||||
"parent_row_no": parent_row_no,
|
||||
"conversion_factor": 1,
|
||||
"do_not_explode": 1,
|
||||
"stock_qty": row.qty,
|
||||
"stock_uom": item_info.stock_uom,
|
||||
},
|
||||
)
|
||||
|
||||
doc.save()
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
def get_item_details(item_code):
|
||||
return frappe.get_cached_value(
|
||||
"Item", item_code, ["item_name", "description", "image", "stock_uom", "default_bom"], as_dict=1
|
||||
@@ -486,37 +549,3 @@ def get_parent_row_no(doc, name):
|
||||
frappe.msgprint(_("Parent Row No not found for {0}").format(name), alert=True)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_node(**kwargs):
|
||||
if isinstance(kwargs, str):
|
||||
kwargs = frappe.parse_json(kwargs)
|
||||
|
||||
if isinstance(kwargs, dict):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
items = get_children(parent=kwargs.fg_item, parent_id=kwargs.parent)
|
||||
if kwargs.docname:
|
||||
frappe.delete_doc("BOM Creator Item", kwargs.docname)
|
||||
|
||||
for item in items:
|
||||
frappe.delete_doc("BOM Creator Item", item.name)
|
||||
if item.expandable:
|
||||
delete_node(fg_item=item.value, parent=item.parent_id)
|
||||
|
||||
doc = frappe.get_doc("BOM Creator", kwargs.parent)
|
||||
doc.set_rate_for_items()
|
||||
doc.save()
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def edit_qty(doctype, docname, qty, parent):
|
||||
frappe.db.set_value(doctype, docname, "qty", qty)
|
||||
doc = frappe.get_doc("BOM Creator", parent)
|
||||
doc.set_rate_for_items()
|
||||
doc.save()
|
||||
|
||||
return doc
|
||||
|
||||
@@ -6,10 +6,6 @@ import random
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.manufacturing.doctype.bom_creator.bom_creator import (
|
||||
add_item,
|
||||
add_sub_assembly,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
|
||||
@@ -38,8 +34,7 @@ class TestBOMCreator(FrappeTestCase):
|
||||
conversion_rate=1,
|
||||
)
|
||||
|
||||
add_sub_assembly(
|
||||
parent=doc.name,
|
||||
doc.add_sub_assembly(
|
||||
fg_item=final_product,
|
||||
fg_reference_id=doc.name,
|
||||
bom_item={
|
||||
@@ -91,8 +86,7 @@ class TestBOMCreator(FrappeTestCase):
|
||||
conversion_rate=1,
|
||||
)
|
||||
|
||||
add_item(
|
||||
parent=doc.name,
|
||||
doc.add_item(
|
||||
fg_item=final_product,
|
||||
fg_reference_id=doc.name,
|
||||
item_code="Pedal Assembly",
|
||||
@@ -133,8 +127,7 @@ class TestBOMCreator(FrappeTestCase):
|
||||
conversion_rate=1,
|
||||
)
|
||||
|
||||
add_item(
|
||||
parent=doc.name,
|
||||
doc.add_item(
|
||||
fg_item=final_product,
|
||||
fg_reference_id=doc.name,
|
||||
item_code="Pedal Assembly",
|
||||
@@ -144,9 +137,8 @@ class TestBOMCreator(FrappeTestCase):
|
||||
doc.reload()
|
||||
self.assertEqual(doc.items[0].is_expandable, 0)
|
||||
|
||||
add_sub_assembly(
|
||||
doc.add_sub_assembly(
|
||||
convert_to_sub_assembly=1,
|
||||
parent=doc.name,
|
||||
fg_item=final_product,
|
||||
fg_reference_id=doc.items[0].name,
|
||||
bom_item={
|
||||
@@ -199,8 +191,7 @@ class TestBOMCreator(FrappeTestCase):
|
||||
conversion_rate=1,
|
||||
)
|
||||
|
||||
add_item(
|
||||
parent=doc.name,
|
||||
doc.add_item(
|
||||
fg_item=final_product,
|
||||
fg_reference_id=doc.name,
|
||||
item_code="Pedal Assembly",
|
||||
@@ -210,9 +201,8 @@ class TestBOMCreator(FrappeTestCase):
|
||||
doc.reload()
|
||||
self.assertEqual(doc.items[0].is_expandable, 0)
|
||||
|
||||
add_sub_assembly(
|
||||
doc.add_sub_assembly(
|
||||
convert_to_sub_assembly=1,
|
||||
parent=doc.name,
|
||||
fg_item=final_product,
|
||||
fg_reference_id=doc.items[0].name,
|
||||
bom_item={
|
||||
|
||||
@@ -72,8 +72,9 @@ frappe.ui.form.on("Job Card", {
|
||||
frm.toggle_enable("for_quantity", !has_stock_entry);
|
||||
|
||||
if (!frm.is_new() && has_items && frm.doc.docstatus < 2) {
|
||||
let to_request = frm.doc.for_quantity > frm.doc.transferred_qty;
|
||||
let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
|
||||
const excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
|
||||
const to_transfer = frm.doc.items.some((row) => flt(row.transferred_qty) < flt(row.required_qty));
|
||||
const to_request = to_transfer;
|
||||
|
||||
if (to_request || excess_transfer_allowed) {
|
||||
frm.add_custom_button(
|
||||
@@ -85,10 +86,6 @@ frappe.ui.form.on("Job Card", {
|
||||
);
|
||||
}
|
||||
|
||||
// check if any row has untransferred materials
|
||||
// in case of multiple items in JC
|
||||
let to_transfer = frm.doc.items.some((row) => row.transferred_qty < row.required_qty);
|
||||
|
||||
if (to_transfer || excess_transfer_allowed) {
|
||||
frm.add_custom_button(
|
||||
__("Material Transfer"),
|
||||
@@ -120,7 +117,8 @@ frappe.ui.form.on("Job Card", {
|
||||
frm.doc.docstatus == 0 &&
|
||||
!frm.is_new() &&
|
||||
(frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) &&
|
||||
(frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)
|
||||
(!frm.doc.items.length ||
|
||||
!frm.doc.items.some((row) => flt(row.transferred_qty) < flt(row.required_qty)))
|
||||
) {
|
||||
// if Job Card is link to Work Order, the job card must not be able to start if Work Order not "Started"
|
||||
// and if stock mvt for WIP is required
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"no_copy": 1,
|
||||
"options": "Open\nWork In Progress\nMaterial Transferred\nOn Hold\nSubmitted\nCancelled\nCompleted",
|
||||
"options": "Open\nWork In Progress\nPartially Transferred\nMaterial Transferred\nOn Hold\nSubmitted\nCancelled\nCompleted",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -513,7 +513,7 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-05-12 12:17:17.750857",
|
||||
"modified": "2026-06-22 11:51:16.526778",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card",
|
||||
|
||||
@@ -107,6 +107,7 @@ class JobCard(Document):
|
||||
status: DF.Literal[
|
||||
"Open",
|
||||
"Work In Progress",
|
||||
"Partially Transferred",
|
||||
"Material Transferred",
|
||||
"On Hold",
|
||||
"Submitted",
|
||||
@@ -927,6 +928,8 @@ class JobCard(Document):
|
||||
|
||||
frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty))
|
||||
|
||||
self.set_status(update_status=True)
|
||||
|
||||
def set_transferred_qty(self, update_status=False):
|
||||
"Set total FG Qty in Job Card for which RM was transferred."
|
||||
if not self.items:
|
||||
@@ -980,7 +983,22 @@ class JobCard(Document):
|
||||
self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
|
||||
|
||||
if self.docstatus < 2:
|
||||
if flt(self.for_quantity) <= flt(self.transferred_qty):
|
||||
if self.items:
|
||||
item_data = frappe.get_all(
|
||||
"Job Card Item",
|
||||
filters={"parent": self.name},
|
||||
fields=["transferred_qty", "required_qty"],
|
||||
)
|
||||
all_transferred = item_data and all(
|
||||
flt(d.transferred_qty) >= flt(d.required_qty) for d in item_data
|
||||
)
|
||||
any_transferred = any(flt(d.transferred_qty) > 0 for d in item_data)
|
||||
|
||||
if all_transferred:
|
||||
self.status = "Material Transferred"
|
||||
elif any_transferred:
|
||||
self.status = "Partially Transferred"
|
||||
elif flt(self.for_quantity) <= flt(self.transferred_qty):
|
||||
self.status = "Material Transferred"
|
||||
|
||||
if self.time_logs:
|
||||
@@ -1224,12 +1242,13 @@ def time_diff_in_minutes(string_ed_date, string_st_date):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_job_details(start, end, filters=None):
|
||||
def get_job_details(start: str, end: str, filters: str | None = None):
|
||||
events = []
|
||||
|
||||
event_color = {
|
||||
"Completed": "#cdf5a6",
|
||||
"Material Transferred": "#ffdd9e",
|
||||
"Partially Transferred": "#ffe5b4",
|
||||
"Work In Progress": "#D3D3D3",
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ frappe.listview_settings["Job Card"] = {
|
||||
Completed: "green",
|
||||
Cancelled: "red",
|
||||
"Material Transferred": "blue",
|
||||
"Partially Transferred": "yellow",
|
||||
Open: "red",
|
||||
};
|
||||
const status = doc.status || "Open";
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Item Code",
|
||||
"options": "Item"
|
||||
"options": "Item",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "source_warehouse",
|
||||
@@ -107,7 +108,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-05-12 12:22:18.506904",
|
||||
"modified": "2026-06-23 16:52:37.669110",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card Item",
|
||||
|
||||
@@ -16,7 +16,7 @@ class JobCardItem(Document):
|
||||
|
||||
allow_alternative_item: DF.Check
|
||||
description: DF.Text | None
|
||||
item_code: DF.Link | None
|
||||
item_code: DF.Link
|
||||
item_group: DF.Link | None
|
||||
item_name: DF.Data | None
|
||||
parent: DF.Data
|
||||
|
||||
@@ -1461,6 +1461,68 @@ class TestWorkOrder(FrappeTestCase):
|
||||
self.assertEqual(work_order.required_items[0].transferred_qty, 1)
|
||||
self.assertEqual(work_order.required_items[1].transferred_qty, 2)
|
||||
|
||||
def test_material_transferred_min_fraction_on_partial_pick_list(self):
|
||||
"""Pick-list flow (fg_completed_qty = 0): 'Material Transferred for Manufacturing'
|
||||
must reflect the least-transferred required item (the bottleneck), instead of being
|
||||
marked fully transferred prematurely when only some materials are transferred.
|
||||
"""
|
||||
work_order = make_wo_order_test_record(planned_start_date=now(), qty=2)
|
||||
test_stock_entry.make_stock_entry(
|
||||
item_code="_Test Item", target="_Test Warehouse - _TC", qty=10, basic_rate=5000.0
|
||||
)
|
||||
test_stock_entry.make_stock_entry(
|
||||
item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=10, basic_rate=1000.0
|
||||
)
|
||||
|
||||
required_qty = {row.item_code: flt(row.required_qty) for row in work_order.required_items}
|
||||
|
||||
# pick-list transfer: For Quantity = 0
|
||||
transfer_entry = frappe.get_doc(
|
||||
make_stock_entry(work_order.name, "Material Transfer for Manufacture", 0)
|
||||
)
|
||||
self.assertEqual(transfer_entry.fg_completed_qty, 0.0)
|
||||
|
||||
for item in transfer_entry.items:
|
||||
full_qty = required_qty[item.item_code]
|
||||
item.qty = full_qty if item.item_code == "_Test Item" else full_qty / 2
|
||||
item.transfer_qty = item.qty
|
||||
transfer_entry.submit()
|
||||
|
||||
work_order.reload()
|
||||
transferred_qty = {row.item_code: flt(row.transferred_qty) for row in work_order.required_items}
|
||||
self.assertEqual(transferred_qty["_Test Item"], required_qty["_Test Item"])
|
||||
self.assertEqual(
|
||||
transferred_qty["_Test Item Home Desktop 100"],
|
||||
required_qty["_Test Item Home Desktop 100"] / 2,
|
||||
)
|
||||
# bottleneck fraction = 0.5 -> 0.5 * qty(2) = 1.0
|
||||
self.assertEqual(work_order.material_transferred_for_manufacturing, 1.0)
|
||||
|
||||
def test_material_transferred_full_via_pick_list_flow(self):
|
||||
"""Pick-list flow with every required item fully transferred marks the work order
|
||||
as fully transferred (min fraction = 1.0)."""
|
||||
work_order = make_wo_order_test_record(planned_start_date=now(), qty=2)
|
||||
test_stock_entry.make_stock_entry(
|
||||
item_code="_Test Item", target="_Test Warehouse - _TC", qty=10, basic_rate=5000.0
|
||||
)
|
||||
test_stock_entry.make_stock_entry(
|
||||
item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=10, basic_rate=1000.0
|
||||
)
|
||||
|
||||
required_qty = {row.item_code: flt(row.required_qty) for row in work_order.required_items}
|
||||
|
||||
transfer_entry = frappe.get_doc(
|
||||
make_stock_entry(work_order.name, "Material Transfer for Manufacture", 0)
|
||||
)
|
||||
self.assertEqual(transfer_entry.fg_completed_qty, 0.0)
|
||||
for item in transfer_entry.items:
|
||||
item.qty = required_qty[item.item_code]
|
||||
item.transfer_qty = item.qty
|
||||
transfer_entry.submit()
|
||||
|
||||
work_order.reload()
|
||||
self.assertEqual(work_order.material_transferred_for_manufacturing, 2.0)
|
||||
|
||||
def test_backflushed_batch_raw_materials_based_on_transferred(self):
|
||||
frappe.db.set_single_value(
|
||||
"Manufacturing Settings",
|
||||
@@ -3631,6 +3693,58 @@ class TestWorkOrder(FrappeTestCase):
|
||||
|
||||
self.assertEqual(bin1_at_completion.reserved_qty_for_production, 0)
|
||||
|
||||
@change_settings(
|
||||
"Manufacturing Settings",
|
||||
{"allow_editing_of_items_and_quantities_in_work_order": 1},
|
||||
)
|
||||
def test_manufacture_se_fetches_edited_qty_from_work_order(self):
|
||||
"""When a raw material qty is edited on the Work Order, the Manufacture Stock Entry
|
||||
must consume the edited quantity (scaled to fg_completed_qty) from the Work Order,
|
||||
not the original BOM quantity."""
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
wo_order = make_wo_order_test_record(
|
||||
item="_Test FG Item", qty=10, skip_transfer=1, source_warehouse=warehouse
|
||||
)
|
||||
|
||||
# edit a required item's qty
|
||||
wo_order.required_items[0].db_set("required_qty", flt(wo_order.required_items[0].required_qty) + 7)
|
||||
wo_order.reload()
|
||||
edited_row = wo_order.required_items[0]
|
||||
|
||||
fg_qty = 5
|
||||
se = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", fg_qty))
|
||||
se_qty = {row.item_code: row.qty for row in se.items if row.s_warehouse}
|
||||
|
||||
precision = frappe.get_precision("Stock Entry Detail", "qty")
|
||||
expected = flt(edited_row.required_qty / wo_order.qty * fg_qty, precision)
|
||||
self.assertEqual(flt(se_qty.get(edited_row.item_code)), expected)
|
||||
|
||||
@change_settings(
|
||||
"Manufacturing Settings",
|
||||
{"allow_editing_of_items_and_quantities_in_work_order": 1},
|
||||
)
|
||||
def test_manufacture_se_fetches_item_not_in_bom_from_work_order(self):
|
||||
"""A raw material that is present on the Work Order but not on the BOM must still be
|
||||
fetched into the Manufacture Stock Entry, proving items are sourced from the Work
|
||||
Order's required_items rather than re-derived from the BOM."""
|
||||
extra_item = make_item(
|
||||
"_Test WO Extra Raw Material", {"is_stock_item": 1, "valuation_rate": 100}
|
||||
).name
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
wo_order = make_wo_order_test_record(
|
||||
item="_Test FG Item", qty=10, skip_transfer=1, source_warehouse=warehouse
|
||||
)
|
||||
|
||||
original_item = wo_order.required_items[0].item_code
|
||||
wo_order.required_items[0].db_set("item_code", extra_item)
|
||||
wo_order.reload()
|
||||
|
||||
se = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 5))
|
||||
se_items = [row.item_code for row in se.items if row.s_warehouse]
|
||||
|
||||
self.assertIn(extra_item, se_items)
|
||||
self.assertNotIn(original_item, se_items)
|
||||
|
||||
|
||||
def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||
|
||||
@@ -979,17 +979,24 @@ erpnext.work_order = {
|
||||
},
|
||||
|
||||
create_pick_list: function (frm, purpose = "Material Transfer for Manufacture") {
|
||||
this.show_prompt_for_qty_input(frm, purpose)
|
||||
.then((data) => {
|
||||
return frappe.xcall("erpnext.manufacturing.doctype.work_order.work_order.create_pick_list", {
|
||||
const max = this.get_max_transferable_qty(frm, purpose);
|
||||
|
||||
const get_pick_list = (for_qty) =>
|
||||
frappe
|
||||
.xcall("erpnext.manufacturing.doctype.work_order.work_order.create_pick_list", {
|
||||
source_name: frm.doc.name,
|
||||
for_qty: data.qty,
|
||||
for_qty: for_qty,
|
||||
})
|
||||
.then((pick_list) => {
|
||||
frappe.model.sync(pick_list);
|
||||
frappe.set_route("Form", pick_list.doctype, pick_list.name);
|
||||
});
|
||||
})
|
||||
.then((pick_list) => {
|
||||
frappe.model.sync(pick_list);
|
||||
frappe.set_route("Form", pick_list.doctype, pick_list.name);
|
||||
});
|
||||
|
||||
if (max <= 0) {
|
||||
get_pick_list(frm.doc.qty);
|
||||
} else {
|
||||
this.show_prompt_for_qty_input(frm, purpose).then((data) => get_pick_list(data.qty));
|
||||
}
|
||||
},
|
||||
|
||||
make_consumption_se: function (frm, backflush_raw_materials_based_on) {
|
||||
|
||||
@@ -1241,6 +1241,36 @@ class WorkOrder(Document):
|
||||
"transferred_qty", (transferred_items.get(row.item_code) or 0.0), update_modified=False
|
||||
)
|
||||
|
||||
self.recompute_material_transferred_for_manufacturing(transferred_items)
|
||||
|
||||
def recompute_material_transferred_for_manufacturing(self, transferred_items):
|
||||
"""Set material_transferred_for_manufacturing based on actual item-level transfers, not fg_completed_qty."""
|
||||
# When fg_completed_qty > 0 (direct stock entries, excess transfer), preserve the
|
||||
# SUM(fg_completed_qty) approach so excess-transfer tracking works correctly.
|
||||
sum_fg_completed_qty = self.get_transferred_or_manufactured_qty("Material Transfer for Manufacture")
|
||||
if sum_fg_completed_qty:
|
||||
self.db_set("material_transferred_for_manufacturing", sum_fg_completed_qty)
|
||||
return
|
||||
|
||||
# Pick list flow sets fg_completed_qty=0; use min-fraction of actual item transfers
|
||||
# so partial availability does not prematurely mark the work order as fully transferred.
|
||||
required_by_item = {}
|
||||
for row in self.required_items:
|
||||
if not row.include_item_in_manufacturing or flt(row.required_qty) <= 0:
|
||||
continue
|
||||
required_by_item[row.item_code] = required_by_item.get(row.item_code, 0.0) + flt(row.required_qty)
|
||||
|
||||
if not required_by_item:
|
||||
return
|
||||
|
||||
min_fraction = min(
|
||||
flt(transferred_items.get(item_code) or 0) / required_qty
|
||||
for item_code, required_qty in required_by_item.items()
|
||||
)
|
||||
min_fraction = min(min_fraction, 1.0)
|
||||
material_transferred = min_fraction * flt(self.qty)
|
||||
self.db_set("material_transferred_for_manufacturing", material_transferred)
|
||||
|
||||
def update_returned_qty(self):
|
||||
ste = frappe.qb.DocType("Stock Entry")
|
||||
ste_child = frappe.qb.DocType("Stock Entry Detail")
|
||||
|
||||
@@ -19,6 +19,7 @@ from frappe.utils import (
|
||||
time_diff_in_seconds,
|
||||
to_timedelta,
|
||||
)
|
||||
from frappe.utils.data import DateTimeLikeObject
|
||||
|
||||
from erpnext.support.doctype.issue.issue import get_holidays
|
||||
|
||||
@@ -65,7 +66,7 @@ class Workstation(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def before_save(self):
|
||||
self.set_data_based_on_workstation_type()
|
||||
self._set_data_based_on_workstation_type()
|
||||
self.set_hour_rate()
|
||||
self.set_total_working_hours()
|
||||
|
||||
@@ -92,6 +93,10 @@ class Workstation(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_data_based_on_workstation_type(self):
|
||||
self.check_permission("write")
|
||||
self._set_data_based_on_workstation_type()
|
||||
|
||||
def _set_data_based_on_workstation_type(self):
|
||||
if self.workstation_type:
|
||||
fields = [
|
||||
"hour_rate_labour",
|
||||
@@ -166,23 +171,27 @@ class Workstation(Document):
|
||||
return schedule_date
|
||||
|
||||
@frappe.whitelist()
|
||||
def start_job(self, job_card, from_time, employee):
|
||||
def start_job(self, job_card: str, from_time: DateTimeLikeObject, employee: str):
|
||||
doc = frappe.get_doc("Job Card", job_card)
|
||||
doc.check_permission("write")
|
||||
|
||||
doc.append("time_logs", {"from_time": from_time, "employee": employee})
|
||||
doc.save(ignore_permissions=True)
|
||||
doc.save()
|
||||
|
||||
return doc
|
||||
|
||||
@frappe.whitelist()
|
||||
def complete_job(self, job_card, qty, to_time):
|
||||
def complete_job(self, job_card: str, qty: float, to_time: DateTimeLikeObject):
|
||||
doc = frappe.get_doc("Job Card", job_card)
|
||||
doc.check_permission("submit")
|
||||
|
||||
for row in doc.time_logs:
|
||||
if not row.to_time:
|
||||
row.to_time = to_time
|
||||
row.time_in_mins = time_diff_in_hours(row.to_time, row.from_time) / 60
|
||||
row.completed_qty = qty
|
||||
|
||||
doc.save(ignore_permissions=True)
|
||||
doc.save()
|
||||
doc.submit()
|
||||
|
||||
return doc
|
||||
@@ -364,6 +373,8 @@ def check_workstation_for_holiday(workstation, from_datetime, to_datetime):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_workstations(**kwargs):
|
||||
frappe.has_permission("Workstation", "read", throw=True)
|
||||
|
||||
kwargs = frappe._dict(kwargs)
|
||||
_workstation = frappe.qb.DocType("Workstation")
|
||||
|
||||
|
||||
@@ -96,8 +96,8 @@ erpnext.BOMComparisonTool = class BOMComparisonTool {
|
||||
return `
|
||||
<tr>
|
||||
<td>${frappe.meta.get_label(doctype, fieldname)}</td>
|
||||
<td>${value1}</td>
|
||||
<td>${value2}</td>
|
||||
<td>${frappe.utils.escape_html(cstr(value1))}</td>
|
||||
<td>${frappe.utils.escape_html(cstr(value2))}</td>
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
@@ -138,13 +138,17 @@ erpnext.BOMComparisonTool = class BOMComparisonTool {
|
||||
.map((change, i) => {
|
||||
let [fieldname, value1, value2] = change;
|
||||
let th =
|
||||
i === 0 ? `<th rowspan="${values_changed.length}">${item_code}</th>` : "";
|
||||
i === 0
|
||||
? `<th rowspan="${values_changed.length}">${frappe.utils.escape_html(
|
||||
cstr(item_code)
|
||||
)}</th>`
|
||||
: "";
|
||||
return `
|
||||
<tr>
|
||||
${th}
|
||||
<td>${frappe.meta.get_label(child_doctype, fieldname)}</td>
|
||||
<td>${value1}</td>
|
||||
<td>${value2}</td>
|
||||
<td>${frappe.utils.escape_html(cstr(value1))}</td>
|
||||
<td>${frappe.utils.escape_html(cstr(value2))}</td>
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
@@ -177,7 +181,9 @@ erpnext.BOMComparisonTool = class BOMComparisonTool {
|
||||
let html = rows
|
||||
.map((row) => {
|
||||
let [, doc] = row;
|
||||
let cells = fields.map((df) => `<td>${doc[df.fieldname]}</td>`).join("");
|
||||
let cells = fields
|
||||
.map((df) => `<td>${frappe.utils.escape_html(cstr(doc[df.fieldname]))}</td>`)
|
||||
.join("");
|
||||
return `<tr>${cells}</tr>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
@@ -436,3 +436,5 @@ erpnext.patches.v16_0.depends_on_inv_dimensions
|
||||
erpnext.patches.v16_0.clear_procedures_from_receivable_report
|
||||
erpnext.patches.v16_0.migrate_address_contact_custom_fields
|
||||
erpnext.patches.v15_0.set_main_item_code_in_material_request_plan_item
|
||||
erpnext.patches.v16_0.set_posting_datetime_for_sabb_and_drop_indexes
|
||||
execute:frappe.db.set_single_value("Accounts Settings", "pcv_job_timeout", 3600)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import click
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE `tabSerial and Batch Bundle`
|
||||
JOIN `tabStock Ledger Entry`
|
||||
ON `tabSerial and Batch Bundle`.`name` = `tabStock Ledger Entry`.`serial_and_batch_bundle`
|
||||
SET `tabSerial and Batch Bundle`.`posting_datetime` = `tabStock Ledger Entry`.`posting_datetime`
|
||||
WHERE `tabStock Ledger Entry`.`is_cancelled` = 0
|
||||
"""
|
||||
)
|
||||
|
||||
drop_indexes()
|
||||
|
||||
|
||||
def drop_indexes():
|
||||
table = "tabSerial and Batch Bundle"
|
||||
index_list = ["voucher_no_index", "item_code_index", "warehouse_index", "company_index"]
|
||||
|
||||
for index in index_list:
|
||||
if not frappe.db.has_index(table, index):
|
||||
continue
|
||||
|
||||
try:
|
||||
frappe.db.sql_ddl(f"ALTER TABLE `{table}` DROP INDEX `{index}`")
|
||||
click.echo(f"✓ dropped {index} index from {table}")
|
||||
except Exception:
|
||||
frappe.log_error("Failed to drop index")
|
||||
@@ -717,7 +717,7 @@ def set_project_status(project, status):
|
||||
frappe.throw(_("Status must be Cancelled or Completed"))
|
||||
|
||||
project = frappe.get_doc("Project", project)
|
||||
frappe.has_permission(doc=project, throw=True)
|
||||
project.check_permission("write")
|
||||
|
||||
for task in frappe.get_all("Task", dict(project=project.name)):
|
||||
frappe.db.set_value("Task", task.name, "status", status)
|
||||
|
||||
@@ -219,14 +219,10 @@ class BOMConfigurator {
|
||||
},
|
||||
],
|
||||
(data) => {
|
||||
if (!node.data.parent_id) {
|
||||
node.data.parent_id = this.frm.doc.name;
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_item",
|
||||
method: "add_item",
|
||||
doc: this.frm.doc,
|
||||
args: {
|
||||
parent: node.data.parent_id,
|
||||
fg_item: node.data.value,
|
||||
item_code: data.item_code,
|
||||
fg_reference_id: node.data.name || this.frm.doc.name,
|
||||
@@ -255,14 +251,10 @@ class BOMConfigurator {
|
||||
dialog.set_primary_action(__("Add"), () => {
|
||||
let bom_item = dialog.get_values();
|
||||
|
||||
if (!node.data?.parent_id) {
|
||||
node.data.parent_id = this.frm.doc.name;
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_sub_assembly",
|
||||
method: "add_sub_assembly",
|
||||
doc: this.frm.doc,
|
||||
args: {
|
||||
parent: node.data.parent_id,
|
||||
fg_item: node.data.value,
|
||||
fg_reference_id: node.data.name || this.frm.doc.name,
|
||||
bom_item: bom_item,
|
||||
@@ -357,9 +349,9 @@ class BOMConfigurator {
|
||||
let bom_item = dialog.get_values();
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_sub_assembly",
|
||||
method: "add_sub_assembly",
|
||||
doc: this.frm.doc,
|
||||
args: {
|
||||
parent: node.data.parent_id,
|
||||
fg_item: node.data.value,
|
||||
bom_item: bom_item,
|
||||
fg_reference_id: node.data.name || this.frm.doc.name,
|
||||
@@ -389,11 +381,10 @@ class BOMConfigurator {
|
||||
delete_node(node, view) {
|
||||
frappe.confirm(__("Are you sure you want to delete this Item?"), () => {
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.delete_node",
|
||||
method: "delete_node",
|
||||
doc: this.frm.doc,
|
||||
args: {
|
||||
parent: node.data.parent_id,
|
||||
fg_item: node.data.value,
|
||||
doctype: node.data.doctype,
|
||||
docname: node.data.name,
|
||||
},
|
||||
callback: (r) => {
|
||||
@@ -408,16 +399,14 @@ class BOMConfigurator {
|
||||
frappe.prompt(
|
||||
[{ label: __("Qty"), fieldname: "qty", default: qty, fieldtype: "Float", reqd: 1 }],
|
||||
(data) => {
|
||||
let doctype = node.data.doctype || this.frm.doc.doctype;
|
||||
let docname = node.data.name || this.frm.doc.name;
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.edit_qty",
|
||||
method: "edit_qty",
|
||||
doc: this.frm.doc,
|
||||
args: {
|
||||
doctype: doctype,
|
||||
docname: docname,
|
||||
qty: data.qty,
|
||||
parent: node.data.parent_id ? node.data.parent_id : this.frm.doc.name,
|
||||
},
|
||||
callback: (r) => {
|
||||
node.data.qty = data.qty;
|
||||
|
||||
@@ -207,7 +207,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
"method": "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts",
|
||||
"args": {
|
||||
"company": me.frm.doc.company,
|
||||
"account_list": frappe.flags.round_off_applicable_accounts
|
||||
"account_list": frappe.flags.round_off_applicable_accounts,
|
||||
"doc": me.frm.doc,
|
||||
},
|
||||
callback(r) {
|
||||
if (r.message) {
|
||||
|
||||
@@ -362,8 +362,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}, __("Create"));
|
||||
}
|
||||
|
||||
const inspection_type = ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"].includes(this.frm.doc.doctype)
|
||||
? "Incoming" : "Outgoing";
|
||||
const incoming_doctypes = ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"];
|
||||
const incoming_purposes = ["Manufacture", "Material Receipt"];
|
||||
const inspection_type =
|
||||
incoming_doctypes.includes(this.frm.doc.doctype) ||
|
||||
(this.frm.doc.doctype === "Stock Entry" && incoming_purposes.includes(this.frm.doc.purpose))
|
||||
? "Incoming"
|
||||
: "Outgoing";
|
||||
|
||||
let quality_inspection_field = this.frm.get_docfield("items", "quality_inspection");
|
||||
quality_inspection_field.get_route_options_for_new_doc = function(row) {
|
||||
@@ -2474,6 +2479,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
];
|
||||
|
||||
const me = this;
|
||||
const incoming_doctypes = ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"];
|
||||
const incoming_purposes = ["Manufacture", "Material Receipt"];
|
||||
const inspection_type =
|
||||
incoming_doctypes.includes(this.frm.doc.doctype) ||
|
||||
(this.frm.doc.doctype === "Stock Entry" && incoming_purposes.includes(this.frm.doc.purpose))
|
||||
? "Incoming"
|
||||
: "Outgoing";
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Select Items for Quality Inspection"),
|
||||
size: "extra-large",
|
||||
|
||||
@@ -93,11 +93,19 @@ $.extend(erpnext.utils, {
|
||||
]),
|
||||
"blue"
|
||||
);
|
||||
var info = company_wise_info[0];
|
||||
var is_advance = info.balance_label !== "Total Unpaid";
|
||||
var indicator_label =
|
||||
info.balance_label === "Total Advance Paid"
|
||||
? __("Total Advance Paid: {0}", [format_currency(info.balance_amount, info.currency)])
|
||||
: info.balance_label === "Total Advance Received"
|
||||
? __("Total Advance Received: {0}", [
|
||||
format_currency(info.balance_amount, info.currency),
|
||||
])
|
||||
: __("Total Unpaid: {0}", [format_currency(info.balance_amount, info.currency)]);
|
||||
frm.dashboard.add_indicator(
|
||||
__("Total Unpaid: {0}", [
|
||||
format_currency(company_wise_info[0].total_unpaid, company_wise_info[0].currency),
|
||||
]),
|
||||
company_wise_info[0].total_unpaid ? "orange" : "green"
|
||||
indicator_label,
|
||||
is_advance ? "green" : info.balance_amount ? "orange" : "green"
|
||||
);
|
||||
|
||||
if (company_wise_info[0].loyalty_points) {
|
||||
@@ -140,7 +148,14 @@ $.extend(erpnext.utils, {
|
||||
frm.dashboard.stats_area_row.addClass("flex");
|
||||
frm.dashboard.stats_area_row.css("flex-wrap", "wrap");
|
||||
|
||||
var color = info.total_unpaid ? "orange" : "green";
|
||||
var is_advance = info.balance_label !== "Total Unpaid";
|
||||
var color = is_advance ? "green" : info.balance_amount ? "orange" : "green";
|
||||
var balance_label_text =
|
||||
info.balance_label === "Total Advance Paid"
|
||||
? __("Total Advance Paid")
|
||||
: info.balance_label === "Total Advance Received"
|
||||
? __("Total Advance Received")
|
||||
: __("Total Unpaid");
|
||||
|
||||
var indicator = $(
|
||||
'<div class="flex-column col-xs-6">' +
|
||||
@@ -154,8 +169,10 @@ $.extend(erpnext.utils, {
|
||||
'<div class="badge-link small" style="margin-bottom:10px">' +
|
||||
'<span class="indicator ' +
|
||||
color +
|
||||
'">Total Unpaid: ' +
|
||||
format_currency(info.total_unpaid, info.currency) +
|
||||
'">' +
|
||||
balance_label_text +
|
||||
": " +
|
||||
format_currency(info.balance_amount, info.currency) +
|
||||
"</span></div>" +
|
||||
"</div>"
|
||||
).appendTo(frm.dashboard.stats_area_row);
|
||||
@@ -651,6 +668,7 @@ erpnext.utils.update_child_items = function (opts) {
|
||||
read_only: 0,
|
||||
disabled: 0,
|
||||
label: __("Item Code"),
|
||||
formatter: (value) => value,
|
||||
get_query: function () {
|
||||
let filters;
|
||||
if (frm.doc.doctype == "Sales Order") {
|
||||
|
||||
@@ -11,7 +11,10 @@ def get_data():
|
||||
"Bank Account": "party",
|
||||
"Subscription": "party",
|
||||
},
|
||||
"dynamic_links": {"party_name": ["Customer", "quotation_to"]},
|
||||
"dynamic_links": {
|
||||
"party_name": ["Customer", "quotation_to"],
|
||||
"party": ["Customer", "party_type"],
|
||||
},
|
||||
"transactions": [
|
||||
{"label": _("Pre Sales"), "items": ["Opportunity", "Quotation"]},
|
||||
{"label": _("Orders"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]},
|
||||
|
||||
@@ -10,6 +10,7 @@ import frappe.utils
|
||||
from frappe import _, qb
|
||||
from frappe.contacts.doctype.address.address import get_company_address
|
||||
from frappe.desk.notifications import clear_doctype_notifications
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.query_builder.functions import Sum
|
||||
@@ -20,7 +21,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
update_linked_doc,
|
||||
validate_inter_company_party,
|
||||
)
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.party import CROSS_PARTY_FIELD_NO_MAP, get_party_account
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
from erpnext.manufacturing.doctype.blanket_order.blanket_order import (
|
||||
validate_against_blanket_order,
|
||||
@@ -1332,7 +1333,9 @@ def get_events(start, end, filters=None):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_order_for_default_supplier(source_name, selected_items=None, target_doc=None):
|
||||
def make_purchase_order_for_default_supplier(
|
||||
source_name: str, selected_items: str | list | None = None, target_doc: str | Document | None = None
|
||||
):
|
||||
"""Creates Purchase Order for each Supplier. Returns a list of doc objects."""
|
||||
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
@@ -1361,7 +1364,6 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
|
||||
target.shipping_rule = ""
|
||||
target.tc_name = ""
|
||||
target.terms = ""
|
||||
target.payment_terms_template = ""
|
||||
target.payment_schedule = []
|
||||
|
||||
default_price_list = frappe.get_value("Supplier", supplier, "default_price_list")
|
||||
@@ -1418,16 +1420,7 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
|
||||
{
|
||||
"Sales Order": {
|
||||
"doctype": "Purchase Order",
|
||||
"field_no_map": [
|
||||
"address_display",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"contact_person",
|
||||
"taxes_and_charges",
|
||||
"shipping_address",
|
||||
"dispatch_address",
|
||||
],
|
||||
"field_no_map": [*CROSS_PARTY_FIELD_NO_MAP],
|
||||
"validation": {"docstatus": ["=", 1]},
|
||||
},
|
||||
"Sales Order Item": {
|
||||
@@ -1492,7 +1485,9 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
||||
def make_purchase_order(
|
||||
source_name: str, selected_items: str | list | None = None, target_doc: str | Document | None = None
|
||||
):
|
||||
if not selected_items:
|
||||
return
|
||||
|
||||
@@ -1520,7 +1515,6 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
||||
target.shipping_rule = ""
|
||||
target.tc_name = ""
|
||||
target.terms = ""
|
||||
target.payment_terms_template = ""
|
||||
target.payment_schedule = []
|
||||
|
||||
if is_drop_ship_order(target):
|
||||
@@ -1559,16 +1553,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
||||
{
|
||||
"Sales Order": {
|
||||
"doctype": "Purchase Order",
|
||||
"field_no_map": [
|
||||
"address_display",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"contact_person",
|
||||
"taxes_and_charges",
|
||||
"shipping_address",
|
||||
"dispatch_address",
|
||||
],
|
||||
"field_no_map": [*CROSS_PARTY_FIELD_NO_MAP],
|
||||
"validation": {"docstatus": ["=", 1]},
|
||||
},
|
||||
"Sales Order Item": {
|
||||
|
||||
@@ -24,6 +24,8 @@ from erpnext.selling.doctype.sales_order.sales_order import (
|
||||
create_pick_list,
|
||||
make_delivery_note,
|
||||
make_material_request,
|
||||
make_purchase_order,
|
||||
make_purchase_order_for_default_supplier,
|
||||
make_raw_material_request,
|
||||
make_sales_invoice,
|
||||
make_work_orders,
|
||||
@@ -1364,8 +1366,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
Tests if the the Product Bundles in the Items table of Sales Orders are replaced with
|
||||
their child items(from the Packed Items table) on creating a Purchase Order from it.
|
||||
"""
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order
|
||||
|
||||
product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0})
|
||||
make_item("_Test Bundle Item 1", {"is_stock_item": 1})
|
||||
make_item("_Test Bundle Item 2", {"is_stock_item": 1})
|
||||
@@ -1394,8 +1394,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
"""
|
||||
Tests if the packed item's `ordered_qty` is updated with the quantity of the Purchase Order
|
||||
"""
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order
|
||||
|
||||
product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0})
|
||||
make_item("_Test Bundle Item 1", {"is_stock_item": 1})
|
||||
make_item("_Test Bundle Item 2", {"is_stock_item": 1})
|
||||
@@ -2419,8 +2417,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertRaises(frappe.ValidationError, so1.update_status, "Draft")
|
||||
|
||||
def test_item_tax_transfer_from_sales_to_purchase(self):
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order
|
||||
|
||||
item_tax = frappe.new_doc("Item Tax Template")
|
||||
item_tax.title = "Test Item Tax Template"
|
||||
item_tax.company = "_Test Company"
|
||||
@@ -2521,6 +2517,33 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertFalse(so.per_billed)
|
||||
self.assertEqual(so.status, "To Deliver and Bill")
|
||||
|
||||
def test_make_purchase_order_does_not_inherit_party_fields(self):
|
||||
"""
|
||||
Customer-derived fields must not leak from a drop-ship SO into the PO.
|
||||
"""
|
||||
so_items = [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"warehouse": "",
|
||||
"qty": 1,
|
||||
"rate": 100,
|
||||
"delivered_by_supplier": 1,
|
||||
"supplier": "_Test Supplier",
|
||||
}
|
||||
]
|
||||
so = make_sales_order(item_list=so_items, do_not_submit=True)
|
||||
so.tax_category = "_Test Tax Category 1"
|
||||
so.language = "ar"
|
||||
so.payment_terms_template = "_Test Payment Term Template"
|
||||
so.submit()
|
||||
|
||||
po = make_purchase_order_for_default_supplier(so.name, selected_items=so_items)[0]
|
||||
|
||||
supplier = frappe.get_doc("Supplier", "_Test Supplier")
|
||||
self.assertEqual(po.tax_category or None, supplier.tax_category or None)
|
||||
self.assertEqual(po.language or None, supplier.language or None)
|
||||
self.assertEqual(po.payment_terms_template or None, supplier.payment_terms or None)
|
||||
|
||||
def test_pending_quantity_after_update_item_during_invoice_creation(self):
|
||||
so = make_sales_order(qty=30, rate=100)
|
||||
|
||||
|
||||
@@ -40,15 +40,6 @@ erpnext.PointOfSale.Controller = class {
|
||||
in_list_view: 1,
|
||||
label: __("Opening Amount"),
|
||||
options: "company:company_currency",
|
||||
onchange: function () {
|
||||
dialog.fields_dict.balance_details.df.data.some((d) => {
|
||||
if (d.idx == this.doc.idx) {
|
||||
d.opening_amount = this.value;
|
||||
dialog.fields_dict.balance_details.grid.refresh();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
const fetch_pos_payment_methods = () => {
|
||||
|
||||
@@ -20,7 +20,6 @@ field_map = {
|
||||
}
|
||||
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
columns, data = get_columns(filters), get_data(filters)
|
||||
return columns, data
|
||||
|
||||
@@ -16,7 +16,7 @@ def execute(filters=None):
|
||||
days_since_last_order = filters.get("days_since_last_order")
|
||||
doctype = filters.get("doctype")
|
||||
|
||||
if doctype not in {"Sales Order", "Sales Invoice"}:
|
||||
if doctype not in ("Sales Order", "Sales Invoice"):
|
||||
frappe.throw(_("Invalid value {0} for 'Doctype'").format(doctype))
|
||||
|
||||
if cint(days_since_last_order) <= 0:
|
||||
|
||||
@@ -427,14 +427,16 @@ class Analytics:
|
||||
break
|
||||
|
||||
def get_groups(self):
|
||||
if self.filters.tree_type == "Territory":
|
||||
parent = "parent_territory"
|
||||
if self.filters.tree_type == "Customer Group":
|
||||
parent = "parent_customer_group"
|
||||
if self.filters.tree_type == "Item Group":
|
||||
parent = "parent_item_group"
|
||||
if self.filters.tree_type == "Supplier Group":
|
||||
parent = "parent_supplier_group"
|
||||
parent_field_map = {
|
||||
"Territory": "parent_territory",
|
||||
"Customer Group": "parent_customer_group",
|
||||
"Item Group": "parent_item_group",
|
||||
"Supplier Group": "parent_supplier_group",
|
||||
}
|
||||
if self.filters.tree_type not in parent_field_map:
|
||||
frappe.throw(_("Invalid Tree Type {0}").format(self.filters.tree_type))
|
||||
|
||||
parent = parent_field_map[self.filters.tree_type]
|
||||
|
||||
self.depth_map = frappe._dict()
|
||||
|
||||
@@ -453,6 +455,9 @@ class Analytics:
|
||||
def get_teams(self):
|
||||
self.depth_map = frappe._dict()
|
||||
|
||||
if not frappe.db.exists("DocType", self.filters.doc_type):
|
||||
frappe.throw(_("Invalid Document Type {0}").format(self.filters.doc_type))
|
||||
|
||||
self.group_entries = frappe.db.sql(
|
||||
f""" select * from (select "Order Types" as name, 0 as lft,
|
||||
2 as rgt, '' as parent union select distinct order_type as name, 1 as lft, 1 as rgt, "Order Types" as parent
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint, qb
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder import Case, Criterion
|
||||
|
||||
from erpnext import get_company_currency
|
||||
|
||||
@@ -13,6 +13,8 @@ def execute(filters=None):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
validate_filters(filters)
|
||||
|
||||
columns = get_columns(filters)
|
||||
entries = get_entries(filters)
|
||||
item_details = get_item_details()
|
||||
@@ -49,10 +51,17 @@ def execute(filters=None):
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
def validate_filters(filters):
|
||||
ALLOWED_DOCTYPES = ["Sales Order", "Sales Invoice", "Delivery Note"]
|
||||
|
||||
if not filters.get("doc_type"):
|
||||
msgprint(_("Please select the document type first"), raise_exception=1)
|
||||
|
||||
if filters.get("doc_type") not in ALLOWED_DOCTYPES:
|
||||
frappe.throw(_("{0}, {1} or {2} are the only allowed options.").format(*ALLOWED_DOCTYPES))
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
columns = [
|
||||
{
|
||||
"label": _(filters["doc_type"]),
|
||||
@@ -146,50 +155,60 @@ def get_columns(filters):
|
||||
|
||||
|
||||
def get_entries(filters):
|
||||
date_field = filters["doc_type"] == "Sales Order" and "transaction_date" or "posting_date"
|
||||
if filters["doc_type"] == "Sales Order":
|
||||
qty_field = "delivered_qty"
|
||||
else:
|
||||
qty_field = "qty"
|
||||
conditions, values = get_conditions(filters, date_field)
|
||||
doc_type = filters["doc_type"]
|
||||
|
||||
entries = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
dt.name, dt.customer, dt.territory, dt.{} as posting_date, dt_item.item_code,
|
||||
st.sales_person, st.allocated_percentage, dt_item.warehouse,
|
||||
CASE
|
||||
WHEN dt.status = "Closed" THEN dt_item.{} * dt_item.conversion_factor
|
||||
ELSE dt_item.stock_qty
|
||||
END as stock_qty,
|
||||
CASE
|
||||
WHEN dt.status = "Closed" THEN (dt_item.base_net_rate * dt_item.{} * dt_item.conversion_factor)
|
||||
ELSE dt_item.base_net_amount
|
||||
END as base_net_amount,
|
||||
CASE
|
||||
WHEN dt.status = "Closed" THEN ((dt_item.base_net_rate * dt_item.{} * dt_item.conversion_factor) * st.allocated_percentage/100)
|
||||
ELSE dt_item.base_net_amount * st.allocated_percentage/100
|
||||
END as contribution_amt
|
||||
FROM
|
||||
`tab{}` dt, `tab{} Item` dt_item, `tabSales Team` st
|
||||
WHERE
|
||||
st.parent = dt.name and dt.name = dt_item.parent and st.parenttype = {}
|
||||
and dt.docstatus = 1 {} order by st.sales_person, dt.name desc
|
||||
""".format(
|
||||
date_field,
|
||||
qty_field,
|
||||
qty_field,
|
||||
qty_field,
|
||||
filters["doc_type"],
|
||||
filters["doc_type"],
|
||||
"%s",
|
||||
conditions,
|
||||
),
|
||||
tuple([filters["doc_type"], *values]),
|
||||
as_dict=1,
|
||||
date_field = "transaction_date" if doc_type == "Sales Order" else "posting_date"
|
||||
qty_field = "delivered_qty" if doc_type == "Sales Order" else "qty"
|
||||
|
||||
dt = frappe.qb.DocType(doc_type)
|
||||
dt_item = frappe.qb.DocType(f"{doc_type} Item")
|
||||
st = frappe.qb.DocType("Sales Team")
|
||||
|
||||
calc_qty = dt_item[qty_field] * dt_item.conversion_factor
|
||||
calc_net_amount = dt_item.base_net_rate * calc_qty
|
||||
|
||||
stock_qty_case = Case().when(dt.status == "Closed", calc_qty).else_(dt_item.stock_qty).as_("stock_qty")
|
||||
|
||||
base_net_amount_case = (
|
||||
Case()
|
||||
.when(dt.status == "Closed", calc_net_amount)
|
||||
.else_(dt_item.base_net_amount)
|
||||
.as_("base_net_amount")
|
||||
)
|
||||
|
||||
return entries
|
||||
contribution_amt_case = (
|
||||
Case()
|
||||
.when(dt.status == "Closed", (calc_net_amount * st.allocated_percentage / 100))
|
||||
.else_(dt_item.base_net_amount * st.allocated_percentage / 100)
|
||||
.as_("contribution_amt")
|
||||
)
|
||||
|
||||
query = (
|
||||
frappe.get_query(dt, filters=filters, ignore_permissions=False)
|
||||
.join(dt_item)
|
||||
.on(dt.name == dt_item.parent)
|
||||
.join(st)
|
||||
.on(dt.name == st.parent)
|
||||
.select(
|
||||
dt.name,
|
||||
dt.customer,
|
||||
dt.territory,
|
||||
dt[date_field].as_("posting_date"),
|
||||
dt_item.item_code,
|
||||
st.sales_person,
|
||||
st.allocated_percentage,
|
||||
dt_item.warehouse,
|
||||
stock_qty_case,
|
||||
base_net_amount_case,
|
||||
contribution_amt_case,
|
||||
)
|
||||
.where(st.parenttype == doc_type)
|
||||
.where(dt.docstatus == 1)
|
||||
)
|
||||
|
||||
query = query.orderby(st.sales_person).orderby(dt.name, order=frappe.qb.desc)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_conditions(filters, date_field):
|
||||
|
||||
@@ -120,7 +120,9 @@ class AuthorizationControl(TransactionBase):
|
||||
if val == 1:
|
||||
add_cond += " and system_user = {}".format(frappe.db.escape(session["user"]))
|
||||
elif val == 2:
|
||||
add_cond += " and system_role IN %s" % ("('" + "','".join(frappe.get_roles()) + "')")
|
||||
add_cond += " and system_role IN (%s)" % ", ".join(
|
||||
frappe.db.escape(r) for r in frappe.get_roles()
|
||||
)
|
||||
else:
|
||||
add_cond += " and ifnull(system_user,'') = '' and ifnull(system_role,'') = ''"
|
||||
|
||||
@@ -203,8 +205,8 @@ class AuthorizationControl(TransactionBase):
|
||||
and docstatus != 2
|
||||
""".format(
|
||||
"%s",
|
||||
"'" + "','".join(frappe.get_roles()) + "'",
|
||||
"'" + "','".join(final_based_on) + "'",
|
||||
", ".join(frappe.db.escape(r) for r in frappe.get_roles()),
|
||||
", ".join(frappe.db.escape(b) for b in final_based_on),
|
||||
"%s",
|
||||
),
|
||||
(doctype_name, company),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user