How to override method in frappe?

I would like to mention 3 things:

  • 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.

4 Likes

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:

Executing Code On Doctype Events (frappeframework.com)

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

erpnext.projects.report.billing_summary.get_timesheet_details =
my_app.my_app.billing_summary.get_timesheet_details

The workaround I’m using is, in myapp/salary_slip.py, instead of this:
from erpnext.projects.report.billing_summary import get_timesheet_details

I use:
from my_app.my_app.billing_summary import get_timesheet_details

PS: I’m using monkey patch for the above fixes.

1 Like

“monkey patch” ?
You can find some inspiration here :

check __init__.py

Sure not sustainable way to do it but work

EDIT : Sorry didn’t check previous post that give CSF_TZ repo with is also a good inpiration source for monkey patch

@FHenry hello! I’m using monkey patch already. Sorry I didn’t mention that.

I’ve just edited my reply above.

1 Like

this monkey patch is not same as in CSF_TZ repo and https://gitee.com/yuzelin/erpnext_oob/tree/master/erpnext_oob

how it works

in aap.init.py

# -*- 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.

3 Likes

Yes, It worked

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?

@johnwick
Hii
Have you you found any solution for this ? I am facing same problem.

can you explain with some examples for build_my_thing works?

could you please explain more what the problem you did?

Write these in your custom app

Override class method:

from erpnext.selling.doctype.sales_order.sales_order import SalesOrder

SalesOrder.validate = yourmethod

Override standard method:

from frappe import utils

utils.cstr = yourmethod

Where in the init.py file ?

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 hook for that.

Let’s continue this discussion :slight_smile:

Monkey patch is ok. Using hooks.py is ok.

Now let’s get into more details.

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.

Now I want to add a custom function just before

return data

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’m kinda stuck.

Any advice?

hey @Mohammed_Redha did you find the solution to this

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

erpnext.stock.report.stock_balance.test_stock_balance.TestStockBalance = custom_app.update_table.TestStockBalance

and create edit my app.init.py file like monkey patch put i do not know in monkey patch folder what should i do

Well described the problem of the framework.

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. :slight_smile: