mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-16 18:24:10 +00:00
Merge pull request #55980 from frappe/version-15-hotfix
chore: release v15
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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -579,10 +579,12 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def merge_account(old, new):
|
def merge_account(old, new):
|
||||||
_ensure_idle_system()
|
_ensure_idle_system()
|
||||||
# Validate properties before merging
|
|
||||||
new_account = frappe.get_cached_doc("Account", new)
|
new_account = frappe.get_cached_doc("Account", new)
|
||||||
old_account = frappe.get_cached_doc("Account", old)
|
old_account = frappe.get_cached_doc("Account", old)
|
||||||
|
|
||||||
|
new_account.check_permission("write")
|
||||||
|
old_account.check_permission("write")
|
||||||
|
|
||||||
if not new_account:
|
if not new_account:
|
||||||
throw(_("Account {0} does not exist").format(new))
|
throw(_("Account {0} does not exist").format(new))
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,14 @@ class BankClearance(Document):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def update_clearance_date(self):
|
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"):
|
for d in self.get("payment_entries"):
|
||||||
if d.clearance_date:
|
if d.clearance_date:
|
||||||
if not d.payment_document:
|
if not d.payment_document:
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ class BisectAccountingStatements(Document):
|
|||||||
|
|
||||||
cur_node.save()
|
cur_node.save()
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist(methods=["POST"])
|
||||||
def build_tree(self):
|
def build_tree(self):
|
||||||
frappe.db.delete("Bisect Nodes")
|
frappe.db.delete("Bisect Nodes")
|
||||||
|
|
||||||
|
|||||||
@@ -11,22 +11,28 @@ frappe.ui.form.on("Currency Exchange Settings", {
|
|||||||
},
|
},
|
||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
if (r && r.message) {
|
if (r && r.message) {
|
||||||
|
let result = [],
|
||||||
|
params = {};
|
||||||
if (frm.doc.service_provider == "exchangerate.host") {
|
if (frm.doc.service_provider == "exchangerate.host") {
|
||||||
let result = ["result"];
|
result = ["result"];
|
||||||
let params = {
|
params = {
|
||||||
date: "{transaction_date}",
|
date: "{transaction_date}",
|
||||||
from: "{from_currency}",
|
from: "{from_currency}",
|
||||||
to: "{to_currency}",
|
to: "{to_currency}",
|
||||||
};
|
};
|
||||||
add_param(frm, r.message, params, result);
|
|
||||||
} else if (["frankfurter.app", "frankfurter.dev"].includes(frm.doc.service_provider)) {
|
} else if (["frankfurter.app", "frankfurter.dev"].includes(frm.doc.service_provider)) {
|
||||||
let result = ["rates", "{to_currency}"];
|
result = ["rates", "{to_currency}"];
|
||||||
let params = {
|
params = {
|
||||||
base: "{from_currency}",
|
base: "{from_currency}",
|
||||||
symbols: "{to_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",
|
"fieldname": "service_provider",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Service Provider",
|
"label": "Service Provider",
|
||||||
"options": "frankfurter.dev\nexchangerate.host\nCustom",
|
"options": "frankfurter.dev\nexchangerate.host\nfrankfurter.dev - v2\nCustom",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-11-25 13:03:41.896424",
|
"modified": "2026-06-15 11:25:55.873110",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Currency Exchange Settings",
|
"name": "Currency Exchange Settings",
|
||||||
@@ -121,24 +121,11 @@
|
|||||||
"write": 1
|
"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,
|
"email": 1,
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"role": "Accounts User",
|
"role": "Accounts User",
|
||||||
"share": 1,
|
"share": 1
|
||||||
"write": 1
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"row_format": "Dynamic",
|
"row_format": "Dynamic",
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document):
|
|||||||
disabled: DF.Check
|
disabled: DF.Check
|
||||||
req_params: DF.Table[CurrencyExchangeSettingsDetails]
|
req_params: DF.Table[CurrencyExchangeSettingsDetails]
|
||||||
result_key: DF.Table[CurrencyExchangeSettingsResult]
|
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
|
url: DF.Data | None
|
||||||
use_http: DF.Check
|
use_http: DF.Check
|
||||||
# end: auto-generated types
|
# 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": "base", "value": "{from_currency}"})
|
||||||
self.append("req_params", {"key": "symbols", "value": "{to_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):
|
def validate_parameters(self):
|
||||||
params = {}
|
params = {}
|
||||||
for row in self.req_params:
|
for row in self.req_params:
|
||||||
@@ -105,13 +113,20 @@ class CurrencyExchangeSettings(Document):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
|
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":
|
if service_provider == "exchangerate.host":
|
||||||
api = "api.exchangerate.host/convert"
|
api = "api.exchangerate.host/convert"
|
||||||
elif service_provider == "frankfurter.app":
|
elif service_provider == "frankfurter.app":
|
||||||
api = "api.frankfurter.app/{transaction_date}"
|
api = "api.frankfurter.app/{transaction_date}"
|
||||||
elif service_provider == "frankfurter.dev":
|
elif service_provider == "frankfurter.dev":
|
||||||
api = "api.frankfurter.dev/v1/{transaction_date}"
|
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://"
|
protocol = "https://"
|
||||||
if use_http:
|
if use_http:
|
||||||
|
|||||||
@@ -3038,7 +3038,7 @@ def get_payment_entry(
|
|||||||
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
|
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()
|
pe.set_amounts()
|
||||||
|
|
||||||
# If PE is created from PR directly, then no need to find open PRs for the references
|
# 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()
|
si.submit()
|
||||||
|
|
||||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700)
|
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_no = si.name
|
||||||
pe.reference_date = nowdate()
|
pe.reference_date = nowdate()
|
||||||
|
|
||||||
@@ -612,6 +614,8 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
pe = get_payment_entry(
|
pe = get_payment_entry(
|
||||||
"Sales Invoice", si.name, party_amount=20, bank_account="_Test Bank - _TC", bank_amount=900
|
"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_no = "1"
|
||||||
pe.reference_date = "2016-01-01"
|
pe.reference_date = "2016-01-01"
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ from erpnext import get_company_currency
|
|||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
get_accounting_dimensions,
|
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 (
|
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||||
get_payment_entry,
|
get_payment_entry,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
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.accounts.utils import get_account_currency, get_currency_precision
|
||||||
from erpnext.utilities import payment_app_import_guard
|
from erpnext.utilities import payment_app_import_guard
|
||||||
|
|
||||||
|
|||||||
@@ -195,7 +195,12 @@ class TestPaymentRequest(FrappeTestCase):
|
|||||||
return_doc=1,
|
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(
|
expected_gle = dict(
|
||||||
(d[0], d)
|
(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 = make_payment_request(dt=po_doc.doctype, dn=po_doc.name, recipient_id="nabin@erpnext.com")
|
||||||
pr = frappe.get_doc(pr).save().submit()
|
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.base_paid_amount, 800)
|
||||||
self.assertEqual(pe.paid_amount, 800)
|
self.assertEqual(pe.paid_amount, 800)
|
||||||
self.assertEqual(pe.base_received_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
|
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()
|
@frappe.whitelist()
|
||||||
def pause_job_for_doc(docname: str | None = None):
|
def pause_job_for_doc(docname: str | None = None):
|
||||||
if docname:
|
if docname:
|
||||||
|
frappe.has_permission("Process Payment Reconciliation", "write", doc=docname, throw=True)
|
||||||
frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Paused")
|
frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Paused")
|
||||||
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": docname})
|
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": docname})
|
||||||
if log:
|
if log:
|
||||||
@@ -142,6 +143,8 @@ def trigger_job_for_doc(docname: str | None = None):
|
|||||||
if not docname:
|
if not docname:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
frappe.has_permission("Process Payment Reconciliation", "write", doc=docname, throw=True)
|
||||||
|
|
||||||
if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
|
if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format(
|
_("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format(
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ class ProcessPeriodClosingVoucher(Document):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def start_pcv_processing(docname: str):
|
def start_pcv_processing(docname: str):
|
||||||
if frappe.db.get_value("Process Period Closing Voucher", docname, "status") in ["Queued", "Running"]:
|
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")
|
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running")
|
||||||
|
|
||||||
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
|
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
|
||||||
|
|||||||
@@ -493,6 +493,7 @@ def download_statements(document_name):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def send_emails(document_name, from_scheduler=False, posting_date=None):
|
def send_emails(document_name, from_scheduler=False, posting_date=None):
|
||||||
doc = frappe.get_doc("Process Statement Of Accounts", document_name)
|
doc = frappe.get_doc("Process Statement Of Accounts", document_name)
|
||||||
|
doc.check_permission()
|
||||||
report = get_report_pdf(doc, consolidated=False)
|
report = get_report_pdf(doc, consolidated=False)
|
||||||
|
|
||||||
if report:
|
if report:
|
||||||
|
|||||||
@@ -2086,6 +2086,7 @@ def make_stock_entry(source_name, target_doc=None):
|
|||||||
def change_release_date(name, release_date=None):
|
def change_release_date(name, release_date=None):
|
||||||
if frappe.db.exists("Purchase Invoice", name):
|
if frappe.db.exists("Purchase Invoice", name):
|
||||||
pi = frappe.get_doc("Purchase Invoice", name)
|
pi = frappe.get_doc("Purchase Invoice", name)
|
||||||
|
pi.check_permission()
|
||||||
pi.db_set("release_date", release_date)
|
pi.db_set("release_date", release_date)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -154,12 +154,13 @@ class RepostAccountingLedger(Document):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@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
|
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||||
|
|
||||||
frappe.flags.through_repost_accounting_ledger = True
|
frappe.flags.through_repost_accounting_ledger = True
|
||||||
if account_repost_doc:
|
if account_repost_doc:
|
||||||
repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc)
|
repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc)
|
||||||
|
repost_doc.check_permission("write")
|
||||||
|
|
||||||
if repost_doc.docstatus == 1:
|
if repost_doc.docstatus == 1:
|
||||||
# Prevent repost on invoices with deferred accounting
|
# Prevent repost on invoices with deferred accounting
|
||||||
|
|||||||
@@ -510,11 +510,6 @@ def get_party_advance_account(party_type, party, company):
|
|||||||
return account
|
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 get_party_account_currency(party_type, party, company):
|
||||||
def generator():
|
def generator():
|
||||||
party_account = get_party_account(party_type, party, company)
|
party_account = get_party_account(party_type, party, company)
|
||||||
@@ -549,11 +544,19 @@ def get_party_gle_currency(party_type, party, company):
|
|||||||
|
|
||||||
def get_party_gle_account(party_type, party, company):
|
def get_party_gle_account(party_type, party, company):
|
||||||
def generator():
|
def generator():
|
||||||
existing_gle_account = frappe.db.sql(
|
gl = qb.DocType("GL Entry")
|
||||||
"""select account from `tabGL Entry`
|
existing_gle_account = (
|
||||||
where docstatus=1 and company=%(company)s and party_type=%(party_type)s and party=%(party)s
|
qb.from_(gl)
|
||||||
limit 1""",
|
.select(gl.account)
|
||||||
{"company": company, "party_type": party_type, "party": party},
|
.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
|
return existing_gle_account[0][0] if existing_gle_account else None
|
||||||
|
|||||||
@@ -922,8 +922,28 @@ class ReceivablePayableReport:
|
|||||||
if self.filters.project:
|
if self.filters.project:
|
||||||
self.qb_selection_filter.append(self.ple.project.isin(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()
|
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):
|
def get_cost_center_conditions(self):
|
||||||
cost_center_list = get_cost_centers_with_children(self.filters.cost_center)
|
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))
|
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)
|
self.assertEqual(len(report[1]), 1)
|
||||||
row = report[1][0]
|
row = report[1][0]
|
||||||
self.assertEqual([si.name, project.name, 60], [row.voucher_no, row.project, row.outstanding])
|
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)
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ class TestUtils(unittest.TestCase):
|
|||||||
purchase_invoice.submit()
|
purchase_invoice.submit()
|
||||||
|
|
||||||
payment_entry = get_payment_entry(purchase_invoice.doctype, purchase_invoice.name)
|
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.paid_amount = 15725
|
||||||
payment_entry.deductions = []
|
payment_entry.deductions = []
|
||||||
payment_entry.save()
|
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))
|
throw(_("{0} '{1}' not in Fiscal Year {2}").format(_(label), formatdate(date), fiscal_year))
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def get_balance_on(
|
def get_balance_on(
|
||||||
account=None,
|
account=None,
|
||||||
date=None,
|
date=None,
|
||||||
@@ -1387,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
|
Renames the document by adding the number as a prefix to the current name and updates
|
||||||
all transaction where it was present.
|
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")
|
validate_field_number("Cost Center", docname, cost_center_number, company, "cost_center_number")
|
||||||
|
|
||||||
if cost_center_number:
|
if cost_center_number:
|
||||||
|
|||||||
@@ -662,7 +662,7 @@ class PurchaseOrder(BuyingController):
|
|||||||
|
|
||||||
def update_subcontracting_order_status(self):
|
def update_subcontracting_order_status(self):
|
||||||
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
|
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:
|
if self.is_subcontracted and not self.is_old_subcontracting_flow:
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ def refresh_scorecards():
|
|||||||
def make_all_scorecards(docname):
|
def make_all_scorecards(docname):
|
||||||
sc = frappe.get_doc("Supplier Scorecard", docname)
|
sc = frappe.get_doc("Supplier Scorecard", docname)
|
||||||
supplier = frappe.get_doc("Supplier", sc.supplier)
|
supplier = frappe.get_doc("Supplier", sc.supplier)
|
||||||
|
supplier.check_permission("write")
|
||||||
|
|
||||||
start_date = getdate(supplier.creation)
|
start_date = getdate(supplier.creation)
|
||||||
end_date = get_scorecard_date(sc.period, start_date)
|
end_date = get_scorecard_date(sc.period, start_date)
|
||||||
|
|||||||
@@ -297,7 +297,8 @@ def get_message():
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@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(
|
frappe.db.set_value(
|
||||||
"Item Default",
|
"Item Default",
|
||||||
{"parent": item_code, "company": company},
|
{"parent": item_code, "company": company},
|
||||||
|
|||||||
@@ -533,6 +533,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.so_detail = source_doc.so_detail
|
||||||
target_doc.expense_account = source_doc.expense_account
|
target_doc.expense_account = source_doc.expense_account
|
||||||
target_doc.dn_detail = source_doc.name
|
target_doc.dn_detail = source_doc.name
|
||||||
|
target_doc.cost_center = source_doc.cost_center
|
||||||
if default_warehouse_for_sales_return:
|
if default_warehouse_for_sales_return:
|
||||||
target_doc.warehouse = default_warehouse_for_sales_return
|
target_doc.warehouse = default_warehouse_for_sales_return
|
||||||
elif doctype == "Sales Invoice" or doctype == "POS Invoice":
|
elif doctype == "Sales Invoice" or doctype == "POS Invoice":
|
||||||
|
|||||||
@@ -1694,7 +1694,7 @@ def check_item_quality_inspection(doctype: str, docstatus: str | int, items: str
|
|||||||
|
|
||||||
inspection_fieldname = inspection_fieldname_map.get(doctype)
|
inspection_fieldname = inspection_fieldname_map.get(doctype)
|
||||||
if inspection_fieldname is None:
|
if inspection_fieldname is None:
|
||||||
return []
|
return items if doctype == "Stock Entry" else []
|
||||||
|
|
||||||
allow_after_transaction = cint(docstatus) == 1 and frappe.get_single_value(
|
allow_after_transaction = cint(docstatus) == 1 and frappe.get_single_value(
|
||||||
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
|
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"opportunity_section",
|
"opportunity_section",
|
||||||
"close_opportunity_after_days",
|
"close_opportunity_after_days",
|
||||||
"column_break_9",
|
"column_break_9",
|
||||||
|
"enable_opportunity_creation_from_contact_us",
|
||||||
"quotation_section",
|
"quotation_section",
|
||||||
"default_valid_till",
|
"default_valid_till",
|
||||||
"section_break_13",
|
"section_break_13",
|
||||||
@@ -98,13 +99,19 @@
|
|||||||
"fieldname": "update_timestamp_on_new_communication",
|
"fieldname": "update_timestamp_on_new_communication",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Update timestamp on new communication"
|
"label": "Update timestamp on new communication"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "enable_opportunity_creation_from_contact_us",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Enable Opportunity Creation from Contact Us"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-cog",
|
"icon": "fa fa-cog",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-01-16 16:12:14.889455",
|
"modified": "2026-06-11 23:09:49.750381",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "CRM Settings",
|
"name": "CRM Settings",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
@@ -20,8 +21,20 @@ class CRMSettings(Document):
|
|||||||
carry_forward_communication_and_comments: DF.Check
|
carry_forward_communication_and_comments: DF.Check
|
||||||
close_opportunity_after_days: DF.Int
|
close_opportunity_after_days: DF.Int
|
||||||
default_valid_till: DF.Data | None
|
default_valid_till: DF.Data | None
|
||||||
|
enable_opportunity_creation_from_contact_us: DF.Check
|
||||||
update_timestamp_on_new_communication: DF.Check
|
update_timestamp_on_new_communication: DF.Check
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
frappe.db.set_default("campaign_naming_by", self.get("campaign_naming_by", ""))
|
frappe.db.set_default("campaign_naming_by", self.get("campaign_naming_by", ""))
|
||||||
|
self.validate_enable_opportunity_creation_from_contact_us()
|
||||||
|
|
||||||
|
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."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from frappe.contacts.address_and_contact import (
|
|||||||
)
|
)
|
||||||
from frappe.email.inbox import link_communication_to_document
|
from frappe.email.inbox import link_communication_to_document
|
||||||
from frappe.model.mapper import get_mapped_doc
|
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.accounts.party import set_taxes
|
||||||
from erpnext.controllers.selling_controller import SellingController
|
from erpnext.controllers.selling_controller import SellingController
|
||||||
@@ -171,9 +171,6 @@ class Lead(SellingController, CRMNote):
|
|||||||
if self.email_id == self.lead_owner:
|
if self.email_id == self.lead_owner:
|
||||||
frappe.throw(_("Lead Owner cannot be same as the Lead Email Address"))
|
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):
|
def link_to_contact(self):
|
||||||
# update contact links
|
# update contact links
|
||||||
if self.contact_doc:
|
if self.contact_doc:
|
||||||
@@ -471,7 +468,7 @@ def get_lead_details(lead, posting_date=None, company=None, doctype=None):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@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"""
|
"""raise a issue from email"""
|
||||||
|
|
||||||
doc = frappe.get_doc("Communication", communication)
|
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_mandatory = True
|
||||||
lead.flags.ignore_permissions = True
|
|
||||||
lead.insert()
|
lead.insert()
|
||||||
|
|
||||||
lead_name = lead.name
|
lead_name = lead.name
|
||||||
@@ -523,7 +519,7 @@ def get_lead_with_phone_number(number):
|
|||||||
def add_lead_to_prospect(lead, prospect):
|
def add_lead_to_prospect(lead, prospect):
|
||||||
prospect = frappe.get_doc("Prospect", prospect)
|
prospect = frappe.get_doc("Prospect", prospect)
|
||||||
prospect.append("leads", {"lead": lead})
|
prospect.append("leads", {"lead": lead})
|
||||||
prospect.save(ignore_permissions=True)
|
prospect.save()
|
||||||
|
|
||||||
carry_forward_communication_and_comments = frappe.db.get_single_value(
|
carry_forward_communication_and_comments = frappe.db.get_single_value(
|
||||||
"CRM Settings", "carry_forward_communication_and_comments"
|
"CRM Settings", "carry_forward_communication_and_comments"
|
||||||
|
|||||||
@@ -522,7 +522,9 @@ def auto_close_opportunity():
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@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
|
from erpnext.crm.doctype.lead.lead import make_lead_from_communication
|
||||||
|
|
||||||
doc = frappe.get_doc("Communication", communication)
|
doc = frappe.get_doc("Communication", communication)
|
||||||
@@ -540,7 +542,7 @@ def make_opportunity_from_communication(communication, company, ignore_communica
|
|||||||
"opportunity_from": opportunity_from,
|
"opportunity_from": opportunity_from,
|
||||||
"party_name": lead,
|
"party_name": lead,
|
||||||
}
|
}
|
||||||
).insert(ignore_permissions=True)
|
).insert()
|
||||||
|
|
||||||
link_communication_to_document(doc, "Opportunity", opportunity.name, ignore_communication_links)
|
link_communication_to_document(doc, "Opportunity", opportunity.name, ignore_communication_links)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ from frappe.utils import cstr, now, today
|
|||||||
from pypika import functions
|
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):
|
def update_lead_phone_numbers(contact, method):
|
||||||
if contact.phone_nos:
|
if contact.phone_nos:
|
||||||
contact_lead = contact.get_link_for("Lead")
|
contact_lead = contact.get_link_for("Lead")
|
||||||
|
|||||||
@@ -355,6 +355,9 @@ doc_events = {
|
|||||||
"Event": {
|
"Event": {
|
||||||
"after_insert": "erpnext.crm.utils.link_events_with_prospect",
|
"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": {
|
"Sales Invoice": {
|
||||||
"on_submit": [
|
"on_submit": [
|
||||||
"erpnext.regional.create_transaction_log",
|
"erpnext.regional.create_transaction_log",
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ frappe.ui.form.on("BOM", {
|
|||||||
|
|
||||||
with_operations: function (frm) {
|
with_operations: function (frm) {
|
||||||
frm.set_df_property("fg_based_operating_cost", "hidden", frm.doc.with_operations ? 1 : 0);
|
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) {
|
fg_based_operating_cost: function (frm) {
|
||||||
@@ -438,7 +441,7 @@ frappe.ui.form.on("BOM", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
routing(frm) {
|
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({
|
frappe.call({
|
||||||
doc: frm.doc,
|
doc: frm.doc,
|
||||||
method: "get_routing",
|
method: "get_routing",
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ class BOMCreator(Document):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def add_boms(self):
|
def add_boms(self):
|
||||||
|
self.check_permission("submit")
|
||||||
self.submit()
|
self.submit()
|
||||||
|
|
||||||
def set_rate_for_items(self):
|
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))
|
frappe.throw(_("Please set {0} in BOM Creator {1}").format(_(label), self.name))
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.enqueue_create_boms()
|
self.enqueue_bom_creation()
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def enqueue_create_boms(self):
|
def enqueue_create_boms(self):
|
||||||
|
self.check_permission("submit")
|
||||||
|
self.enqueue_bom_creation()
|
||||||
|
|
||||||
|
def enqueue_bom_creation(self):
|
||||||
frappe.enqueue(
|
frappe.enqueue(
|
||||||
self.create_boms,
|
self.create_boms,
|
||||||
queue="short",
|
queue="short",
|
||||||
@@ -281,6 +286,21 @@ class BOMCreator(Document):
|
|||||||
|
|
||||||
frappe.msgprint(_("BOMs creation failed"))
|
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):
|
def create_bom(self, row, production_item_wise_rm):
|
||||||
bom_creator_item = row.name if row.name != self.name else ""
|
bom_creator_item = row.name if row.name != self.name else ""
|
||||||
if frappe.db.exists(
|
if frappe.db.exists(
|
||||||
@@ -336,18 +356,157 @@ class BOMCreator(Document):
|
|||||||
production_item_wise_rm[(row.item_code, row.name)].bom_no = bom.name
|
production_item_wise_rm[(row.item_code, row.name)].bom_no = bom.name
|
||||||
|
|
||||||
@frappe.whitelist()
|
@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")
|
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()
|
@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):
|
if isinstance(kwargs, str):
|
||||||
kwargs = frappe.parse_json(kwargs)
|
kwargs = frappe.parse_json(kwargs)
|
||||||
|
|
||||||
if isinstance(kwargs, dict):
|
if isinstance(kwargs, dict):
|
||||||
kwargs = frappe._dict(kwargs)
|
kwargs = frappe._dict(kwargs)
|
||||||
|
|
||||||
|
frappe.has_permission("BOM Creator", "read", doc=kwargs.parent_id, throw=True)
|
||||||
|
|
||||||
fields = [
|
fields = [
|
||||||
"item_code as value",
|
"item_code as value",
|
||||||
"item_name as title",
|
"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")
|
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):
|
def get_item_details(item_code):
|
||||||
return frappe.get_cached_value(
|
return frappe.get_cached_value(
|
||||||
"Item", item_code, ["item_name", "description", "image", "stock_uom", "default_bom"], as_dict=1
|
"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)
|
frappe.msgprint(_("Parent Row No not found for {0}").format(name), alert=True)
|
||||||
|
|
||||||
return None
|
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
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
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
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|
||||||
|
|
||||||
@@ -38,8 +34,7 @@ class TestBOMCreator(FrappeTestCase):
|
|||||||
conversion_rate=1,
|
conversion_rate=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
add_sub_assembly(
|
doc.add_sub_assembly(
|
||||||
parent=doc.name,
|
|
||||||
fg_item=final_product,
|
fg_item=final_product,
|
||||||
fg_reference_id=doc.name,
|
fg_reference_id=doc.name,
|
||||||
bom_item={
|
bom_item={
|
||||||
@@ -91,8 +86,7 @@ class TestBOMCreator(FrappeTestCase):
|
|||||||
conversion_rate=1,
|
conversion_rate=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
add_item(
|
doc.add_item(
|
||||||
parent=doc.name,
|
|
||||||
fg_item=final_product,
|
fg_item=final_product,
|
||||||
fg_reference_id=doc.name,
|
fg_reference_id=doc.name,
|
||||||
item_code="Pedal Assembly",
|
item_code="Pedal Assembly",
|
||||||
@@ -133,8 +127,7 @@ class TestBOMCreator(FrappeTestCase):
|
|||||||
conversion_rate=1,
|
conversion_rate=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
add_item(
|
doc.add_item(
|
||||||
parent=doc.name,
|
|
||||||
fg_item=final_product,
|
fg_item=final_product,
|
||||||
fg_reference_id=doc.name,
|
fg_reference_id=doc.name,
|
||||||
item_code="Pedal Assembly",
|
item_code="Pedal Assembly",
|
||||||
@@ -144,9 +137,8 @@ class TestBOMCreator(FrappeTestCase):
|
|||||||
doc.reload()
|
doc.reload()
|
||||||
self.assertEqual(doc.items[0].is_expandable, 0)
|
self.assertEqual(doc.items[0].is_expandable, 0)
|
||||||
|
|
||||||
add_sub_assembly(
|
doc.add_sub_assembly(
|
||||||
convert_to_sub_assembly=1,
|
convert_to_sub_assembly=1,
|
||||||
parent=doc.name,
|
|
||||||
fg_item=final_product,
|
fg_item=final_product,
|
||||||
fg_reference_id=doc.items[0].name,
|
fg_reference_id=doc.items[0].name,
|
||||||
bom_item={
|
bom_item={
|
||||||
@@ -199,8 +191,7 @@ class TestBOMCreator(FrappeTestCase):
|
|||||||
conversion_rate=1,
|
conversion_rate=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
add_item(
|
doc.add_item(
|
||||||
parent=doc.name,
|
|
||||||
fg_item=final_product,
|
fg_item=final_product,
|
||||||
fg_reference_id=doc.name,
|
fg_reference_id=doc.name,
|
||||||
item_code="Pedal Assembly",
|
item_code="Pedal Assembly",
|
||||||
@@ -210,9 +201,8 @@ class TestBOMCreator(FrappeTestCase):
|
|||||||
doc.reload()
|
doc.reload()
|
||||||
self.assertEqual(doc.items[0].is_expandable, 0)
|
self.assertEqual(doc.items[0].is_expandable, 0)
|
||||||
|
|
||||||
add_sub_assembly(
|
doc.add_sub_assembly(
|
||||||
convert_to_sub_assembly=1,
|
convert_to_sub_assembly=1,
|
||||||
parent=doc.name,
|
|
||||||
fg_item=final_product,
|
fg_item=final_product,
|
||||||
fg_reference_id=doc.items[0].name,
|
fg_reference_id=doc.items[0].name,
|
||||||
bom_item={
|
bom_item={
|
||||||
|
|||||||
@@ -3631,6 +3631,58 @@ class TestWorkOrder(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertEqual(bin1_at_completion.reserved_qty_for_production, 0)
|
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):
|
def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse):
|
||||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from frappe.utils import (
|
|||||||
time_diff_in_seconds,
|
time_diff_in_seconds,
|
||||||
to_timedelta,
|
to_timedelta,
|
||||||
)
|
)
|
||||||
|
from frappe.utils.data import DateTimeLikeObject
|
||||||
|
|
||||||
from erpnext.support.doctype.issue.issue import get_holidays
|
from erpnext.support.doctype.issue.issue import get_holidays
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ class Workstation(Document):
|
|||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
def before_save(self):
|
def before_save(self):
|
||||||
self.set_data_based_on_workstation_type()
|
self._set_data_based_on_workstation_type()
|
||||||
self.set_hour_rate()
|
self.set_hour_rate()
|
||||||
self.set_total_working_hours()
|
self.set_total_working_hours()
|
||||||
|
|
||||||
@@ -92,6 +93,10 @@ class Workstation(Document):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def set_data_based_on_workstation_type(self):
|
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:
|
if self.workstation_type:
|
||||||
fields = [
|
fields = [
|
||||||
"hour_rate_labour",
|
"hour_rate_labour",
|
||||||
@@ -166,23 +171,27 @@ class Workstation(Document):
|
|||||||
return schedule_date
|
return schedule_date
|
||||||
|
|
||||||
@frappe.whitelist()
|
@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 = frappe.get_doc("Job Card", job_card)
|
||||||
|
doc.check_permission("write")
|
||||||
|
|
||||||
doc.append("time_logs", {"from_time": from_time, "employee": employee})
|
doc.append("time_logs", {"from_time": from_time, "employee": employee})
|
||||||
doc.save(ignore_permissions=True)
|
doc.save()
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
@frappe.whitelist()
|
@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 = frappe.get_doc("Job Card", job_card)
|
||||||
|
doc.check_permission("submit")
|
||||||
|
|
||||||
for row in doc.time_logs:
|
for row in doc.time_logs:
|
||||||
if not row.to_time:
|
if not row.to_time:
|
||||||
row.to_time = to_time
|
row.to_time = to_time
|
||||||
row.time_in_mins = time_diff_in_hours(row.to_time, row.from_time) / 60
|
row.time_in_mins = time_diff_in_hours(row.to_time, row.from_time) / 60
|
||||||
row.completed_qty = qty
|
row.completed_qty = qty
|
||||||
|
|
||||||
doc.save(ignore_permissions=True)
|
doc.save()
|
||||||
doc.submit()
|
doc.submit()
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
@@ -364,6 +373,8 @@ def check_workstation_for_holiday(workstation, from_datetime, to_datetime):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_workstations(**kwargs):
|
def get_workstations(**kwargs):
|
||||||
|
frappe.has_permission("Workstation", "read", throw=True)
|
||||||
|
|
||||||
kwargs = frappe._dict(kwargs)
|
kwargs = frappe._dict(kwargs)
|
||||||
_workstation = frappe.qb.DocType("Workstation")
|
_workstation = frappe.qb.DocType("Workstation")
|
||||||
|
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ erpnext.BOMComparisonTool = class BOMComparisonTool {
|
|||||||
return `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${frappe.meta.get_label(doctype, fieldname)}</td>
|
<td>${frappe.meta.get_label(doctype, fieldname)}</td>
|
||||||
<td>${value1}</td>
|
<td>${frappe.utils.escape_html(cstr(value1))}</td>
|
||||||
<td>${value2}</td>
|
<td>${frappe.utils.escape_html(cstr(value2))}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
})
|
})
|
||||||
@@ -138,13 +138,17 @@ erpnext.BOMComparisonTool = class BOMComparisonTool {
|
|||||||
.map((change, i) => {
|
.map((change, i) => {
|
||||||
let [fieldname, value1, value2] = change;
|
let [fieldname, value1, value2] = change;
|
||||||
let th =
|
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 `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
${th}
|
${th}
|
||||||
<td>${frappe.meta.get_label(child_doctype, fieldname)}</td>
|
<td>${frappe.meta.get_label(child_doctype, fieldname)}</td>
|
||||||
<td>${value1}</td>
|
<td>${frappe.utils.escape_html(cstr(value1))}</td>
|
||||||
<td>${value2}</td>
|
<td>${frappe.utils.escape_html(cstr(value2))}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
})
|
})
|
||||||
@@ -177,7 +181,9 @@ erpnext.BOMComparisonTool = class BOMComparisonTool {
|
|||||||
let html = rows
|
let html = rows
|
||||||
.map((row) => {
|
.map((row) => {
|
||||||
let [, doc] = 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>`;
|
return `<tr>${cells}</tr>`;
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
|
|||||||
@@ -717,7 +717,7 @@ def set_project_status(project, status):
|
|||||||
frappe.throw(_("Status must be Cancelled or Completed"))
|
frappe.throw(_("Status must be Cancelled or Completed"))
|
||||||
|
|
||||||
project = frappe.get_doc("Project", project)
|
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)):
|
for task in frappe.get_all("Task", dict(project=project.name)):
|
||||||
frappe.db.set_value("Task", task.name, "status", status)
|
frappe.db.set_value("Task", task.name, "status", status)
|
||||||
|
|||||||
@@ -219,14 +219,10 @@ class BOMConfigurator {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
(data) => {
|
(data) => {
|
||||||
if (!node.data.parent_id) {
|
|
||||||
node.data.parent_id = this.frm.doc.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_item",
|
method: "add_item",
|
||||||
|
doc: this.frm.doc,
|
||||||
args: {
|
args: {
|
||||||
parent: node.data.parent_id,
|
|
||||||
fg_item: node.data.value,
|
fg_item: node.data.value,
|
||||||
item_code: data.item_code,
|
item_code: data.item_code,
|
||||||
fg_reference_id: node.data.name || this.frm.doc.name,
|
fg_reference_id: node.data.name || this.frm.doc.name,
|
||||||
@@ -255,14 +251,10 @@ class BOMConfigurator {
|
|||||||
dialog.set_primary_action(__("Add"), () => {
|
dialog.set_primary_action(__("Add"), () => {
|
||||||
let bom_item = dialog.get_values();
|
let bom_item = dialog.get_values();
|
||||||
|
|
||||||
if (!node.data?.parent_id) {
|
|
||||||
node.data.parent_id = this.frm.doc.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_sub_assembly",
|
method: "add_sub_assembly",
|
||||||
|
doc: this.frm.doc,
|
||||||
args: {
|
args: {
|
||||||
parent: node.data.parent_id,
|
|
||||||
fg_item: node.data.value,
|
fg_item: node.data.value,
|
||||||
fg_reference_id: node.data.name || this.frm.doc.name,
|
fg_reference_id: node.data.name || this.frm.doc.name,
|
||||||
bom_item: bom_item,
|
bom_item: bom_item,
|
||||||
@@ -357,9 +349,9 @@ class BOMConfigurator {
|
|||||||
let bom_item = dialog.get_values();
|
let bom_item = dialog.get_values();
|
||||||
|
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_sub_assembly",
|
method: "add_sub_assembly",
|
||||||
|
doc: this.frm.doc,
|
||||||
args: {
|
args: {
|
||||||
parent: node.data.parent_id,
|
|
||||||
fg_item: node.data.value,
|
fg_item: node.data.value,
|
||||||
bom_item: bom_item,
|
bom_item: bom_item,
|
||||||
fg_reference_id: node.data.name || this.frm.doc.name,
|
fg_reference_id: node.data.name || this.frm.doc.name,
|
||||||
@@ -389,11 +381,10 @@ class BOMConfigurator {
|
|||||||
delete_node(node, view) {
|
delete_node(node, view) {
|
||||||
frappe.confirm(__("Are you sure you want to delete this Item?"), () => {
|
frappe.confirm(__("Are you sure you want to delete this Item?"), () => {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.delete_node",
|
method: "delete_node",
|
||||||
|
doc: this.frm.doc,
|
||||||
args: {
|
args: {
|
||||||
parent: node.data.parent_id,
|
|
||||||
fg_item: node.data.value,
|
fg_item: node.data.value,
|
||||||
doctype: node.data.doctype,
|
|
||||||
docname: node.data.name,
|
docname: node.data.name,
|
||||||
},
|
},
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
@@ -408,16 +399,14 @@ class BOMConfigurator {
|
|||||||
frappe.prompt(
|
frappe.prompt(
|
||||||
[{ label: __("Qty"), fieldname: "qty", default: qty, fieldtype: "Float", reqd: 1 }],
|
[{ label: __("Qty"), fieldname: "qty", default: qty, fieldtype: "Float", reqd: 1 }],
|
||||||
(data) => {
|
(data) => {
|
||||||
let doctype = node.data.doctype || this.frm.doc.doctype;
|
|
||||||
let docname = node.data.name || this.frm.doc.name;
|
let docname = node.data.name || this.frm.doc.name;
|
||||||
|
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.edit_qty",
|
method: "edit_qty",
|
||||||
|
doc: this.frm.doc,
|
||||||
args: {
|
args: {
|
||||||
doctype: doctype,
|
|
||||||
docname: docname,
|
docname: docname,
|
||||||
qty: data.qty,
|
qty: data.qty,
|
||||||
parent: node.data.parent_id ? node.data.parent_id : this.frm.doc.name,
|
|
||||||
},
|
},
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
node.data.qty = data.qty;
|
node.data.qty = data.qty;
|
||||||
|
|||||||
@@ -362,8 +362,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
}, __("Create"));
|
}, __("Create"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const inspection_type = ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"].includes(this.frm.doc.doctype)
|
const incoming_doctypes = ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"];
|
||||||
? "Incoming" : "Outgoing";
|
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");
|
let quality_inspection_field = this.frm.get_docfield("items", "quality_inspection");
|
||||||
quality_inspection_field.get_route_options_for_new_doc = function(row) {
|
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 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({
|
const dialog = new frappe.ui.Dialog({
|
||||||
title: __("Select Items for Quality Inspection"),
|
title: __("Select Items for Quality Inspection"),
|
||||||
size: "extra-large",
|
size: "extra-large",
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ def execute(filters=None):
|
|||||||
if not filters:
|
if not filters:
|
||||||
filters = {}
|
filters = {}
|
||||||
|
|
||||||
|
validate_filters(filters)
|
||||||
|
|
||||||
columns = get_columns(filters)
|
columns = get_columns(filters)
|
||||||
entries = get_entries(filters)
|
entries = get_entries(filters)
|
||||||
item_details = get_item_details()
|
item_details = get_item_details()
|
||||||
@@ -49,10 +51,17 @@ def execute(filters=None):
|
|||||||
return columns, data
|
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"):
|
if not filters.get("doc_type"):
|
||||||
msgprint(_("Please select the document type first"), raise_exception=1)
|
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 = [
|
columns = [
|
||||||
{
|
{
|
||||||
"label": _(filters["doc_type"]),
|
"label": _(filters["doc_type"]),
|
||||||
|
|||||||
@@ -207,7 +207,8 @@ frappe.ui.form.on("Company", {
|
|||||||
label: __("Please enter the company name to confirm"),
|
label: __("Please enter the company name to confirm"),
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
description: __(
|
description: __(
|
||||||
"Please make sure you really want to delete all the transactions for this company. Your master data will remain as it is. This action cannot be undone."
|
"Please make sure you really want to delete all the transactions for {0}. Your master data will remain as it is. This action cannot be undone.",
|
||||||
|
[frappe.utils.bold(frm.doc.name)]
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
function (data) {
|
function (data) {
|
||||||
@@ -227,7 +228,7 @@ frappe.ui.form.on("Company", {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
__("Delete all the Transactions for this Company"),
|
__("Delete all the Transactions for {0}", [frappe.utils.bold(frm.doc.name)]),
|
||||||
__("Delete")
|
__("Delete")
|
||||||
);
|
);
|
||||||
d.get_primary_btn().addClass("btn-danger");
|
d.get_primary_btn().addClass("btn-danger");
|
||||||
|
|||||||
@@ -68,13 +68,16 @@ def patched_requests_get(*args, **kwargs):
|
|||||||
if kwargs["params"].get("date") and kwargs["params"].get("from") and kwargs["params"].get("to"):
|
if kwargs["params"].get("date") and kwargs["params"].get("from") and kwargs["params"].get("to"):
|
||||||
if test_exchange_values.get(kwargs["params"]["date"]):
|
if test_exchange_values.get(kwargs["params"]["date"]):
|
||||||
return PatchResponse({"result": test_exchange_values[kwargs["params"]["date"]]}, 200)
|
return PatchResponse({"result": test_exchange_values[kwargs["params"]["date"]]}, 200)
|
||||||
elif args[0].startswith("https://api.frankfurter.dev") and kwargs.get("params"):
|
elif args[0].startswith("https://api.frankfurter.dev/v1") and kwargs.get("params"):
|
||||||
if kwargs["params"].get("base") and kwargs["params"].get("symbols"):
|
if kwargs["params"].get("base") and kwargs["params"].get("symbols"):
|
||||||
date = args[0].replace("https://api.frankfurter.dev/v1/", "")
|
date = args[0].replace("https://api.frankfurter.dev/v1/", "")
|
||||||
if test_exchange_values.get(date):
|
if test_exchange_values.get(date):
|
||||||
return PatchResponse(
|
return PatchResponse(
|
||||||
{"rates": {kwargs["params"].get("symbols"): test_exchange_values.get(date)}}, 200
|
{"rates": {kwargs["params"].get("symbols"): test_exchange_values.get(date)}}, 200
|
||||||
)
|
)
|
||||||
|
elif args[0].startswith("https://api.frankfurter.dev/v2") and kwargs.get("params"):
|
||||||
|
if kwargs["params"].get("date") and test_exchange_values.get(kwargs["params"]["date"]):
|
||||||
|
return PatchResponse({"rate": test_exchange_values.get(kwargs["params"]["date"])}, 200)
|
||||||
|
|
||||||
return PatchResponse({"rates": None}, 404)
|
return PatchResponse({"rates": None}, 404)
|
||||||
|
|
||||||
|
|||||||
@@ -118,7 +118,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-user",
|
"icon": "fa fa-user",
|
||||||
"modified": "2022-06-28 10:29:14.151380",
|
"modified": "2026-06-16 16:04:12.762960",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Setup",
|
"module": "Setup",
|
||||||
"name": "Driver",
|
"name": "Driver",
|
||||||
@@ -173,6 +173,18 @@
|
|||||||
"role": "Delivery Manager",
|
"role": "Delivery Manager",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
|
|||||||
@@ -64,15 +64,11 @@ class Employee(NestedSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_user_details(self):
|
def validate_user_details(self):
|
||||||
if self.user_id:
|
if not self.user_id:
|
||||||
data = frappe.db.get_value("User", self.user_id, ["enabled"], as_dict=1)
|
return
|
||||||
|
|
||||||
if not data:
|
self.validate_for_enabled_user_id()
|
||||||
self.user_id = None
|
self.validate_duplicate_user_id()
|
||||||
return
|
|
||||||
|
|
||||||
self.validate_for_enabled_user_id(data.get("enabled", 0))
|
|
||||||
self.validate_duplicate_user_id()
|
|
||||||
|
|
||||||
def update_nsm_model(self):
|
def update_nsm_model(self):
|
||||||
frappe.utils.nestedset.update_nsm(self)
|
frappe.utils.nestedset.update_nsm(self)
|
||||||
@@ -83,6 +79,7 @@ class Employee(NestedSet):
|
|||||||
if self.user_id:
|
if self.user_id:
|
||||||
self.update_user()
|
self.update_user()
|
||||||
self.update_user_permissions()
|
self.update_user_permissions()
|
||||||
|
self.update_user_status()
|
||||||
self.reset_employee_emails_cache()
|
self.reset_employee_emails_cache()
|
||||||
|
|
||||||
def update_user_permissions(self):
|
def update_user_permissions(self):
|
||||||
@@ -184,12 +181,20 @@ class Employee(NestedSet):
|
|||||||
if not self.relieving_date:
|
if not self.relieving_date:
|
||||||
throw(_("Please enter relieving date."))
|
throw(_("Please enter relieving date."))
|
||||||
|
|
||||||
def validate_for_enabled_user_id(self, enabled):
|
def validate_for_enabled_user_id(self):
|
||||||
if enabled is None:
|
if not frappe.db.exists("User", self.user_id):
|
||||||
frappe.throw(_("User {0} does not exist").format(self.user_id))
|
frappe.throw(_("User {0} does not exist").format(self.user_id))
|
||||||
|
|
||||||
|
def update_user_status(self):
|
||||||
|
if not self.user_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
user = frappe.get_doc("User", self.user_id)
|
||||||
|
enabled = user.enabled
|
||||||
if self.status != "Active" and enabled or self.status == "Active" and enabled == 0:
|
if self.status != "Active" and enabled or self.status == "Active" and enabled == 0:
|
||||||
frappe.db.set_value("User", self.user_id, "enabled", not enabled)
|
user.enabled = not enabled
|
||||||
|
# Keep linked User status in sync from the Employee lifecycle and record the audit log.
|
||||||
|
user.save(ignore_permissions=True)
|
||||||
|
|
||||||
def validate_duplicate_user_id(self):
|
def validate_duplicate_user_id(self):
|
||||||
Employee = frappe.qb.DocType("Employee")
|
Employee = frappe.qb.DocType("Employee")
|
||||||
@@ -321,6 +326,9 @@ def deactivate_sales_person(status=None, employee=None):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_user(employee, user=None, email=None):
|
def create_user(employee, user=None, email=None):
|
||||||
emp = frappe.get_doc("Employee", employee)
|
emp = frappe.get_doc("Employee", employee)
|
||||||
|
emp.check_permission("write")
|
||||||
|
if emp.user_id:
|
||||||
|
frappe.throw(_("Employee {0} already has a linked user").format(emp.name))
|
||||||
|
|
||||||
employee_name = emp.employee_name.split(" ")
|
employee_name = emp.employee_name.split(" ")
|
||||||
middle_name = last_name = ""
|
middle_name = last_name = ""
|
||||||
|
|||||||
@@ -89,7 +89,7 @@
|
|||||||
"icon": "icon-legal",
|
"icon": "icon-legal",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-04-29 22:51:49.285298",
|
"modified": "2026-06-06 16:35:34.394675",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Setup",
|
"module": "Setup",
|
||||||
"name": "Terms and Conditions",
|
"name": "Terms and Conditions",
|
||||||
@@ -135,13 +135,32 @@
|
|||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "Accounts User",
|
"role": "Accounts Manager",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"role": "Stock User"
|
"role": "Stock User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "HR User",
|
||||||
|
"select": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"read": 1,
|
||||||
|
"role": "HR Manager",
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts User",
|
||||||
|
"share": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
|
|||||||
@@ -209,6 +209,8 @@ class TransactionDeletionRecord(Document):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def start_deletion_tasks(self):
|
def start_deletion_tasks(self):
|
||||||
|
self.check_permission("write")
|
||||||
|
|
||||||
# This method is the entry point for the chain of events that follow
|
# This method is the entry point for the chain of events that follow
|
||||||
self.db_set("status", "Running")
|
self.db_set("status", "Running")
|
||||||
self.enqueue_task(task="Delete Bins")
|
self.enqueue_task(task="Delete Bins")
|
||||||
|
|||||||
@@ -90,14 +90,7 @@ def set_single_defaults():
|
|||||||
def setup_currency_exchange():
|
def setup_currency_exchange():
|
||||||
ces = frappe.get_single("Currency Exchange Settings")
|
ces = frappe.get_single("Currency Exchange Settings")
|
||||||
try:
|
try:
|
||||||
ces.set("result_key", [])
|
ces.service_provider = "frankfurter.dev - v2"
|
||||||
ces.set("req_params", [])
|
|
||||||
|
|
||||||
ces.api_endpoint = "https://api.frankfurter.dev/v1/{transaction_date}"
|
|
||||||
ces.append("result_key", {"key": "rates"})
|
|
||||||
ces.append("result_key", {"key": "{to_currency}"})
|
|
||||||
ces.append("req_params", {"key": "base", "value": "{from_currency}"})
|
|
||||||
ces.append("req_params", {"key": "symbols", "value": "{to_currency}"})
|
|
||||||
ces.save()
|
ces.save()
|
||||||
except frappe.ValidationError:
|
except frappe.ValidationError:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
|
|||||||
if entries:
|
if entries:
|
||||||
return flt(entries[0].exchange_rate)
|
return flt(entries[0].exchange_rate)
|
||||||
|
|
||||||
if frappe.get_cached_value("Currency Exchange Settings", "Currency Exchange Settings", "disabled"):
|
if frappe.get_single_value("Currency Exchange Settings", "disabled"):
|
||||||
return 0.00
|
return 0.00
|
||||||
|
|
||||||
pegged_currencies = {}
|
pegged_currencies = {}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Item",
|
"label": "Item",
|
||||||
|
"link_filters": "[\n [\"Item\", \"has_batch_no\", \"=\", 1],\n [\"Item\", \"is_stock_item\", \"=\", 1]\n]",
|
||||||
"oldfieldname": "item",
|
"oldfieldname": "item",
|
||||||
"oldfieldtype": "Link",
|
"oldfieldtype": "Link",
|
||||||
"options": "Item",
|
"options": "Item",
|
||||||
@@ -208,7 +209,7 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"links": [],
|
"links": [],
|
||||||
"max_attachments": 5,
|
"max_attachments": 5,
|
||||||
"modified": "2023-11-09 12:17:28.339975",
|
"modified": "2026-06-16 16:01:26.556324",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Batch",
|
"name": "Batch",
|
||||||
|
|||||||
@@ -335,7 +335,9 @@ def get_default_address(out, name):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_contact_display(contact):
|
def get_contact_display(contact: str):
|
||||||
|
frappe.has_permission("Contact", "read", doc=contact, throw=True)
|
||||||
|
|
||||||
contact_info = frappe.db.get_value(
|
contact_info = frappe.db.get_value(
|
||||||
"Contact", contact, ["first_name", "last_name", "phone", "mobile_no"], as_dict=1
|
"Contact", contact, ["first_name", "last_name", "phone", "mobile_no"], as_dict=1
|
||||||
)
|
)
|
||||||
@@ -373,6 +375,7 @@ def sanitize_address(address):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def notify_customers(delivery_trip):
|
def notify_customers(delivery_trip):
|
||||||
delivery_trip = frappe.get_doc("Delivery Trip", delivery_trip)
|
delivery_trip = frappe.get_doc("Delivery Trip", delivery_trip)
|
||||||
|
delivery_trip.check_permission()
|
||||||
|
|
||||||
context = delivery_trip.as_dict()
|
context = delivery_trip.as_dict()
|
||||||
|
|
||||||
@@ -436,7 +439,9 @@ def get_attachments(delivery_stop):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_driver_email(driver):
|
def get_driver_email(driver: str):
|
||||||
|
frappe.has_permission("Driver", "read", doc=driver, throw=True)
|
||||||
|
|
||||||
employee = frappe.db.get_value("Driver", driver, "employee")
|
employee = frappe.db.get_value("Driver", driver, "employee")
|
||||||
email = frappe.db.get_value("Employee", employee, "prefered_email")
|
email = frappe.db.get_value("Employee", employee, "prefered_email")
|
||||||
return {"email": email}
|
return {"email": email}
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ frappe.ui.form.on("Repost Item Valuation", {
|
|||||||
}).addClass("btn-primary");
|
}).addClass("btn-primary");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frm.trigger("show_update_valuation_field");
|
||||||
|
|
||||||
frm.trigger("show_reposting_progress");
|
frm.trigger("show_reposting_progress");
|
||||||
|
|
||||||
if (frm.doc.status === "Queued" && frm.doc.docstatus === 1) {
|
if (frm.doc.status === "Queued" && frm.doc.docstatus === 1) {
|
||||||
@@ -97,6 +99,13 @@ frappe.ui.form.on("Repost Item Valuation", {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
show_update_valuation_field(frm) {
|
||||||
|
frm.toggle_display(
|
||||||
|
"recalculate_valuation_rate",
|
||||||
|
["Purchase Receipt", "Purchase Invoice", "Stock Entry"].includes(frm.doc.voucher_type)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
execute_reposting(frm) {
|
execute_reposting(frm) {
|
||||||
frm.add_custom_button(__("Start Reposting"), () => {
|
frm.add_custom_button(__("Start Reposting"), () => {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
@@ -157,6 +166,7 @@ frappe.ui.form.on("Repost Item Valuation", {
|
|||||||
|
|
||||||
voucher_type: function (frm) {
|
voucher_type: function (frm) {
|
||||||
frm.trigger("set_company_on_transaction");
|
frm.trigger("set_company_on_transaction");
|
||||||
|
frm.trigger("show_update_valuation_field");
|
||||||
},
|
},
|
||||||
|
|
||||||
voucher_no: function (frm) {
|
voucher_no: function (frm) {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"via_landed_cost_voucher",
|
"via_landed_cost_voucher",
|
||||||
"allow_zero_rate",
|
"allow_zero_rate",
|
||||||
"recreate_stock_ledgers",
|
"recreate_stock_ledgers",
|
||||||
"amended_from",
|
"recalculate_valuation_rate",
|
||||||
"error_section",
|
"error_section",
|
||||||
"error_log",
|
"error_log",
|
||||||
"reposting_info_section",
|
"reposting_info_section",
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
"gl_reposting_index",
|
"gl_reposting_index",
|
||||||
"reposting_data_file",
|
"reposting_data_file",
|
||||||
"vouchers_based_on_item_and_warehouse_section",
|
"vouchers_based_on_item_and_warehouse_section",
|
||||||
|
"amended_from",
|
||||||
"total_vouchers",
|
"total_vouchers",
|
||||||
"column_break_yqwo",
|
"column_break_yqwo",
|
||||||
"vouchers_posted"
|
"vouchers_posted"
|
||||||
@@ -237,13 +238,21 @@
|
|||||||
"label": "Reposting Data File",
|
"label": "Reposting Data File",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "Only works for Purchase Receipt, Purchase Invoice and Stock Entry",
|
||||||
|
"fieldname": "recalculate_valuation_rate",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Recalculate Valuation Rate",
|
||||||
|
"show_description_on_click": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-03-27 19:59:58.637964",
|
"modified": "2026-06-16 17:30:42.715321",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Repost Item Valuation",
|
"name": "Repost Item Valuation",
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class RepostItemValuation(Document):
|
|||||||
items_to_be_repost: DF.Code | None
|
items_to_be_repost: DF.Code | None
|
||||||
posting_date: DF.Date
|
posting_date: DF.Date
|
||||||
posting_time: DF.Time | None
|
posting_time: DF.Time | None
|
||||||
|
recalculate_valuation_rate: DF.Check
|
||||||
recreate_stock_ledgers: DF.Check
|
recreate_stock_ledgers: DF.Check
|
||||||
reposting_data_file: DF.Attach | None
|
reposting_data_file: DF.Attach | None
|
||||||
reposting_reference: DF.Data | None
|
reposting_reference: DF.Data | None
|
||||||
@@ -303,6 +304,12 @@ class RepostItemValuation(Document):
|
|||||||
filters,
|
filters,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _recalculate_valuation_rate(self):
|
||||||
|
doc = frappe.get_doc(self.voucher_type, self.voucher_no)
|
||||||
|
doc.update_valuation_rate()
|
||||||
|
for item in doc.items:
|
||||||
|
item.db_set("valuation_rate", item.valuation_rate)
|
||||||
|
|
||||||
def recreate_stock_ledger_entries(self):
|
def recreate_stock_ledger_entries(self):
|
||||||
"""Recreate Stock Ledger Entries for the transaction."""
|
"""Recreate Stock Ledger Entries for the transaction."""
|
||||||
if self.based_on == "Transaction" and self.recreate_stock_ledgers:
|
if self.based_on == "Transaction" and self.recreate_stock_ledgers:
|
||||||
@@ -331,6 +338,12 @@ def repost(doc):
|
|||||||
if not frappe.flags.in_test:
|
if not frappe.flags.in_test:
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
|
if (
|
||||||
|
doc.voucher_type in ["Purchase Receipt", "Purchase Invoice", "Stock Entry"]
|
||||||
|
and doc.recalculate_valuation_rate
|
||||||
|
):
|
||||||
|
doc._recalculate_valuation_rate()
|
||||||
|
|
||||||
if doc.recreate_stock_ledgers:
|
if doc.recreate_stock_ledgers:
|
||||||
doc.recreate_stock_ledger_entries()
|
doc.recreate_stock_ledger_entries()
|
||||||
|
|
||||||
|
|||||||
@@ -419,6 +419,62 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
|
|||||||
self.assertRaises(frappe.ValidationError, riv.save)
|
self.assertRaises(frappe.ValidationError, riv.save)
|
||||||
doc.cancel()
|
doc.cancel()
|
||||||
|
|
||||||
|
def test_recalculate_valuation_rate_for_purchase_receipt(self):
|
||||||
|
item = self.make_item().name
|
||||||
|
|
||||||
|
# receive item at rate 100
|
||||||
|
pr = make_purchase_receipt(item_code=item, qty=1, rate=100)
|
||||||
|
self.assertSLEs(pr, [{"incoming_rate": 100}])
|
||||||
|
|
||||||
|
# change the rate from 100 to 150
|
||||||
|
pr.load_from_db()
|
||||||
|
pr.items[0].db_set(
|
||||||
|
{
|
||||||
|
"base_net_amount": 150,
|
||||||
|
"net_rate": 150,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# repost with recalculate valuation rate
|
||||||
|
riv = frappe.get_doc(
|
||||||
|
doctype="Repost Item Valuation",
|
||||||
|
based_on="Transaction",
|
||||||
|
voucher_type=pr.doctype,
|
||||||
|
voucher_no=pr.name,
|
||||||
|
recalculate_valuation_rate=1,
|
||||||
|
posting_date=pr.posting_date,
|
||||||
|
posting_time=pr.posting_time,
|
||||||
|
)
|
||||||
|
riv.submit()
|
||||||
|
|
||||||
|
# incoming rate after reposting should be 150
|
||||||
|
self.assertSLEs(pr, [{"incoming_rate": 150}])
|
||||||
|
|
||||||
|
def test_recalculate_valuation_rate_for_stock_entry(self):
|
||||||
|
item = self.make_item().name
|
||||||
|
|
||||||
|
# receive item at rate 100
|
||||||
|
se = make_stock_entry(item_code=item, target="_Test Warehouse - _TC", qty=1, rate=100)
|
||||||
|
self.assertSLEs(se, [{"incoming_rate": 100}])
|
||||||
|
|
||||||
|
# change the rate from 100 to 150
|
||||||
|
se.items[0].db_set("basic_rate", 150)
|
||||||
|
|
||||||
|
# repost with recalculate valuation rate
|
||||||
|
riv = frappe.get_doc(
|
||||||
|
doctype="Repost Item Valuation",
|
||||||
|
based_on="Transaction",
|
||||||
|
voucher_type=se.doctype,
|
||||||
|
voucher_no=se.name,
|
||||||
|
recalculate_valuation_rate=1,
|
||||||
|
posting_date=se.posting_date,
|
||||||
|
posting_time=se.posting_time,
|
||||||
|
)
|
||||||
|
riv.submit()
|
||||||
|
|
||||||
|
# incoming rate after reposting should be 150
|
||||||
|
self.assertSLEs(se, [{"incoming_rate": 150}])
|
||||||
|
|
||||||
def test_remove_attached_file(self):
|
def test_remove_attached_file(self):
|
||||||
item_code = make_item("_Test Remove Attached File Item", properties={"is_stock_item": 1})
|
item_code = make_item("_Test Remove Attached File Item", properties={"is_stock_item": 1})
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,9 @@ def get_contact_name(ref_doctype, docname):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_company_contact(user):
|
def get_company_contact(user: str):
|
||||||
|
frappe.has_permission("User", "read", throw=True)
|
||||||
|
|
||||||
contact = frappe.db.get_value(
|
contact = frappe.db.get_value(
|
||||||
"User",
|
"User",
|
||||||
user,
|
user,
|
||||||
|
|||||||
@@ -201,10 +201,11 @@ frappe.ui.form.on("Stock Entry", {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let quality_inspection_field = frm.get_docfield("items", "quality_inspection");
|
let quality_inspection_field = frm.get_docfield("items", "quality_inspection");
|
||||||
|
const incoming_purposes = ["Manufacture", "Material Receipt"];
|
||||||
quality_inspection_field.get_route_options_for_new_doc = function (row) {
|
quality_inspection_field.get_route_options_for_new_doc = function (row) {
|
||||||
if (frm.is_new()) return {};
|
if (frm.is_new()) return {};
|
||||||
return {
|
return {
|
||||||
inspection_type: "Incoming",
|
inspection_type: incoming_purposes.includes(frm.doc.purpose) ? "Incoming" : "Outgoing",
|
||||||
reference_type: frm.doc.doctype,
|
reference_type: frm.doc.doctype,
|
||||||
reference_name: frm.doc.name,
|
reference_name: frm.doc.name,
|
||||||
child_row_reference: row.doc.name,
|
child_row_reference: row.doc.name,
|
||||||
|
|||||||
@@ -2467,9 +2467,24 @@ class StockEntry(StockController):
|
|||||||
):
|
):
|
||||||
self.get_unconsumed_raw_materials()
|
self.get_unconsumed_raw_materials()
|
||||||
|
|
||||||
|
elif self.pro_doc and (
|
||||||
|
self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture"
|
||||||
|
):
|
||||||
|
if not self.fg_completed_qty:
|
||||||
|
frappe.throw(_("{0} is mandatory").format(_(self.meta.get_label("fg_completed_qty"))))
|
||||||
|
|
||||||
|
item_dict = self.get_work_order_raw_materials(self.fg_completed_qty)
|
||||||
|
|
||||||
|
for item in item_dict.values():
|
||||||
|
if self.pro_doc.from_wip_warehouse:
|
||||||
|
item["from_warehouse"] = self.pro_doc.wip_warehouse
|
||||||
|
item["to_warehouse"] = ""
|
||||||
|
|
||||||
|
self.add_to_stock_entry_detail(item_dict)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if not self.fg_completed_qty:
|
if not self.fg_completed_qty:
|
||||||
frappe.throw(_("Manufacturing Quantity is mandatory"))
|
frappe.throw(_("{0} is mandatory").format(_(self.meta.get_label("fg_completed_qty"))))
|
||||||
|
|
||||||
item_dict = self.get_bom_raw_materials(self.fg_completed_qty)
|
item_dict = self.get_bom_raw_materials(self.fg_completed_qty)
|
||||||
|
|
||||||
@@ -2724,6 +2739,56 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
return item_dict
|
return item_dict
|
||||||
|
|
||||||
|
def get_work_order_raw_materials(self, qty):
|
||||||
|
item_dict = frappe._dict()
|
||||||
|
|
||||||
|
used_alternative_items = get_used_alternative_items(
|
||||||
|
subcontract_order_field=self.subcontract_data.order_field, work_order=self.work_order
|
||||||
|
)
|
||||||
|
|
||||||
|
for d in self.pro_doc.get("required_items"):
|
||||||
|
item_qty = flt(
|
||||||
|
(d.required_qty / self.pro_doc.qty) * qty, frappe.get_precision("Stock Entry Detail", "qty")
|
||||||
|
)
|
||||||
|
from_warehouse = (
|
||||||
|
d.source_warehouse
|
||||||
|
if self.pro_doc.skip_transfer and not self.pro_doc.from_wip_warehouse
|
||||||
|
else self.from_warehouse or d.source_warehouse
|
||||||
|
)
|
||||||
|
|
||||||
|
item_row = frappe._dict(
|
||||||
|
{
|
||||||
|
"item_code": d.item_code,
|
||||||
|
"item_name": d.item_name,
|
||||||
|
"description": d.description,
|
||||||
|
"qty": item_qty,
|
||||||
|
"stock_uom": d.stock_uom,
|
||||||
|
"uom": d.stock_uom,
|
||||||
|
"conversion_factor": 1,
|
||||||
|
"from_warehouse": from_warehouse,
|
||||||
|
"allow_alternative_item": d.allow_alternative_item
|
||||||
|
and self.pro_doc.allow_alternative_item,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if d.item_code in used_alternative_items:
|
||||||
|
alt = used_alternative_items.get(d.item_code)
|
||||||
|
item_row.update(
|
||||||
|
{
|
||||||
|
"item_code": alt.item_code,
|
||||||
|
"item_name": alt.item_name,
|
||||||
|
"stock_uom": alt.stock_uom,
|
||||||
|
"uom": alt.uom,
|
||||||
|
"conversion_factor": alt.conversion_factor,
|
||||||
|
"description": alt.description,
|
||||||
|
"original_item": d.item_code,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
item_dict[d.item_code] = item_row
|
||||||
|
|
||||||
|
return item_dict
|
||||||
|
|
||||||
def get_bom_scrap_material(self, qty):
|
def get_bom_scrap_material(self, qty):
|
||||||
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
|
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
|
||||||
|
|
||||||
@@ -3410,10 +3475,12 @@ class StockEntry(StockController):
|
|||||||
def update_subcontracting_order_status(self):
|
def update_subcontracting_order_status(self):
|
||||||
if self.subcontracting_order and self.purpose in ["Send to Subcontractor", "Material Transfer"]:
|
if self.subcontracting_order and self.purpose in ["Send to Subcontractor", "Material Transfer"]:
|
||||||
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
|
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
|
||||||
update_subcontracting_order_status,
|
set_subcontracting_order_status,
|
||||||
)
|
)
|
||||||
|
|
||||||
update_subcontracting_order_status(self.subcontracting_order)
|
# Trusted submit/cancel flow — a Stock operation must not require Subcontracting Order
|
||||||
|
# write permission, so use the no-check internal helper (not the whitelisted boundary).
|
||||||
|
set_subcontracting_order_status(self.subcontracting_order)
|
||||||
|
|
||||||
def update_pick_list_status(self):
|
def update_pick_list_status(self):
|
||||||
from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status
|
from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status
|
||||||
|
|||||||
@@ -1094,6 +1094,327 @@ class TestStockEntry(FrappeTestCase):
|
|||||||
repack.insert()
|
repack.insert()
|
||||||
self.assertRaises(frappe.ValidationError, repack.submit)
|
self.assertRaises(frappe.ValidationError, repack.submit)
|
||||||
|
|
||||||
|
def test_check_item_quality_inspection_returns_items_for_stock_entry(self):
|
||||||
|
from erpnext.controllers.stock_controller import check_item_quality_inspection
|
||||||
|
|
||||||
|
items = [
|
||||||
|
{"item_code": "_Test Item", "qty": 1},
|
||||||
|
{"item_code": "_Test Item Home Desktop 100", "qty": 1},
|
||||||
|
]
|
||||||
|
|
||||||
|
se_result = check_item_quality_inspection("Stock Entry", 0, items)
|
||||||
|
self.assertEqual(len(se_result), 2)
|
||||||
|
|
||||||
|
# a doctype not in the inspection fieldname map and not a Stock Entry returns nothing
|
||||||
|
self.assertEqual(check_item_quality_inspection("Material Request", 0, items), [])
|
||||||
|
|
||||||
|
@change_settings("Stock Settings", {"action_if_quality_inspection_is_rejected": "Stop"})
|
||||||
|
def test_quality_inspection_across_stock_entry_purposes(self):
|
||||||
|
from erpnext.controllers.stock_controller import (
|
||||||
|
QualityInspectionRejectedError,
|
||||||
|
QualityInspectionRequiredError,
|
||||||
|
check_item_quality_inspection,
|
||||||
|
)
|
||||||
|
from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
|
||||||
|
create_quality_inspection,
|
||||||
|
)
|
||||||
|
|
||||||
|
item_code = "_Test Item For QI Purposes"
|
||||||
|
if not frappe.db.exists("Item", item_code):
|
||||||
|
create_item(item_code, is_stock_item=1)
|
||||||
|
|
||||||
|
s_wh = "Stores - _TC"
|
||||||
|
t_wh = "_Test Warehouse - _TC"
|
||||||
|
# stock the source warehouse for transfer / issue purposes
|
||||||
|
make_stock_entry(item_code=item_code, target=s_wh, qty=100, basic_rate=100)
|
||||||
|
|
||||||
|
# purpose -> warehouses for the moved row; inward (with target) requires QI
|
||||||
|
purposes = {
|
||||||
|
"Material Receipt": {"to_warehouse": t_wh},
|
||||||
|
"Material Transfer": {"from_warehouse": s_wh, "to_warehouse": t_wh},
|
||||||
|
"Material Issue": {"from_warehouse": s_wh},
|
||||||
|
}
|
||||||
|
|
||||||
|
for purpose, warehouses in purposes.items():
|
||||||
|
with self.subTest(purpose=purpose):
|
||||||
|
needs_qi = "to_warehouse" in warehouses
|
||||||
|
|
||||||
|
se = make_stock_entry(
|
||||||
|
item_code=item_code,
|
||||||
|
qty=5,
|
||||||
|
basic_rate=100,
|
||||||
|
purpose=purpose,
|
||||||
|
inspection_required=True,
|
||||||
|
do_not_submit=True,
|
||||||
|
**warehouses,
|
||||||
|
)
|
||||||
|
|
||||||
|
# QI can be created from the Stock Entry for any purpose
|
||||||
|
allowed = check_item_quality_inspection("Stock Entry", 0, se.as_dict().get("items"))
|
||||||
|
self.assertTrue(any(row.get("item_code") == item_code for row in allowed))
|
||||||
|
|
||||||
|
if not needs_qi:
|
||||||
|
# outward-only entry: QI is not enforced
|
||||||
|
se.submit()
|
||||||
|
self.assertEqual(se.docstatus, 1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# inward entry without QI must block submission
|
||||||
|
self.assertRaises(QualityInspectionRequiredError, se.submit)
|
||||||
|
|
||||||
|
# a rejected QI must also block submission
|
||||||
|
se_rej = make_stock_entry(
|
||||||
|
item_code=item_code,
|
||||||
|
qty=5,
|
||||||
|
basic_rate=100,
|
||||||
|
purpose=purpose,
|
||||||
|
inspection_required=True,
|
||||||
|
do_not_submit=True,
|
||||||
|
**warehouses,
|
||||||
|
)
|
||||||
|
create_quality_inspection(
|
||||||
|
reference_type="Stock Entry",
|
||||||
|
reference_name=se_rej.name,
|
||||||
|
item_code=item_code,
|
||||||
|
inspection_type="Incoming",
|
||||||
|
status="Rejected",
|
||||||
|
)
|
||||||
|
se_rej.reload()
|
||||||
|
self.assertRaises(QualityInspectionRejectedError, se_rej.submit)
|
||||||
|
|
||||||
|
# a submitted, accepted QI links itself to the inward row; submission then succeeds
|
||||||
|
se_ok = make_stock_entry(
|
||||||
|
item_code=item_code,
|
||||||
|
qty=5,
|
||||||
|
basic_rate=100,
|
||||||
|
purpose=purpose,
|
||||||
|
inspection_required=True,
|
||||||
|
do_not_submit=True,
|
||||||
|
**warehouses,
|
||||||
|
)
|
||||||
|
create_quality_inspection(
|
||||||
|
reference_type="Stock Entry",
|
||||||
|
reference_name=se_ok.name,
|
||||||
|
item_code=item_code,
|
||||||
|
inspection_type="Incoming",
|
||||||
|
status="Accepted",
|
||||||
|
)
|
||||||
|
se_ok.reload()
|
||||||
|
se_ok.submit()
|
||||||
|
self.assertEqual(se_ok.docstatus, 1)
|
||||||
|
|
||||||
|
@change_settings("Stock Settings", {"action_if_quality_inspection_is_rejected": "Stop"})
|
||||||
|
def test_quality_inspection_required_for_manufacture(self):
|
||||||
|
from erpnext.controllers.stock_controller import (
|
||||||
|
QualityInspectionRejectedError,
|
||||||
|
QualityInspectionRequiredError,
|
||||||
|
)
|
||||||
|
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||||
|
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||||
|
make_stock_entry as make_wo_stock_entry,
|
||||||
|
)
|
||||||
|
from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
|
||||||
|
create_quality_inspection,
|
||||||
|
)
|
||||||
|
|
||||||
|
wo = make_wo_order_test_record(qty=1)
|
||||||
|
make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=100)
|
||||||
|
make_stock_entry(
|
||||||
|
item_code="_Test Item Home Desktop 100", target="Stores - _TC", qty=10, basic_rate=100
|
||||||
|
)
|
||||||
|
|
||||||
|
# transfer raw materials to WIP (no inspection on the transfer)
|
||||||
|
transfer = frappe.get_doc(make_wo_stock_entry(wo.name, "Material Transfer for Manufacture", 1))
|
||||||
|
for d in transfer.get("items"):
|
||||||
|
d.s_warehouse = "Stores - _TC"
|
||||||
|
transfer.insert()
|
||||||
|
transfer.submit()
|
||||||
|
|
||||||
|
# manufacture with inspection required
|
||||||
|
mfg = frappe.get_doc(make_wo_stock_entry(wo.name, "Manufacture", 1))
|
||||||
|
mfg.inspection_required = 1
|
||||||
|
mfg.insert()
|
||||||
|
self.assertRaises(QualityInspectionRequiredError, mfg.submit)
|
||||||
|
|
||||||
|
# a rejected QI on the finished-good row must also block submission
|
||||||
|
qi = create_quality_inspection(
|
||||||
|
reference_type="Stock Entry",
|
||||||
|
reference_name=mfg.name,
|
||||||
|
item_code=wo.production_item,
|
||||||
|
inspection_type="Incoming",
|
||||||
|
status="Rejected",
|
||||||
|
)
|
||||||
|
mfg.reload()
|
||||||
|
self.assertRaises(QualityInspectionRejectedError, mfg.submit)
|
||||||
|
|
||||||
|
# accepting the QI then allows submission
|
||||||
|
frappe.db.set_value("Quality Inspection", qi.name, "status", "Accepted")
|
||||||
|
mfg.reload()
|
||||||
|
mfg.submit()
|
||||||
|
self.assertEqual(mfg.docstatus, 1)
|
||||||
|
|
||||||
|
@change_settings("Stock Settings", {"action_if_quality_inspection_is_rejected": "Stop"})
|
||||||
|
def test_quality_inspection_required_for_material_transfer_for_manufacture(self):
|
||||||
|
from erpnext.controllers.stock_controller import (
|
||||||
|
QualityInspectionRejectedError,
|
||||||
|
QualityInspectionRequiredError,
|
||||||
|
)
|
||||||
|
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||||
|
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||||
|
make_stock_entry as make_wo_stock_entry,
|
||||||
|
)
|
||||||
|
from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
|
||||||
|
create_quality_inspection,
|
||||||
|
)
|
||||||
|
|
||||||
|
wo = make_wo_order_test_record(qty=1)
|
||||||
|
make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=100)
|
||||||
|
make_stock_entry(
|
||||||
|
item_code="_Test Item Home Desktop 100", target="Stores - _TC", qty=10, basic_rate=100
|
||||||
|
)
|
||||||
|
|
||||||
|
transfer = frappe.get_doc(make_wo_stock_entry(wo.name, "Material Transfer for Manufacture", 1))
|
||||||
|
for d in transfer.get("items"):
|
||||||
|
d.s_warehouse = "Stores - _TC"
|
||||||
|
transfer.inspection_required = 1
|
||||||
|
transfer.insert()
|
||||||
|
self.assertRaises(QualityInspectionRequiredError, transfer.submit)
|
||||||
|
|
||||||
|
# a rejected QI on any row moved into WIP must block submission;
|
||||||
|
# every raw-material row moved into WIP needs a QI
|
||||||
|
qis = []
|
||||||
|
for item_code in {d.item_code for d in transfer.items if d.t_warehouse}:
|
||||||
|
qis.append(
|
||||||
|
create_quality_inspection(
|
||||||
|
reference_type="Stock Entry",
|
||||||
|
reference_name=transfer.name,
|
||||||
|
item_code=item_code,
|
||||||
|
inspection_type="Incoming",
|
||||||
|
status="Rejected",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
transfer.reload()
|
||||||
|
self.assertRaises(QualityInspectionRejectedError, transfer.submit)
|
||||||
|
|
||||||
|
# accepting every QI then allows submission
|
||||||
|
for qi in qis:
|
||||||
|
frappe.db.set_value("Quality Inspection", qi.name, "status", "Accepted")
|
||||||
|
transfer.reload()
|
||||||
|
transfer.submit()
|
||||||
|
self.assertEqual(transfer.docstatus, 1)
|
||||||
|
|
||||||
|
def test_quality_inspection_required_for_send_to_subcontractor(self):
|
||||||
|
from erpnext.controllers.stock_controller import QualityInspectionRequiredError
|
||||||
|
from erpnext.controllers.subcontracting_controller import make_rm_stock_entry
|
||||||
|
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||||
|
get_subcontracting_order,
|
||||||
|
make_service_item,
|
||||||
|
)
|
||||||
|
from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
|
||||||
|
create_quality_inspection,
|
||||||
|
)
|
||||||
|
|
||||||
|
make_service_item("Subcontracted Service Item 1")
|
||||||
|
sco = get_subcontracting_order(
|
||||||
|
service_items=[
|
||||||
|
{
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"item_code": "Subcontracted Service Item 1",
|
||||||
|
"qty": 10,
|
||||||
|
"rate": 500,
|
||||||
|
"fg_item": "_Test FG Item",
|
||||||
|
"fg_item_qty": 10,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=100, basic_rate=100)
|
||||||
|
make_stock_entry(
|
||||||
|
item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=100, basic_rate=100
|
||||||
|
)
|
||||||
|
|
||||||
|
se = frappe.get_doc(make_rm_stock_entry(sco.name))
|
||||||
|
se.from_warehouse = "_Test Warehouse - _TC"
|
||||||
|
se.to_warehouse = "_Test Warehouse - _TC"
|
||||||
|
se.stock_entry_type = "Send to Subcontractor"
|
||||||
|
se.inspection_required = 1
|
||||||
|
se.insert()
|
||||||
|
self.assertRaises(QualityInspectionRequiredError, se.submit)
|
||||||
|
|
||||||
|
for item_code in {row.item_code for row in se.items if row.t_warehouse}:
|
||||||
|
create_quality_inspection(
|
||||||
|
reference_type="Stock Entry",
|
||||||
|
reference_name=se.name,
|
||||||
|
item_code=item_code,
|
||||||
|
inspection_type="Outgoing",
|
||||||
|
status="Accepted",
|
||||||
|
)
|
||||||
|
se.reload()
|
||||||
|
se.submit()
|
||||||
|
self.assertEqual(se.docstatus, 1)
|
||||||
|
|
||||||
|
@change_settings("Stock Settings", {"action_if_quality_inspection_is_rejected": "Stop"})
|
||||||
|
def test_quality_inspection_required_for_disassemble(self):
|
||||||
|
from erpnext.controllers.stock_controller import (
|
||||||
|
QualityInspectionRejectedError,
|
||||||
|
QualityInspectionRequiredError,
|
||||||
|
)
|
||||||
|
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||||
|
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||||
|
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||||
|
make_stock_entry as make_wo_stock_entry,
|
||||||
|
)
|
||||||
|
from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
|
||||||
|
create_quality_inspection,
|
||||||
|
)
|
||||||
|
|
||||||
|
source_warehouse = "Stores - _TC"
|
||||||
|
fg_item = make_item("Test Disassemble FG QI", {"is_stock_item": 1}).name
|
||||||
|
raw_materials = ["Test Disassemble RM QI 1", "Test Disassemble RM QI 2"]
|
||||||
|
for item in raw_materials:
|
||||||
|
make_item(item, {"is_stock_item": 1})
|
||||||
|
make_stock_entry(item_code=item, target=source_warehouse, qty=5, basic_rate=100)
|
||||||
|
|
||||||
|
make_bom(item=fg_item, source_warehouse=source_warehouse, raw_materials=raw_materials)
|
||||||
|
|
||||||
|
wo = make_wo_order_test_record(
|
||||||
|
item=fg_item, qty=1, source_warehouse=source_warehouse, skip_transfer=1
|
||||||
|
)
|
||||||
|
|
||||||
|
# manufacture the FG so there is something to disassemble
|
||||||
|
mfg = frappe.get_doc(make_wo_stock_entry(wo.name, "Manufacture", 1))
|
||||||
|
for row in mfg.items:
|
||||||
|
if row.item_code in raw_materials:
|
||||||
|
row.s_warehouse = source_warehouse
|
||||||
|
mfg.submit()
|
||||||
|
|
||||||
|
# disassemble with inspection required -> the component rows need a QI
|
||||||
|
dis = frappe.get_doc(make_wo_stock_entry(wo.name, "Disassemble", 1))
|
||||||
|
dis.inspection_required = 1
|
||||||
|
dis.insert()
|
||||||
|
self.assertRaises(QualityInspectionRequiredError, dis.submit)
|
||||||
|
|
||||||
|
# a rejected QI on any disassembled component row must also block submission
|
||||||
|
qis = []
|
||||||
|
for item_code in {row.item_code for row in dis.items if row.t_warehouse}:
|
||||||
|
qis.append(
|
||||||
|
create_quality_inspection(
|
||||||
|
reference_type="Stock Entry",
|
||||||
|
reference_name=dis.name,
|
||||||
|
item_code=item_code,
|
||||||
|
inspection_type="Outgoing",
|
||||||
|
status="Rejected",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
dis.reload()
|
||||||
|
self.assertRaises(QualityInspectionRejectedError, dis.submit)
|
||||||
|
|
||||||
|
# accepting every QI then allows submission
|
||||||
|
for qi in qis:
|
||||||
|
frappe.db.set_value("Quality Inspection", qi.name, "status", "Accepted")
|
||||||
|
dis.reload()
|
||||||
|
dis.submit()
|
||||||
|
self.assertEqual(dis.docstatus, 1)
|
||||||
|
|
||||||
def test_customer_provided_parts_se(self):
|
def test_customer_provided_parts_se(self):
|
||||||
create_item("CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0)
|
create_item("CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0)
|
||||||
se = make_stock_entry(
|
se = make_stock_entry(
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class StockRepostingSettings(Document):
|
|||||||
if diff < 10:
|
if diff < 10:
|
||||||
self.end_time = get_time_str(add_to_date(self.start_time, hours=10, as_datetime=True))
|
self.end_time = get_time_str(add_to_date(self.start_time, hours=10, as_datetime=True))
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist(methods=["POST"])
|
||||||
def convert_to_item_wh_reposting(self):
|
def convert_to_item_wh_reposting(self):
|
||||||
"""Convert Transaction reposting to Item Warehouse based reposting if Item Based Reposting has enabled."""
|
"""Convert Transaction reposting to Item Warehouse based reposting if Item Based Reposting has enabled."""
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"print_hide_if_no_value": 0,
|
"print_hide_if_no_value": 0,
|
||||||
"read_only": 0,
|
"read_only": 0,
|
||||||
"report_hide": 0,
|
"report_hide": 0,
|
||||||
"reqd": 0,
|
"reqd": 1,
|
||||||
"search_index": 0,
|
"search_index": 0,
|
||||||
"set_only_once": 0,
|
"set_only_once": 0,
|
||||||
"unique": 0
|
"unique": 0
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
"issingle": 0,
|
"issingle": 0,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"modified": "2016-07-11 03:28:09.626948",
|
"modified": "2026-06-11 23:02:54.800673",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "UOM Conversion Detail",
|
"name": "UOM Conversion Detail",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class UOMConversionDetail(Document):
|
|||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
uom: DF.Link | None
|
uom: DF.Link
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -134,12 +134,15 @@ def get_linked_cancelled_sabb(filters):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def fix_sabb_entries(selected_rows):
|
def fix_sabb_entries(selected_rows: str | list):
|
||||||
|
frappe.has_permission("Serial and Batch Bundle", "write", throw=True)
|
||||||
|
|
||||||
if isinstance(selected_rows, str):
|
if isinstance(selected_rows, str):
|
||||||
selected_rows = frappe.parse_json(selected_rows)
|
selected_rows = frappe.parse_json(selected_rows)
|
||||||
|
|
||||||
for row in selected_rows:
|
for row in selected_rows:
|
||||||
doc = frappe.get_doc("Serial and Batch Bundle", row.get("name"))
|
doc = frappe.get_doc("Serial and Batch Bundle", row.get("name"))
|
||||||
|
doc.check_permission("write")
|
||||||
if doc.is_cancelled == 0 and not frappe.db.get_value(
|
if doc.is_cancelled == 0 and not frappe.db.get_value(
|
||||||
"Stock Ledger Entry",
|
"Stock Ledger Entry",
|
||||||
{"serial_and_batch_bundle": doc.name, "is_cancelled": 0},
|
{"serial_and_batch_bundle": doc.name, "is_cancelled": 0},
|
||||||
|
|||||||
@@ -306,6 +306,11 @@ class FIFOSlots:
|
|||||||
# prepare single sle voucher detail lookup
|
# prepare single sle voucher detail lookup
|
||||||
self.prepare_stock_reco_voucher_wise_count()
|
self.prepare_stock_reco_voucher_wise_count()
|
||||||
|
|
||||||
|
if stock_ledger_entries is None:
|
||||||
|
# nested queries invalidate the streaming cursor below,
|
||||||
|
# so batchwise valuation flags must be resolved beforehand
|
||||||
|
self._prefetch_batchwise_valuations()
|
||||||
|
|
||||||
with frappe.db.unbuffered_cursor():
|
with frappe.db.unbuffered_cursor():
|
||||||
if stock_ledger_entries is None:
|
if stock_ledger_entries is None:
|
||||||
stock_ledger_entries = self._get_stock_ledger_entries()
|
stock_ledger_entries = self._get_stock_ledger_entries()
|
||||||
@@ -423,12 +428,38 @@ class FIFOSlots:
|
|||||||
|
|
||||||
def _get_batchwise_valuation(self, batch_no: str):
|
def _get_batchwise_valuation(self, batch_no: str):
|
||||||
if batch_no not in self.batchwise_valuation_by_batch:
|
if batch_no not in self.batchwise_valuation_by_batch:
|
||||||
|
# only reachable when stock ledger entries are passed in directly;
|
||||||
|
# the streaming path prefetches all flags before iteration
|
||||||
self.batchwise_valuation_by_batch[batch_no] = frappe.db.get_value(
|
self.batchwise_valuation_by_batch[batch_no] = frappe.db.get_value(
|
||||||
"Batch", batch_no, "use_batchwise_valuation"
|
"Batch", batch_no, "use_batchwise_valuation"
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.batchwise_valuation_by_batch[batch_no]
|
return self.batchwise_valuation_by_batch[batch_no]
|
||||||
|
|
||||||
|
def _prefetch_batchwise_valuations(self) -> None:
|
||||||
|
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
|
batch = frappe.qb.DocType("Batch")
|
||||||
|
to_date = get_datetime(self.filters.get("to_date") + " 23:59:59")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(sle)
|
||||||
|
.left_join(batch)
|
||||||
|
.on(sle.batch_no == batch.name)
|
||||||
|
.select(sle.batch_no, batch.use_batchwise_valuation)
|
||||||
|
.distinct()
|
||||||
|
.where(
|
||||||
|
(sle.batch_no.isnotnull())
|
||||||
|
& (sle.company == self.filters.get("company"))
|
||||||
|
& (sle.posting_datetime <= to_date)
|
||||||
|
& (sle.is_cancelled != 1)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
query = self._apply_filter(query, sle, "item_code")
|
||||||
|
|
||||||
|
for batch_no, use_batchwise_valuation in query.run():
|
||||||
|
self.batchwise_valuation_by_batch[batch_no] = use_batchwise_valuation
|
||||||
|
|
||||||
def _init_key_stores(self, row: dict) -> tuple:
|
def _init_key_stores(self, row: dict) -> tuple:
|
||||||
"Initialise keys and FIFO Queue."
|
"Initialise keys and FIFO Queue."
|
||||||
|
|
||||||
|
|||||||
@@ -1438,6 +1438,80 @@ class TestStockAgeing(FrappeTestCase):
|
|||||||
item_result["fifo_queue"], [[batch_no.upper(), 1, 5.0, getdate(add_days(base_date, -2)), 50.0]]
|
item_result["fifo_queue"], [[batch_no.upper(), 1, 5.0, getdate(add_days(base_date, -2)), 50.0]]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_legacy_batch_no_sle_with_streaming_cursor(self):
|
||||||
|
"""SLEs carrying the legacy batch_no field must not trigger nested
|
||||||
|
queries while entries stream through an unbuffered cursor."""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from frappe.utils import add_days, nowdate
|
||||||
|
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
|
||||||
|
get_batch_from_bundle,
|
||||||
|
)
|
||||||
|
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||||
|
create_stock_reconciliation,
|
||||||
|
)
|
||||||
|
|
||||||
|
suffix = frappe.generate_hash(length=8).upper()
|
||||||
|
item_code = make_item(
|
||||||
|
f"Test Stock Ageing Legacy Batch {suffix}",
|
||||||
|
{
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"batch_number_series": f"SA-LEG-{suffix}-.###",
|
||||||
|
"valuation_method": "FIFO",
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
base_date = nowdate()
|
||||||
|
|
||||||
|
reco = create_stock_reconciliation(
|
||||||
|
item_code=item_code,
|
||||||
|
warehouse=warehouse,
|
||||||
|
qty=10,
|
||||||
|
rate=10,
|
||||||
|
posting_date=add_days(base_date, -2),
|
||||||
|
posting_time="10:00:00",
|
||||||
|
)
|
||||||
|
batch_no = get_batch_from_bundle(reco.items[0].serial_and_batch_bundle)
|
||||||
|
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
|
||||||
|
|
||||||
|
create_stock_reconciliation(
|
||||||
|
item_code=item_code,
|
||||||
|
warehouse=warehouse,
|
||||||
|
qty=5,
|
||||||
|
rate=10,
|
||||||
|
batch_no=batch_no,
|
||||||
|
posting_date=add_days(base_date, -1),
|
||||||
|
posting_time="10:00:00",
|
||||||
|
)
|
||||||
|
|
||||||
|
# mimic pre-bundle data where SLEs carry batch_no directly
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
{"item_code": item_code},
|
||||||
|
"batch_no",
|
||||||
|
batch_no,
|
||||||
|
)
|
||||||
|
|
||||||
|
filters = frappe._dict(
|
||||||
|
company="_Test Company",
|
||||||
|
to_date=base_date,
|
||||||
|
ranges=["30", "60", "90"],
|
||||||
|
item_code=item_code,
|
||||||
|
)
|
||||||
|
fifo_slots = FIFOSlots(filters)
|
||||||
|
|
||||||
|
# fetch row by row so the streaming result set is still active
|
||||||
|
# while each stock ledger entry is processed
|
||||||
|
with patch("frappe.database.database.SQL_ITERATOR_BATCH_SIZE", 1):
|
||||||
|
slots = fifo_slots.generate()
|
||||||
|
|
||||||
|
self.assertEqual(fifo_slots.batchwise_valuation_by_batch.get(batch_no), 1)
|
||||||
|
self.assertEqual(slots[item_code]["total_qty"], 5.0)
|
||||||
|
|
||||||
|
|
||||||
def generate_item_and_item_wh_wise_slots(filters, sle):
|
def generate_item_and_item_wh_wise_slots(filters, sle):
|
||||||
"Return results with and without 'show_warehouse_wise_stock'"
|
"Return results with and without 'show_warehouse_wise_stock'"
|
||||||
|
|||||||
@@ -277,12 +277,13 @@ class StockBalanceReport:
|
|||||||
qty_dict.opening_qty -= self.stock_reco_voucher_wise_count.get(entry.voucher_detail_no, 0)
|
qty_dict.opening_qty -= self.stock_reco_voucher_wise_count.get(entry.voucher_detail_no, 0)
|
||||||
qty_dict.bal_qty = 0.0
|
qty_dict.bal_qty = 0.0
|
||||||
qty_diff = flt(entry.actual_qty)
|
qty_diff = flt(entry.actual_qty)
|
||||||
|
value_diff = flt(entry.stock_value_difference)
|
||||||
else:
|
else:
|
||||||
qty_diff = flt(entry.qty_after_transaction) - flt(qty_dict.bal_qty)
|
qty_diff = flt(entry.qty_after_transaction) - flt(qty_dict.bal_qty)
|
||||||
|
value_diff = flt(entry.stock_value) - flt(qty_dict.bal_val)
|
||||||
else:
|
else:
|
||||||
qty_diff = flt(entry.actual_qty)
|
qty_diff = flt(entry.actual_qty)
|
||||||
|
value_diff = flt(entry.stock_value_difference)
|
||||||
value_diff = flt(entry.stock_value_difference)
|
|
||||||
|
|
||||||
if entry.posting_date < self.from_date or entry.voucher_no in self.opening_vouchers.get(
|
if entry.posting_date < self.from_date or entry.voucher_no in self.opening_vouchers.get(
|
||||||
entry.voucher_type, []
|
entry.voucher_type, []
|
||||||
|
|||||||
@@ -1188,6 +1188,7 @@ class update_entries_after:
|
|||||||
sle.recalculate_rate
|
sle.recalculate_rate
|
||||||
or self.has_landed_cost_based_on_pi(sle)
|
or self.has_landed_cost_based_on_pi(sle)
|
||||||
or (sle.voucher_type == "Stock Entry" and sle.actual_qty > 0 and is_repack_entry(sle.voucher_no))
|
or (sle.voucher_type == "Stock Entry" and sle.actual_qty > 0 and is_repack_entry(sle.voucher_no))
|
||||||
|
or (self.repost_doc and self.repost_doc.get("recalculate_valuation_rate"))
|
||||||
):
|
):
|
||||||
rate = self.get_incoming_outgoing_rate_from_transaction(sle)
|
rate = self.get_incoming_outgoing_rate_from_transaction(sle)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.model.document import Document
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
|
|
||||||
@@ -363,9 +364,18 @@ def get_mapped_subcontracting_receipt(source_name, target_doc=None):
|
|||||||
return target_doc
|
return target_doc
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
def set_subcontracting_order_status(sco: str | Document, status: str | None = None):
|
||||||
def update_subcontracting_order_status(sco, status=None):
|
|
||||||
if isinstance(sco, str):
|
if isinstance(sco, str):
|
||||||
sco = frappe.get_doc("Subcontracting Order", sco)
|
sco = frappe.get_doc("Subcontracting Order", sco)
|
||||||
|
|
||||||
sco.update_status(status)
|
sco.update_status(status)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def update_subcontracting_order_status(sco: str | Document, status: str | None = None):
|
||||||
|
"""Whitelisted boundary for direct API/UI calls — enforces write permission, then delegates."""
|
||||||
|
if isinstance(sco, str):
|
||||||
|
sco = frappe.get_doc("Subcontracting Order", sco)
|
||||||
|
|
||||||
|
sco.check_permission("write")
|
||||||
|
set_subcontracting_order_status(sco, status)
|
||||||
|
|||||||
@@ -336,6 +336,62 @@ class TestSubcontractingOrder(FrappeTestCase):
|
|||||||
bin_after_cancel_sco.reserved_qty_for_sub_contract, bin_before_sco.reserved_qty_for_sub_contract
|
bin_after_cancel_sco.reserved_qty_for_sub_contract, bin_before_sco.reserved_qty_for_sub_contract
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_send_to_subcontractor_ste_submit_without_sco_write_permission(self):
|
||||||
|
"""A Stock-only user (can submit Stock Entries but has no Subcontracting Order write) must be
|
||||||
|
able to submit and cancel a 'Send to Subcontractor' Stock Entry. The SCO status update on the
|
||||||
|
on_submit/on_cancel path goes through the no-permission-check internal helper, not the
|
||||||
|
whitelisted API boundary.
|
||||||
|
|
||||||
|
Regression: the permission hardening put check_permission('write') on the shared status
|
||||||
|
function, so a Stock Manager (no SCO write) hit PermissionError submitting/cancelling the
|
||||||
|
Stock Entry. The suite otherwise runs as Administrator and never caught it."""
|
||||||
|
from frappe.core.doctype.user_permission.test_user_permission import create_user
|
||||||
|
|
||||||
|
make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=10, basic_rate=100)
|
||||||
|
|
||||||
|
service_items = [
|
||||||
|
{
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"item_code": "Subcontracted Service Item 1",
|
||||||
|
"qty": 10,
|
||||||
|
"rate": 100,
|
||||||
|
"fg_item": "_Test FG Item",
|
||||||
|
"fg_item_qty": 10,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
sco = get_subcontracting_order(service_items=service_items)
|
||||||
|
|
||||||
|
rm_items = [
|
||||||
|
{
|
||||||
|
"item_code": "_Test FG Item",
|
||||||
|
"rm_item_code": "_Test Item",
|
||||||
|
"item_name": "_Test Item",
|
||||||
|
"qty": 10,
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"rate": 100,
|
||||||
|
"amount": 1000,
|
||||||
|
"stock_uom": "Nos",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
ste = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items))
|
||||||
|
ste.to_warehouse = "_Test Warehouse 1 - _TC"
|
||||||
|
ste.save()
|
||||||
|
|
||||||
|
stock_user = create_user("test_sco_stock_only@example.com", "Stock Manager")
|
||||||
|
self.assertFalse(
|
||||||
|
frappe.has_permission("Subcontracting Order", "write", user=stock_user.name),
|
||||||
|
"Precondition: the Stock-only user must not have Subcontracting Order write permission.",
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.set_user(stock_user.name)
|
||||||
|
try:
|
||||||
|
ste.reload()
|
||||||
|
ste.submit() # must not raise PermissionError on the SCO status update
|
||||||
|
ste.reload()
|
||||||
|
ste.cancel() # same on the cancel path
|
||||||
|
finally:
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
def test_exploded_items(self):
|
def test_exploded_items(self):
|
||||||
item_code = "_Test Subcontracted FG Item 11"
|
item_code = "_Test Subcontracted FG Item 11"
|
||||||
make_subcontracted_item(item_code=item_code)
|
make_subcontracted_item(item_code=item_code)
|
||||||
|
|||||||
@@ -161,7 +161,6 @@ class SubcontractingReceipt(SubcontractingController):
|
|||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.validate_closed_subcontracting_order()
|
self.validate_closed_subcontracting_order()
|
||||||
self.validate_available_qty_for_consumption()
|
|
||||||
self.validate_bom_required_qty()
|
self.validate_bom_required_qty()
|
||||||
self.update_status_updater_args()
|
self.update_status_updater_args()
|
||||||
self.update_prevdoc_status()
|
self.update_prevdoc_status()
|
||||||
@@ -506,32 +505,6 @@ class SubcontractingReceipt(SubcontractingController):
|
|||||||
_("Row #{0}: Accepted Warehouse and Rejected Warehouse cannot be same").format(item.idx)
|
_("Row #{0}: Accepted Warehouse and Rejected Warehouse cannot be same").format(item.idx)
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_available_qty_for_consumption(self):
|
|
||||||
if (
|
|
||||||
frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
|
|
||||||
== "BOM"
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
for item in self.get("supplied_items"):
|
|
||||||
precision = item.precision("consumed_qty")
|
|
||||||
if (
|
|
||||||
item.available_qty_for_consumption
|
|
||||||
and flt(item.available_qty_for_consumption, precision) - flt(item.consumed_qty, precision) < 0
|
|
||||||
):
|
|
||||||
msg = _(
|
|
||||||
"""Row {0}: Consumed Qty {1} {2} must be less than or equal to Available Qty For Consumption
|
|
||||||
{3} {4} in Consumed Items Table."""
|
|
||||||
).format(
|
|
||||||
item.idx,
|
|
||||||
flt(item.consumed_qty, precision),
|
|
||||||
item.stock_uom,
|
|
||||||
flt(item.available_qty_for_consumption, precision),
|
|
||||||
item.stock_uom,
|
|
||||||
)
|
|
||||||
|
|
||||||
frappe.throw(msg)
|
|
||||||
|
|
||||||
def validate_bom_required_qty(self):
|
def validate_bom_required_qty(self):
|
||||||
if (
|
if (
|
||||||
frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
|
frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
|
||||||
|
|||||||
@@ -118,7 +118,9 @@ class Issue(Document):
|
|||||||
communication.save()
|
communication.save()
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def split_issue(self, subject, communication_id):
|
def split_issue(self, subject: str, communication_id: str):
|
||||||
|
self.check_permission("write")
|
||||||
|
|
||||||
# Bug: Pressing enter doesn't send subject
|
# Bug: Pressing enter doesn't send subject
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
@@ -274,7 +276,7 @@ def make_task(source_name, target_doc=None):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_issue_from_communication(communication, ignore_communication_links=False):
|
def make_issue_from_communication(communication: str, ignore_communication_links: bool = False):
|
||||||
"""raise a issue from email"""
|
"""raise a issue from email"""
|
||||||
|
|
||||||
doc = frappe.get_doc("Communication", communication)
|
doc = frappe.get_doc("Communication", communication)
|
||||||
@@ -286,7 +288,7 @@ def make_issue_from_communication(communication, ignore_communication_links=Fals
|
|||||||
"raised_by": doc.sender or "",
|
"raised_by": doc.sender or "",
|
||||||
"raised_by_phone": doc.phone_no or "",
|
"raised_by_phone": doc.phone_no or "",
|
||||||
}
|
}
|
||||||
).insert(ignore_permissions=True)
|
).insert()
|
||||||
|
|
||||||
link_communication_to_document(doc, "Issue", issue.name, ignore_communication_links)
|
link_communication_to_document(doc, "Issue", issue.name, ignore_communication_links)
|
||||||
|
|
||||||
|
|||||||
@@ -79,10 +79,6 @@
|
|||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gravatar-top{
|
|
||||||
margin-top:8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-hg{
|
.progress-hg{
|
||||||
margin-bottom: 30!important;
|
margin-bottom: 30!important;
|
||||||
height:2px;
|
height:2px;
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.rate_limiter import rate_limit
|
||||||
from frappe.utils import escape_html
|
from frappe.utils import escape_html
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True, methods=["POST"])
|
||||||
|
@rate_limit(limit=10, seconds=3 * 60)
|
||||||
def send_message(sender, message, subject="Website Query"):
|
def send_message(sender, message, subject="Website Query"):
|
||||||
from frappe.www.contact import send_message as website_send_message
|
from frappe.www.contact import send_message as website_send_message
|
||||||
|
|
||||||
@@ -14,6 +16,14 @@ def send_message(sender, message, subject="Website Query"):
|
|||||||
|
|
||||||
message = escape_html(message)
|
message = escape_html(message)
|
||||||
|
|
||||||
|
oppotunity_creation = frappe.get_single_value(
|
||||||
|
"CRM Settings", "enable_opportunity_creation_from_contact_us"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not oppotunity_creation:
|
||||||
|
# Meant to silently fail instead of throwing error.
|
||||||
|
return
|
||||||
|
|
||||||
lead = customer = None
|
lead = customer = None
|
||||||
customer = frappe.db.sql(
|
customer = frappe.db.sql(
|
||||||
"""select distinct dl.link_name from `tabDynamic Link` dl
|
"""select distinct dl.link_name from `tabDynamic Link` dl
|
||||||
|
|||||||
Reference in New Issue
Block a user