My solution was inspired by other one from this thread. Thank you, guys.
The other solution used init.py instead of hooks.py, but that has a side effect: if you uninstall the app, the patches are still loaded (after restart). It may be a bug in frappe, or a feature in python - I don’t know, yet. Using hooks.py solves the problem.
If your patch doesn’t work or works in selected places only, check one more thing. Some functions can be called in more than one namespace. For example: frappe.utils.data.fmt_money, or frappe.utils.fmt_money (without “data”). That’s because the functions are imported in module’s init file (from [something] import *). I lost a few hours searching for the bug… The solution: assign your custom function to both namespaced versions.
A lot of great information in here. I have my doc_events working.
My question is : Is there an event that is triggered when a NewDoc Entry (like Payment Entry when you add a new entry) is created?
I see that there is a “onload” event and many others, but is there a “oncreated” or “onadded”? To override the validate function as soon as a new doc of that type is made?
I am trying to override the validate function for Payment Entry, I want to override its validate function as soon as a new payment is added, the problem I am having is that most of the events that I am aware of are triggered AFTER the original validate function is ran which does not help overriding.
Thank you for any feedback!
I’ve seen an incomplete event list in this documentation:
To make things more complicated, I’m trying to override non-class method get_timesheet_details() in report/billing_summary.py.
My story:
I’m using Custom App, of which in hooks.py I overrode erpnext.payroll.doctype.salary_slip.salary_slip.set_time_sheet() method. This is working fine as it is inside the SalarySlip class.
Now, myapp.salary_slip.set_time_sheet() calls erpnext.projects.report.billing_summary.get_timesheet_details(). Unfortunately I need to override this method too (see this issue)
In myapp/salary_slip.py, I import:
from erpnext.projects.report.billing_summary import get_timesheet_details
and therefore it will call the non-overridden get_timesheet_details().
In myapp/hooks.py:
import erpnext.projects.report.billing_summary
import myapp_app.my_app.billing_summary
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
import importlib
import frappe
patches_loaded = False
__version__ = '13.0.0'
def console(*data):
frappe.publish_realtime("out_to_console", data, user=frappe.session.user)
def load_monkey_patches():
global patches_loaded
if (
patches_loaded
or not getattr(frappe, "conf", None)
or not "erpnext_oob" in frappe.get_installed_apps()
):
return
for app in frappe.get_installed_apps():
if app in ['frappe', 'erpnext']: continue
folder = frappe.get_app_path(app, "monkey_patches")
if not os.path.exists(folder): continue
for module_name in os.listdir(folder):
if not module_name.endswith(".py") or module_name == "__init__.py":
continue
importlib.import_module(f"{app}.monkey_patches.{module_name[:-3]}")
patches_loaded = True
connect = frappe.connect
def custom_connect(*args, **kwargs):
out = connect(*args, **kwargs)
if frappe.conf.auto_commit_on_many_writes:
frappe.db.auto_commit_on_many_writes = 1
load_monkey_patches()
return out
frappe.connect = custom_connect
create monkey_patches folder under your app folder.
create any py file, monkey patch like below
import frappe
from frappe.utils import cstr
from frappe.model.base_document import BaseDocument
import json
def get_owner_username(self):
return frappe.db.get_value('User', self.owner, 'full_name')
def get_submit_username(self):
"""变更记录data字段数据格式
changed:[[其它字段,旧值,新值]
['docstatus', 0, 1]
]"""
try:
if not self.meta.is_submittable:
return
filters={'ref_doctype': self.doctype, 'docname': self.name, 'data': ('like', '%docstatus%')}
version_list = frappe.get_all('Version', filters = filters, fields=['owner','data'], order_by="creation desc")
for version in version_list:
data = json.loads(version.data)
found = [f for f in data.get('changed') if f[0] =='docstatus' and f[-1] ==1]
if found:
return frappe.db.get_value('User', version.owner, 'full_name')
except:
pass
def _validate_selects(self):
if frappe.flags.in_import:
return
for df in self.meta.get_select_fields():
if df.fieldname=="naming_series" or not (self.get(df.fieldname) and df.options):
continue
options = (df.options or "").split("\n")
#支持分号(;)分隔的值与标签,以解决下拉值一词多义问题
options = [o.split(";")[0] for o in options if o]
# if only empty options
if not filter(None, options):
continue
# strip and set
self.set(df.fieldname, cstr(self.get(df.fieldname)).strip())
value = self.get(df.fieldname)
if value not in options and not (frappe.flags.in_test and value.startswith("_T-")):
# show an elaborate message
prefix = _("Row #{0}:").format(self.idx) if self.get("parentfield") else ""
label = _(self.meta.get_label(df.fieldname))
comma_options = '", "'.join(_(each) for each in options)
frappe.throw(_('{0} {1} cannot be "{2}". It should be one of "{3}"').format(prefix, label,
value, comma_options))
BaseDocument.get_owner_username = get_owner_username
BaseDocument.get_submit_username = get_submit_username
BaseDocument._validate_selects = _validate_selects
you can check the source code from the repository.
i need to change frappe.utils.format_date function but none of hooks.py method and monkey_patching method worked for me!!!
in second method when i use function just after load_monkey_patches(), value has been returned from my new function but in other places such as site, value returned from frappe.
any idea?
Can someone please confirm frappe.core doctype classes are also part of this? I am trying to override “UserPermission” class (from frappe.core.doctype) and it is not working. However docTypes in ERPNext I am able to.
There is a non-Class method that I override successfully.
File to be overridden: frappe.desk.reportview.py
Method to be overridden: get_form_params(), the original code is shown below:
def get_form_params():
“”“Stringify GET request parameters.”“”
data = frappe._dict(frappe.local.form_dict)
clean_params(data)
validate_args(data)
return data
The clean_params(data) and validate_args(data) will call other functions within reportview.py.
The problem is that I need to copy the whole of reportview.py to my new override.py because some function doesn’t do a “return”; “data” will be lost during the chain of calls.
The new block looks like:
def get_form_params():
“”“Stringify GET request parameters.”“”
data = frappe._dict(frappe.local.form_dict)
frappe.desk.reportview.clean_params(data) ← added frappe.desk.reportview. so it will reference the original reportview.py
frappe.desk.reportview.validate_args(data) ← added frappe.desk.reportview. so it will reference the original reportview.py
new_function(data) ← the new function I need to process “data”
return data
I don’t want to copy N functions from reportview.py to override.py because it will be a nightmare to maintain.
I rise a problem…
This solution works only if your sites have the same app installed, because monkeypatch is applied on the same ERP instance and affects multiple sites.
The side effect of this solution result in calling a module that could be not installed in other sites and produce an error.
For example, suppose app1 override a method of erpnext, and suppose to have the following configuration:
site1: erpnext, app1
site2: erpnext, app2
Monkeypatch will affect both the sites not only site1, and produce an error in site2 because frappe couldn’t not find app1 because the instance is the same…
Frappe’s team should take in charge this request, fix should be applied in Frappe core and we don’t have to think to complex and futile workaroud because, solution should came from Frappe.
monkey patch to overrides report functions ?
i did in my hook.py
import erpnext.stock.report.stock_balance.test_stock_balance
import custom_app.update_table
Python monkey patch whether in hooks.py or init.py is still not the right way to go IMHO.
At least (if not multi extension), the overriding of non-class method should be in a proper hook point (similary to overriding the whitelisted method) so there is no need for work around.
Note: I saw this problem araised from time to time, hope Framework team has permanent fix soon.