diff --git a/addons_extensions/aspl_equipment_qrcode_generator/__init__.py b/addons_extensions/aspl_equipment_qrcode_generator/__init__.py new file mode 100644 index 000000000..e7843e8bf --- /dev/null +++ b/addons_extensions/aspl_equipment_qrcode_generator/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +from odoo import api, SUPERUSER_ID + +from . import models +from . import report +from . import wizard + + +# TODO: Generate Sequence For each company for Equipment +def pre_init_hook(env): + company_ids = env['res.company'].search([]) + for company_id in company_ids: + sequence_id = env['ir.sequence'].search( + [('name', '=', 'Equipment Company Sequence'), ('company_id', '=', company_id.id)]) + if not sequence_id: + env['ir.sequence'].create({ + 'name': 'Equipment Company Sequence', + 'prefix': company_id.id, + 'padding': 5, + 'number_increment': 1, + 'company_id': company_id.id + }) diff --git a/addons_extensions/aspl_equipment_qrcode_generator/__manifest__.py b/addons_extensions/aspl_equipment_qrcode_generator/__manifest__.py new file mode 100644 index 000000000..f2d08eb66 --- /dev/null +++ b/addons_extensions/aspl_equipment_qrcode_generator/__manifest__.py @@ -0,0 +1,25 @@ + +{ + "name": "QR Code on Equipment", + 'category': '', + "summary": "Add QR Code on equipment .", + 'license': 'LGPL-3', + "price": 00.00, + 'description': """ + The Equipment Management Module generates unique QR codes for each asset, offering instant details and direct Odoo profile access for seamless management. + """, + "author": "Raman Marikanti", + "depends": ['account','maintenance'], + "external_dependencies": { + 'python': ['qrcode'] + }, + "data": [ + 'views/maintenance_equipment.xml', + 'security/ir.model.access.csv', + 'report/custom_qrcode.xml', + 'wizard/equipment_label_layout_views.xml', + ], + 'pre_init_hook': 'pre_init_hook', + "application": True, + "installable": True, +} diff --git a/addons_extensions/aspl_equipment_qrcode_generator/models/__init__.py b/addons_extensions/aspl_equipment_qrcode_generator/models/__init__.py new file mode 100644 index 000000000..e176a441b --- /dev/null +++ b/addons_extensions/aspl_equipment_qrcode_generator/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import maintenance_equipment +from . import res_company diff --git a/addons_extensions/aspl_equipment_qrcode_generator/models/maintenance_equipment.py b/addons_extensions/aspl_equipment_qrcode_generator/models/maintenance_equipment.py new file mode 100644 index 000000000..4e7b2172c --- /dev/null +++ b/addons_extensions/aspl_equipment_qrcode_generator/models/maintenance_equipment.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields + + +class MaintenanceEquipment(models.Model): + _inherit = 'maintenance.equipment' + + qr_code = fields.Binary("QR Code") + comp_serial_no = fields.Char("Inventory Serial No", tracking=True) + serial_no = fields.Char('Mfg. Serial Number', copy=False) + + def action_print_qrcode_layout(self): + action = self.env['ir.actions.act_window']._for_xml_id('aspl_equipment_qrcode_generator.action_open_label_layout_equipment') + action['context'] = {'default_equipment_ids': self.ids} + return action + + def generate_serial_no(self): + for equipment_id in self: + if not equipment_id.comp_serial_no: + company_id = equipment_id.company_id.id + sequence_id = self.env['ir.sequence'].search( + [('name', '=', 'Equipment Company Sequence'), ('company_id', '=', company_id)]) + if sequence_id: + data = sequence_id._next() + equipment_id.write({ + 'comp_serial_no': data + }) diff --git a/addons_extensions/aspl_equipment_qrcode_generator/models/res_company.py b/addons_extensions/aspl_equipment_qrcode_generator/models/res_company.py new file mode 100644 index 000000000..5d23f92da --- /dev/null +++ b/addons_extensions/aspl_equipment_qrcode_generator/models/res_company.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +from odoo import models, api + + +class ResCompany(models.Model): + _inherit = 'res.company' + + @api.model_create_multi + def create(self, vals): + result = super(ResCompany, self).create(vals) + sequence_id = self.env['ir.sequence'].search( + [('name', '=', 'Equipment Company Sequence'), ('company_id', '=', result.id)]) + if not sequence_id: + self.env['ir.sequence'].create({ + 'name': 'Equipment Company Sequence', + 'prefix': result.id, + 'padding': 5, + 'number_increment': 1, + 'company_id': result.id + }) + return result diff --git a/addons_extensions/aspl_equipment_qrcode_generator/report/__init__.py b/addons_extensions/aspl_equipment_qrcode_generator/report/__init__.py new file mode 100644 index 000000000..d46550c1b --- /dev/null +++ b/addons_extensions/aspl_equipment_qrcode_generator/report/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import custom_qrcode_generator diff --git a/addons_extensions/aspl_equipment_qrcode_generator/report/custom_qrcode.xml b/addons_extensions/aspl_equipment_qrcode_generator/report/custom_qrcode.xml new file mode 100644 index 000000000..bdb3f1ebd --- /dev/null +++ b/addons_extensions/aspl_equipment_qrcode_generator/report/custom_qrcode.xml @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + A4 Label Sheet + + A4 + 0 + 0 + Portrait + 0 + 0 + 0 + 0 + + 96 + + + + Equipment QR-code (PDF) + maintenance.equipment + qweb-pdf + aspl_equipment_qrcode_generator.maintenance_quip + aspl_equipment_qrcode_generator.maintenance_quip + + 'Products Labels - %s' % (object.name) + + report + + + \ No newline at end of file diff --git a/addons_extensions/aspl_equipment_qrcode_generator/report/custom_qrcode_generator.py b/addons_extensions/aspl_equipment_qrcode_generator/report/custom_qrcode_generator.py new file mode 100644 index 000000000..3e83581fb --- /dev/null +++ b/addons_extensions/aspl_equipment_qrcode_generator/report/custom_qrcode_generator.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 +import math +from io import BytesIO + +import qrcode +from odoo import models + + +def generate_qr_code(value): + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=20, + border=4, + ) + qr.add_data(value) + qr.make(fit=True) + img = qr.make_image() + temp = BytesIO() + img.save(temp, format="PNG") + qr_img = base64.b64encode(temp.getvalue()) + return qr_img + + +def _prepare_data(env, data): + equipment_label_layout_id = env['equipment.label.layout'].browse(data['equipment_label_layout_id']) + equipment_dict = {} + equipment_ids = equipment_label_layout_id.equipment_ids + for equipment in equipment_ids: + if not equipment.name: + continue + equipment_dict[equipment] = 1 + combine_equipment_detail = "" + + # Generate Equipment Redirect LInk + url = env['ir.config_parameter'].sudo().get_param('web.base.url') + menuId = env.ref('maintenance.menu_equipment_form').sudo().id + actionId = env.ref('maintenance.hr_equipment_action').sudo().id + + equipment_link = url + '/web#id=' + str(equipment.id) + '&menu_id=' + str(menuId) + '&action=' + str( + actionId) + '&model=maintenance.equipment&view_type=form' + + # Prepare main Equipment Detail + main_equipment_detail = "" + main_equipment_detail = main_equipment_detail.join( + "Name: " + str(equipment.name) + "\n" + + "Model: " + str(equipment.model) + "\n" + + "Mfg serial no: " + str(equipment.serial_no) + "\n" + "Warranty Exp. Date: " +str(equipment.warranty_date) + "\n" + "Category: " +str(equipment.category_id.name)+ "\n" + "Contact No: "+str(equipment.technician_user_id.phone) +"\n" + "Contact Email: "+str(equipment.technician_user_id.login) + ) + # main_equipment_detail = equipment_link + '\n' + '\n' + main_equipment_detail + + # Prepare Child Equipment Detail + combine_equipment_detail = main_equipment_detail + + combine_equipment_detail += '\n' + '\n' + equipment_link + + # Generate Qr Code depends on Details + qr_image = generate_qr_code(combine_equipment_detail) + equipment.write({ + 'qr_code': qr_image + }) + env.cr.commit() + page_numbers = (len(equipment_ids) - 1) // (equipment_label_layout_id.rows * equipment_label_layout_id.columns) + 1 + + dict_equipment = { + 'rows': equipment_label_layout_id.rows, + 'columns': equipment_label_layout_id.columns, + 'page_numbers': page_numbers, + 'equipment_data': equipment_dict + } + return dict_equipment + + +class ReportProductTemplateLabel(models.AbstractModel): + _name = 'report.aspl_equipment_qrcode_generator.maintenance_quip' + _description = 'Equipment QR-code Report' + + def _get_report_values(self, docids, data): + return _prepare_data(self.env, data) diff --git a/addons_extensions/aspl_equipment_qrcode_generator/security/ir.model.access.csv b/addons_extensions/aspl_equipment_qrcode_generator/security/ir.model.access.csv new file mode 100644 index 000000000..c950bd953 --- /dev/null +++ b/addons_extensions/aspl_equipment_qrcode_generator/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_equipment_label_layout,access.equipment_label_layout,model_equipment_label_layout,,1,1,1,1 \ No newline at end of file diff --git a/addons_extensions/aspl_equipment_qrcode_generator/static/description/icon.png b/addons_extensions/aspl_equipment_qrcode_generator/static/description/icon.png new file mode 100644 index 000000000..8c9fe1923 Binary files /dev/null and b/addons_extensions/aspl_equipment_qrcode_generator/static/description/icon.png differ diff --git a/addons_extensions/aspl_equipment_qrcode_generator/views/maintenance_equipment.xml b/addons_extensions/aspl_equipment_qrcode_generator/views/maintenance_equipment.xml new file mode 100644 index 000000000..339680397 --- /dev/null +++ b/addons_extensions/aspl_equipment_qrcode_generator/views/maintenance_equipment.xml @@ -0,0 +1,56 @@ + + + + + maintenance.equipment.tree.inherit + maintenance.equipment + + + + +
+
+
+
+
+ + + maintenance.equipment.form.inherit + maintenance.equipment + + + + + + + + + +
+
+ +
+
+
+ + + Generate Serial Number + + + list + code + action = records.generate_serial_no() + + + + Print QR-Code + + + form + code + action = records.action_print_qrcode_layout() + + +
diff --git a/addons_extensions/aspl_equipment_qrcode_generator/wizard/__init__.py b/addons_extensions/aspl_equipment_qrcode_generator/wizard/__init__.py new file mode 100644 index 000000000..0ccb5a51f --- /dev/null +++ b/addons_extensions/aspl_equipment_qrcode_generator/wizard/__init__.py @@ -0,0 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import equipment_label_layout diff --git a/addons_extensions/aspl_equipment_qrcode_generator/wizard/equipment_label_layout.py b/addons_extensions/aspl_equipment_qrcode_generator/wizard/equipment_label_layout.py new file mode 100644 index 000000000..71598c803 --- /dev/null +++ b/addons_extensions/aspl_equipment_qrcode_generator/wizard/equipment_label_layout.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import _, api, fields, models + + +class EquipmentLabelLayout(models.TransientModel): + _name = 'equipment.label.layout' + _description = 'Choose the sheet layout to print the labels' + + print_format = fields.Selection([ + ('2x5', '2 x 5'), + ('2x7', '2 x 7'), + ('4x7', '4 x 7')], string="Format", default='2x5', required=True) + equipment_ids = fields.Many2many('maintenance.equipment') + rows = fields.Integer(compute='_compute_dimensions') + columns = fields.Integer(compute='_compute_dimensions') + + @api.depends('print_format') + def _compute_dimensions(self): + for wizard in self: + if 'x' in wizard.print_format: + columns, rows = wizard.print_format.split('x')[:2] + wizard.columns = int(columns) + wizard.rows = int(rows) + else: + wizard.columns, wizard.rows = 1, 1 + + + def process_label(self): + xml_id = 'aspl_equipment_qrcode_generator.report_equipment_label' + data = { + 'equipment_label_layout_id':self.id + } + + return self.env.ref(xml_id).report_action(None, data=data) \ No newline at end of file diff --git a/addons_extensions/aspl_equipment_qrcode_generator/wizard/equipment_label_layout_views.xml b/addons_extensions/aspl_equipment_qrcode_generator/wizard/equipment_label_layout_views.xml new file mode 100644 index 000000000..9e7dc5ca2 --- /dev/null +++ b/addons_extensions/aspl_equipment_qrcode_generator/wizard/equipment_label_layout_views.xml @@ -0,0 +1,30 @@ + + + + equipment.label.layout.form + equipment.label.layout + primary + +
+ + + + + +
+
+
+
+
+ + + Choose Labels Layout + equipment.label.layout + + new + +
diff --git a/addons_extensions/menu_control_center/__init__.py b/addons_extensions/menu_control_center/__init__.py new file mode 100644 index 000000000..9a7e03ede --- /dev/null +++ b/addons_extensions/menu_control_center/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/addons_extensions/menu_control_center/__manifest__.py b/addons_extensions/menu_control_center/__manifest__.py new file mode 100644 index 000000000..385729848 --- /dev/null +++ b/addons_extensions/menu_control_center/__manifest__.py @@ -0,0 +1,28 @@ +{ + 'name': 'Menu Access Control', + 'version': '1.0', + 'summary': 'Control menu visibility based on users or companies', + 'description': """ + This module allows administrators to configure menu visibility + based on selected users or companies. Active parent menus can + be generated and assigned for access control. + """, + 'category': 'Tools', + 'author': 'PRANAY', + 'website': 'https://ftprotech.in', + 'depends': ['base','hr'], + 'data': [ + 'security/ir.model.access.csv', + 'data/data.xml', + 'views/menu_access_control_views.xml', + ], + # 'assets': { + # 'web.assets_backend': [ + # 'menu_control_center/static/src/js/menu_service.js', + # ], + # }, + 'installable': True, + 'application': True, + 'auto_install': False, + 'license': 'LGPL-3', +} diff --git a/addons_extensions/menu_control_center/data/data.xml b/addons_extensions/menu_control_center/data/data.xml new file mode 100644 index 000000000..4cc52ea05 --- /dev/null +++ b/addons_extensions/menu_control_center/data/data.xml @@ -0,0 +1,28 @@ + + + + + Human Resources + + + + Administration + + + + Development + + + + Quality Assurance + + + IT Support + + + + FINANCE + + + + \ No newline at end of file diff --git a/addons_extensions/menu_control_center/models/__init__.py b/addons_extensions/menu_control_center/models/__init__.py new file mode 100644 index 000000000..52e35f001 --- /dev/null +++ b/addons_extensions/menu_control_center/models/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import menu \ No newline at end of file diff --git a/addons_extensions/menu_control_center/models/menu.py b/addons_extensions/menu_control_center/models/menu.py new file mode 100644 index 000000000..c82f101c0 --- /dev/null +++ b/addons_extensions/menu_control_center/models/menu.py @@ -0,0 +1,67 @@ +from odoo import models, fields, api, tools, _ +from collections import defaultdict + + +class IrUiMenu(models.Model): + _inherit = 'ir.ui.menu' + + @api.model + @tools.ormcache('frozenset(self.env.user.groups_id.ids)', 'debug') + def _visible_menu_ids(self, debug=False): + """ Return the ids of the menu items visible to the user. """ + # retrieve all menus, and determine which ones are visible + context = {'ir.ui.menu.full_list': True} + menus = self.with_context(context).search_fetch([], ['action', 'parent_id']).sudo() + + # first discard all menus with groups the user does not have + + group_ids = set(self.env.user._get_group_ids()) + if not debug: + hide_menus_list = self.env['menu.access.control'].sudo().search([('user_ids','ilike',self.env.user.id)]).access_menu_line_ids.filtered(lambda menu: not(menu.is_main_menu)).menu_id.ids + + menus = menus.filtered(lambda menu: (menu.id not in hide_menus_list)) + group_ids = group_ids - { + self.env['ir.model.data']._xmlid_to_res_id('base.group_no_one', raise_if_not_found=False)} + menus = menus.filtered( + lambda menu: not (menu.groups_id and group_ids.isdisjoint(menu.groups_id._ids))) + + # take apart menus that have an action + actions_by_model = defaultdict(set) + for action in menus.mapped('action'): + if action: + actions_by_model[action._name].add(action.id) + existing_actions = { + action + for model_name, action_ids in actions_by_model.items() + for action in self.env[model_name].browse(action_ids).exists() + } + action_menus = menus.filtered(lambda m: m.action and m.action in existing_actions) + folder_menus = menus - action_menus + visible = self.browse() + + # process action menus, check whether their action is allowed + access = self.env['ir.model.access'] + MODEL_BY_TYPE = { + 'ir.actions.act_window': 'res_model', + 'ir.actions.report': 'model', + 'ir.actions.server': 'model_name', + } + + # performance trick: determine the ids to prefetch by type + prefetch_ids = defaultdict(list) + for action in action_menus.mapped('action'): + prefetch_ids[action._name].append(action.id) + + for menu in action_menus: + action = menu.action + action = action.with_prefetch(prefetch_ids[action._name]) + model_name = action._name in MODEL_BY_TYPE and action[MODEL_BY_TYPE[action._name]] + if not model_name or access.check(model_name, 'read', False): + # make menu visible, and its folder ancestors, too + visible += menu + menu = menu.parent_id + while menu and menu in folder_menus and menu not in visible: + visible += menu + menu = menu.parent_id + + return set(visible.ids) \ No newline at end of file diff --git a/addons_extensions/menu_control_center/models/models.py b/addons_extensions/menu_control_center/models/models.py new file mode 100644 index 000000000..a1f11067e --- /dev/null +++ b/addons_extensions/menu_control_center/models/models.py @@ -0,0 +1,65 @@ +# models/models.py +from odoo import models, fields, api, tools, _ +from collections import defaultdict + +class MenuControlUnits(models.Model): + _name = 'menu.control.units' + _rec_name = 'unit_name' + + _sql_constraints = [ + ('unique_unit_name', 'UNIQUE(unit_name)', "'Unit Name' already defined. Please don't confuse me 😤.") + ] + + unit_name = fields.Char(string='Unit Name',required=True) + department_ids = fields.Many2many('hr.department') + + user_ids = fields.Many2many('res.users') + + def generate_department_user_ids(self): + for rec in self: + user_ids = self.env['hr.employee'].sudo().search([('department_id','in',rec.department_ids.ids)]).user_id.ids + self.write({ + 'user_ids': [(6, 0, user_ids)] + }) + +class MenuAccessControl(models.Model): + _name = 'menu.access.control' + _description = 'Menu Access Control' + _rec_name = 'control_unit' + + _sql_constraints = [ + ('unique_control_unit', 'UNIQUE(control_unit)', "Only one service can exist with a specific control_unit. Please don't confuse me 🤪.") + ] + + control_unit = fields.Many2one('menu.control.units',required=True) + user_ids = fields.Many2many('res.users', string="Users", related='control_unit.user_ids') + + + access_menu_line_ids = fields.One2many( + 'menu.access.line', 'access_control_id', + string="Accessible Menus" + ) + + def action_generate_menus(self): + """Button to fetch active top-level menus and populate access lines.""" + menu_lines = [] + active_menus = self.env['ir.ui.menu'].search([ + ('parent_id', '=', False), # top-level menus + ('active', '=', True) + ]) + for menu in active_menus: + menu_lines.append((0, 0, { + 'menu_id': menu.id, + 'is_main_menu': True + })) + self.access_menu_line_ids = menu_lines + + +class MenuAccessLine(models.Model): + _name = 'menu.access.line' + _description = 'Menu Access Line' + _rec_name = 'menu_id' + + access_control_id = fields.Many2one('menu.access.control', ondelete='cascade') + menu_id = fields.Many2one('ir.ui.menu', string="Menu") + is_main_menu = fields.Boolean(string="Is Main Menu", default=True) diff --git a/addons_extensions/menu_control_center/security/ir.model.access.csv b/addons_extensions/menu_control_center/security/ir.model.access.csv new file mode 100644 index 000000000..d7edab9d1 --- /dev/null +++ b/addons_extensions/menu_control_center/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_menu_access_control,access.menu.access.control,model_menu_access_control,hr.group_hr_manager,1,1,1,1 +access_menu_access_line,access.menu.access.line,model_menu_access_line,hr.group_hr_manager,1,1,1,1 +access_menu_control_units,access.menu.control.units,model_menu_control_units,hr.group_hr_manager,1,1,1,1 \ No newline at end of file diff --git a/addons_extensions/menu_control_center/static/src/js/menu_service.js b/addons_extensions/menu_control_center/static/src/js/menu_service.js new file mode 100644 index 000000000..fb647a3ec --- /dev/null +++ b/addons_extensions/menu_control_center/static/src/js/menu_service.js @@ -0,0 +1,44 @@ +///** @odoo-module **/ +// +//import { browser } from "@web/core/browser/browser"; +//import { makeEnv, startServices } from "@web/env"; +//import { session } from "@web/session"; +//import { _t } from "@web/core/l10n/translation"; +//import { browser } from "@web/core/browser/browser"; +//import { MenuService } from "@web/services/menu_service"; +// +//export const menuService = { +// dependencies: MenuService.dependencies, +// start(env, deps) { +// const menu = super.start(env, deps); +// +// // Override the getApps method +// const originalGetApps = menu.getApps; +// menu.getApps = async function() { +// // Get the original apps +// let apps = await originalGetApps.call(this); +// +// // Fetch your custom menu access data +// const accessData = await this.orm.call( +// 'menu.access.control', +// 'get_user_access_menus', +// [] +// ); +// +// // If access data exists, filter the apps +// if (accessData && accessData.allowed_menu_ids) { +// apps = apps.filter(app => +// accessData.allowed_menu_ids.includes(app.id) +// ); +// } +// +// return apps; +// }; +// +// return menu; +// }, +//}; +// +//// Register the service +//import { registry } from "@web/core/registry"; +//registry.category("services").add("menu", menuService, { force: true }); \ No newline at end of file diff --git a/addons_extensions/menu_control_center/views/menu_access_control_views.xml b/addons_extensions/menu_control_center/views/menu_access_control_views.xml new file mode 100644 index 000000000..7dd71421d --- /dev/null +++ b/addons_extensions/menu_control_center/views/menu_access_control_views.xml @@ -0,0 +1,100 @@ + + + + menu.control.units.list + menu.control.units + + + + + + + + + + menu.control.units.form + menu.control.units + +
+ + + + +