first commit

This commit is contained in:
Suherdy Yacob 2026-05-31 22:04:20 +07:00
commit ea9bf82a6c
11 changed files with 275 additions and 0 deletions

21
.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# Distribution / packaging
*.egg-info/
dist/
build/
# Virtual environments
.venv/
env/
# IDE
.idea/
.vscode/
*.swp
# Odoo
*.pot

59
README.md Normal file
View File

@ -0,0 +1,59 @@
Employee PIN Generator
======================
.. contents::
: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
--------
- Auto-generates a 6-digit numeric PIN when a new employee is created.
- PIN uniqueness is enforced at the database level (UNIQUE constraint on ``employee_pin``).
- No two employees — even in different companies — can share the same PIN.
- A **Regenerate PIN** button is available on the employee form (Private Information tab) for HR managers and users.
- A confirmation wizard is shown before replacing the current PIN, displaying the existing value.
Technical Details
-----------------
- Model extended: ``hr.employee``
- New field: ``employee_pin`` (Char, size 6, unique)
- New transient model: ``hr.employee.regenerate.pin.wizard``
- PIN generation retries up to 1,000 times to avoid collision in a crowded pool.
Installation
------------
1. Copy ``hr_employee_pin`` into your custom addons path.
2. Update the addons list in Odoo (Settings → Activate Developer Mode → Update App List).
3. Install **Employee PIN Generator** from the Apps menu.
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
-----
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:
1. Open an employee record.
2. Go to the **Private Information** tab.
3. Find the *Employee PIN* section and click **Regenerate PIN**.
4. Confirm in the dialog.
5. A success notification will show the new PIN.
Author
------
Suherdy Yacob
License
-------
LGPL-3

2
__init__.py Normal file
View File

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

21
__manifest__.py Normal file
View File

@ -0,0 +1,21 @@
{
'name': 'Employee PIN Generator',
'version': '19.0.1.0.0',
'category': 'Human Resources',
'summary': 'Randomly generate a unique 6-digit PIN for each employee',
'description': """
This module adds a unique 6-digit PIN to each employee record.
The PIN is globally unique across all companies.
A new PIN can be regenerated at any time via a button.
""",
'author': 'Suherdy Yacob',
'depends': ['hr'],
'data': [
'security/ir.model.access.csv',
'wizard/regenerate_pin_wizard_views.xml',
'views/hr_employee_views.xml',
],
'installable': True,
'application': False,
'license': 'LGPL-3',
}

1
models/__init__.py Normal file
View File

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

77
models/hr_employee.py Normal file
View File

@ -0,0 +1,77 @@
import random
import logging
from odoo import api, fields, models, _
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class HrEmployee(models.Model):
_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 = [
(
'employee_pin_unique',
'UNIQUE(employee_pin)',
'The Employee PIN must be unique across all employees and companies.',
)
]
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
@api.model
def _generate_unique_pin(self):
"""Return a random 6-digit string that is not yet used by any employee."""
existing = set(
self.sudo().search([('employee_pin', '!=', False)]).mapped('employee_pin')
)
for _attempt in range(1000):
pin = '{:06d}'.format(random.randint(0, 999999))
if pin not in existing:
return pin
raise UserError(
_('Could not generate a unique PIN after 1000 attempts. '
'The PIN pool may be exhausted.')
)
# ------------------------------------------------------------------
# ORM overrides
# ------------------------------------------------------------------
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if not vals.get('employee_pin'):
vals['employee_pin'] = self._generate_unique_pin()
return super().create(vals_list)
# ------------------------------------------------------------------
# Actions
# ------------------------------------------------------------------
def action_regenerate_pin(self):
"""Regenerate a new unique PIN for this employee (called from button)."""
self.ensure_one()
new_pin = self._generate_unique_pin()
self.employee_pin = new_pin
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('PIN Regenerated'),
'message': _('New PIN for %s: %s') % (self.name, new_pin),
'type': 'success',
'sticky': False,
},
}

View File

@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_employee_regenerate_pin_wizard_manager,hr.employee.regenerate.pin.wizard manager,model_hr_employee_regenerate_pin_wizard,hr.group_hr_manager,1,1,1,1
access_hr_employee_regenerate_pin_wizard_user,hr.employee.regenerate.pin.wizard user,model_hr_employee_regenerate_pin_wizard,hr.group_hr_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_hr_employee_regenerate_pin_wizard_manager hr.employee.regenerate.pin.wizard manager model_hr_employee_regenerate_pin_wizard hr.group_hr_manager 1 1 1 1
3 access_hr_employee_regenerate_pin_wizard_user hr.employee.regenerate.pin.wizard user model_hr_employee_regenerate_pin_wizard hr.group_hr_user 1 1 1 0

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Extend the Settings tab on hr.employee form -->
<record id="view_employee_pin_form" model="ir.ui.view">
<field name="name">hr.employee.pin.form</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
<!-- Insert employee PIN after the existing 'PIN Code' field
inside the Attendance/Point of Sale group (Settings tab). -->
<field name="pin" position="after">
<field name="employee_pin" string="Employee PIN (6-digit)" readonly="1"/>
<div>
<button name="%(hr_employee_pin.action_regenerate_pin_wizard)d"
string="Regenerate PIN"
type="action"
icon="fa-refresh"
class="btn-secondary"
context="{'default_employee_id': id}"/>
</div>
</field>
</field>
</record>
</odoo>

1
wizard/__init__.py Normal file
View File

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

View File

@ -0,0 +1,24 @@
from odoo import fields, models, _
class RegeneratePinWizard(models.TransientModel):
"""Confirmation wizard before regenerating the employee PIN."""
_name = 'hr.employee.regenerate.pin.wizard'
_description = 'Regenerate Employee PIN Wizard'
employee_id = fields.Many2one(
'hr.employee',
string='Employee',
required=True,
readonly=True,
)
current_pin = fields.Char(
string='Current PIN',
related='employee_id.employee_pin',
readonly=True,
)
def action_confirm_regenerate(self):
self.ensure_one()
return self.employee_id.action_regenerate_pin()

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Wizard form view -->
<record id="view_regenerate_pin_wizard_form" model="ir.ui.view">
<field name="name">hr.employee.regenerate.pin.wizard.form</field>
<field name="model">hr.employee.regenerate.pin.wizard</field>
<field name="arch" type="xml">
<form string="Regenerate Employee PIN">
<group>
<field name="employee_id" readonly="1"/>
<field name="current_pin" readonly="1"/>
</group>
<p class="text-muted">
A new random 6-digit PIN will be generated and assigned to this employee.
The old PIN will be permanently replaced.
</p>
<footer>
<button name="action_confirm_regenerate"
string="Regenerate PIN"
type="object"
class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<!-- Action to open wizard -->
<record id="action_regenerate_pin_wizard" model="ir.actions.act_window">
<field name="name">Regenerate PIN</field>
<field name="res_model">hr.employee.regenerate.pin.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="context">{'default_employee_id': active_id}</field>
</record>
</odoo>