Sitemap
Damla Yaşar

Damla Yaşar

Developer

Odoo Partial Approval Offer Management Module — Part 2

3 min readMay 11, 2026

--

In many companies, the sales workflow does not end with a simple “Approved” or “Rejected” quotation. 🚀

Partial Approval Offer Management

A customer may approve only specific quotation lines while cancelling the remaining items.
For example:

  • A quotation contains 5 products
  • The customer approves only 2 products
  • The remaining 3 products are cancelled

In standard workflows, sales teams usually create a completely new quotation template to manage this situation. However, this approach causes an important CRM problem:

  • Original quotation history becomes fragmented
  • Approval timeline is lost
  • Product performance analysis becomes inaccurate
  • Sales cycle tracking becomes harder
  • Customer behavior data becomes inconsistent

Solution

This module introduces a Partial Approval System for Odoo quotations. If you want to use this module please contact with me.

Features

  • Approve selected quotation lines
  • Cancel remaining lines with cancellation reasons
  • Keep all actions inside the original quotation
  • Track approval and cancellation history
  • Preserve CRM sales timeline
  • Analyze customer decision patterns
  • Measure product acceptance performance
  • Improve pipeline analytics

Example Workflow

  1. Sales team sends quotation to customer
  2. Customer partially approves the offer
  3. Salesperson selects approved lines
  4. Remaining lines are marked as cancelled
  5. Cancellation reasons are recorded
  6. Original quotation remains fully traceable

This helps companies better understand:

  • Which products are frequently rejected
  • Average approval duration
  • Customer purchasing behavior
  • Salesperson performance
  • Pipeline conversion quality

CRM should track the complete sales journey from start to finish.
This module helps preserve that journey without losing quotation history.

Get Damla Yaşar’s stories in your inbox

Join Medium for free to get updates from this writer.

In the following days, I will also share pipeline analytics and reporting ideas built on top of this workflow in Odoo.

Odoo Code Architecture & Extension Logic 🍀
Odoo has a very stable and modular code structure.
This architecture allows developers to easily extend existing features or build completely new modules without breaking the core system.

1. Module Structure is Strict

Every Odoo module follows a standard structure:

The __init__.py file is especially important because it defines which Python files will be loaded by Odoo.

2. Module Naming & Structure Matters

Odoo relies heavily on naming conventions:

  • Folder name = module technical name
  • Python class _name = database model name
  • XML IDs = referenced system-wide

So consistency between:

  • file structure
  • model names
  • XML records

is critical for the system to work properly.

3. Everything Works with Inheritance

One of the most powerful concepts in Odoo is inheritance-based extension.

Instead of modifying core code, you extend it.

from odoo import models, fields, api
from odoo.exceptions import ValidationError


class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'

🔑 You are NOT rewriting the module, You are extending existing functionality

Manifest.xml

this is the anothoer important script for odoo, if you are not create this script, you module will not work smoothly.

{
'name': 'Sale Line Partial Approval',
'version': '1.0',
'author': 'damlaYasarr',
'category': 'Sales',
'depends': ['sale'],
'data': [
'views/sale_order_views.xml',
],
'installable': True,
'application': False,
}

Each folder you have to create _init_.py script and then you show your from . import sale_order_line

from odoo import models, fields, api
from odoo.exceptions import ValidationError


class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'

line_approval_state = fields.Selection([
('pending', 'Bekliyor'),
('approved', 'Onaylandı'),
('rejected', 'Reddedildi'),
], default='pending', string="Satır Onay Durumu")

loss_reason_id = fields.Many2one(
'crm.lost.reason',
string="Kayıp Nedeni",
domain=lambda self: self._loss_reason_domain(),
)

Purpose of Views

Views define:

  • How records are displayed in UI
  • Which fields are visible
  • Layout of forms, lists, kanban, etc.
  • User interaction structure

In short:

Python = logic
XML = UI representation

<odoo>
<record id="view_order_form_partial_approval" model="ir.ui.view">
<field name="name">sale.order.form.partial.approval</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='order_line']//list" position="inside">
<field name="line_approval_state"/>
<field name="loss_reason_id"
attrs="{'invisible':[('line_approval_state','!=','rejected')]}"/>
</xpath>

</field>
</record>
</odoo>

Views in Odoo are defined using XML files.
They control how the data from Python models is displayed in the user interface.
The fields defined in Python models are referenced inside XML views to render forms, lists, and other UI components.

Thank you for reading!

--

--