# Créer son module pour Odoo

## Configurer l'environnement de déploiement

Il nous faut ajouter certaines librairies python au projet.

Pour cela nous allons:
- ajouter un fichier `requirements.txt` au dossier `etc`
- ajouter un script `entrypoint.sh` qui sera lancé par le compose

### Fichier `etc/requirements.txt`

??? note "requirements.txt"
    ```
    astor
    python-stdnum>=1.18
    python-magic
    requests_oauthlib
    factur-x
    unicodecsv
    email-validator
    py3o.template
    py3o.formats
    pypdf2
    python-jose
    plotly==5.4.0
    ```

### Fichier `entrypoint.sh`

??? note "entrypoint.sh"
    ```
    #!/bin/bash

    set -e

    # set the postgres database host, port, user and password according to the environment
    # and pass them as arguments to the odoo process if not present in the config file
    : ${HOST:=${DB_PORT_5432_TCP_ADDR:='db'}}
    : ${PORT:=${DB_PORT_5432_TCP_PORT:=5432}}
    : ${USER:=${DB_ENV_POSTGRES_USER:=${POSTGRES_USER:='odoo'}}}
    : ${PASSWORD:=${DB_ENV_POSTGRES_PASSWORD:=${POSTGRES_PASSWORD:='odoo'}}}

    # install python packages
    pip3 install pip --upgrade
    pip3 install -r /etc/odoo/requirements.txt

    # sed -i 's|raise werkzeug.exceptions.BadRequest(msg)|self.jsonrequest = {}|g' /usr/lib/python3/dist-packages/odoo/http.py

    DB_ARGS=()
    function check_config() {
        param="$1"
        value="$2"
        if grep -q -E "^\s*\b${param}\b\s*=" "$ODOO_RC" ; then       
            value=$(grep -E "^\s*\b${param}\b\s*=" "$ODOO_RC" |cut -d " " -f3|sed 's/["\n\r]//g')
        fi;
        DB_ARGS+=("--${param}")
        DB_ARGS+=("${value}")
    }
    check_config "db_host" "$HOST"
    check_config "db_port" "$PORT"
    check_config "db_user" "$USER"
    check_config "db_password" "$PASSWORD"

    case "$1" in
        -- | odoo)
            shift
            if [[ "$1" == "scaffold" ]] ; then
                exec odoo "$@"
            else
                wait-for-psql.py ${DB_ARGS[@]} --timeout=30
                exec odoo "$@" "${DB_ARGS[@]}"
            fi
            ;;
        -*)
            wait-for-psql.py ${DB_ARGS[@]} --timeout=30
            exec odoo "$@" "${DB_ARGS[@]}"
            ;;
        *)
            exec "$@"
    esac

    exit 1
    ```



## Créer un script d'installation automatisée

Ce script python va permettre d'automatiser l'installation des modules de base

??? note "init.py"
    ```
    import odoorpc
    import subprocess
    import os
    import shutil
    import time

    # Configuration
    ODOO_URL = 'http://localhost:10014'
    ODOO_DB = 'odoo'
    ODOO_USERNAME = 'garagenum@gmail.com'
    ODOO_PASSWORD = 'bellinux'

    # Applications to install
    APPS_TO_INSTALL = [
        'sale_management',
        'account',
        'crm',
        'website',
        'stock',
        'purchase',
        'project',
        'mass_mailing',
        'hr_expense',
        'hr_holidays',
        'hr_recruitment',
        'hr',
        'website_slides',
        'mail',
        'contacts',
        'calendar',
        'survey',
        'board',
        'mass_mailing_sms',
        'note',
        'website_forum',
        'hr_skills',
        'website_hr_recruitment',
        'hr_contract',
        'stock_account',
        'website_sms',
        'purchase_stock',
        'account_edi',
        'account_edi_facturx',
        'account_edi_ubl',
        'account_payment',
        'account_qr_code_sepa',
        'analytic',
        'attachment_indexation',
        'auth_oauth',
        'auth_signup',
        'auth_totp',
        'auth_totp_portal',
        'barcodes',
        'base',
        'base_automation',
        'base_iban',
        'base_import',
        'base_setup',
        'base_vat',
        'bus',
        'calendar_sms',
        'delivery',
        'digest',
        'fetchmail',
        'google_recaptcha',
        'hr_gamification',
        'hr_org_chart',
        'hr_skills_slides',
        'hr_skills_survey',
        'iap',
        'iap_mail',
        'l10n_fr',
        'link_tracker',
        'mail_bot',
        'mail_bot_hr',
        'mass_mailing_sale',
        'mass_mailing_slides',
        'payment',
        'payment_fix_register_token',
        'payment_transfer',
        'portal_rating',
        'procurement_jit',
        'product',
        'rating',
        'resource',
        'sale',
        'sale_expense',
        'sale_project',
        'sale_purchase',
        'sale_purchase_stock',
        'sale_stock',
        'sales_team',
        'sms',
        'snailmail',
        'snailmail_account',
        'social_media',
        'stock_sms',
        'uom',
        'utm',
        'web',
        'web_editor',
        'web_kanban_gauge',
        'web_tour',
        'web_unsplash',
        'website_form',
        'website_form_project',
        'website_links',
        'website_mail',
        'website_mass_mailing',
        'website_partner',
        'website_profile',
        'website_slides_forum',
        'website_slides_survey',
        'gamification',
        'portal',
        'http_routing',
        'phone_validation',
        'partner_autocomplete'
    ]

    MODULES_TO_INSTALL = [
        { 'url': 'https://github.com/OCA/account-analytic',
          'apps': [
            {'name': 'account_move_update_analytic'},
            {'name': '/analytic_distribution_widget_remove_save'},
        ]},
        { 'url': 'https://github.com/OCA/account-budgeting',
          'apps': [
            {'name': 'account_budget_oca'},
        ]},     
        { 'url': 'https://github.com/OCA/account-closing',
        'apps': [
            {'name': 'account_cutoff_base'},
            {'name': 'account_invoice_start_end_dates'},
            {'name': 'account_cutoff_start_end_dates'},
            {'name': 'account_cutoff_accrual_picking'},
            {'name': 'account_cutoff_accrual_subscription'},
        ]},
        { 'url': 'https://github.com/OCA/account-financial-reporting',
          'apps': [
            {'name': 'account_financial_report'},
            {'name': 'mis_builder_cash_flow'},
        ]},             
        { 'url': 'https://github.com/OCA/account-financial-tools',
          'apps': [
            {'name': 'account_asset_management'},
            {'name': 'account_cash_deposit'},
            {'name': 'account_fiscal_year'},
            {'name': 'account_move_budget'},
            {'name': 'account_move_line_purchase_info'},
            {'name': 'account_move_line_sale_info'},
            {'name': 'account_move_template'},
            {'name': 'account_netting'},
            {'name': 'account_template_active'},
            {'name': 'account_usability'},
        ]},
        { 'url': 'https://github.com/OCA/account-invoice-reporting',
          'apps': [
            {'name': 'account_comment_template'},
            {'name': 'partner_time_to_pay'},
            {'name': 'account_invoice_line_report'},
        ]},
        { 'url': 'https://github.com/OCA/account-invoicing',
          'apps': [
            {'name': 'account_invoice_fixed_discount'},
            {'name': 'account_invoice_payment_term_date_due'},
            {'name': 'account_invoice_refund_line_selection'},
            {'name': 'account_invoice_refund_link'},
            {'name': 'account_menu_invoice_refund'},
            {'name': 'account_move_tier_validation'},
            {'name': 'account_tax_group_widget_base_amount'},
            {'name': 'partner_invoicing_mode'},
            {'name': 'partner_invoicing_mode_monthly'},
        ]},
        { 'url': 'https://github.com/OCA/account-financial-reporting',
          'apps': [
            {'name': 'account_financial_report'},
            {'name': 'mis_builder_cash_flow'},
        ]}, 
        { 'url': 'https://github.com/OCA/account-payment',
          'apps': [
            {'name': 'account_payment_notification'},
            {'name': 'account_due_list'},
        ]}, 
        {'url': 'https://github.com/OCA/account-reconcile',
        'apps': [  
            {'name': 'account_reconciliation_widget'},
            {'name': 'account_partner_reconcile'},
            {'name': 'account_move_base_import'},
            {'name': 'account_move_line_reconcile_manual'},
            {'name': 'account_reconcile_oca'},
            {'name': 'account_statement_base'},   
        ]},  
        {'url': 'https://github.com/OCA/agreement',
         'apps': [
            {'name': 'agreement'},
            {'name': 'agreement_legal'},
         ]},
        {'url': 'https://github.com/OCA/bank-payment',
        'apps': [
            {'name': 'account_payment_mode'},
        ]},
        {'url': 'https://github.com/OCA/bank-statement-import',
        'apps': [
            {'name': 'account_statement_import_base'},
            {'name': 'account_statement_import_file'},
            {'name': 'account_statement_import_online'},
            {'name': 'account_statement_import_sheet_file'},
        ]},
        {'url': 'https://github.com/OCA/community-data-files',
         'apps': [
            {'name': 'base_unece'},
            {'name': 'account_payment_unece'},
            {'name': 'account_tax_unece'},
            {'name': 'base_currency_iso_4217'},
            {'name': 'base_iso3166'},
            {'name': 'company_sanitary_registry'},
            {'name': 'uom_unece'},
        ]},
        {'url': 'https://github.com/OCA/data-protection',
         'apps': [
            {'name': 'privacy'},
            {'name': 'privacy_consent'},
        ]},
        {'url': 'https://github.com/OCA/dms',
         'apps': [
            {'name': 'dms'},
        ]},
        {'url': 'https://github.com/OCA/donation',
            'apps': [
            {'name': 'donation_base'},
            {'name': 'donation'},
            {'name': 'donation_bank_statement_oca'}
        ]},
        {'url': 'https://github.com/OCA/edi',
         'apps': [
            {'name': 'account_einvoice_generate'},
            {'name': 'account_invoice_facturx'},
            {'name': 'account_invoice_facturx_py3o'},
            {'name': 'base_business_document_import'},
            {'name': 'base_edi'},
            {'name': 'base_facturx'},
            {'name': 'pdf_helper'},
        ]},
        {'url': 'https://github.com/OCA/hr',
         'apps': [
            {'name': 'hr_course'},
            {'name': 'hr_employee_age'},
            {'name': 'hr_employee_birthday_mail'},
            {'name': 'hr_employee_calendar_planning'},
            {'name': 'hr_employee_document'},
            {'name': 'hr_employee_firstname'},
            {'name': 'hr_holidays_settings'},
            {'name': 'hr_personal_equipment_request'},
         ]}
        {'url': 'https://github.com/OCA/hr-expense',
            'apps': [
            {'name': 'hr_expense_cancel'},
            {'name': 'hr_expense_invoice'},
            {'name': 'hr_expense_payment'},
            {'name': 'hr_expense_tier_validation'},
        ]},
        {'url': 'https://github.com/OCA/hr-holidays',
         'apps': [
            {'name': 'hr_holidays_natural_period'},
            {'name': 'hr_holidays_public'},
         ]},
        {'url': 'https://github.com/OCA/knowledge',
         'apps': [
            {'name': 'document_knowledge'},
            {'name': 'document_page'},
            {'name': 'document_page_approval'},
            {'name': 'document_page_group'},
            {'name': 'document_page_reference'},
            {'name': 'document_page_tag'},
            {'name': 'document_url'}
         ]},
        { 'url': 'https://github.com/OCA/l10n-france',
        'apps': [
            {'name': 'l10n_fr_account_invoice_facturx'},
            {'name': 'l10n_fr_account_tax_unece'},
            {'name': 'l10n_fr_account_vat_return'},
            {'name': 'l10n_fr_account_vat_return_teledec'},
            {'name': 'l10n_fr_chorus_account'},
            {'name': 'l10n_fr_chorus_facturx'},
            {'name': 'l10n_fr_chorus_sale'},
            {'name': 'l10n_fr_department'},
            {'name': 'l10n_fr_department_oversea'},
            {'name': 'l10n_fr_fec_oca'},
            {'name': 'l10n_fr_hr_check_ssnid'},
            {'name': 'l10n_fr_mis_reports'},
            {'name': 'l10n_fr_oca},
            {'name': 'l10n_fr_siret'},
            {'name': 'l10n_fr_siret_lookup'},
        ]},        
        { 'url': 'https://github.com/OCA/mis-builder',
        'apps': [
            {'name': 'mis_builder'},
            {'name': 'mis_builder_budget'}
        ]},  
        {'url': 'https://github.com/OCA/partner-contact',
         'apps': [
            {'name': 'partner_address_street3'},
            {'name': 'partner_address_two_lines'},
            {'name': 'partner_company_type'},
            {'name': 'partner_contact_access_link'},
            {'name': 'partner_contact_address_default'},
            {'name': 'partner_email_check'},
            {'name': 'partner_email_duplicate_warn'},
            {'name': 'partner_firstname'},
            {'name': 'partner_mobile_duplicate_warn'},
            {'name': 'partner_subject_to_vat'},
            {'name': 'partner_vat_unique'},    
        ]},
        {'url': 'https://github.com/OCA/payroll',
         'apps': [
            {'name': 'payroll'},
            {'name': 'payroll_account'}
        ]},
        {'url': 'https://github.com/OCA/project',
         'apps': [
            {'name': 'project_department'},
            {'name': 'project_duplicate_subtask'},
            {'name': 'project_hr'},
            {'name': 'project_list'},
            {'name': 'project_parent'},
            {'name': 'project_parent_task_filter'},
            {'name': 'project_task_add_very_high'},
            {'name': 'project_task_link'},
            {'name': 'project_task_stage_mgmt'},
            {'name': 'project_template'},
            {'name': 'project_timeline'},
            {'name': 'project_timeline_hr_timesheet'},
            {'name': 'project_timesheet_time_control'},
        ]},
        {'url': 'https://github.com/OCA/reporting-engine',
          'apps': [
            {'name': 'bi_view_editor'},
            {'name': 'bi_view_editor_spreadsheet_dashboard'},
            {'name': 'report_py3o'},
            {'name': 'report_qweb_pdf_watermark'},
            {'name': 'report_wkhtmltopdf_param'},
        ]},
        {'url': 'https://github.com/OCA/sale-workflow',
         'apps': [
            {'name': 'partner_sale_pivot'},
            {'name': 'product_form_sale_link'},
            {'name': 'sale_advance_payment},
            {'name': 'sale_cancel_reason'},
            {'name': 'sale_delivery_state'},
            {'name': 'sale_discount_display_amount'},
            {'name': 'sale_fixed_discount'},
            {'name': 'sale_force_invoiced'},
            {'name': 'sale_order_general_discount'}
            {'name': 'sale_order_invoice_amount'},
            {'name': 'sale_order_line_menu'},
            {'name': 'sale_order_line_tag'},
            {'name': 'sale_order_revision'},
            {'name': 'sale_start_end_dates'},
            {'name': 'sale_substate'},
            {'name': 'sale_tier_validation'},
         ]},
        {'url': 'https://github.com/OCA/server-auth',
         'apps': [
            {'name': 'auth_oidc'},
         ]},
         {'url': 'https://github.com/OCA/server-brand',
          'apps': [
            {'name': 'disable_odoo_online'},
            {'name': 'hr_expense_remove_mobile_link'},
            {'name': 'portal_odoo_debranding'},
            {'name': 'remove_odoo_enterprise'},
          ]},
        {'url': 'https://github.com/OCA/server-tools',
         'apps': [
            {'name': 'attachment_unindex_content'},
            {'name': 'base_fontawesome'},
            {'name': 'base_name_search_improved'},
            {'name': 'base_view_inheritance_extension'},
            {'name': 'iap_alternative_provider'},
            {'name': 'module_auto_update'},
         ]},
        { 'url': 'https://github.com/OCA/server-ux',
        'apps': [
            {'name': 'base_cancel_confirm'},
            {'name': 'base_custom_filter'},
            {'name': 'base_menu_visibility_restriction'},
            {'name': 'base_optional_quick_create'},
            {'name': 'base_revision'},
            {'name': 'base_search_custom_field_filter'},
            {'name': 'base_substate'},
            {'name': 'base_technical_features'},
            {'name': 'base_tier_validation'},
            {'name': 'date_range'},
            {'name': 'date_range_account'},
            {'name': 'filter_multi_user'},
            {'name': 'multi_step_wizard'},
            {'name': 'user_all_groups'}
        ]}, 
        {'url': 'https://github.com/OCA/sign',
         'apps': [
            {'name': 'sign_oca'},
         ]}
        {'url': 'https://github.com/OCA/social',
        'apps': [
            {'name': 'base_search_mail_content'},
            {'name': 'email_template_qweb'},
            {'name': 'mail_activity_board'},
            {'name': 'mail_activity_done'},
            {'name': 'mail_attach_existing_attachment'},
            {'name': 'mail_composer_cc_bcc'},
            {'name': 'mail_debrand'},
            {'name': 'mail_layout_preview'},
            {'name': 'mail_inline_css'},
            {'name': 'mail_optional_follower_notification'},
            {'name': 'mail_tracking'},
            {'name': 'mail_tracking_mass_mailing'},
            {'name': 'mass_mailing_list_dynamic'},
            {'name': 'mass_mailing_partner'},
            {'name': 'mass_mailing_resend'},
            {'name': 'mass_mailing_unique'},
        ]},
        {'url': 'https://github.com/OCA/spreadsheet',
         'apps': [
            {'name': 'spreadsheet_dashboard_oca'},
            {'name': 'spreadsheet_oca'},
         ]},
        {'url': 'https://github.com/OCA/survey',
         'apps': [
            {'name': 'survey_question_type_five_star'},
        ]},
        {'url': 'https://github.com/OCA/timesheet',
         'apps': [
            {'name': 'hr_timesheet_begin_end'}
            {'name': 'hr_timesheet_sheet'},
            {'name': 'hr_timesheet_task_domain'},
            {'name': 'hr_timesheet_time_type'},
            {'name': '}
         ]}
        {'url': 'https://github.com/OCA/vertical-association',
         'apps': [
            {'name: 'web_advanced_search'},
            {'name': 'web_calendar_slot_duration'},
            {'name': 'web_chatter_position'},
            {'name': 'web_dark_mode'},
            {'name': 'web_dialog_size'},
            {'name': 'web_group_expand'},
            {'name': 'web_help'},
            {'name': 'web_ir_actions_act_window_page'},
            {'name': 'web_listview_range_select'},
            {'name': 'web_m2x_options'},
            {'name': 'web_no_bubble'},
            {'name': 'web_remember_tree_column_width'},
            {'name': 'web_responsive'},
            {'name': 'web_search_with_and'},
            {'name': 'web_theme_classic'},
            {'name': 'web_timeline'},
            {'name': 'web_tree_many2one_clickable'},
            {'name': 'web_widget_dropdown_dynamic'},
            {'name': 'web_widget_numeric_step'},
            {'name': 'web_widget_open_tab'},
            {'name': 'web_widget_plotly_chart'}
         ]},
        {'url': 'https://github.com/OCA/website',
         'apps': [
        {'name': 'website_odoo_debranding'},
          ]},
        {'url': 'https://github.com/OCA/website-cms',
         'apps': [
            {'name': 'cms_form'},
            {'name': 'cms_status_message'}
         ]}
        # {'url': 'https://git.legaragenumerique.fr/odoo/gn_odoo',
        # 'apps': [
        #     {'name': 'gn_discount'},
        #     {'name': 'gn_donations'}
        # ]},
    ]

    def configure_folders():
        #os.makedirs('addons', exist_ok=True)
        subprocess.run(["setfacl", "-m", "u:101:rwx", './addons'], check=True)
        subprocess.run(["setfacl", "-d", "-m", "u:101:rwx", './addons'], check=True)
        subprocess.run(["setfacl", "-m", "u:101:rwx", './etc'], check=True)
        subprocess.run(["setfacl", "-d", "-m", "u:101:rwx", './etc'], check=True)

    def merge_directories(src, dst):
        for item in os.listdir(src):
            if item != '.git':
                s = os.path.join(src, item)
                d = os.path.join(dst, item)
                if os.path.isdir(s):
                    if not os.path.exists(d):
                        shutil.copytree(s, d)
                    else:
                        merge_directories(s, d)
                else:
                    shutil.copy2(s, d)

    def connect_odoo():
        # Connect to the Odoo server
        odoo = odoorpc.ODOO('localhost', port=10014)
        odoo.login(ODOO_DB, ODOO_USERNAME, ODOO_PASSWORD)
        return odoo


    def install_app(odoo, odoo_app):
        # Check if each module is installed or not
        print(f"Checking {odoo_app} installation...")
        module_id = odoo.env['ir.module.module'].search([('name', '=', odoo_app)])
        if module_id:
            module = odoo.env['ir.module.module'].browse(module_id)[0]
            if module.state not in ['installed', 'to upgrade']:
                time.sleep(0.3)
                module.button_immediate_install()
                print(f"Module {odoo_app} has been installed.")
            else:
                print(f"Module {odoo_app} is already installed.")
        else:
            print(f"Module {odoo_app} not found.")

    def install_apps(odoo):
        for odoo_app in APPS_TO_INSTALL:
            install_app(odoo, odoo_app)
            time.sleep(1)

    def dl_modules(group):
        repo_url = group['url'].rstrip('/')
        addons_path = "addons"

        sparse_checkout_lines = []
        for app in group['apps']:
            if app['name'] != None:
                app_dir = app['dir'] if 'dir' in app else app['name']
                sparse_checkout_lines.append(f"{app_dir}\n")

        clone_path = 'tmp' if group['apps'][0]['name'] != None else os.path.join('tmp', group['url'].split('/')[-1])
        clone_full_path = os.path.join(addons_path, clone_path)
        git_dir_path = os.path.join(clone_full_path, '.git')
        sparse_checkout_file_path = os.path.join(git_dir_path, 'info', 'sparse-checkout')
        branch_name = "16.0"

        print("starting git clone")
        subprocess.run(["git", "clone", "--filter=blob:none", "--sparse", "--branch", branch_name, "--depth", "1", repo_url, clone_path], cwd=addons_path, check=True)

        if not os.path.isdir(sparse_checkout_file_path):
            subprocess.run(["git", "config", "core.sparseCheckout", "true"], cwd=clone_full_path, check=True)

        with open(sparse_checkout_file_path, "w") as sparse_checkout_file:
            sparse_checkout_file.writelines(sparse_checkout_lines)

        print("starting git checkout")
        subprocess.run(["git", "checkout", branch_name], cwd=clone_full_path, check=True)

        src_path = os.path.join(addons_path, 'tmp')
        merge_directories(src_path, addons_path)
        shutil.rmtree(src_path)

        print(f"Modules {', '.join(app['name']  if app['name'] else group['url'] for app in group['apps'])} has been downloaded")

    def update_module_list(odoo):
        ModuleUpdate = odoo.env['base.module.update']
        module_update_id = ModuleUpdate.create({})
        module_update = ModuleUpdate.browse(module_update_id)
        module_update.update_module()
        updated_modules = module_update.updated
        added_modules = module_update.added
        print(f"Updated modules: {updated_modules}, Added modules: {added_modules}")

    def install_modules(odoo):
        for group in MODULES_TO_INSTALL:
            dl_modules(group)
        time.sleep(1)
        update_module_list(odoo)
        time.sleep(1)
        for group in MODULES_TO_INSTALL:
            for app in group['apps']:
                app_name = app['name'] if app['name'] != None else group['url'].split('/')[-1]
                install_app(odoo, app_name)

    def main():
        configure_folders()
        odoo = connect_odoo()
        install_apps(odoo)
        time.sleep(3)
        install_modules(odoo)
        print("Script execution finished!")

    main()

    ```