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