How to override method in frappe?

I think override_doctype_class hook is part of v13 and above.

1 Like

@revant_one @netchampfaris Is there any way to override the method not belong to a class?

2 Likes

You can refer below :

2 Likes

any updates for version 13.

lifesaver! Exactly what I was looking for. I needed to disable the amounts in CoA as it’s using too much computing resource for very big transactions, when we press expand all.

I used the above method (by revant_one) to override the validate method of EmployeeCheckin doctype. It worked perfectly.

But, something else broke. I have a custom script (JS) on after_save of EmployeeCheckin. This code isn’t executed anymore. Can anyone explain why this happens? How do I get some front end code running on after_save?

I was once told (i don’t remember in what thread) that after_save hook will be triggered no matter how the doc is inserted/saved.
So if it is not (as is my case) it might be a bug. Report it on the github.

refer to this app GitHub - aakvatech/CSF_TZ for monkey patching any method

  1. copy and adapt this file into your own custom app
    CSF_TZ/__init__.py at master · aakvatech/CSF_TZ · GitHub
  2. create python file under monkey_patches folder like this file.
    CSF_TZ/db_transaction_writes.py at master · aakvatech/CSF_TZ · GitHub
5 Likes

How to override python method which isn’t inside class?

for example, get_leaves_for_period method inside Leave Application package?

I tried:

myapp/myapp/hr/leave_application.py

from erpnext.hr.doctype.leave_application import leave_application

def get_leaves_for_period_new(employee, leave_type, from_date, to_date, do_not_skip_expired_leaves=False):
    frappe.throw(_("hello"))


def override(doc=None, target=None):
    leave_application.get_pending_leaves_for_period = get_pending_leaves_for_period_new


then in hooks.py

doc_events = {
    "Leave Application": {

        "validate": "myapp.myapp.hr.leave_application.override",

        "onload": "myapp.myapp.hr.leave_application.override",

        "refresh": "myapp.myapp.hr.leave_application.override"

    }

and this approach not working.

did you try this? Hooks

ye, but this is about overriding doctype class, but method get_pending_leaves_for_period inside leave_application is not a class method, it’s a method which is inside leave_application.py but not in Leave Application class.

Did you check this one ?

It uses

import frappe.utils.data
import custom_money_format.modified_scripts.data

frappe.utils.fmt_money = custom_money_format.modified_scripts.data.custom_fmt_money

in the hooks.py.

2 Likes

Yes, also…

my hooks.py:

import erpnext.hr.doctype.leave_application.leave_application
import myapp.myapp.hr.leave_application

erpnext.hr.doctype.leave_application.leave_application.get_pending_leaves_for_period = myapp.myapp.hr.leave_application.get_pending_leaves_for_period_new

and not working

ah… there was a problem somewhere else…

i realised that i override this function again with main one in some other place… (donkey)

sorry, nevermind.

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