mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-14 10:23:04 +00:00
Compare commits
35 Commits
v15.111.0
...
version-15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe3f44f643 | ||
|
|
8b3a0fe045 | ||
|
|
3d6a4eebee | ||
|
|
0a3c53b16d | ||
|
|
ef3046dca2 | ||
|
|
5addc66301 | ||
|
|
08d9b8275d | ||
|
|
46b3e0c385 | ||
|
|
559c95c8a8 | ||
|
|
d4605771da | ||
|
|
209977f6a3 | ||
|
|
eec11ac7b2 | ||
|
|
7c78aa6e5d | ||
|
|
f4e6f14342 | ||
|
|
48886467ec | ||
|
|
baafb95e74 | ||
|
|
f4630273ad | ||
|
|
a65629da1a | ||
|
|
808ca06801 | ||
|
|
74da3b8775 | ||
|
|
c3a3eb3df3 | ||
|
|
d4baf0aeba | ||
|
|
e40999c879 | ||
|
|
559585fb7b | ||
|
|
ee3f502538 | ||
|
|
f37727d399 | ||
|
|
b8d507e496 | ||
|
|
11359b0ac2 | ||
|
|
7639a3360e | ||
|
|
f1fc9e3261 | ||
|
|
b9a694bb37 | ||
|
|
c03a66a1bf | ||
|
|
02e38e80a7 | ||
|
|
2905669af4 | ||
|
|
c6176500d2 |
10
.greptile/config.json
Normal file
10
.greptile/config.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"disabledLabels": [
|
||||
"conflicts"
|
||||
],
|
||||
"context": {
|
||||
"repos": [
|
||||
"frappe/frappe"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.111.0"
|
||||
__version__ = "15.108.3"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -90,7 +90,14 @@ class BankClearance(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_clearance_date(self):
|
||||
clearance_date_updated = False
|
||||
payment_docs = []
|
||||
for d in self.get("payment_entries"):
|
||||
if d.payment_document not in payment_docs:
|
||||
payment_docs.append(d.payment_document)
|
||||
|
||||
for doctype in payment_docs:
|
||||
frappe.has_permission(doctype, "write", throw=True)
|
||||
|
||||
for d in self.get("payment_entries"):
|
||||
if d.clearance_date:
|
||||
if not d.payment_document:
|
||||
|
||||
@@ -154,12 +154,13 @@ class RepostAccountingLedger(Document):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def start_repost(account_repost_doc=str) -> None:
|
||||
def start_repost(account_repost_doc: str | None = None) -> None:
|
||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
|
||||
frappe.flags.through_repost_accounting_ledger = True
|
||||
if account_repost_doc:
|
||||
repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc)
|
||||
repost_doc.check_permission("write")
|
||||
|
||||
if repost_doc.docstatus == 1:
|
||||
# Prevent repost on invoices with deferred accounting
|
||||
|
||||
@@ -511,7 +511,8 @@ def get_party_advance_account(party_type, party, company):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_party_bank_account(party_type, party):
|
||||
def get_party_bank_account(party_type: str, party: str):
|
||||
frappe.has_permission("Bank Account", "read", throw=True)
|
||||
return frappe.db.get_value("Bank Account", {"party_type": party_type, "party": party, "is_default": 1})
|
||||
|
||||
|
||||
|
||||
@@ -922,8 +922,28 @@ class ReceivablePayableReport:
|
||||
if self.filters.project:
|
||||
self.qb_selection_filter.append(self.ple.project.isin(self.filters.project))
|
||||
|
||||
self.add_user_permission_filters()
|
||||
|
||||
self.add_accounting_dimensions_filters()
|
||||
|
||||
def add_user_permission_filters(self):
|
||||
# Party is a dynamic link, so match conditions cannot auto-apply Customer/Supplier user permissions
|
||||
from frappe.core.doctype.user_permission.user_permission import get_user_permissions
|
||||
from frappe.permissions import get_allowed_docs_for_doctype
|
||||
|
||||
user_permissions = get_user_permissions()
|
||||
if not user_permissions:
|
||||
return
|
||||
|
||||
for party_type in self.party_type:
|
||||
if party_type not in user_permissions:
|
||||
continue
|
||||
|
||||
allowed_parties = get_allowed_docs_for_doctype(user_permissions[party_type], party_type)
|
||||
self.qb_selection_filter.append(
|
||||
(self.ple.party_type != party_type) | self.ple.party.isin(allowed_parties or [""])
|
||||
)
|
||||
|
||||
def get_cost_center_conditions(self):
|
||||
cost_center_list = get_cost_centers_with_children(self.filters.cost_center)
|
||||
self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list))
|
||||
|
||||
@@ -1253,3 +1253,53 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
row = report[1][0]
|
||||
self.assertEqual([si.name, project.name, 60], [row.voucher_no, row.project, row.outstanding])
|
||||
|
||||
def test_accounts_receivable_respects_user_permissions(self):
|
||||
# Party is a dynamic link on Payment Ledger Entry, so user permissions on Customer
|
||||
# must be applied explicitly. The report should only show permitted customers.
|
||||
|
||||
# Running the report writes an access log that commits, so these invoices survive
|
||||
# tearDown's rollback. Delete and commit them so they don't leak into other tests.
|
||||
def remove_committed_entries():
|
||||
self.clear_old_entries()
|
||||
frappe.db.commit() # nosemgrep
|
||||
|
||||
self.addCleanup(remove_committed_entries)
|
||||
|
||||
original_customer = self.customer
|
||||
second_customer = "_Test AR Perm Customer"
|
||||
|
||||
# create_customer overrides self.customer, so build the restricted invoice first
|
||||
self.create_customer(customer_name=second_customer)
|
||||
self.create_sales_invoice(no_payment_schedule=True)
|
||||
|
||||
self.customer = original_customer
|
||||
allowed_invoice = self.create_sales_invoice(no_payment_schedule=True)
|
||||
|
||||
test_user = "test_ar_user_permission@example.com"
|
||||
if not frappe.db.exists("User", test_user):
|
||||
user = frappe.new_doc("User")
|
||||
user.email = test_user
|
||||
user.first_name = "AR Perm"
|
||||
user.append("roles", {"role": "Accounts User"})
|
||||
user.save()
|
||||
|
||||
frappe.permissions.add_user_permission("Customer", original_customer, test_user)
|
||||
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"party_type": "Customer",
|
||||
"report_date": today(),
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
|
||||
frappe.set_user(test_user)
|
||||
try:
|
||||
report = execute(filters)
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
parties = {row.party for row in report[1]}
|
||||
self.assertIn(original_customer, parties)
|
||||
self.assertNotIn(second_customer, parties)
|
||||
self.assertEqual(allowed_invoice.customer, original_customer)
|
||||
|
||||
@@ -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.expense_account = source_doc.expense_account
|
||||
target_doc.dn_detail = source_doc.name
|
||||
target_doc.cost_center = source_doc.cost_center
|
||||
if default_warehouse_for_sales_return:
|
||||
target_doc.warehouse = default_warehouse_for_sales_return
|
||||
elif doctype == "Sales Invoice" or doctype == "POS Invoice":
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"opportunity_section",
|
||||
"close_opportunity_after_days",
|
||||
"column_break_9",
|
||||
"enable_opportunity_creation_from_contact_us",
|
||||
"quotation_section",
|
||||
"default_valid_till",
|
||||
"section_break_13",
|
||||
@@ -98,13 +99,19 @@
|
||||
"fieldname": "update_timestamp_on_new_communication",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update timestamp on new communication"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enable_opportunity_creation_from_contact_us",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Opportunity Creation from Contact Us"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-16 16:12:14.889455",
|
||||
"modified": "2026-06-11 23:09:49.750381",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "CRM Settings",
|
||||
@@ -144,4 +151,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
@@ -20,8 +21,20 @@ class CRMSettings(Document):
|
||||
carry_forward_communication_and_comments: DF.Check
|
||||
close_opportunity_after_days: DF.Int
|
||||
default_valid_till: DF.Data | None
|
||||
enable_opportunity_creation_from_contact_us: DF.Check
|
||||
update_timestamp_on_new_communication: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
frappe.db.set_default("campaign_naming_by", self.get("campaign_naming_by", ""))
|
||||
self.validate_enable_opportunity_creation_from_contact_us()
|
||||
|
||||
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.model.mapper import get_mapped_doc
|
||||
from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_email_address
|
||||
from frappe.utils import comma_and, get_link_to_form, validate_email_address
|
||||
|
||||
from erpnext.accounts.party import set_taxes
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
@@ -171,9 +171,6 @@ class Lead(SellingController, CRMNote):
|
||||
if self.email_id == self.lead_owner:
|
||||
frappe.throw(_("Lead Owner cannot be same as the Lead Email Address"))
|
||||
|
||||
if self.is_new() or not self.image:
|
||||
self.image = has_gravatar(self.email_id)
|
||||
|
||||
def link_to_contact(self):
|
||||
# update contact links
|
||||
if self.contact_doc:
|
||||
@@ -471,7 +468,7 @@ def get_lead_details(lead, posting_date=None, company=None, doctype=None):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_lead_from_communication(communication, ignore_communication_links=False):
|
||||
def make_lead_from_communication(communication: str, ignore_communication_links: bool = False):
|
||||
"""raise a issue from email"""
|
||||
|
||||
doc = frappe.get_doc("Communication", communication)
|
||||
@@ -490,7 +487,6 @@ def make_lead_from_communication(communication, ignore_communication_links=False
|
||||
}
|
||||
)
|
||||
lead.flags.ignore_mandatory = True
|
||||
lead.flags.ignore_permissions = True
|
||||
lead.insert()
|
||||
|
||||
lead_name = lead.name
|
||||
|
||||
@@ -522,7 +522,9 @@ def auto_close_opportunity():
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_opportunity_from_communication(communication, company, ignore_communication_links=False):
|
||||
def make_opportunity_from_communication(
|
||||
communication: str, company: str, ignore_communication_links: bool = False
|
||||
):
|
||||
from erpnext.crm.doctype.lead.lead import make_lead_from_communication
|
||||
|
||||
doc = frappe.get_doc("Communication", communication)
|
||||
@@ -540,7 +542,7 @@ def make_opportunity_from_communication(communication, company, ignore_communica
|
||||
"opportunity_from": opportunity_from,
|
||||
"party_name": lead,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
).insert()
|
||||
|
||||
link_communication_to_document(doc, "Opportunity", opportunity.name, ignore_communication_links)
|
||||
|
||||
|
||||
@@ -5,6 +5,11 @@ from frappe.utils import cstr, now, today
|
||||
from pypika import functions
|
||||
|
||||
|
||||
def disable_opportunity_creation_on_contact_us_disabled(doc, method):
|
||||
if doc.is_disabled:
|
||||
frappe.db.set_single_value("CRM Settings", "enable_opportunity_creation_from_contact_us", 0)
|
||||
|
||||
|
||||
def update_lead_phone_numbers(contact, method):
|
||||
if contact.phone_nos:
|
||||
contact_lead = contact.get_link_for("Lead")
|
||||
|
||||
@@ -355,6 +355,9 @@ doc_events = {
|
||||
"Event": {
|
||||
"after_insert": "erpnext.crm.utils.link_events_with_prospect",
|
||||
},
|
||||
"Contact Us Settings": {
|
||||
"on_update": "erpnext.crm.utils.disable_opportunity_creation_on_contact_us_disabled",
|
||||
},
|
||||
"Sales Invoice": {
|
||||
"on_submit": [
|
||||
"erpnext.regional.create_transaction_log",
|
||||
|
||||
@@ -75,6 +75,9 @@ frappe.ui.form.on("BOM", {
|
||||
|
||||
with_operations: function (frm) {
|
||||
frm.set_df_property("fg_based_operating_cost", "hidden", frm.doc.with_operations ? 1 : 0);
|
||||
if (frm.doc.routing && frm.doc.with_operations && !frm.doc.operations.length) {
|
||||
frm.trigger("routing");
|
||||
}
|
||||
},
|
||||
|
||||
fg_based_operating_cost: function (frm) {
|
||||
@@ -438,7 +441,7 @@ frappe.ui.form.on("BOM", {
|
||||
},
|
||||
|
||||
routing(frm) {
|
||||
if (frm.doc.routing && frm.doc.with_operations && !frm.doc.operations) {
|
||||
if (frm.doc.routing && frm.doc.with_operations && !frm.doc.operations.length) {
|
||||
frappe.call({
|
||||
doc: frm.doc,
|
||||
method: "get_routing",
|
||||
|
||||
@@ -152,6 +152,7 @@ class BOMCreator(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_boms(self):
|
||||
self.check_permission("submit")
|
||||
self.submit()
|
||||
|
||||
def set_rate_for_items(self):
|
||||
@@ -209,10 +210,14 @@ class BOMCreator(Document):
|
||||
frappe.throw(_("Please set {0} in BOM Creator {1}").format(_(label), self.name))
|
||||
|
||||
def on_submit(self):
|
||||
self.enqueue_create_boms()
|
||||
self.enqueue_bom_creation()
|
||||
|
||||
@frappe.whitelist()
|
||||
def enqueue_create_boms(self):
|
||||
self.check_permission("submit")
|
||||
self.enqueue_bom_creation()
|
||||
|
||||
def enqueue_bom_creation(self):
|
||||
frappe.enqueue(
|
||||
self.create_boms,
|
||||
queue="short",
|
||||
@@ -281,6 +286,21 @@ class BOMCreator(Document):
|
||||
|
||||
frappe.msgprint(_("BOMs creation failed"))
|
||||
|
||||
@frappe.whitelist()
|
||||
def edit_qty(self, docname: str, qty: float):
|
||||
if not frappe.db.exists("BOM Creator Item", {"name": docname, "parent": self.name}):
|
||||
frappe.throw(_("BOM Creator Item {0} does not exist").format(docname))
|
||||
|
||||
for row in self.items:
|
||||
if row.name == docname:
|
||||
row.qty = flt(qty)
|
||||
break
|
||||
|
||||
self.set_rate_for_items()
|
||||
self.save()
|
||||
|
||||
return self
|
||||
|
||||
def create_bom(self, row, production_item_wise_rm):
|
||||
bom_creator_item = row.name if row.name != self.name else ""
|
||||
if frappe.db.exists(
|
||||
@@ -336,18 +356,157 @@ class BOMCreator(Document):
|
||||
production_item_wise_rm[(row.item_code, row.name)].bom_no = bom.name
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_default_bom(self, item_code) -> str:
|
||||
def get_default_bom(self, item_code: str) -> str:
|
||||
self.check_permission("read")
|
||||
return frappe.get_cached_value("Item", item_code, "default_bom")
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_item(self, **kwargs):
|
||||
if isinstance(kwargs, str):
|
||||
kwargs = frappe.parse_json(kwargs)
|
||||
|
||||
if isinstance(kwargs, dict):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
item_info = get_item_details(kwargs.item_code)
|
||||
|
||||
parent_row_no = ""
|
||||
if kwargs.fg_reference_id and self.name != kwargs.fg_reference_id:
|
||||
parent_row_no = get_parent_row_no(self, kwargs.fg_reference_id)
|
||||
|
||||
kwargs.update(
|
||||
{
|
||||
"uom": item_info.stock_uom,
|
||||
"stock_uom": item_info.stock_uom,
|
||||
"conversion_factor": 1,
|
||||
}
|
||||
)
|
||||
|
||||
if parent_row_no:
|
||||
kwargs.update({"parent_row_no": parent_row_no})
|
||||
|
||||
for key in BOM_ITEM_FIELDS:
|
||||
if key not in kwargs:
|
||||
kwargs[key] = ""
|
||||
|
||||
self.append("items", kwargs)
|
||||
self.save()
|
||||
|
||||
return self
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_sub_assembly(self, **kwargs):
|
||||
if isinstance(kwargs, str):
|
||||
kwargs = frappe.parse_json(kwargs)
|
||||
|
||||
if isinstance(kwargs, dict):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
bom_item = frappe.parse_json(kwargs.bom_item)
|
||||
|
||||
name = kwargs.fg_reference_id
|
||||
parent_row_no = ""
|
||||
if not kwargs.convert_to_sub_assembly:
|
||||
item_info = get_item_details(bom_item.item_code)
|
||||
parent_row_no = get_parent_row_no(self, kwargs.fg_reference_id)
|
||||
|
||||
item_row = self.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": bom_item.item_code,
|
||||
"qty": bom_item.qty,
|
||||
"uom": item_info.stock_uom,
|
||||
"fg_item": kwargs.fg_item,
|
||||
"conversion_factor": 1,
|
||||
"parent_row_no": parent_row_no,
|
||||
"fg_reference_id": name,
|
||||
"stock_qty": bom_item.qty,
|
||||
"do_not_explode": 1,
|
||||
"is_expandable": 1,
|
||||
"stock_uom": item_info.stock_uom,
|
||||
"allow_alternative_item": kwargs.allow_alternative_item,
|
||||
},
|
||||
)
|
||||
|
||||
parent_row_no = item_row.idx
|
||||
name = ""
|
||||
else:
|
||||
parent_row_no = get_parent_row_no(self, kwargs.fg_reference_id)
|
||||
|
||||
for row in bom_item.get("items"):
|
||||
row = frappe._dict(row)
|
||||
item_info = get_item_details(row.item_code)
|
||||
self.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": row.item_code,
|
||||
"qty": row.qty,
|
||||
"fg_item": bom_item.item_code,
|
||||
"uom": item_info.stock_uom,
|
||||
"fg_reference_id": name,
|
||||
"parent_row_no": parent_row_no,
|
||||
"conversion_factor": 1,
|
||||
"do_not_explode": 1,
|
||||
"stock_qty": row.qty,
|
||||
"stock_uom": item_info.stock_uom,
|
||||
},
|
||||
)
|
||||
|
||||
self.save()
|
||||
|
||||
return self
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_node(self, **kwargs):
|
||||
if isinstance(kwargs, str):
|
||||
kwargs = frappe.parse_json(kwargs)
|
||||
|
||||
if isinstance(kwargs, dict):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
updated = False
|
||||
if kwargs.docname:
|
||||
row = next((row for row in self.items if row.name == kwargs.docname), None)
|
||||
if not row:
|
||||
frappe.throw(_("BOM Creator Item with name {0} does not exist").format(kwargs.docname))
|
||||
|
||||
row.delete()
|
||||
self.remove(row)
|
||||
updated = True
|
||||
|
||||
items = get_children(parent=kwargs.fg_item, parent_id=self.name)
|
||||
if items:
|
||||
for item in items:
|
||||
updated = True
|
||||
child_row = next((row for row in self.items if row.name == item.name), None)
|
||||
if child_row:
|
||||
child_row.delete()
|
||||
self.remove(child_row)
|
||||
|
||||
if item.expandable:
|
||||
self.delete_node(fg_item=item.value)
|
||||
|
||||
if updated:
|
||||
self.set_rate_for_items()
|
||||
self.save()
|
||||
|
||||
return self
|
||||
|
||||
return frappe._dict()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_children(doctype=None, parent=None, **kwargs):
|
||||
def get_children(doctype: str | None = None, parent: str | None = None, **kwargs):
|
||||
# by default get_children takes first parameter as doctype, so added in the function
|
||||
|
||||
if isinstance(kwargs, str):
|
||||
kwargs = frappe.parse_json(kwargs)
|
||||
|
||||
if isinstance(kwargs, dict):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
frappe.has_permission("BOM Creator", "read", doc=kwargs.parent_id, throw=True)
|
||||
|
||||
fields = [
|
||||
"item_code as value",
|
||||
"item_name as title",
|
||||
@@ -373,102 +532,6 @@ def get_children(doctype=None, parent=None, **kwargs):
|
||||
return frappe.get_all("BOM Creator Item", fields=fields, filters=query_filters, order_by="idx")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_item(**kwargs):
|
||||
if isinstance(kwargs, str):
|
||||
kwargs = frappe.parse_json(kwargs)
|
||||
|
||||
if isinstance(kwargs, dict):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
doc = frappe.get_doc("BOM Creator", kwargs.parent)
|
||||
item_info = get_item_details(kwargs.item_code)
|
||||
|
||||
parent_row_no = ""
|
||||
if kwargs.fg_reference_id and doc.name != kwargs.fg_reference_id:
|
||||
parent_row_no = get_parent_row_no(doc, kwargs.fg_reference_id)
|
||||
|
||||
kwargs.update(
|
||||
{
|
||||
"uom": item_info.stock_uom,
|
||||
"stock_uom": item_info.stock_uom,
|
||||
"conversion_factor": 1,
|
||||
}
|
||||
)
|
||||
|
||||
if parent_row_no:
|
||||
kwargs.update({"parent_row_no": parent_row_no})
|
||||
|
||||
doc.append("items", kwargs)
|
||||
doc.save()
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_sub_assembly(**kwargs):
|
||||
if isinstance(kwargs, str):
|
||||
kwargs = frappe.parse_json(kwargs)
|
||||
|
||||
if isinstance(kwargs, dict):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
doc = frappe.get_doc("BOM Creator", kwargs.parent)
|
||||
bom_item = frappe.parse_json(kwargs.bom_item)
|
||||
|
||||
name = kwargs.fg_reference_id
|
||||
parent_row_no = ""
|
||||
if not kwargs.convert_to_sub_assembly:
|
||||
item_info = get_item_details(bom_item.item_code)
|
||||
parent_row_no = get_parent_row_no(doc, kwargs.fg_reference_id)
|
||||
|
||||
item_row = doc.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": bom_item.item_code,
|
||||
"qty": bom_item.qty,
|
||||
"uom": item_info.stock_uom,
|
||||
"fg_item": kwargs.fg_item,
|
||||
"conversion_factor": 1,
|
||||
"parent_row_no": parent_row_no,
|
||||
"fg_reference_id": name,
|
||||
"stock_qty": bom_item.qty,
|
||||
"do_not_explode": 1,
|
||||
"is_expandable": 1,
|
||||
"stock_uom": item_info.stock_uom,
|
||||
"allow_alternative_item": kwargs.allow_alternative_item,
|
||||
},
|
||||
)
|
||||
|
||||
parent_row_no = item_row.idx
|
||||
name = ""
|
||||
else:
|
||||
parent_row_no = get_parent_row_no(doc, kwargs.fg_reference_id)
|
||||
|
||||
for row in bom_item.get("items"):
|
||||
row = frappe._dict(row)
|
||||
item_info = get_item_details(row.item_code)
|
||||
doc.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": row.item_code,
|
||||
"qty": row.qty,
|
||||
"fg_item": bom_item.item_code,
|
||||
"uom": item_info.stock_uom,
|
||||
"fg_reference_id": name,
|
||||
"parent_row_no": parent_row_no,
|
||||
"conversion_factor": 1,
|
||||
"do_not_explode": 1,
|
||||
"stock_qty": row.qty,
|
||||
"stock_uom": item_info.stock_uom,
|
||||
},
|
||||
)
|
||||
|
||||
doc.save()
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
def get_item_details(item_code):
|
||||
return frappe.get_cached_value(
|
||||
"Item", item_code, ["item_name", "description", "image", "stock_uom", "default_bom"], as_dict=1
|
||||
@@ -486,37 +549,3 @@ def get_parent_row_no(doc, name):
|
||||
frappe.msgprint(_("Parent Row No not found for {0}").format(name), alert=True)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_node(**kwargs):
|
||||
if isinstance(kwargs, str):
|
||||
kwargs = frappe.parse_json(kwargs)
|
||||
|
||||
if isinstance(kwargs, dict):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
items = get_children(parent=kwargs.fg_item, parent_id=kwargs.parent)
|
||||
if kwargs.docname:
|
||||
frappe.delete_doc("BOM Creator Item", kwargs.docname)
|
||||
|
||||
for item in items:
|
||||
frappe.delete_doc("BOM Creator Item", item.name)
|
||||
if item.expandable:
|
||||
delete_node(fg_item=item.value, parent=item.parent_id)
|
||||
|
||||
doc = frappe.get_doc("BOM Creator", kwargs.parent)
|
||||
doc.set_rate_for_items()
|
||||
doc.save()
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def edit_qty(doctype, docname, qty, parent):
|
||||
frappe.db.set_value(doctype, docname, "qty", qty)
|
||||
doc = frappe.get_doc("BOM Creator", parent)
|
||||
doc.set_rate_for_items()
|
||||
doc.save()
|
||||
|
||||
return doc
|
||||
|
||||
@@ -6,10 +6,6 @@ import random
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.manufacturing.doctype.bom_creator.bom_creator import (
|
||||
add_item,
|
||||
add_sub_assembly,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
|
||||
@@ -38,8 +34,7 @@ class TestBOMCreator(FrappeTestCase):
|
||||
conversion_rate=1,
|
||||
)
|
||||
|
||||
add_sub_assembly(
|
||||
parent=doc.name,
|
||||
doc.add_sub_assembly(
|
||||
fg_item=final_product,
|
||||
fg_reference_id=doc.name,
|
||||
bom_item={
|
||||
@@ -91,8 +86,7 @@ class TestBOMCreator(FrappeTestCase):
|
||||
conversion_rate=1,
|
||||
)
|
||||
|
||||
add_item(
|
||||
parent=doc.name,
|
||||
doc.add_item(
|
||||
fg_item=final_product,
|
||||
fg_reference_id=doc.name,
|
||||
item_code="Pedal Assembly",
|
||||
@@ -133,8 +127,7 @@ class TestBOMCreator(FrappeTestCase):
|
||||
conversion_rate=1,
|
||||
)
|
||||
|
||||
add_item(
|
||||
parent=doc.name,
|
||||
doc.add_item(
|
||||
fg_item=final_product,
|
||||
fg_reference_id=doc.name,
|
||||
item_code="Pedal Assembly",
|
||||
@@ -144,9 +137,8 @@ class TestBOMCreator(FrappeTestCase):
|
||||
doc.reload()
|
||||
self.assertEqual(doc.items[0].is_expandable, 0)
|
||||
|
||||
add_sub_assembly(
|
||||
doc.add_sub_assembly(
|
||||
convert_to_sub_assembly=1,
|
||||
parent=doc.name,
|
||||
fg_item=final_product,
|
||||
fg_reference_id=doc.items[0].name,
|
||||
bom_item={
|
||||
@@ -199,8 +191,7 @@ class TestBOMCreator(FrappeTestCase):
|
||||
conversion_rate=1,
|
||||
)
|
||||
|
||||
add_item(
|
||||
parent=doc.name,
|
||||
doc.add_item(
|
||||
fg_item=final_product,
|
||||
fg_reference_id=doc.name,
|
||||
item_code="Pedal Assembly",
|
||||
@@ -210,9 +201,8 @@ class TestBOMCreator(FrappeTestCase):
|
||||
doc.reload()
|
||||
self.assertEqual(doc.items[0].is_expandable, 0)
|
||||
|
||||
add_sub_assembly(
|
||||
doc.add_sub_assembly(
|
||||
convert_to_sub_assembly=1,
|
||||
parent=doc.name,
|
||||
fg_item=final_product,
|
||||
fg_reference_id=doc.items[0].name,
|
||||
bom_item={
|
||||
|
||||
@@ -19,6 +19,7 @@ from frappe.utils import (
|
||||
time_diff_in_seconds,
|
||||
to_timedelta,
|
||||
)
|
||||
from frappe.utils.data import DateTimeLikeObject
|
||||
|
||||
from erpnext.support.doctype.issue.issue import get_holidays
|
||||
|
||||
@@ -65,7 +66,7 @@ class Workstation(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def before_save(self):
|
||||
self.set_data_based_on_workstation_type()
|
||||
self._set_data_based_on_workstation_type()
|
||||
self.set_hour_rate()
|
||||
self.set_total_working_hours()
|
||||
|
||||
@@ -92,6 +93,10 @@ class Workstation(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_data_based_on_workstation_type(self):
|
||||
self.check_permission("write")
|
||||
self._set_data_based_on_workstation_type()
|
||||
|
||||
def _set_data_based_on_workstation_type(self):
|
||||
if self.workstation_type:
|
||||
fields = [
|
||||
"hour_rate_labour",
|
||||
@@ -166,23 +171,27 @@ class Workstation(Document):
|
||||
return schedule_date
|
||||
|
||||
@frappe.whitelist()
|
||||
def start_job(self, job_card, from_time, employee):
|
||||
def start_job(self, job_card: str, from_time: DateTimeLikeObject, employee: str):
|
||||
doc = frappe.get_doc("Job Card", job_card)
|
||||
doc.check_permission("write")
|
||||
|
||||
doc.append("time_logs", {"from_time": from_time, "employee": employee})
|
||||
doc.save(ignore_permissions=True)
|
||||
doc.save()
|
||||
|
||||
return doc
|
||||
|
||||
@frappe.whitelist()
|
||||
def complete_job(self, job_card, qty, to_time):
|
||||
def complete_job(self, job_card: str, qty: float, to_time: DateTimeLikeObject):
|
||||
doc = frappe.get_doc("Job Card", job_card)
|
||||
doc.check_permission("submit")
|
||||
|
||||
for row in doc.time_logs:
|
||||
if not row.to_time:
|
||||
row.to_time = to_time
|
||||
row.time_in_mins = time_diff_in_hours(row.to_time, row.from_time) / 60
|
||||
row.completed_qty = qty
|
||||
|
||||
doc.save(ignore_permissions=True)
|
||||
doc.save()
|
||||
doc.submit()
|
||||
|
||||
return doc
|
||||
@@ -364,6 +373,8 @@ def check_workstation_for_holiday(workstation, from_datetime, to_datetime):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_workstations(**kwargs):
|
||||
frappe.has_permission("Workstation", "read", throw=True)
|
||||
|
||||
kwargs = frappe._dict(kwargs)
|
||||
_workstation = frappe.qb.DocType("Workstation")
|
||||
|
||||
|
||||
@@ -96,8 +96,8 @@ erpnext.BOMComparisonTool = class BOMComparisonTool {
|
||||
return `
|
||||
<tr>
|
||||
<td>${frappe.meta.get_label(doctype, fieldname)}</td>
|
||||
<td>${value1}</td>
|
||||
<td>${value2}</td>
|
||||
<td>${frappe.utils.escape_html(cstr(value1))}</td>
|
||||
<td>${frappe.utils.escape_html(cstr(value2))}</td>
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
@@ -138,13 +138,17 @@ erpnext.BOMComparisonTool = class BOMComparisonTool {
|
||||
.map((change, i) => {
|
||||
let [fieldname, value1, value2] = change;
|
||||
let th =
|
||||
i === 0 ? `<th rowspan="${values_changed.length}">${item_code}</th>` : "";
|
||||
i === 0
|
||||
? `<th rowspan="${values_changed.length}">${frappe.utils.escape_html(
|
||||
cstr(item_code)
|
||||
)}</th>`
|
||||
: "";
|
||||
return `
|
||||
<tr>
|
||||
${th}
|
||||
<td>${frappe.meta.get_label(child_doctype, fieldname)}</td>
|
||||
<td>${value1}</td>
|
||||
<td>${value2}</td>
|
||||
<td>${frappe.utils.escape_html(cstr(value1))}</td>
|
||||
<td>${frappe.utils.escape_html(cstr(value2))}</td>
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
@@ -177,7 +181,9 @@ erpnext.BOMComparisonTool = class BOMComparisonTool {
|
||||
let html = rows
|
||||
.map((row) => {
|
||||
let [, doc] = row;
|
||||
let cells = fields.map((df) => `<td>${doc[df.fieldname]}</td>`).join("");
|
||||
let cells = fields
|
||||
.map((df) => `<td>${frappe.utils.escape_html(cstr(doc[df.fieldname]))}</td>`)
|
||||
.join("");
|
||||
return `<tr>${cells}</tr>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
@@ -219,14 +219,10 @@ class BOMConfigurator {
|
||||
},
|
||||
],
|
||||
(data) => {
|
||||
if (!node.data.parent_id) {
|
||||
node.data.parent_id = this.frm.doc.name;
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_item",
|
||||
method: "add_item",
|
||||
doc: this.frm.doc,
|
||||
args: {
|
||||
parent: node.data.parent_id,
|
||||
fg_item: node.data.value,
|
||||
item_code: data.item_code,
|
||||
fg_reference_id: node.data.name || this.frm.doc.name,
|
||||
@@ -255,14 +251,10 @@ class BOMConfigurator {
|
||||
dialog.set_primary_action(__("Add"), () => {
|
||||
let bom_item = dialog.get_values();
|
||||
|
||||
if (!node.data?.parent_id) {
|
||||
node.data.parent_id = this.frm.doc.name;
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_sub_assembly",
|
||||
method: "add_sub_assembly",
|
||||
doc: this.frm.doc,
|
||||
args: {
|
||||
parent: node.data.parent_id,
|
||||
fg_item: node.data.value,
|
||||
fg_reference_id: node.data.name || this.frm.doc.name,
|
||||
bom_item: bom_item,
|
||||
@@ -357,9 +349,9 @@ class BOMConfigurator {
|
||||
let bom_item = dialog.get_values();
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_sub_assembly",
|
||||
method: "add_sub_assembly",
|
||||
doc: this.frm.doc,
|
||||
args: {
|
||||
parent: node.data.parent_id,
|
||||
fg_item: node.data.value,
|
||||
bom_item: bom_item,
|
||||
fg_reference_id: node.data.name || this.frm.doc.name,
|
||||
@@ -389,11 +381,10 @@ class BOMConfigurator {
|
||||
delete_node(node, view) {
|
||||
frappe.confirm(__("Are you sure you want to delete this Item?"), () => {
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.delete_node",
|
||||
method: "delete_node",
|
||||
doc: this.frm.doc,
|
||||
args: {
|
||||
parent: node.data.parent_id,
|
||||
fg_item: node.data.value,
|
||||
doctype: node.data.doctype,
|
||||
docname: node.data.name,
|
||||
},
|
||||
callback: (r) => {
|
||||
@@ -408,16 +399,14 @@ class BOMConfigurator {
|
||||
frappe.prompt(
|
||||
[{ label: __("Qty"), fieldname: "qty", default: qty, fieldtype: "Float", reqd: 1 }],
|
||||
(data) => {
|
||||
let doctype = node.data.doctype || this.frm.doc.doctype;
|
||||
let docname = node.data.name || this.frm.doc.name;
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.edit_qty",
|
||||
method: "edit_qty",
|
||||
doc: this.frm.doc,
|
||||
args: {
|
||||
doctype: doctype,
|
||||
docname: docname,
|
||||
qty: data.qty,
|
||||
parent: node.data.parent_id ? node.data.parent_id : this.frm.doc.name,
|
||||
},
|
||||
callback: (r) => {
|
||||
node.data.qty = data.qty;
|
||||
|
||||
@@ -13,6 +13,8 @@ def execute(filters=None):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
validate_filters(filters)
|
||||
|
||||
columns = get_columns(filters)
|
||||
entries = get_entries(filters)
|
||||
item_details = get_item_details()
|
||||
@@ -49,10 +51,17 @@ def execute(filters=None):
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
def validate_filters(filters):
|
||||
ALLOWED_DOCTYPES = ["Sales Order", "Sales Invoice", "Delivery Note"]
|
||||
|
||||
if not filters.get("doc_type"):
|
||||
msgprint(_("Please select the document type first"), raise_exception=1)
|
||||
|
||||
if filters.get("doc_type") not in ALLOWED_DOCTYPES:
|
||||
frappe.throw(_("{0}, {1} or {2} are the only allowed options.").format(*ALLOWED_DOCTYPES))
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
columns = [
|
||||
{
|
||||
"label": _(filters["doc_type"]),
|
||||
|
||||
@@ -64,15 +64,11 @@ class Employee(NestedSet):
|
||||
)
|
||||
|
||||
def validate_user_details(self):
|
||||
if self.user_id:
|
||||
data = frappe.db.get_value("User", self.user_id, ["enabled"], as_dict=1)
|
||||
if not self.user_id:
|
||||
return
|
||||
|
||||
if not data:
|
||||
self.user_id = None
|
||||
return
|
||||
|
||||
self.validate_for_enabled_user_id(data.get("enabled", 0))
|
||||
self.validate_duplicate_user_id()
|
||||
self.validate_for_enabled_user_id()
|
||||
self.validate_duplicate_user_id()
|
||||
|
||||
def update_nsm_model(self):
|
||||
frappe.utils.nestedset.update_nsm(self)
|
||||
@@ -83,6 +79,7 @@ class Employee(NestedSet):
|
||||
if self.user_id:
|
||||
self.update_user()
|
||||
self.update_user_permissions()
|
||||
self.update_user_status()
|
||||
self.reset_employee_emails_cache()
|
||||
|
||||
def update_user_permissions(self):
|
||||
@@ -184,12 +181,20 @@ class Employee(NestedSet):
|
||||
if not self.relieving_date:
|
||||
throw(_("Please enter relieving date."))
|
||||
|
||||
def validate_for_enabled_user_id(self, enabled):
|
||||
if enabled is None:
|
||||
def validate_for_enabled_user_id(self):
|
||||
if not frappe.db.exists("User", 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:
|
||||
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):
|
||||
Employee = frappe.qb.DocType("Employee")
|
||||
|
||||
@@ -209,6 +209,8 @@ class TransactionDeletionRecord(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def start_deletion_tasks(self):
|
||||
self.check_permission("write")
|
||||
|
||||
# This method is the entry point for the chain of events that follow
|
||||
self.db_set("status", "Running")
|
||||
self.enqueue_task(task="Delete Bins")
|
||||
|
||||
@@ -335,7 +335,9 @@ def get_default_address(out, name):
|
||||
|
||||
|
||||
@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", contact, ["first_name", "last_name", "phone", "mobile_no"], as_dict=1
|
||||
)
|
||||
@@ -436,7 +438,9 @@ def get_attachments(delivery_stop):
|
||||
|
||||
|
||||
@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")
|
||||
email = frappe.db.get_value("Employee", employee, "prefered_email")
|
||||
return {"email": email}
|
||||
|
||||
@@ -123,7 +123,9 @@ def get_contact_name(ref_doctype, docname):
|
||||
|
||||
|
||||
@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(
|
||||
"User",
|
||||
user,
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
@@ -74,7 +74,7 @@
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2016-07-11 03:28:09.626948",
|
||||
"modified": "2026-06-11 23:02:54.800673",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "UOM Conversion Detail",
|
||||
@@ -84,4 +84,4 @@
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"track_seen": 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class UOMConversionDetail(Document):
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
uom: DF.Link | None
|
||||
uom: DF.Link
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
||||
@@ -134,12 +134,15 @@ def get_linked_cancelled_sabb(filters):
|
||||
|
||||
|
||||
@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):
|
||||
selected_rows = frappe.parse_json(selected_rows)
|
||||
|
||||
for row in selected_rows:
|
||||
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(
|
||||
"Stock Ledger Entry",
|
||||
{"serial_and_batch_bundle": doc.name, "is_cancelled": 0},
|
||||
|
||||
@@ -306,6 +306,11 @@ class FIFOSlots:
|
||||
# prepare single sle voucher detail lookup
|
||||
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():
|
||||
if stock_ledger_entries is None:
|
||||
stock_ledger_entries = self._get_stock_ledger_entries()
|
||||
@@ -423,12 +428,38 @@ class FIFOSlots:
|
||||
|
||||
def _get_batchwise_valuation(self, batch_no: str):
|
||||
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(
|
||||
"Batch", batch_no, "use_batchwise_valuation"
|
||||
)
|
||||
|
||||
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:
|
||||
"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]]
|
||||
)
|
||||
|
||||
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):
|
||||
"Return results with and without 'show_warehouse_wise_stock'"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import flt
|
||||
|
||||
@@ -364,8 +365,9 @@ def get_mapped_subcontracting_receipt(source_name, target_doc=None):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_subcontracting_order_status(sco, status=None):
|
||||
def update_subcontracting_order_status(sco: str | Document, status: str | None = None):
|
||||
if isinstance(sco, str):
|
||||
sco = frappe.get_doc("Subcontracting Order", sco)
|
||||
|
||||
sco.check_permission("write")
|
||||
sco.update_status(status)
|
||||
|
||||
@@ -118,7 +118,9 @@ class Issue(Document):
|
||||
communication.save()
|
||||
|
||||
@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
|
||||
from copy import deepcopy
|
||||
|
||||
@@ -274,7 +276,7 @@ def make_task(source_name, target_doc=None):
|
||||
|
||||
|
||||
@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"""
|
||||
|
||||
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_phone": doc.phone_no or "",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
).insert()
|
||||
|
||||
link_communication_to_document(doc, "Issue", issue.name, ignore_communication_links)
|
||||
|
||||
|
||||
@@ -79,10 +79,6 @@
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.gravatar-top{
|
||||
margin-top:8px;
|
||||
}
|
||||
|
||||
.progress-hg{
|
||||
margin-bottom: 30!important;
|
||||
height:2px;
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.rate_limiter import rate_limit
|
||||
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"):
|
||||
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)
|
||||
|
||||
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
|
||||
customer = frappe.db.sql(
|
||||
"""select distinct dl.link_name from `tabDynamic Link` dl
|
||||
|
||||
Reference in New Issue
Block a user