Odoo is an open-source platform for ERP and business applications that provides a powerful framework for customization and expansion. This blog examines "monkey patching"— a method used to enhance or modify existing functionality in Odoo 18. We'll explore what monkey patching is and how it can be implemented in the Odoo 18 environment.
What is Monkey Patching
Monkey patching is a technique that allows developers to change or enhance the behavior of existing code during runtime. It enables modifications without having to alter the original source code directly.
This method is especially useful when there's a need to add new functionality, fix bugs, or override existing logic without modifying the original source code.
Typically, monkey patching is carried out in a structured manner.
# Original class
class MyClass:
def original_method(self):
print("This is the original method")
# Monkey patching the class with a new method
def new_method(self):
print("This is the new method")
MyClass.original_method = new_method
When new_method is assigned to MyClass.original_method, the original method is effectively replaced. As a result, any call to the original method will now execute the new one, allowing behavior changes without directly modifying the initial method.
Example: Splitting Deliveries in Sales Orders
Consider a scenario involving a sales order where the goal is to create separate deliveries for each product listed. By default, a single delivery—known as a stock picking—is created for the entire order, covering all products. To change this behavior, we need to add a field on each order line to specify the delivery date for individual products. Additionally, we must adjust the _assign_picking method in the stock.move model, as this function handles the creation of pickings for each move in the workflow.
Inheriting and Introducing a Field in the Order Line:
To enable the separation of deliveries according to their delivery dates, a new field must be added to the sales order line to capture the specific delivery date for each item.
class SaleOrderlineInherit(models.Model):
"""Inheriting sale order line to add delivery date field"""
_inherit = 'sale.order.line'
delivery_date = fields.Date("Delivery Date")
Incorporate the field into the views.
<odoo>
<record id="view_order_form" model="ir.ui.view">
<field name="name">sale.order.inherit.monkey.patching</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='order_line']/list/field[@name='price_unit']"
position="before">
<field name="delivery_date"/>
</xpath>
</field>
</record>
</odoo>
Modifying the _prepare_procurement_values Method.
While splitting deliveries, ensuring that the scheduled delivery date matches the delivery date set on each order line is essential. To achieve this, monkey patching must be applied to the _prepare_procurement_values method within the sale order line model.
def prepare_procurement_values(self, group_id=False):
Order_date = self.date_order
Order_id = self.order_id
deadline_date = self.delivery_date or (
order_id.order_date +
timedelta(Days= self.customer_lead or 0.0)
)
planned_date = deadline_date - timedelta(
days=self.order_id.companyid.security_lead)
values = {
'group_id': group_id,
'sale_line_id': self.id,
'date_planned': planned_date,
'date_deadline': deadline_date,
'route_ids': self.route_id,
'warehouse_id': order_id.warhouse_id or False,
'product_description_variants': self.with_context(
lang=order_id.partner_id.lang).
_get_sale_order_line_multiline_description_variants(),
'company_id': order_id.company_id,
'product_packaging_id': self.product_packaging_id,
'sequence': self.sequence,
}
return values
SaleOrderLine._prepare_procurement_values = _prepare_procurement_values
In this case, the date_deadline value has been modified to align with the delivery date provided in the related order line. If no delivery date is defined, it defaults to the order date, adjusted by the product’s customer lead time.
After defining the new method, it is assigned to the SaleOrderLine class using:
SaleOrderLine._prepare_procurement_values = _prepare_procurement_values
Inheriting the Stock Move Model
Next, we enhance the behavior of the stock.move model by inheriting it using the _inherit attribute
class StockMoveInherits(models.Model):
"""inheriting the stock move model"""
_inherit = 'stock.move'
Modifying the _assign_picking method.
After that, we update the _assign_picking method to implement the desired logic for splitting deliveries.
def _assign_picking(self):
pickings = self.env['stock.picking']
grouped_moves = groupby(self, key=lambda m: m._key_assign_picking())
for group, moves in grouped_moves:
moves = self.env['stock.move'].concat(*moves)
new_picking = False
pickings = moves[0]._search_picking_for_assignation()
if pickings:
vals = {}
if any(pickings.partner_id.id != m.partner_id.id
for m in moves):
vals['partner_id'] = False
if any(pickings.origin != m.origin for m in moves):
vals['origin'] = False
if vals:
pickings.write(vals)
else:
moves = moves.filtered(lambda m: float_compare(
m.product_uom_qty, 0.0, precision_rounding=
m.product_uom.rounding) >= 0)
if not moves:
continue
new_picking = True
pick_values = moves._get_new_picking_values()
sale_order = self.env['sale.order'].search([
('name', '=', pick_values['origin'])])
for move in moves:
picking = picking.create(
move._get_new_picking_values())
move.write({
'picking_id': pickings.id
})
move._assign_picking_post_process(
new=new_picking)
return True
StockMove._assign_picking = _assign_picking
Let's explain the code.
pickings = self.env['stock.picking']
We start by initializing a variable called picking as an empty recordset of the stock.picking model.
grouped_moves = groupby(self, key=lambda m: m._key_assign_picking())
for group, moves in grouped_moves:
moves = self.env['stock.move'].concat(*moves)
The grouped_moves variable is created using the groupby function, which organizes the moves according to a key obtained from each move's _key_assign_picking method. It then loops through each group and merges the moves together.
pickings = moves[0]._search_picking_for_assignation()
Next, it uses the first move in the group to locate a suitable picking by calling the _search_picking_for_assignation method.
if pickings:
vals = {}
if any(pickings.partner_id.id != m.partner_id.id
for m in moves):
vals['partner_id'] = False
if any(pickings.origin != m.origin for m in moves):
vals['origin'] = False
if vals:
pickings.write(vals)
If a picking is found, the partner_id and origin fields are updated accordingly. If no pickings exist, the code proceeds to identify and exclude any moves with negative quantities.
else:
moves = moves.filtered(
lambda m: float_compare(
m.product_uom_qty, 0.0, precision_rounding=
m.product_uom.rounding) >= 0)
if not moves:
continue
new_picking = True
pick_values = moves._get_new_picking_values()
sale_order = self.env['sale.order'].search([
('name', '=', pick_values['origin'])])
If any moves remain after the filtering process, a new picking is created using the values retrieved from the _get_new_picking_values method.
for move in moves:
picking =picking.create(move._get_new_picking_values())
move.write({
'picking_id': picking.id
})
move._assign_picking_post_process(
new=new_picking)
Subsequently, a distinct picking is generated for each individual move.
StockMove._assign_picking = _assign_picking
To wrap up, the custom function is linked to the _assign_picking method of the StockMove class. By implementing these monkey patches, we’ve effectively achieved the desired outcome of splitting deliveries in the sale order.
To read more about what Monkey Patching is & How It Can Be Applied in Odoo 17, refer to our blog What is Monkey Patching & How It Can Be Applied in Odoo 17.