first commit
This commit is contained in:
commit
ea9bf82a6c
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal 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
59
README.md
Normal 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
2
__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import wizard
|
||||
21
__manifest__.py
Normal file
21
__manifest__.py
Normal 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
1
models/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import hr_employee
|
||||
77
models/hr_employee.py
Normal file
77
models/hr_employee.py
Normal 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,
|
||||
},
|
||||
}
|
||||
3
security/ir.model.access.csv
Normal file
3
security/ir.model.access.csv
Normal 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
|
||||
|
28
views/hr_employee_views.xml
Normal file
28
views/hr_employee_views.xml
Normal 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
1
wizard/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import regenerate_pin_wizard
|
||||
24
wizard/regenerate_pin_wizard.py
Normal file
24
wizard/regenerate_pin_wizard.py
Normal 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()
|
||||
38
wizard/regenerate_pin_wizard_views.xml
Normal file
38
wizard/regenerate_pin_wizard_views.xml
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user