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 -
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?
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.
@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?
Like I said in my last post, there’s no way to override arbitrary functions. Your options are:
Edit the source code of ERPNext directly, which is straightforward but requires you to manually merge changes on every version update.
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:
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:
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.
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:
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.
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:
Use BootInfo to trigger the patch
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.
@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.