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

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

Odoo, a platform for open-source ERP and business applications, offers a robust framework for customization and extension.
This blog explores the concept of “monkey patching” as a technique employed to augment existing functionality in Odoo17.
Let’s delve into what monkey patching entails and how it can be implemented within the Odoo17 environment.

What is Monkey Patching

Monkey Patching stands as a technique enabling the modification or extension of the behavior of existing code during runtime. This approach facilitates alterations to the code without necessitating modifications to the source code itself.
It proves valuable in scenarios where there is a need to introduce new features, address bugs, or override existing code without directly altering the source code.
The application of monkey patching typically follows a structured approach.
# 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 substituted with the new method. Consequently, whenever the original method is invoked, the new method is executed, thereby effecting changes without modifying the original method itself.
Example: Splitting Deliveries in Sale Orders
Let’s explore a scenario involving a sales order where the objective is to divide deliveries for each product listed in the order. A singular delivery, referred to as stock picking, is generated for the entire sales order encompassing all products. To accomplish this, it is necessary to introduce a field in the order line to indicate the delivery date for each product. Furthermore, a modification of the _assign_picking method within the stock.move model is required. This method is responsible for crafting a pick for each move.
Inheriting and Introducing a Field in the Order Line:
To facilitate the splitting of deliveries based on the delivery date, it is essential to incorporate a delivery date field within 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")
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']/tree/field[@name='price_unit']"
                  position="before">
               <field name="delivery_date"/>
           </xpath>
       </field>
   </record>
</odoo>
Modifying the _prepare_procurement_values Method.
When dividing deliveries, the scheduled delivery date should align with the one specified in the order line for each product. To accomplish this, it is necessary to apply monkey patching to the _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
In this instance, we have adjusted the date_deadline value to match the delivery date specified in the corresponding order line. In cases where the delivery date is not specified, we set it as the order date by incorporating the customer lead time of the product.
Subsequently, we allocate the newly created method to the SaleOrderLine class.
SaleOrderLine._prepare_procurement_values = _prepare_procurement_values

Inheriting the Stock Move Model

Following that, we extend the functionality of the stock move model by inheriting it using the _inherit attribute.
class StockMoveInherits(models.Model):
    """inheriting the stock move model"""
    _inherit = 'stock.move'
Adjusting the _assign_picking method.
Following that, we modify the _assign_picking method to accomplish the intended 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']
Initially, we define a variable named 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 generated using the groupby function, which groups the moves based on a key derived from the _key_assign_picking of each move. Subsequently, it iterates through each move group and concatenates them.
pickings = moves[0]._search_picking_for_assignation()
Subsequently, utilizing the first move, it searches for a picking by employing 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 ‘pickings’ is located, the fields partner_id and origin are updated. In the absence of a ‘pickings’, the code 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 after filtering, a new picking is established, drawing upon the picking values acquired 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 move.
StockMove._assign_picking = _assign_picking
In conclusion, the newly developed function is assigned to the _assign_picking method of the StockMove class. Through these applied monkey patches, we have successfully realized the targeted functionality of splitting deliveries within the sale order. It’s advisable to explore alternative approaches when deemed suitable. This encapsulates the process of applying monkey patching in Odoo 17.
To read more about Monkey Patching & how it can be applied in Odoo 16, refer to our blog What is Monkey Patching & How It Can Be Applied in Odoo 16

Similar Posts

Leave a Reply

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