How can we override ERPNext core functions?

In my current project we need to modify the default calculation of ‘Purchase Order Item’ table, which is a Child table of ‘Purchase Order’. By default each ‘Amount’ field of the purchase order item is calculated by multiplying the ‘Quantity’ field and ‘Rate’ field of that row. Here is the default calculation -
Amount = Quantity * Rate

But in our project we had to introduce some custom fields, like Kg Per Drum, Conversion Rate, Utility etc. And we need all of them to be involved on the final amount calculation for every item row of the table. This is our Purchase Order Item Table. I am giving the Expected Calculations Below -

I already tried to alter the calculation with client script and custom .py file which is adding in the hooks.py files doc_events section -

("Purchase Order"): {
		"validate": ["erpnext.buying.doctype.purchase_order.custom_hooks.items_table_calculation"]
	},

The custom_hooks files code is -

from __future__ import unicode_literals
from pydoc import doc
import frappe


def items_table_calculation(doc, action):
	final_amount = 0
	for row in doc.items:
		row.total_weight = row.qty * row.kg_per_drum
		row.inmon = row.total_weight / row.conversion
		row.amount = (row.inmon * row.rate) + row.utility
		final_amount += row.amount

The problem is, after saving, child tables calculation shows ok but in the Total and Grand Total show qty*rate calculations result. How can I show my customized calculated amount in these fields?

And I figured it out that this calculation is handled by the taxes and totals.py and taxes and totals.js file. But if we change the taxes and totals.py file, the change doesn’t show immediately. And we really don’t want to change the core files here, so we need to know how we can override the functions here?

Hi there,

There aren’t hooks to override arbitrary python, so you’d have to find where in the Purchase Order controller those methods from taxes_and_totals.py are getting called. It’ll get messy, as there’s a fair bit of inheritance going on, but it’s doable.

May be this is what you are looking for override_doctype_class

1 Like

@mujeerhashmi, I have tried. Can you please show me how can I change Purchase Order Item child table table’s value customize from override_doctype_class?

How can I override functions from these files?

taxes_and _totals.js
taxes_and _totals.py

If anyone already done override functions or class to change amount calculations, please share.

1 Like

Like I said in my last post, there’s no way to override arbitrary functions. Your options are:

  1. Edit the source code of ERPNext directly, which is straightforward but requires you to manually merge changes on every version update.
  2. Use the override_doctype_class hook to create a custom controller for Purchase Orders, which you can load from a custom app. This has the advantage of not getting overwritten on every update. The disadvantage is that you won’t be able to edit taxes_and_totals.py directly, but you can find the places where purchase_order.py calls taxes_and_totals.py and replace those calls with calls to other methods.

It’s a bit simpler for the js if you’re willing to do some monkey patching. You just need to load a custom javascript file that overwrites the erpnext.taxes_and_totals variable defined here:

6 Likes

If I use custom javascript, is it effects after save in database?

FYI, you can also use monkey patching with Python. I needed to override erpnext.controllers.taxes_and_totals.calculate_taxes_and_totals. Here is what I did:

  1. Create a new module called my_app.my_module and a file inside it called override_calculate_taxes_and_totals.py with a new class like this:
from erpnext.controllers import taxes_and_totals

class my_app_calculate_taxes_and_totals(
        taxes_and_totals.calculate_taxes_and_totals
):
    """Override for calculating taxes and totals in MyApp."""
    <your changes here>

taxes_and_totals.calculate_taxes_and_totals = my_app_calculate_taxes_and_totals

Note that I import the module, not the class, and reference the class as taxes_and_totals.calculate_taxes_and_totals.

The last line changes the reference for taxes_and_totals.calculate_taxes_and_totals to my_app_calculate_taxes_and_totals so any other imports in standard ERPNext code get my class instead of the original class.

  1. Make sure your code runs before any other imports:

This only works if the code above runs before the standard code imports taxes_and_totals.calculate_taxes_and_totals so you can add the following line to hooks.py:

extend_bootinfo = [
	"my_app.startup.boot.boot_session",
]

and create a module called my_app.startup with a file called boot.py as so:

# This is the important line
from my_app.my_module import override_calculate_taxes_and_totals


def boot_session(bootinfo):
    """boot session."""

We don’t need the boot_session function to do anything. It is the import at the top that matters. Because this code runs every time a user logs in, the patch is set and the modified code will run. Of course, because I am inheriting from the original class, the original function is available as a super() reference.

3 Likes

This is interesting. Because monkey have some know problem.

  1. If declared in an app’s init.py, it is loaded and used even the app is not installed.
  2. If declared in hooks.py, it is loaded only if the app is installed, but only works in dev environment, and not in production.

Do you think by indirectly use extend_bootinfo, is this the correct way of using monkey patch?

Thanks!

Tested it. I think this is the best way for Monkey Patch so far.

I did have another thought on how to perform Monkey Patching. You could create a background job that always runs on system startup. This might be better than using the bootinfo process. The downside of bootinfo is it only runs when a user logs in. So if you have applications calling ERPNext APIs before any person logs in they don’t trigger bootinfo and therefore don’t get the patch. If a user logins in first then the API gets the patch. That is the only problem I have seen so far. So the two options:

  1. Use BootInfo to trigger the patch
  2. Create a background job to trigger the patch

Either way, you need to make sure the patch is initiated before an API call is made.

Yes, bootinfo has its problem as you said. I experiences that someimt it is latched and some time is not.

But I don’t really get how the scheduled job trigger will help? :confused:

Any sample?

can’t it be placed in a custom app hooks.py?
In my opinion, it should be called only when the custom app installed to the site.

1 Like

@firmanarihere Yes, you are right, putting the include in hooks.py works and is easier to implement. I did see a little bit of an issue where hooks.py did not run until the user went to the Home page so I am still working that out.

Regarding the scheduled job, the idea was to modify the OS level start script to use bench terminal to kick off a job on system startup every time the system starts. Unfortunately there is no system startup job event so the only way I can see running a job at system start is to add it to the startup script. The job would just perform the monkey patch so it would be a very simple job.

For now, I am going to use hooks.py and if I see any issues with that I will report it back here.