From ea9bf82a6cc6ef58b10785014b99df16605e8b39 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Sun, 31 May 2026 22:04:20 +0700 Subject: [PATCH] first commit --- .gitignore | 21 +++++++ README.md | 59 ++++++++++++++++++++ __init__.py | 2 + __manifest__.py | 21 +++++++ models/__init__.py | 1 + models/hr_employee.py | 77 ++++++++++++++++++++++++++ security/ir.model.access.csv | 3 + views/hr_employee_views.xml | 28 ++++++++++ wizard/__init__.py | 1 + wizard/regenerate_pin_wizard.py | 24 ++++++++ wizard/regenerate_pin_wizard_views.xml | 38 +++++++++++++ 11 files changed, 275 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 __init__.py create mode 100644 __manifest__.py create mode 100644 models/__init__.py create mode 100644 models/hr_employee.py create mode 100644 security/ir.model.access.csv create mode 100644 views/hr_employee_views.xml create mode 100644 wizard/__init__.py create mode 100644 wizard/regenerate_pin_wizard.py create mode 100644 wizard/regenerate_pin_wizard_views.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c51ee02 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..a96261d --- /dev/null +++ b/README.md @@ -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 diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..9b42961 --- /dev/null +++ b/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..b51ecab --- /dev/null +++ b/__manifest__.py @@ -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', +} diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..e11a62f --- /dev/null +++ b/models/__init__.py @@ -0,0 +1 @@ +from . import hr_employee diff --git a/models/hr_employee.py b/models/hr_employee.py new file mode 100644 index 0000000..82baba9 --- /dev/null +++ b/models/hr_employee.py @@ -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, + }, + } diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv new file mode 100644 index 0000000..a754c10 --- /dev/null +++ b/security/ir.model.access.csv @@ -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 diff --git a/views/hr_employee_views.xml b/views/hr_employee_views.xml new file mode 100644 index 0000000..26554a1 --- /dev/null +++ b/views/hr_employee_views.xml @@ -0,0 +1,28 @@ + + + + + + hr.employee.pin.form + hr.employee + + + + + + +
+
+
+ +
+
+ +
diff --git a/wizard/__init__.py b/wizard/__init__.py new file mode 100644 index 0000000..a899670 --- /dev/null +++ b/wizard/__init__.py @@ -0,0 +1 @@ +from . import regenerate_pin_wizard diff --git a/wizard/regenerate_pin_wizard.py b/wizard/regenerate_pin_wizard.py new file mode 100644 index 0000000..d299bdc --- /dev/null +++ b/wizard/regenerate_pin_wizard.py @@ -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() diff --git a/wizard/regenerate_pin_wizard_views.xml b/wizard/regenerate_pin_wizard_views.xml new file mode 100644 index 0000000..5a6bb65 --- /dev/null +++ b/wizard/regenerate_pin_wizard_views.xml @@ -0,0 +1,38 @@ + + + + + + hr.employee.regenerate.pin.wizard.form + hr.employee.regenerate.pin.wizard + +
+ + + + +

+ A new random 6-digit PIN will be generated and assigned to this employee. + The old PIN will be permanently replaced. +

+
+
+
+
+
+ + + + Regenerate PIN + hr.employee.regenerate.pin.wizard + form + new + {'default_employee_id': active_id} + + +