diff --git a/bemade_add_follower_no_sendmail_default/__init__.py b/bemade_add_follower_no_sendmail_default/__init__.py
new file mode 100644
index 0000000..02565c9
--- /dev/null
+++ b/bemade_add_follower_no_sendmail_default/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+from . import models
+
+
+
diff --git a/bemade_add_follower_no_sendmail_default/__manifest__.py b/bemade_add_follower_no_sendmail_default/__manifest__.py
new file mode 100644
index 0000000..9f654d9
--- /dev/null
+++ b/bemade_add_follower_no_sendmail_default/__manifest__.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+{
+ "name": "Change default when adding follower",
+ "version": "15.0.0.0.1",
+ "category": "Extra Tools",
+ 'summary': 'Change default when adding follower',
+ "description": """
+ Change default when adding follower
+ Send mail false by default
+ """,
+ "author": "Bemade",
+ 'website': 'https://www.bemade.org',
+ "depends": [
+ 'mail',
+ ],
+ "data": [
+ ],
+ "auto_install": False,
+ "installable": True,
+ 'license': 'OPL-1'
+}
diff --git a/bemade_add_follower_no_sendmail_default/models/__init__.py b/bemade_add_follower_no_sendmail_default/models/__init__.py
new file mode 100644
index 0000000..cd332dd
--- /dev/null
+++ b/bemade_add_follower_no_sendmail_default/models/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+from . import mail_wizard_invite
diff --git a/bemade_add_follower_no_sendmail_default/models/mail_wizard_invite.py b/bemade_add_follower_no_sendmail_default/models/mail_wizard_invite.py
new file mode 100644
index 0000000..986fde3
--- /dev/null
+++ b/bemade_add_follower_no_sendmail_default/models/mail_wizard_invite.py
@@ -0,0 +1,8 @@
+# Copyright Bemade.org
+from odoo import models, fields, api
+
+
+class MailWizardInviteDefault(models.TransientModel):
+ _inherit = 'mail.wizard.invite'
+
+ send_mail = fields.Boolean(default=False, help="If true, an invitation email will be sent to the recipient")
diff --git a/bemade_attachments_cleanup/__init__.py b/bemade_attachments_cleanup/__init__.py
new file mode 100644
index 0000000..4027237
--- /dev/null
+++ b/bemade_attachments_cleanup/__init__.py
@@ -0,0 +1 @@
+from . import wizard
diff --git a/bemade_attachments_cleanup/__manifest__.py b/bemade_attachments_cleanup/__manifest__.py
new file mode 100644
index 0000000..2d18b49
--- /dev/null
+++ b/bemade_attachments_cleanup/__manifest__.py
@@ -0,0 +1,22 @@
+{
+ 'name': "Attachment Cleanup",
+ 'version': '1.0.1',
+ 'author': "Bemade",
+ 'website': "http://www.bemade.org",
+ 'category': 'Tools',
+ 'license': 'OPL-1',
+ 'summary': "Find and delete missing attachments",
+ 'description': """
+ This module adds a wizard in the settings menu to find and delete missing attachments.
+ The wizard lists all attachments that are not found in the filestore, and allows the user to select and delete them.
+ Please use this tool with caution, as deleting attachments cannot be undone.
+ """,
+ 'depends': ['base'],
+ 'data': [
+ 'wizard/attachment_cleanup_wizard_view.xml',
+ 'security/ir.model.access.csv',
+
+ ],
+ 'installable': True,
+ 'application': False,
+}
diff --git a/bemade_attachments_cleanup/i18n/fr_CA.po b/bemade_attachments_cleanup/i18n/fr_CA.po
new file mode 100644
index 0000000..5b3b39b
--- /dev/null
+++ b/bemade_attachments_cleanup/i18n/fr_CA.po
@@ -0,0 +1,37 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * attachment_cleanup
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 15.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2023-06-08 12:00+0000\n"
+"PO-Revision-Date: 2023-06-08 12:00+0000\n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"Language: fr_CA\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: \n"
+
+#. module: attachment_cleanup
+#: model:ir.actions.act_window,name:attachment_cleanup.action_open_attachment_cleanup_wizard
+msgid "Cleanup Attachments"
+msgstr "Nettoyer les pièces jointes"
+
+#. module: attachment_cleanup
+#: model:ir.ui.view,arch_db:attachment_cleanup.view_attachment_cleanup_wizard
+msgid "Delete"
+msgstr "Supprimer"
+
+#. module: attachment_cleanup
+#: model:ir.ui.view,arch_db:attachment_cleanup.view_attachment_cleanup_wizard
+msgid "Cancel"
+msgstr "Annuler"
+
+#. module: attachment_cleanup
+#: model:ir.model.fields,field_description:attachment_cleanup.field_attachment_cleanup_wizard_attachment_ids
+msgid "Missing Attachments"
+msgstr "Pièces jointes manquantes"
diff --git a/bemade_attachments_cleanup/security/ir.model.access.csv b/bemade_attachments_cleanup/security/ir.model.access.csv
new file mode 100644
index 0000000..735f299
--- /dev/null
+++ b/bemade_attachments_cleanup/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_attachment_cleanup_wizard,access.attachment.cleanup.wizard,model_attachment_cleanup_wizard,base.group_system,1,1,1,1
diff --git a/bemade_attachments_cleanup/wizard/__init__.py b/bemade_attachments_cleanup/wizard/__init__.py
new file mode 100644
index 0000000..c5856e0
--- /dev/null
+++ b/bemade_attachments_cleanup/wizard/__init__.py
@@ -0,0 +1 @@
+from . import attachment_cleanup_wizard
diff --git a/bemade_attachments_cleanup/wizard/attachment_cleanup_wizard.py b/bemade_attachments_cleanup/wizard/attachment_cleanup_wizard.py
new file mode 100644
index 0000000..f2446a3
--- /dev/null
+++ b/bemade_attachments_cleanup/wizard/attachment_cleanup_wizard.py
@@ -0,0 +1,45 @@
+from odoo import api, fields, models
+import os
+
+
+class AttachmentCleanupWizard(models.TransientModel):
+ _name = 'attachment.cleanup.wizard'
+ _description = 'Attachment Cleanup Wizard'
+
+ attachment_ids = fields.Many2many(
+ 'ir.attachment',
+ string='Missing Attachments',
+ readonly=True,
+ help='Attachments that are not found in the filestore'
+ )
+
+ @api.model
+ def default_get(self, fields_list):
+ res = super(AttachmentCleanupWizard, self).default_get(fields_list)
+
+ attachments_1 = self.sudo().env['ir.attachment'].search([])
+
+ self._cr.execute("SELECT id FROM ir_attachment WHERE company_id = %s", (self.env.company.id,))
+
+ attachments = self._cr.fetchall()
+
+ filestore_location = self.env['ir.attachment']._filestore()
+
+ missing_attachments = []
+ for attachment_id in attachments:
+ # if attachment.id == 47984:
+ # print(attachment.store_fname)
+ attachment = self.env['ir.attachment'].browse(attachment_id)
+
+ if attachment.store_fname:
+ file_path = os.path.join(filestore_location, attachment.store_fname)
+ if not os.path.exists(file_path):
+ missing_attachments.append(attachment.id)
+
+ if missing_attachments:
+ res.update({'attachment_ids': [(6, 0, missing_attachments)]})
+ return res
+
+ def action_cleanup_attachments(self):
+ self.ensure_one()
+ self.attachment_ids.unlink()
diff --git a/bemade_attachments_cleanup/wizard/attachment_cleanup_wizard_view.xml b/bemade_attachments_cleanup/wizard/attachment_cleanup_wizard_view.xml
new file mode 100644
index 0000000..b652785
--- /dev/null
+++ b/bemade_attachments_cleanup/wizard/attachment_cleanup_wizard_view.xml
@@ -0,0 +1,32 @@
+
+
+ attachment.cleanup.wizard.view
+ attachment.cleanup.wizard
+
+
+
+
+
+
+ Cleanup Attachments
+ attachment.cleanup.wizard
+ form
+
+ new
+
+
+
+
diff --git a/bemade_fetchmail_only_production/__init__.py b/bemade_fetchmail_only_production/__init__.py
new file mode 100644
index 0000000..017bbdb
--- /dev/null
+++ b/bemade_fetchmail_only_production/__init__.py
@@ -0,0 +1,4 @@
+# Copyright (C) 2023 Bemade.org
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from . import models
\ No newline at end of file
diff --git a/bemade_fetchmail_only_production/__manifest__.py b/bemade_fetchmail_only_production/__manifest__.py
new file mode 100644
index 0000000..200ad44
--- /dev/null
+++ b/bemade_fetchmail_only_production/__manifest__.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+{
+ "name": "Fetchmail Only on production environment",
+ "version": "15.0.0.0.1",
+ "category": "Extra Tools",
+ 'summary': 'Fetchmail Only on production environment',
+ "description": """
+ Fetchmail Only on production environment
+ """,
+ "author": "Bemade",
+ 'website': 'https://www.bemade.org',
+ "depends": [
+ 'fetchmail',
+ ],
+ "data": [
+ ],
+ "auto_install": True,
+ "installable": True,
+ 'license': 'OPL-1'
+}
diff --git a/bemade_fetchmail_only_production/models/__init__.py b/bemade_fetchmail_only_production/models/__init__.py
new file mode 100644
index 0000000..1286927
--- /dev/null
+++ b/bemade_fetchmail_only_production/models/__init__.py
@@ -0,0 +1,4 @@
+# Copyright (C) 2023 Bemade.org
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from . import fetchmail_server
\ No newline at end of file
diff --git a/bemade_fetchmail_only_production/models/fetchmail_server.py b/bemade_fetchmail_only_production/models/fetchmail_server.py
new file mode 100644
index 0000000..c14ee9d
--- /dev/null
+++ b/bemade_fetchmail_only_production/models/fetchmail_server.py
@@ -0,0 +1,23 @@
+# Copyright (C) 2023 Bemade.org
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+import logging
+
+# Import the required classes and decorators from Odoo
+from odoo import api, models
+from urllib.parse import urlparse
+
+_logger = logging.getLogger(__name__)
+
+class fetchmail_server(models.Model):
+ _inherit = 'fetchmail.server'
+
+ @api.model
+ def fetch_mail(self):
+ if urlparse(self.env['ir.config_parameter'].sudo().get_param('web.base.url')).netloc == \
+ urlparse('https://erp.durpro.com/').netloc:
+ return super(fetchmail_server, self).fetch_mail()
+ else:
+ # Add log message
+ _logger.info("Trying to fetch email, current URL don't match with production URL, so we don't fetch email")
+ return True
diff --git a/bemade_hide_decimal_on_unit/__init__.py b/bemade_hide_decimal_on_unit/__init__.py
new file mode 100644
index 0000000..b4dcd09
--- /dev/null
+++ b/bemade_hide_decimal_on_unit/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# from . import models
+
+
+
diff --git a/bemade_hide_decimal_on_unit/__manifest__.py b/bemade_hide_decimal_on_unit/__manifest__.py
new file mode 100644
index 0000000..d0bc66e
--- /dev/null
+++ b/bemade_hide_decimal_on_unit/__manifest__.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+{
+ "name": "Hide Decimal on unit",
+ "version": "15.0.0.1.1",
+ "category": "Extra Tools",
+ 'summary': 'Hide decimal on Qty when there is no decimal',
+ "description": """
+ Hide decimal on Qty when there is no decimal
+ """,
+ "author": "Bemade",
+ 'website': 'https://www.bemade.org',
+ "depends": [
+ 'sale',
+ 'purchase'
+ ],
+ "data": [
+ 'views/sale.xml',
+ 'views/purchase.xml'
+ ],
+ "auto_install": False,
+ "installable": True,
+ 'license': 'OPL-1'
+}
diff --git a/bemade_hide_decimal_on_unit/views/purchase.xml b/bemade_hide_decimal_on_unit/views/purchase.xml
new file mode 100644
index 0000000..313c6e3
--- /dev/null
+++ b/bemade_hide_decimal_on_unit/views/purchase.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
diff --git a/bemade_hide_decimal_on_unit/views/sale.xml b/bemade_hide_decimal_on_unit/views/sale.xml
new file mode 100644
index 0000000..f1fc7d5
--- /dev/null
+++ b/bemade_hide_decimal_on_unit/views/sale.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ |
+
+
+
+
+
+
+
+ |
+
+
+
\ No newline at end of file
diff --git a/bemade_reordering_rules_chatter/__init__.py b/bemade_reordering_rules_chatter/__init__.py
new file mode 100644
index 0000000..02565c9
--- /dev/null
+++ b/bemade_reordering_rules_chatter/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+from . import models
+
+
+
diff --git a/bemade_reordering_rules_chatter/__manifest__.py b/bemade_reordering_rules_chatter/__manifest__.py
new file mode 100644
index 0000000..ef89fa1
--- /dev/null
+++ b/bemade_reordering_rules_chatter/__manifest__.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+{
+ "name": "Chatter on the Reordering Rules",
+ "version": "15.0.0.0.1",
+ "category": "Extra Tools",
+ 'license': 'GPL-3',
+ 'summary': 'Add chatter on the reordering rules',
+ "description": """
+ Add chatter on the reordering rules
+ """,
+ "author": "Bemade",
+ 'website': 'https://www.bemade.org',
+ "depends": [
+ 'stock',
+ ],
+ "data": [
+ 'views/stock_warehouse_orderpoint.xml',
+ ],
+ "auto_install": False,
+ "installable": True,
+}
diff --git a/bemade_reordering_rules_chatter/models/__init__.py b/bemade_reordering_rules_chatter/models/__init__.py
new file mode 100644
index 0000000..5024fb8
--- /dev/null
+++ b/bemade_reordering_rules_chatter/models/__init__.py
@@ -0,0 +1 @@
+from . import stock_warehouse_orderpoint
diff --git a/bemade_reordering_rules_chatter/models/stock_warehouse_orderpoint.py b/bemade_reordering_rules_chatter/models/stock_warehouse_orderpoint.py
new file mode 100644
index 0000000..7a8eccb
--- /dev/null
+++ b/bemade_reordering_rules_chatter/models/stock_warehouse_orderpoint.py
@@ -0,0 +1,19 @@
+from odoo import models, fields
+
+class StockWarehouseOrderpoint(models.Model):
+ _name = 'stock.warehouse.orderpoint'
+ _inherit = ['stock.warehouse.orderpoint', 'mail.thread', 'mail.activity.mixin']
+
+ name = fields.Char(tracking=True)
+ trigger = fields.Selection(tracking=True)
+ active = fields.Boolean(tracking=True)
+ snoozed_until = fields.Date(tracking=True)
+ location_id = fields.Many2one(tracking=True)
+ product_tmpl_id = fields.Many2one(tracking=True)
+ product_id = fields.Many2one(tracking=True)
+ product_min_qty = fields.Float(tracking=True)
+ product_max_qty = fields.Float(tracking=True)
+ qty_multiple = fields.Float(tracking=True)
+ group_id = fields.Many2one(tracking=True)
+ company_id = fields.Many2one(tracking=True)
+ route_id = fields.Many2one(tracking=True)
diff --git a/bemade_reordering_rules_chatter/views/stock_warehouse_orderpoint.xml b/bemade_reordering_rules_chatter/views/stock_warehouse_orderpoint.xml
new file mode 100644
index 0000000..5b65877
--- /dev/null
+++ b/bemade_reordering_rules_chatter/views/stock_warehouse_orderpoint.xml
@@ -0,0 +1,29 @@
+
+
+
+
+ stock.warehouse.orderpoint.tree.editable.config.chatter
+ stock.warehouse.orderpoint
+
+
+
+
+
+
+
+
+
+ stock.warehouse.orderpoint.form.chatter
+ stock.warehouse.orderpoint
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bemade_so_and_po_only_company/__init__.py b/bemade_so_and_po_only_company/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bemade_so_and_po_only_company/__manifest__.py b/bemade_so_and_po_only_company/__manifest__.py
new file mode 100644
index 0000000..5cc5617
--- /dev/null
+++ b/bemade_so_and_po_only_company/__manifest__.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+{
+ "name": "Force Partner to be a company on SO and PO",
+ "version": "15.0.0.0.1",
+ "category": "Extra Tools",
+ 'summary': 'Force Partner to be a company on SO and PO',
+ "description": """
+Force Partner to be a company on SO and PO
+""",
+ "author": "Bemade",
+ 'website': 'https://www.bemade.org',
+ "depends": [
+ 'stock','sale',
+ ],
+ "data": [
+ 'views/sale_order.xml',
+ 'views/purchase_order.xml',
+ ],
+ "auto_install": False,
+ "installable": True,
+ 'license': 'OPL-1'
+}
diff --git a/bemade_so_and_po_only_company/views/purchase_order.xml b/bemade_so_and_po_only_company/views/purchase_order.xml
new file mode 100644
index 0000000..6bd2fb9
--- /dev/null
+++ b/bemade_so_and_po_only_company/views/purchase_order.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ Purchase Order Form View Inherit
+ purchase.order
+
+
+
+ [('is_company', '=', True)]
+
+
+
+
+
\ No newline at end of file
diff --git a/bemade_so_and_po_only_company/views/sale_order.xml b/bemade_so_and_po_only_company/views/sale_order.xml
new file mode 100644
index 0000000..17c7bad
--- /dev/null
+++ b/bemade_so_and_po_only_company/views/sale_order.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ Sale Order Form View Inherit
+ sale.order
+
+
+
+ [('is_company', '=', True)]
+
+
+
+
+
\ No newline at end of file
diff --git a/bemade_so_followers_to_picking/__init__.py b/bemade_so_followers_to_picking/__init__.py
new file mode 100644
index 0000000..9a7e03e
--- /dev/null
+++ b/bemade_so_followers_to_picking/__init__.py
@@ -0,0 +1 @@
+from . import models
\ No newline at end of file
diff --git a/bemade_so_followers_to_picking/__manifest__.py b/bemade_so_followers_to_picking/__manifest__.py
new file mode 100644
index 0000000..7e714b6
--- /dev/null
+++ b/bemade_so_followers_to_picking/__manifest__.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+{
+ "name": "Add SO Followers to Picking",
+ "version": "15.0.0.0.1",
+ "category": "Extra Tools",
+ 'summary': 'Add SO Followers to Picking',
+ "description": """
+ Add SO Followers to Picking
+ """,
+ "author": "Bemade",
+ 'website': 'https://www.bemade.org',
+ "depends": [
+ 'sale',
+ 'purchase'
+ ],
+ "data": [
+ ],
+ "auto_install": False,
+ "installable": True,
+ 'license': 'OPL-1'
+}
diff --git a/bemade_so_followers_to_picking/models/__init__.py b/bemade_so_followers_to_picking/models/__init__.py
new file mode 100644
index 0000000..ae4c272
--- /dev/null
+++ b/bemade_so_followers_to_picking/models/__init__.py
@@ -0,0 +1 @@
+from . import stock_picking
diff --git a/bemade_so_followers_to_picking/models/stock_picking.py b/bemade_so_followers_to_picking/models/stock_picking.py
new file mode 100644
index 0000000..48a35b4
--- /dev/null
+++ b/bemade_so_followers_to_picking/models/stock_picking.py
@@ -0,0 +1,17 @@
+from odoo import models, api
+
+
+class StockPicking(models.Model):
+ _inherit = 'stock.picking'
+
+ @api.model
+ def create(self, vals):
+ picking = super(StockPicking, self).create(vals)
+
+ if 'origin' in vals:
+ sale_order = self.env['sale.order'].search([('name', '=', vals['origin'])], limit=1)
+ if sale_order:
+ for follower in sale_order.message_follower_ids:
+ picking.message_subscribe(partner_ids=[follower.partner_id.id])
+
+ return picking
diff --git a/bemade_user_custom_apps_order/__init__.py b/bemade_user_custom_apps_order/__init__.py
new file mode 100644
index 0000000..795abc5
--- /dev/null
+++ b/bemade_user_custom_apps_order/__init__.py
@@ -0,0 +1,27 @@
+# Copyright (C) 2023 Bemade.org
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+# Import the models package so Odoo knows to load our models
+from . import models
+
+# Import the Environment for creating an environment for superuser
+# Import SUPERUSER_ID for using as the user id for the superuser
+from odoo.api import Environment, SUPERUSER_ID
+
+# This is the function that gets called after the module installation
+def post_init_hook(cr, registry):
+ # Create an environment with the database cursor and SUPERUSER_ID
+ env = Environment(cr, SUPERUSER_ID, {})
+
+ menus = env['ir.ui.menu'].search([('parent_id', '=', False)])
+
+ # For each user in the system,
+ for user in env['res.users'].search([]):
+ # Iterate over each sequence
+ for menu in menus:
+ # Create a new record in 'res.users.menu.order' with the user, menu and sequence
+ env['res.users.menu.order'].create({
+ 'user_id': user.id,
+ 'menu_id': menu.id
+ })
+
diff --git a/bemade_user_custom_apps_order/__manifest__.py b/bemade_user_custom_apps_order/__manifest__.py
new file mode 100644
index 0000000..ebd81f0
--- /dev/null
+++ b/bemade_user_custom_apps_order/__manifest__.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+{
+ 'name': 'User Custom App Order',
+ 'version': '1.0.1',
+ 'license': 'GPL-3',
+ 'author': 'Bemade',
+ 'website': 'http://www.bemade.org',
+ 'category': 'Extra Tools',
+ 'summary': 'Allows users to customize the order of apps in the app switcher',
+ 'description': """
+User Custom App Order
+=====================
+This module allows each user to customize the order of their apps in the app switcher in Odoo.
+
+Features:
+---------
+* Users can choose the order of their apps in the app switcher.
+* Users can only modify their own app order. Administrators can modify the order for any user.
+* The module also automatically handles the creation of app order records for new users and new apps.
+
+Installation:
+-------------
+No specific steps required for the installation.
+
+Configuration:
+--------------
+No specific configuration required for this module.
+ """,
+ 'depends': ['hr',],
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'views/user_menu_sequence.xml',
+ 'views/res_users_view.xml'
+ ],
+ 'assets': {
+ 'web.assets_tests': ['bemade_user_custom_apps_order/static/tests/tours/custom_app_order_tour.js'],
+ },
+ 'installable': True,
+ 'application': False,
+ 'auto_install': False,
+ 'post_init_hook': 'post_init_hook',
+}
diff --git a/bemade_user_custom_apps_order/models/__init__.py b/bemade_user_custom_apps_order/models/__init__.py
new file mode 100644
index 0000000..5afa068
--- /dev/null
+++ b/bemade_user_custom_apps_order/models/__init__.py
@@ -0,0 +1,9 @@
+# Copyright (C) 2023 Bemade.org
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+# Import the models to extend
+from . import res_users
+from . import ir_ui_menu
+
+# Import the new model
+from . import res_users_menu_order
diff --git a/bemade_user_custom_apps_order/models/ir_ui_menu.py b/bemade_user_custom_apps_order/models/ir_ui_menu.py
new file mode 100644
index 0000000..60226d2
--- /dev/null
+++ b/bemade_user_custom_apps_order/models/ir_ui_menu.py
@@ -0,0 +1,84 @@
+# Copyright (C) 2023 Bemade.org
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+# Import the required classes and decorators from Odoo
+from odoo import api, models
+
+class IrUiMenu(models.Model):
+ _inherit = 'ir.ui.menu'
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ # Override the create method to handle new top-level menus
+ records = super(IrUiMenu, self).create(vals_list)
+
+ menus = self.env['ir.ui.menu'].search([('parent_id', '=', False)])
+
+ # For each new menu
+ for record in records:
+ # If the menu is a top-level menu
+ if not record.parent_id and record.id in menus.ids:
+ # Create a 'res.users.menu.order' record for each user
+ for user in self.env['res.users'].search([]):
+ self.env['res.users.menu.order'].create({
+ 'user_id': user.id,
+ 'menu_id': record.id,
+ 'sequence': record.sequence,
+ })
+ return records
+
+ def unlink(self):
+ # Override the unlink method to delete associated 'res.users.menu.order' records
+ self.env['res.users.menu.order'].search([
+ ('menu_id', 'in', self.ids)
+ ]).unlink()
+ return super(IrUiMenu, self).unlink()
+
+ def write(self, vals):
+ # Call the super method to perform the default write operation
+ res = super(IrUiMenu, self).write(vals)
+
+ menus = self.env['ir.ui.menu'].search([('parent_id', '=', False)])
+
+
+ for self_record in self:
+ if 'parent_id' in vals:
+ if vals.get('parent_id') == False and self_record in menus:
+ # If the menu becomes a top-level menu, create a 'res.users.menu.order' record for each user
+ users = self.env['res.users'].search([])
+ for user in users:
+ existing_order = self.env['res.users.menu.order'].search([
+ ('user_id', '=', user.id),
+ ('menu_id', '=', self_record.id),
+ ], limit=1)
+ if not existing_order:
+ self.env['res.users.menu.order'].create({
+ 'user_id': user.id,
+ 'menu_id': self_record.id,
+ 'sequence': self_record.sequence,
+ })
+ else:
+ # Delete the corresponding 'res.users.menu.order' records
+ self.env['res.users.menu.order'].search([('menu_id', '=', self_record.id)]).unlink()
+ return res
+
+ @api.model
+ def load_menus(self, debug):
+ menus = super().load_menus(debug)
+
+ # Retrieve the user's menu order records
+ menu_order_records = self.env['res.users.menu.order'].search([('user_id', '=', self.env.uid)])
+
+ # Create a dictionary that maps app ids to sequences
+ sequence_dict = {record.menu_id.id: record.sequence for record in menu_order_records}
+
+ # Retrieve the menu records for the IDs in menus['root']['children']
+ menu_records = self.browse(menus['root']['children'])
+
+ # Sort the menu records based on the sequences in the dictionary
+ sorted_menu_records = menu_records.sorted(key=lambda menu: sequence_dict.get(menu.id, 9999))
+
+ # Replace menus['root']['children'] with the IDs of the sorted menu records
+ menus['root']['children'] = sorted_menu_records.ids
+
+ return menus
\ No newline at end of file
diff --git a/bemade_user_custom_apps_order/models/res_users.py b/bemade_user_custom_apps_order/models/res_users.py
new file mode 100644
index 0000000..b90a615
--- /dev/null
+++ b/bemade_user_custom_apps_order/models/res_users.py
@@ -0,0 +1,63 @@
+# Copyright (C) 2023 Bemade.org
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import models, api, fields
+
+class ResUsers(models.Model):
+ _inherit = 'res.users'
+
+ # Define the One2many field for the app order records
+ app_order_ids = fields.One2many(
+ 'res.users.menu.order', 'user_id', string='App Order Records')
+
+ @property
+ def SELF_READABLE_FIELDS(self):
+ return super().SELF_READABLE_FIELDS + ['app_order_ids']
+
+ @property
+ def SELF_WRITEABLE_FIELDS(self):
+ return super().SELF_WRITEABLE_FIELDS + ['app_order_ids']
+
+ @api.model
+ def create(self, vals):
+ """
+ Overridden to create a new app order record for each top-level menu for the new user when a new user is created.
+ """
+ # Create the user
+ new_user = super().create(vals)
+
+ # Create a new app order record for each top-level menu for the new user
+ menus = self.env['ir.ui.menu'].search([('parent_id', '=', False)])
+ for menu in menus:
+ self.env['res.users.menu.order'].create({
+ 'user_id': new_user.id,
+ 'menu_id': menu.id,
+ 'sequence': 9999, # Or some other default sequence
+ })
+
+ return new_user
+
+ def unlink(self):
+ """
+ Overridden to delete the app order records for the user being deleted when a user is deleted.
+ """
+ # Delete the app order records for the user being deleted
+ self.env['res.users.menu.order'].search([('user_id', 'in', self.ids)]).unlink()
+
+ # Delete the user
+ return super().unlink()
+
+ def write(self, vals):
+ """
+ Overridden to allow users to modify their own 'app_order_ids' field.
+ """
+ if 'app_order_ids' in vals and self.env.uid == self.id:
+ # may be called with sudo can be replaced with WRITINGFIELDRIGHTS but works for now
+ super(ResUsers, self.sudo()).write(vals)
+ else:
+ super().write(vals)
+
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'reload',
+ }
diff --git a/bemade_user_custom_apps_order/models/res_users_menu_order.py b/bemade_user_custom_apps_order/models/res_users_menu_order.py
new file mode 100644
index 0000000..8c4d88b
--- /dev/null
+++ b/bemade_user_custom_apps_order/models/res_users_menu_order.py
@@ -0,0 +1,28 @@
+# Copyright (C) 2023 Bemade.org
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+# Import the required classes and decorators from Odoo
+from odoo import models, fields, api
+
+class ResUsersMenuOrder(models.Model):
+ """
+ This model represents the preferred order in which a user wants to see their apps.
+ Each record represents the order of one app for one user.
+ """
+
+ _name = 'res.users.menu.order'
+ _description = 'User Menu Order'
+
+ user_id = fields.Many2one('res.users', string='User', required=True, )
+ # The user who has set a preferred order for their apps.
+
+ menu_id = fields.Many2one('ir.ui.menu', string='Menu', required=True)
+ # The app that the user has set a preferred order for.
+
+ sequence = fields.Integer(string='Sequence')
+ # The order of the app. A lower sequence means the app will appear earlier.
+
+ _sql_constraints = [
+ ('user_menu_uniq', 'unique(user_id, menu_id)', 'A user should only have one order record per menu!'),
+ ]
+
diff --git a/bemade_user_custom_apps_order/security/ir.model.access.csv b/bemade_user_custom_apps_order/security/ir.model.access.csv
new file mode 100644
index 0000000..b2095ab
--- /dev/null
+++ b/bemade_user_custom_apps_order/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_res_users_menu_order_all,res.users.menu.order.all,model_res_users_menu_order,base.group_user,1,1,1,1
+access_res_users_menu_order_admin,res.users.menu.order.admin,model_res_users_menu_order,base.group_system,1,1,1,1
diff --git a/bemade_user_custom_apps_order/static/tests/tours/custom_app_order_tour.js b/bemade_user_custom_apps_order/static/tests/tours/custom_app_order_tour.js
new file mode 100644
index 0000000..7ffaf2a
--- /dev/null
+++ b/bemade_user_custom_apps_order/static/tests/tours/custom_app_order_tour.js
@@ -0,0 +1,19 @@
+/** @odoo-module */
+
+import tour from 'web_tour.tour';
+
+tour.register('custom_app_order_tour', {
+ test: true,
+ url: '/web',
+ }, /* Make sure the test task is visible via the Project > Task Templates path */
+ [
+ {
+ trigger: 'div.o_user_menu button.dropdown-toggle',
+ },
+ {
+ trigger: 'span[data-menu="settings"]:contains(My Profile)'
+ },
+ {
+ trigger: 'a.nav-link:contains(App Order)'
+ }
+ ])
\ No newline at end of file
diff --git a/bemade_user_custom_apps_order/tests/__init__.py b/bemade_user_custom_apps_order/tests/__init__.py
new file mode 100644
index 0000000..5ec47e5
--- /dev/null
+++ b/bemade_user_custom_apps_order/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_user_custom_app_order
diff --git a/bemade_user_custom_apps_order/tests/test_user_custom_app_order.py b/bemade_user_custom_apps_order/tests/test_user_custom_app_order.py
new file mode 100644
index 0000000..778ed81
--- /dev/null
+++ b/bemade_user_custom_apps_order/tests/test_user_custom_app_order.py
@@ -0,0 +1,36 @@
+from odoo.tests import TransactionCase, HttpCase, tagged
+from odoo import Command
+
+
+class TestCustomAppOrder(TransactionCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ user_group = cls.env.ref('base.group_user')
+
+ Users = cls.env['res.users'].with_context({'no_reset_password': True})
+ Employees = cls.env['hr.employee']
+
+ user_partner = cls.env['res.partner'].create({
+ 'name': 'User Partner',
+ })
+ cls.user = Users.create({
+ 'name': 'Regular User',
+ 'login': 'reguser',
+ 'partner_id': user_partner.id,
+ 'password': 'reguser',
+ 'email': 'regular_user@test.co',
+ 'groups_id': [(6, 0, [user_group.id])]
+ })
+ user_employee = Employees.create({
+ 'name': 'Regular User',
+ 'address_home_id': user_partner.id,
+ 'user_id': cls.user.id,
+ })
+
+
+@tagged('-at_install', 'post_install')
+class TestCustomAppOrderTours(HttpCase, TestCustomAppOrder):
+
+ def test_as_regular_user(self):
+ self.start_tour('/web', 'custom_app_order_tour', login='reguser')
diff --git a/bemade_user_custom_apps_order/views/res_users_view.xml b/bemade_user_custom_apps_order/views/res_users_view.xml
new file mode 100644
index 0000000..2aff54a
--- /dev/null
+++ b/bemade_user_custom_apps_order/views/res_users_view.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+ res.users.preferences.form.inherit
+ res.users
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bemade_user_custom_apps_order/views/user_menu_sequence.xml b/bemade_user_custom_apps_order/views/user_menu_sequence.xml
new file mode 100644
index 0000000..a23c298
--- /dev/null
+++ b/bemade_user_custom_apps_order/views/user_menu_sequence.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+