refactor: replace custom employee_pin field with Odoo's standard pin field and update UI for inline regeneration

This commit is contained in:
Suherdy Yacob 2026-05-31 22:24:55 +07:00
parent ea9bf82a6c
commit 406f353b98
4 changed files with 29 additions and 48 deletions

View File

@ -1,52 +1,41 @@
Employee PIN Generator Employee PIN Generator
====================== ======================
.. contents:: This Odoo 19 module automatically assigns a unique, randomly generated 6-digit PIN to every employee record using Odoo's standard PIN Code field. The PIN is enforced to be globally unique across all companies in the database via a SQL UNIQUE constraint.
:local:
:depth: 1
Overview
--------
This Odoo 19 module automatically assigns a unique, randomly generated 6-digit PIN to every employee record. The PIN is enforced to be globally unique across **all companies** in the same Odoo database via a SQL UNIQUE constraint.
Features Features
-------- --------
- Auto-generates a 6-digit numeric PIN when a new employee is created. - Auto-generates a unique 6-digit numeric PIN when a new employee is created.
- PIN uniqueness is enforced at the database level (UNIQUE constraint on ``employee_pin``). - Reuses Odoo's standard PIN Code field (used in Point of Sale and HR Attendance).
- No two employees — even in different companies — can share the same PIN. - PIN uniqueness is enforced at the database level across all companies.
- A **Regenerate PIN** button is available on the employee form (Private Information tab) for HR managers and users. - A Regenerate PIN button is available inline with the PIN Code field on the employee form.
- A confirmation wizard is shown before replacing the current PIN, displaying the existing value. - A confirmation wizard is shown before replacing the current PIN.
Technical Details Technical Details
----------------- -----------------
- Model extended: ``hr.employee`` - Model extended: hr.employee
- New field: ``employee_pin`` (Char, size 6, unique) - Field modified: pin (Odoo's standard PIN Code)
- New transient model: ``hr.employee.regenerate.pin.wizard`` - New transient model: hr.employee.regenerate.pin.wizard
- PIN generation retries up to 1,000 times to avoid collision in a crowded pool. - PIN generation retries up to 1,000 times to avoid collision.
Installation Installation
------------ ------------
1. Copy ``hr_employee_pin`` into your custom addons path. 1. Copy hr_employee_pin into your custom addons path.
2. Update the addons list in Odoo (Settings → Activate Developer Mode → Update App List). 2. Update the addons list in Odoo.
3. Install **Employee PIN Generator** from the Apps menu. 3. Install Employee PIN Generator.
4. Existing employees will NOT receive a PIN automatically — use the **Regenerate PIN** button on each record, or run a one-time migration script.
Usage Usage
----- -----
When creating a new employee the PIN is generated automatically and shown in the **Private Information** tab under the *Employee PIN* section.
To regenerate the PIN: To regenerate the PIN:
1. Open an employee record. 1. Open an employee record.
2. Go to the **Private Information** tab. 2. Go to the Settings tab under the Attendance/Point of Sale section.
3. Find the *Employee PIN* section and click **Regenerate PIN**. 3. Click the Regenerate PIN button inline with the PIN Code field.
4. Confirm in the dialog. 4. Confirm in the dialog.
5. A success notification will show the new PIN.
Author Author
------ ------

View File

@ -10,19 +10,11 @@ _logger = logging.getLogger(__name__)
class HrEmployee(models.Model): class HrEmployee(models.Model):
_inherit = 'hr.employee' _inherit = 'hr.employee'
employee_pin = fields.Char(
string='Employee PIN',
size=6,
copy=False,
help='Unique 6-digit PIN for this employee. '
'Globally unique across all companies.',
)
_sql_constraints = [ _sql_constraints = [
( (
'employee_pin_unique', 'pin_unique',
'UNIQUE(employee_pin)', 'UNIQUE(pin)',
'The Employee PIN must be unique across all employees and companies.', 'The Employee PIN Code must be unique across all employees and companies.',
) )
] ]
@ -34,7 +26,7 @@ class HrEmployee(models.Model):
def _generate_unique_pin(self): def _generate_unique_pin(self):
"""Return a random 6-digit string that is not yet used by any employee.""" """Return a random 6-digit string that is not yet used by any employee."""
existing = set( existing = set(
self.sudo().search([('employee_pin', '!=', False)]).mapped('employee_pin') self.sudo().search([('pin', '!=', False)]).mapped('pin')
) )
for _attempt in range(1000): for _attempt in range(1000):
pin = '{:06d}'.format(random.randint(0, 999999)) pin = '{:06d}'.format(random.randint(0, 999999))
@ -52,8 +44,8 @@ class HrEmployee(models.Model):
@api.model_create_multi @api.model_create_multi
def create(self, vals_list): def create(self, vals_list):
for vals in vals_list: for vals in vals_list:
if not vals.get('employee_pin'): if not vals.get('pin'):
vals['employee_pin'] = self._generate_unique_pin() vals['pin'] = self._generate_unique_pin()
return super().create(vals_list) return super().create(vals_list)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@ -64,7 +56,7 @@ class HrEmployee(models.Model):
"""Regenerate a new unique PIN for this employee (called from button).""" """Regenerate a new unique PIN for this employee (called from button)."""
self.ensure_one() self.ensure_one()
new_pin = self._generate_unique_pin() new_pin = self._generate_unique_pin()
self.employee_pin = new_pin self.pin = new_pin
return { return {
'type': 'ir.actions.client', 'type': 'ir.actions.client',
'tag': 'display_notification', 'tag': 'display_notification',

View File

@ -8,16 +8,16 @@
<field name="inherit_id" ref="hr.view_employee_form"/> <field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<!-- Insert employee PIN after the existing 'PIN Code' field <!-- Replace the standard PIN field to make it readonly and add a Regenerate button inline -->
inside the Attendance/Point of Sale group (Settings tab). --> <field name="pin" position="replace">
<field name="pin" position="after"> <label for="pin" string="PIN Code"/>
<field name="employee_pin" string="Employee PIN (6-digit)" readonly="1"/> <div class="o_row">
<div> <field name="pin" readonly="1" class="oe_inline"/>
<button name="%(hr_employee_pin.action_regenerate_pin_wizard)d" <button name="%(hr_employee_pin.action_regenerate_pin_wizard)d"
string="Regenerate PIN" string="Regenerate PIN"
type="action" type="action"
icon="fa-refresh" icon="fa-refresh"
class="btn-secondary" class="btn btn-link"
context="{'default_employee_id': id}"/> context="{'default_employee_id': id}"/>
</div> </div>
</field> </field>

View File

@ -15,7 +15,7 @@ class RegeneratePinWizard(models.TransientModel):
) )
current_pin = fields.Char( current_pin = fields.Char(
string='Current PIN', string='Current PIN',
related='employee_id.employee_pin', related='employee_id.pin',
readonly=True, readonly=True,
) )