first commit

This commit is contained in:
Suherdy Yacob 2026-01-12 11:38:09 +07:00
commit 6b517d3100
8 changed files with 147 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
# Python
__pycache__/
*.py[cod]
*$py.class
# Odoo
*.pot
*.po
# IDE
.vscode/
.idea/
# System
.DS_Store
Thumbs.db

32
README.md Normal file
View File

@ -0,0 +1,32 @@
# Product UOM Change
**Author:** Suherdy Yacob
**Version:** 18.0.1.0.0
**License:** LGPL-3
**Category:** Inventory
## Description
This module allows authorized users to change the Unit of Measure (UOM) of a product even if it has existing stock moves, bypassing the standard Odoo restriction.
It enforces that the new UOM must belong to the **same UOM Category** as the current UOM to maintain data integrity regarding physical quantities.
> **WARNING:** This module uses raw SQL to bypass Odoo's standard validation. While it enforces category consistency, changing UOMs on products with existing stock can lead to inventory valuation and quantity inconsistencies if the conversion factors differ. Use with caution.
## Configuration
No specific configuration is required.
Access to the "Change UOM" feature is restricted to users in the **Administration / Settings** group (`base.group_system`).
## Usage
1. Navigate to **Inventory > Products > Products**.
2. Open the product form for the product you wish to modify.
3. Click on the **Actions** (gear icon) menu.
4. Select **Change UOM**.
5. In the wizard that appears:
* **New UOM**: Select the new Unit of Measure. The list is filtered to show only UOMs in the same category.
* **New Purchase UOM**: (Optional) Select a new UOM for purchasing, if different.
6. Click the **Change UOM** button to apply the changes.
## Technical Details
* **Model:** `product.uom.change.wizard`
* **Access Rights:** Wizard access is granted to internal users, but the action is restricted to system administrators.
* **Logic:** Updates `product.template` directly via SQL to avoid the `write` method constraint check in `stock` module.

1
__init__.py Normal file
View File

@ -0,0 +1 @@
from . import wizard

19
__manifest__.py Normal file
View File

@ -0,0 +1,19 @@
{
'name': 'Product UOM Change',
'version': '18.0.1.0.0',
'summary': 'Allow changing Product UOM even if stock moves exist',
'description': """
This module adds a wizard to allow changing the Unit of Measure (UOM) of a product
even if it has existing stock moves, provided the new UOM is in the same category.
WARNING: This bypasses Odoo's standard validation. Use with caution.
""",
'category': 'Inventory',
'author': 'Suherdy Yacob',
'depends': ['stock', 'product'],
'data': [
'security/ir.model.access.csv',
'wizard/change_uom_views.xml',
],
'license': 'LGPL-3',
}

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_product_uom_change_wizard,product.uom.change.wizard,model_product_uom_change_wizard,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_product_uom_change_wizard product.uom.change.wizard model_product_uom_change_wizard base.group_user 1 1 1 1

1
wizard/__init__.py Normal file
View File

@ -0,0 +1 @@
from . import change_uom

44
wizard/change_uom.py Normal file
View File

@ -0,0 +1,44 @@
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class ProductUomChangeWizard(models.TransientModel):
_name = 'product.uom.change.wizard'
_description = 'Change Product UOM Wizard'
product_tmpl_id = fields.Many2one('product.template', string='Product', required=True, readonly=True)
current_uom_id = fields.Many2one('uom.uom', related='product_tmpl_id.uom_id', string='Current UOM', readonly=True)
uom_category_id = fields.Many2one('uom.category', related='product_tmpl_id.uom_id.category_id', readonly=True)
new_uom_id = fields.Many2one('uom.uom', string='New UOM', required=True)
new_uom_po_id = fields.Many2one('uom.uom', string='New Purchase UOM')
@api.model
def default_get(self, fields):
res = super(ProductUomChangeWizard, self).default_get(fields)
if self._context.get('active_model') == 'product.template' and self._context.get('active_id'):
res['product_tmpl_id'] = self._context['active_id']
elif self._context.get('active_model') == 'product.product' and self._context.get('active_id'):
product = self.env['product.product'].browse(self._context['active_id'])
res['product_tmpl_id'] = product.product_tmpl_id.id
return res
def action_change_uom(self):
self.ensure_one()
if self.new_uom_id.category_id != self.product_tmpl_id.uom_id.category_id:
raise UserError(_("New UOM must be in the same category as the current UOM."))
# Update UOM via SQL to bypass the constraint check in product.template write
self.env.cr.execute("UPDATE product_template SET uom_id = %s WHERE id = %s", (self.new_uom_id.id, self.product_tmpl_id.id))
# Also update Purchase UOM if specified
if self.new_uom_po_id:
if self.new_uom_po_id.category_id != self.product_tmpl_id.uom_po_id.category_id:
raise UserError(_("New Purchase UOM must be in the same category as the current Purchase UOM."))
self.env.cr.execute("UPDATE product_template SET uom_po_id = %s WHERE id = %s", (self.new_uom_po_id.id, self.product_tmpl_id.id))
# Invalidate cache to ensure the new value is read
self.product_tmpl_id.invalidate_recordset(['uom_id', 'uom_po_id'])
# Log note in chatter
self.product_tmpl_id.message_post(body=f"UOM forcefully changed from {self.current_uom_id.name} to {self.new_uom_id.name} via Wizard.")
return {'type': 'ir.actions.act_window_close'}

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_product_uom_change_wizard_form" model="ir.ui.view">
<field name="name">product.uom.change.wizard.form</field>
<field name="model">product.uom.change.wizard</field>
<field name="arch" type="xml">
<form string="Change Product UOM">
<group>
<field name="product_tmpl_id"/>
<field name="current_uom_id"/>
<field name="uom_category_id" invisible="1"/>
<field name="new_uom_id" domain="[('category_id', '=', uom_category_id), ('id', '!=', current_uom_id)]"/>
<field name="new_uom_po_id" domain="[('category_id', '=', uom_category_id)]"/>
</group>
<footer>
<button name="action_change_uom" string="Change UOM" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
</footer>
</form>
</field>
</record>
<record id="action_product_uom_change_wizard" model="ir.actions.act_window">
<field name="name">Change UOM</field>
<field name="res_model">product.uom.change.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="binding_model_id" ref="product.model_product_template"/>
<field name="binding_view_types">form</field>
<field name="groups_id" eval="[(4, ref('base.group_system'))]"/>
</record>
</odoo>