What is Monkey Patching & How It Can Be Applied in Odoo 16

What is Monkey Patching & How It Can Be Applied in Odoo 16

Odoo, an open-source ERP and business application platform, provides a powerful framework for customization and extension. One of the techniques used to enhance existing functionality is “monkey patching.” In this blog, let’s discuss what monkey patching is and how it can be applied in Odoo16.

What is Monkey Patching

Monkey Patching is a technique that allows us to modify or extend the behavior of existing code at runtime. It makes changes to the code without changing the source code.

It is useful when we want to add new features, fix bugs, or to override the existing code.

Here is the structure of applying monkey patching

# 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

By assigning new_method to MyClass.original_method, the code is replaced with the new method. Each time the original method is called, the new method is executed without altering the original method.

Example: Splitting Deliveries in Sale Orders

Let’s consider an example of a sales order where we want to split deliveries for each product in the order line. Currently, a single delivery, known as stock picking, is created for the entire sale order with all the products.  To achieve this we need to add a field to the order line to specify the delivery date for each product. Additionally, we need to patch the _assign_picking method inside the stock.move model, which is responsible for creating a picking for each move.

Inheriting and Adding a field inside order line

To split the delivery based on delivery date, we need to add a delivery date field inside the sales order line.

class SaleOrderlineInherit(models.Model):
    """Inheriting sale order line to add delivery date field"""
    _inherit = 'sale.order.line'
    delivery_date = fields.Date("Delivery Date")

Add the field in the views.

<odoo>
<data>
<record id="sale_order_inherit_form" model="ir.ui.view">
<field name="name">Sale Order</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']/tree/field[@name='price_unit']" position="before">
<field name="delivery_date"/>
</xpath>
</field>
</record>
</data>
</odoo>

Modifying the _prepare_procurement_values Method

When splitting the deliveries, the scheduled date of delivery should be the one provided in the order line for each product. To achieve this, we need to monkey patch _prepare_procurement_values method of 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

Here we have modified the date_deadline value to be the delivery date provided in the corresponding orderline. If the delivery date is not provided, we set it as the order date by adding the customer lead time of the product.

Then, we assign the new method to the SaleOrderLine class

SaleOrderLine._prepare_procurement_values = _prepare_procurement_values

Inheriting the Stock Move Model

Next, we inherit the stock move model using the _inherit attribute. This allows us to extend its functionality. 

class StockMoveInherits(models.Model):
    """inheriting the stock move model"""
    _inherit = 'stock.move'

Modifying the _assign_picking Method.

Next, we make changes to the _assign_picking method to achieve the desired splitting of 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']

First, we define a variable picking as an empty record 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 by the groupby function, which groups the moves based on a key obtained from _key_assign_picking of each move. Then it iterates over each move group and concat.

pickings = moves[0]._search_picking_for_assignation()

Then, using the first move, it searches for a picking using _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 picking is found, the fields partner_id and origin are updated. Otherwise, it checks for negative moves and filters them out.

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 there are any remaining moves, a new picking is created based on the picking values obtained from _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)

 Now separate picking is created for each move.

StockMove._assign_picking = _assign_picking

Finally, assign the new function to the _assign_picking method of StockMove class.

By applying these monkey patches, we were able to achieve the desired functionality of splitting deliveries in the sale order. However, it is recommended to consider alternative approaches when appropriate.

This is how we apply monkey patching in Odoo 16.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *