Enable Dark Mode!
what-is-monkey-patching-how-it-can-be-applied-in-odoo-18.jpg
By: Sidharth P

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

Technical Odoo 18

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.


If you need any assistance in odoo, we are online, please chat with us.



0
Comments



Leave a comment



whatsapp_icon
location

Calicut

Cybrosys Technologies Pvt. Ltd.
Neospace, Kinfra Techno Park
Kakkancherry, Calicut
Kerala, India - 673635

location

Kochi

Cybrosys Technologies Pvt. Ltd.
1st Floor, Thapasya Building,
Infopark, Kakkanad,
Kochi, India - 682030.

location

Bangalore

Cybrosys Techno Solutions
The Estate, 8th Floor,
Dickenson Road,
Bangalore, India - 560042

Send Us A Message