Failed attempt to use `override_doctype_class` 😡

Following a suggestion by @peterg

… I am attempting to use override_doctype_class for the first time. It seems to be completely ignored.

My app directory:

.
├── config
├── docs
├── myApp
    :    :    : 
├── hooks.py
├── overrides
│   └── sales_invoice.py
    :    :    :

My hooks.py:

# along with any modifications made in other Frappe apps
# override_doctype_dashboards = {
#   "Task": "myApp.task.get_dashboard_data"
# }

override_doctype_class = {
    'SalesInvoice': 'app.overrides.sales_invoice.CustomSalesInvoice'
}

My overrides/sales_invoice.py:

import frappe
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice

class CustomSalesInvoice(SalesInvoice):
  def before_save(self):
    print(' === === === ===  BEFORE SAVE  === === === === === ')
    # super(SalesInvoice, self).on_update()

My temporarily altered erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.py:

  def before_save(self):
    print(' *** *** *** ***  BEFORE SAVE  *** *** *** *** *** ')
    set_account_for_mode_of_payment(self)

Resulting log file when I save changes to a draft Sales Invoice:

22:41:58 web.1            | 127.0.0.1 - - [05/Mar/2021 22:41:58] "GET / HTTP/1.0" 404 -
22:45:28 web.1            |  * Detected change in '/home/erpdev/frappe-bench-DELS/apps/myApp/myApp/hooks.py', reloading
22:45:29 web.1            |  * Detected change in '/home/erpdev/frappe-bench-DELS/apps/myApp/myApp/hooks.py', reloading
22:45:30 web.1            |  * Restarting with inotify reloader
22:45:32 web.1            |  * Debugger is active!
22:45:32 web.1            |  * Debugger PIN: 105-941-064
22:45:35 web.1            |  * Detected change in '/home/erpdev/frappe-bench-DELS/apps/myApp/myApp/overrides/sales_invoice.py', reloading
22:45:37 web.1            |  * Restarting with inotify reloader
22:45:38 web.1            |  * Debugger is active!
22:45:38 web.1            |  * Debugger PIN: 105-941-064
22:46:03 web.1            | 127.0.0.1 - - [05/Mar/2021 22:46:03] "POST /api/method/frappe.desk.search.search_link HTTP/1.0" 200 -
22:46:03 web.1            |  * Detected change in '/home/erpdev/frappe-bench-DELS/apps/myApp/myApp/__pycache__/hooks.cpython-38.pyc', reloading
22:46:05 web.1            | 127.0.0.1 - - [05/Mar/2021 22:46:05] "GET /api/method/frappe.desk.form.load.getdoctype?doctype=Item&with_parent=1&cached_timestamp=2021-01-25+20%3A49%3A50.222976&_=1614980205184 HTTP/1.0" 200 -
22:46:06 web.1            |  * Restarting with inotify reloader
22:46:07 web.1            |  * Debugger is active!
22:46:07 web.1            |  * Debugger PIN: 105-941-064
22:46:14 web.1            | 127.0.0.1 - - [05/Mar/2021 22:46:14] "GET /api/method/frappe.desk.form.utils.validate_link?value=Biodox+3000+ppm+litro&options=Item&fetch=&_=1614980205185 HTTP/1.0" 200 -
22:46:15 web.1            | 127.0.0.1 - - [05/Mar/2021 22:46:15] "POST /api/method/erpnext.stock.get_item_details.get_item_details HTTP/1.0" 200 -
22:46:19 web.1            |  *** *** *** ***  BEFORE SAVE  *** *** *** *** *** 
22:46:20 web.1            | 127.0.0.1 - - [05/Mar/2021 22:46:20] "POST /api/method/frappe.desk.form.save.savedocs HTTP/1.0" 200 -
22:46:21 web.1            | 127.0.0.1 - - [05/Mar/2021 22:46:21] "GET /api/method/frappe.desk.notifications.get_open_count?doctype=Sales+Invoice&name=001-003-000000001&items=%5B%22Payment+Entry%22%2C%22Payment+Request%22%2C%22Journal+Entry%22%2C%22Invoice+Discounting%22%2C%22Dunning%22%2C%22Timesheet%22%2C%22Delivery+Note%22%2C%22Sales+Order%22%2C%22POS+Invoice%22%2C%22Sales+Invoice%22%2C%22Auto+Repeat%22%5D&_=1614980205186 HTTP/1.0" 200 -
    :    :    :    :    :    :

I would expect the logged print statement to be :

22:46:19 web.1            |  === === === ===  BEFORE SAVE  === === === === === 

It should not be …

22:46:19 web.1            |  *** *** *** ***  BEFORE SAVE  *** *** *** *** *** 

… because the call to super() is disabled.

I’ll be grateful for any hints as to how to get this to work

Questions

Is there a “please_dont_ignore_my_hooks()” command?

Notes

erpdev@loso:~/frappe-bench-DELS$ bench version
myApp 0.0.1
erpnext 13.0.0-beta.12
frappe 13.0.0-beta.11

erpdev@loso:~/frappe-bench-DELS$ lsb_release -a
Description:  Ubuntu 20.04.2 LTS
Codename: focal

erpdev@loso:~/frappe-bench-DELS$ bench --site dev.erpnext.host clear-cache
erpdev@loso:~/frappe-bench-DELS$ bench --site dev.erpnext.host migrate
Migrating dev.erpnext.host
Updating DocTypes for frappe        : [=============================] 100%
Updating DocTypes for erpnext       : [============================] 100%
Updating DocTypes for myApp    : [=============================] 100%
Updating Dashboard for frappe
Updating Dashboard for erpnext
Updating Dashboard for myApp
Updating customizations for Address
Updating customizations for Contact
Building search index for dev.erpnext.host
erpdev@loso:~/frappe-bench-DELS$ 

It looks like an addressing issue. Is your app is called “myApp”?

In that case, the “app” in ‘app.overrides.sales_invoice.CustomSalesInvoice’ doesn’t look right.

Oh God! The perils of copy’n paste coding. :roll_eyes:

Thanks!

1 Like

Despite help from @peterg this is still not working.

I altered my hooks.py thusly …

override_doctype_class = {
    'SalesInvoice': 'myApp.overrides.sales_invoice.CustomSalesInvoice'
}

… replacing app with myApp.

It did not solve the problem!

My full app directory looks like this:

erpdev@loso:~/frappe-bench-DELS$ tree -L 2 apps/myApp/
apps/myApp/
├── customizations
├── myApp
│   ├── config
│   ├── docs
│   ├── myApp
│   ├── fixtures
│   ├── hooks.py
│   ├── __init__.py
│   ├── modules.txt
│   ├── overrides
│   ├── patches.txt
│   ├── __pycache__
│   └── templates
├── myApp.egg-info
├── LICENSE
├── license.txt
├── MANIFEST.in
├── README.md
├── requirements.txt
├── services
│   ├── einv_svc
│   └── rSYNC.sh
└── setup.py

19 directories, 38 files
erpdev@loso:~/frappe-bench-DELS$ 

So I altered my hooks.py again…

override_doctype_class = {
    'SalesInvoice': 'myApp.myApp.overrides.sales_invoice.CustomSalesInvoice'
}

… replacing myApp with myApp.myApp.

It made no difference, either.

I have no idea how to determine which of those two is correct. What’s the rule here?

It occurred to me that trying to intercept before_save is a bad idea, because it can be intercepted separately as a Document Event hook, so I tried instead to override set_status, the same way.

That also did not work.

I then got suspicious that the hooks.py for my app was not working at all.

I added a doc_event hook:

doc_events = {
	"SalesInvoice": {
		"before_save": "electronic_invoice.crud_events.before_save"
	}
}

That ALSO did not work; so I then tried:

doc_events = {
	"*": {
		"before_save": "electronic_invoice.crud_events.before_save"
	}
}

Yay! It works!

Wut? Wait a sec …

doc_events = {
	"Sales Invoice": {
		"before_save": "electronic_invoice.crud_events.before_save"
	}
}

Gees whiz. Works too!

Now my, class override …

override_doctype_class = {
    'Sales Invoice': 'electronic_invoice.overrides.sales_invoice.CustomSalesInvoice'
}

Well, golly, gosh, darn that works too. :rage:

So the documentation clearly shows that DocType type names in hooks must not have spaces. Correct?

The name of the class is SalesInvoicespaces removed:

class SalesInvoice(SellingController):

The DocType ToDo does not have spaces in its name, so spaces are to be removed … OBVIOUSLY!

*  *  *    WRONG!  *  *  *

Question:

Why does ERPNext documentation HAVE to be filled with land mines?


Any way, on to the next step.

Now I have to add the call to super. Do I do…

    super(SalesInvoice, self).before_save()

… or do I do …

    super(Sales Invoice, self).before_save()

Who the hell knows!

Welcome to the ERPNext guessing game!

(ji$u$ f&k$#g @H$t :poop:)

p.s.

What works is:

    super().before_save()
2 Likes

Martin, I appreciate that you’re frustrated, but the tone of your response here isn’t very helpful. Ironically, your post reveals some significant misunderstandings and gaps, not of ERPNext but just plain old python. That includes myApp vs. myApp.myApp, “Sales Invoice” vs. SalesInvoice, and super(). None of these are frappe design decisions. It’s all just vanilla python. I’m sure many people here, including myself, would be more than happy to explain things that aren’t clear to you as best we can, but the sarcasm is kind of exhausting.

3 Likes

I am sorry to have annoyed you.

I also regret distracting from the major issue, by mentioning the minor issues that you focused on.


The real problem:

In different contexts DocType names are sometimes concatenated and sometimes not.

So:

  • To use a multiple name DocType for code examples, and make it clear when and when not to concatenate, would be ideal.
  • To use a single name DocType for code examples would be ok, but not great.
  • To use a two name DocType that also happens to be the only concatenated two word name is horrible.

Worse still, it’s used in a code example that shows NO error messages of any kind if you get it wrong. Getting it right is like a combination padlock: when everything is correct you get something; anything else, you get nothing.

I have been told off before for being sarcastic and typically I’m told, “if you don’t like it, fix it!” Well, guess what … I do … over and over again, filling in the gaping blanks in the documentation, but getting very little thanks or recognition…

… here: How to fully submit a DocType from Python?

and here: How to create an "Absolute Minimum Script Report"?

and here: [solved] cURL example of an attachment upload RPC call?

and here: How to set a "Naming Series" sequence start number through the RPC API?

and here: How to remove the "Publications" card from the landing page?

and here: How to get 'site_config.json' settings in Python code?

and here: How to call "Chart of Accounts Importer" through RPC API? - #7 by MartinHBramwell

and here: Can't use port 8000 except as NGinx upstream. What am I missing? - #5 by MartinHBramwell

and here: [Solved] What's the right way to version control my customizations? - #2 by MartinHBramwell

and here: How to create an "Absolute Minimum Script Report"?

All of those represent cases where I found the docs to be like product brochure feature lists, “Hey, here’s another cool thing ERPNext can do!”. They’re not written to fully armed the developer to go in and solve any problem. They’re more like, “When you figure out how, you will be able to …”

Very often, the examples only show the trivial case, like here: REST API – RPC, where the typical case is a call with parameters, but the example only shows how it’s done without parameters. Once again, I made the effort to fill in the hole: [Solved] cURL example of a whitelist method RPC call with parameters?

This is the misunderstanding I was talking about. The DocType name is “Sales Invoice”. “SalesInvoice”, on the other hand, is the name of a python class. Structurally, these are very different things. There’s no space because Python class names can’t have spaces. The fact that they have similar but slightly different names shouldn’t be confusing because there’s really no context where it’s structurally unclear which you should be using.

This, for example:

…is obviously wrong. Using a class name in this way is not valid Python. A class name would never be in quotes, and more fundamentally it would be extremely odd to use one as a key in a dict structure. I’m not sure how exactly the documentation frames the issue, but there really isn’t any ambiguity in this context.

1 Like

Not if it is what you do all day long.

For a newcomer, context can be a very hard thing to determine. That’s one of the main problems with the documentation – it is rarely explicit about context, code examples just float in space and it can be a real puzzle to know where to apply the example and where not.

override_doctype_class = {
    'SalesInvoice': 'myApp.myApp.overrides.sales_invoice.CustomSalesInvoice'
}

Oh, really?

I got that directly from the code example I have been complaining about: Override DocType Class

override_doctype_class = {
    'ToDo': 'app.overrides.todo.CustomToDo'
}

Furthermore, it is wrong, but not in the way you say. To get it work I had to do:

override_doctype_class = {
    'Sales Invoice': 'myApp.overrides.sales_invoice.CustomSalesInvoice'
}

Because, it isn’t a class name, it’s a DocType!

2 Likes

Yes. The structure here is straightforward and identical to other parallel structures in hooks:

override_doctype_class = {
    'Doctype Name': 'path.to.python.package'
}

Exactly. That is what I am saying. In hooks.py, it’s always DocType names. Never objects.

Is the thing confusing you the fact that there is no space in “ToDo”? That’s because the name of the doctype is “ToDo”. There’s no such thing as “To Do” in Frappe.

2 Likes

I already made clear the main issue that caused me so much trouble:

I should have been more accurate, instead of stating, “In different contexts DocType names are sometimes concatenated and sometimes not.” , I should have said:

DocType names and the names of their Python handlers can be very similar. Eg, `SalesInvoice’ is the Python handler for the “Sales Invoice” DocType.

However that still sidesteps the booby trap I fell into. This is the boobytrap:

I know that there’s no such thing as “To Do” in Frappe … now, after hours of bashing my head against a wall!

It’s the fact that it is the only concatenated two word DocType name in all of Frappe and ERPNext that makes it the absolute worst DocType to use where DocType names and Python class names are mixed in the same code examples.

I’m not quite clear on your goals in this thread, but I’m glad you seem to have figured out your original question now. I’m sure you’re right that the documentation is sometimes confusing to people new to python. Documentation writing is tedious work. PRs always welcome!

2 Likes