How Do Doc Hooks Work?

In the hooks file, when you specify a doc_hook for a DocType that comes from another module, what happens to the original? Does it also run or is it overridden? And if the first, what order are they run?

So for example, if I have the following in my module hooks file:

doc_events = {
    "User": {
        "validate": "mymodule.overrides.user.validate_user"
    }
}

And then in mymodule.overrides.user.validate_user:

def validate_user(doc, method=None):
    if doc.custom_field = "invalid":
        frappe.throw( _("Invalid Value") )

Will just the above function be executed when a User document is saved or will it also run any other validation functions?

@fiveoaks check this
https://docs.frappe.io/framework/user/en/basics/doctypes/controllers

Thanks, I had already read this. I don’t think this answers my question though. I’m talking about DocTypes that are core or created by other modules. User for example. A core DocType. I suppose I could write a new controller for this and call super() for each hook, but I would rather not. As mentioned above, I see in the hooks.py file the doc_events option which to me appears to allow you to avoid a controller and rather call a function. Am I not understanding that correctly? But if I am, does this override the one in the controller or are they both run?

@fiveoaks If you have doubts and want more control , you can override the entire class . and redefine its functions, it will completely override the original function . but you call it anywhere in your new code using super(CustomSalesInvoice,self).on_submit() .

I did some testing and answered my question however in doing so, also found something concerning. First, what I did:

1. Create New DocType ‘Example’

Kept it very simple. Two data fields, one named field_1 and the other named field_2. This DocType was created in my custom app named example.

2. Added Validate in Controller

In the controller for my Example DocType, I have the following:

import frappe
from frappe.model.document import Document


class Example(Document):
	def validate( self ):
		if self.field_1 is None:
			frappe.throw( "Field 1 must be filled out" )

3. Added DocEvent To Hooks

In my hooks.py file, I added the following:

doc_events = {
    "Example": {
        "validate": "example.example.doctype.example.override.validate"
    }
}

And then I created the file /example/example/doctype/example/override.py with the following content:

import frappe
from frappe.model.document import Document

def validate(doc, method=None):
    if doc.field_2 is None:
        frappe.throw( "Field 2 must be filled out" )

So both fields now are required to be filled out. Field 1 is checked in the actual controller itself and field 2 in the hook.

4. Restarted Frappe

After restarting Frappe, I tested by creating a new document for Example. Initially it looked like it was working as I was originally asking. Both validation functions are being run.

Concerning Part

The part that is concerning though is if I do the following:

After I hit save for either of the above, if I remove the value from the field that I had populated originally and put that value in the other field, hitting Save I would expect an error message saying the other field must be filled out. But no, the document is saving without an error. So it appears the validation is not running again.

So the question is, is this a bug in Frappe or is this expected behavior?

Yes, this is an interesting question.
As far as I know, the "doc_hook " option in the hooks file allows you to override the original function and execute your custom function instead.

In the end, it will call your custom function, not the original function.
What we assume here is that it will execute only your custom function, not both functions together. (This excludes the case where you explicitly use super()).

Just out of curiosity, I added a super() call to my override function:

def validate(doc, method=None):
    super()
    if doc.field_2 is None:
        frappe.throw( "Field 2 must be filled out" )

But this fails with the error “RuntimeError: super(): __class__ cell not found”.

It would be helpful to have some clarity here from the developers. Ideally for my use case I want them both to run and to run for every save. Now if that requires adding super, that’s fine but need clarity.

Hi there, thanks for posting a question with good test cases.

This is expected, as your override method isn’t actually part of the class.

This makes me think the issue isn’t hooks but rather value types. In python, an empty string is not None. After you set a value and then unset it, I suspect that field_1 and field_2 are converting from None to empty strings.

If you change
doc.field_2 is None
to
doc.field_2 is None or len(doc.field_2) == 0

do you get the results you’d expect?

Although maybe it’s a moot point since it appears the super() call isn’t necessary, how would you do that in this case? I’m not a strong python programmer. Fluent in Java but just started learning Python solely for Frappe.

You were right. Once I changed my example above to include the len, it does work. So the conclusion from my tests is that anything put in doc_events in hooks.py is run in addition to controller, it does not override it.