This script processes a solution.json file and combines the menus key from multiple module JSON files to build a complete menu structure. The resulting menu is sorted by position at both the menu and submenu levels. The script outputs a new file named generated-solution.json containing the combined and sorted menu structure.
- Node.js v18 or higher is required
- This version is necessary for modern features like 
BlobandarrayBuffer()support used during ZIP creation. - You can verify your version with:
node -v
 
 - This version is necessary for modern features like 
 
The workbench.js script:
- Reads a solution configuration JSON (e.g., 
HF.json). - Generates supporting files like:
fe-openimis.jsongenerated-menu.jsongenerated-roles.jsoncompose.yml
 - Creates a ZIP archive (
output.zip) containing the above outputs. - (Optional) Pushes the generated solution to a new branch in the 
openimis/solutionsGitHub repo. 
node workbench.jsThis will create output.zip in the project root with the generated solution files.
node workbench.js --publishThis generates the ZIP and:
- Clones the openimis/solutions repository.
 - Creates a new branch based on 
develop. - Pushes the solution into a subfolder matching the default name (
default-solution). 
node workbench.js --publish --folder=coreMIS-testThis behaves like the previous command but publishes the solution inside a subfolder called coreMIS-test.
After running, you will see:
output.zip
solution/
├— fe-openimis.json
├— generated-menu.json
├— generated-roles.json
└— compose.yml
Temporary folders (temp_solutions_repo and unzipped_output) are automatically cleaned between runs.
Ensure your <solution>.json is located at:
./solution/solutions/<solution>.json
You can customize this path in the script if needed.
Install dependencies using:
npm install jszip simple-git yaml unzipperMerge to develop in openimis/solutions is disabled. This script creates a new branch like solution/coreMIS-test and pushes only that branch.
# undelying solution, share the same format
solutions: []
# named element to ensure unicity /  share the same format BUT cannot have modules and solutions
modules: {}
# list of menu to use,
menus: [] 
# list of name of the role to use,
roles: []
# list of name of the source to use, normally found in the source section of another file
fePackages: []
fePackageDefinitions: 
  - PayerModule:  # open imis package name
    - package: "@openimis/fe-payer", # PIP package name
    - git: "https://github.com/openimis/openimis-fe-payer_js", # git URL
    - version: "v1.4.4" # targeted released version
bePackages: []
bePackageDefinitions: 
  - payer:  # openimis package name
    - package: "openimis-be-payer", # NPM package name
    - git: "https://github.com/openimis/openimis-be-payer_py", # git url
    - version: "1.6.1" # targeted released version
services:
  serviceName1
  serviceName2
serviceDefinitions: 
  - serviceName:
    - path: pathToCompose # if  present  will overwright the rest
    - env_file:
      - .env1
      - .env2
initData: {}
- The script reads 
solution.jsonfrom the current working directory. - The 
solution.jsonfile should have a modules key containing a list of module bundle file names (e.g.,social-protection-bundle.json,formal-sector-bundle.json, etc.) 
- Each module file listed under the 
moduleskey should be present in the same directory assolution.json. - Each module file should contain a 
menuskey, which defines its menu structure. - Some modules may also contain a 
solutionskey, listing other module bundles that must be processed even if they are not explicitly included insolution.json. 
- The script reads all the module files, extracts their 
menus, and combines them. - If multiple entries share the same 
id, theirsubmenusare merged instead of duplicating menu items. - Both menus and their 
submenusare sorted by thepositionfield. 
- If a module has a 
solutionskey, the script ensures that the referenced module files are also processed, even if they are not explicitly listed insolution.json. - solutions are resolved recursively to ensure all required modules are included.
 
- Bundles are collections of multiple related modules grouped together in a single file.
 - If a bundle file is listed in 
solution.json, the script will process all its included modules. - Bundles may also have solutions on other bundles, which are resolved recursively.
 
- If any module, bundle, or solutions file is missing, the script will print a warning but will continue processing the available files.
 
{
  "modules": [
    "social-protection-bundle.json",
    "opensearch-report-bundle.json",
    "core-bundle.json"
  ],
  "roles": [
    "role-eo.json"
  ]
}{
  "modules": [
    "individual.json",
    "api-import.json",
    "social-protection.json",
    "payroll.json",
    "payment-cycle.json",
    "deduplication.json"
  ],
  "solutions": [
    "grievance-bundle.json"
  ]
}{
  "modules": [
    "opensearch-report.json"
  ]
}{
  "menus": [
    {
      "position": 1,
      "id": "ClientRegistryMainMenu",
      "name": "Client Registry",
      "icon": "task-icon",
      "description": "Client Registry",
      "submenus": [
        {
          "position": 1,
          "id": "individual.api_imports"
        },
        {
          "position": 3,
          "id": "individual.individuals"
        },
        {
          "position": 4,
          "id": "individual.groups"
        },
        {
          "position": 2,
          "id": "socialProtection.benefitPlans"
        }
      ]
    }
  ]
}- Python 3.x installed on your system.
 
- 
Prepare
solution.json:- Create a file named 
solution.jsonin the directory where you will run the script. - Add a 
moduleskey listing all the module bundles you want to include. Example:{ "modules": [ "social-protection-bundle.json", "opensearch-report-bundle.json", "core-bundle.json", "xxxx-bundle.json", "formal-sector-bundle.json" ] } 
 - Create a file named 
 - 
Prepare
<module>-bundle.json:- Create a file named 
<module>-bundle.jsonin the directory where you will run the script. - Add a 
moduleskey listing all the modules you want to include in bundle. Example:{ "modules": [ "individual.json", "api-import.json", "social-protection.json", "payroll.json", "payment-cycle.json", "deduplication.json" ], "solutions": [ "grievance-bundle.json", "calculation-social-protection-bundle.json" ] } solutionskey: it means that this bundle is related to other bundle and the additional package must be installed and considered in the final output. For example ifgrievance-bundle.jsonis not added insolution.json- this bundle package will be added in the final output even though is not presented insolution.jsonfile.
 - Create a file named 
 - 
Prepare Module Files:
- Place all the module files listed in 
<module>-bundle.jsonfiles in the same directory. Each file should have a structure similar to the example below:{ "menus": [ { "position":10, "id": "TasksMainMenu", "name":"TasksMainMenu", "icon":"task-icon", "description":"Task panel" }, { "mainMenu":"TasksMainMenu", "position":2, "id": "task.tasks", "icon" : "AssignmentTurnedInIcon" }, { "mainMenu":"TasksMainMenu", "position":1, "id":"task.allTasks", "icon" : "AssignmentIcon" } , { "mainMenu": "UserManagementMainMenu", "position":11, "id":"admin.taskExecutionerGroups", "icon":"AssignmentIndIcon" } ], } 
main menus list
from core bundle
- "ClientRegistryMainMenu"
 - "AdministrationMainMenu"
 - "UserManagementMainMenu"
 - "ProfileMainMenu",
 
from task management module
- "TasksMainMenu"
 
from grievance module
- "GrievanceMainMenu"
 
 - Place all the module files listed in 
 - 
Run the Script:
- Save the script as 
build_solution.pyin the same directory assolution.jsonand the module files. - Open a terminal or command prompt and navigate to the directory.
 - Run the script using the command:
python3 build_solution.py
 
 - Save the script as 
 - 
View the Output:
- After the script completes, a new files named 
generated-menu.json,generated-roles.json,fe-openimis.jsonandbe-openimis.jsonwill be created in the same directory. - This file contains the combined and sorted menu structure.
 
 - After the script completes, a new files named 
 
/solution
|-- solution.json
|-- social-protection.json
|-- payroll.json
|-- payment-cycle.json
|-- individual.json
|-- grievance.json
|-- grievance-bundle.json
|-- social-protection-bundle.json
|-- core.json
|-- build_solution.py
- Ensure all files are properly formatted JSON.
 - Missing or malformed files will result in warnings but will not stop the script.
 - Customize the 
solution.jsonfile to include or exclude specific modules based on your requirements. 
Here’s the complete table with all the submenu configurations extracted, including their Name of Submenu, ID of Submenu, Filter, and Route.
| Name of Submenu | ID of Submenu | Filter | Route | 
|---|---|---|---|
| Tasks Management View | task.tasks | 
(rights) => rights.includes(RIGHT_TASKS_MANAGEMENT_SEARCH_ALL) | 
/tasks | 
| Tasks Management All View | task.allTasks | 
(rights) => rights.includes(RIGHT_TASKS_MANAGEMENT_SEARCH_ALL) | 
/AllTasks | 
| Registers | tools.registers | 
(rights) => enablers(rights, RIGHT_REGISTERS) | 
/tools/registers | 
| Extracts | tools.extracts | 
(rights) => enablers(rights, RIGHT_EXTRACTS) | 
/tools/extracts | 
| Reports | tools.reports | 
(rights) => enablers(rights, RIGHT_REPORTS) | 
/tools/reports | 
| Social Protection Benefit Plans | socialProtection.benefitPlans | 
(rights) => rights.includes(RIGHT_BENEFIT_PLAN_SEARCH) | 
/benefitPlans | 
| My Profile | profile.myProfile | 
None | /profile/myProfile | 
| Change Password | profile.changePassword | 
None | /profile/changePassword | 
| Policies | insuree.policies | 
(rights) => rights.includes(RIGHT_POLICY) | 
/policy/policies | 
| Payment Point | legalAndFinance.paymentPoint | 
(rights) => rights.includes(RIGHT_PAYMENT_POINT_SEARCH) | 
/paymentPoints | 
| Payrolls | legalAndFinance.payrolls | 
(rights) => rights.includes(RIGHT_PAYROLL_SEARCH) | 
/payrolls | 
| Payrolls Pending | legalAndFinance.payrollsPending | 
(rights) => rights.includes(RIGHT_PAYROLL_SEARCH) | 
/payrollsPending | 
| Payrolls Approved | legalAndFinance.payrollsApproved | 
(rights) => rights.includes(RIGHT_PAYROLL_SEARCH) | 
/payrollsApproved | 
| Payrolls Reconciled | legalAndFinance.payrollsReconciled | 
(rights) => rights.includes(RIGHT_PAYROLL_SEARCH) | 
/payrollsReconciled | 
| Payments | insuree.payment | 
(rights) => rights.includes(RIGHT_PAYMENT) | 
/payment/payments | 
| Payment Cycles | legalAndFinance.paymentCycles | 
(rights) => rights.includes(RIGHT_PAYMENT_CYCLE_SEARCH) | 
/paymentCycles | 
| Payers | admin.payers | 
(rights) => rights.includes(RIGHT_PAYERS) | 
/payer/payers | 
| Individual Reports | openSearch.individualReports | 
None | /individualReports | 
| Group Reports | openSearch.groupReports | 
None | /groupReports | 
| Beneficiary Reports | openSearch.beneficiaryReports | 
None | /beneficiaryReports | 
| Invoice Reports | openSearch.invoiceReports | 
None | /invoiceReports | 
| Payment Reports | openSearch.paymentReports | 
None | /paymentReports | 
| Grievance Reports | openSearch.grievanceReports | 
None | /grievanceReports | 
| Data Updates Reports | openSearch.dataUpdatesReports | 
None | /dataUpdatesReports | 
| Open Search Config | openSearch.openSearchConfig | 
None | /dashboardConfiguration | 
| Invoices | legalAndFinance.invoices | 
(rights) => rights.filter((r) => r >= RIGHT_INVOICE_SEARCH && r <= RIGHT_INVOICE_AMEND).length > 0 | 
/invoices | 
| Bills | legalAndFinance.bills | 
(rights) => rights.filter((r) => r >= RIGHT_BILL_SEARCH && r <= RIGHT_BILL_AMEND).length > 0 | 
/bills | 
| Add Family or Group | insuree.addFamilyOrGroup | 
(rights) => rights.includes(RIGHT_FAMILY_ADD) | 
/insuree/family | 
| Families or Groups | insuree.familiesOrGroups | 
(rights) => rights.includes(RIGHT_FAMILY) | 
/insuree/families | 
| Insurees | insuree.insurees | 
(rights) => rights.includes(RIGHT_INSUREE) | 
/insuree/insurees | 
| Individuals | individual.individuals | 
(rights) => rights.includes(RIGHT_INDIVIDUAL_SEARCH) | 
/individuals | 
| Groups | individual.groups | 
(rights) => rights.includes(RIGHT_GROUP_SEARCH) | 
/groups | 
| API Imports | individual.api_imports | 
(rights) => rights.includes(RIGHT_INDIVIDUAL_SEARCH) | 
/imports | 
| Grievances | grievance.grievances | 
(rights) => rights.includes(RIGHT_TICKET_SEARCH) | 
/ticket/tickets | 
| Add Grievance | grievance.add | 
(rights) => rights.includes(RIGHT_TICKET_ADD) | 
/ticket/newTicket | 
| Role Management | admin.roleManagement | 
(rights) => rights.includes(RIGHT_ROLE_SEARCH) | 
/roles | 
| Contribution Plans | admin.contributionPlans | 
(rights) => rights.includes(RIGHT_CONTRIBUTION_PLAN_SEARCH) | 
/contributionPlans | 
| Contribution Plan Bundles | admin.contributionPlanBundles | 
(rights) => rights.includes(RIGHT_CONTRIBUTION_PLAN_BUNDLE_SEARCH) | 
/contributionPlanBundles | 
| Payment Plans | legalAndFinance.paymentPlans | 
(rights) => rights.includes(RIGHT_PAYMENT_PLAN_SEARCH) | 
/paymentPlans | 
| Contribution | insuree.contribution | 
(rights) => rights.includes(RIGHT_CONTRIBUTION) | 
/contribution/contributions | 
| Health Facility Claims | claim.healthFacilityClaims | 
(rights) => rights.some((r) => r >= RIGHT_CLAIMREVIEW && r <= RIGHT_PROCESS) | 
/claim/healthFacilities | 
| Reviews | claim.reviews | 
(rights) => rights.some((r) => r >= RIGHT_CLAIMREVIEW && r <= RIGHT_PROCESS) | 
/claim/reviews | 
| Claim Batch (Batch Run) | claim.claimBatch | 
(rights) => !!rights.filter(r => r >= RIGHT_PROCESS && r <= RIGHT_PREVIEW).length | 
/claim_batch | 
| Products | admin.products | 
(rights) => rights.includes(RIGHT_PRODUCTS) | 
/admin/products | 
| Health Facilities | admin.healthFacilities | 
(rights) => rights.includes(RIGHT_HEALTHFACILITIES) | 
/location/healthFacilities | 
| Medical Services Prices List | admin.services | 
(rights) => rights.includes(RIGHT_PRICELISTMS) | 
/medical/pricelists/services | 
| Medical Items Prices List | admin.items | 
(rights) => rights.includes(RIGHT_PRICELISTMI) | 
/medical/pricelists/items | 
| Medical Services | admin.medicalServices | 
(rights) => rights.includes(RIGHT_MEDICALSERVICES) | 
/medical/pricelists/services | 
| Medical Items | admin.medicalItems | 
(rights) => rights.includes(RIGHT_MEDICALITEMS) | 
/medical/pricelists/items | 
| Users | admin.users | 
(rights) => rights.includes(RIGHT_USERS) | 
/admin/users | 
| Locations | admin.locations | 
(rights) => rights.includes(RIGHT_LOCATIONS) | 
/location/locations | 
| Contracts | legalAndFinance.contracts | 
(rights) => rights.includes(RIGHT_POLICYHOLDERCONTRACT_SEARCH) | 
/contracts | 
- Menu Builder Script explanation
 - Conception of deployment Recipe Strategy
 - More detailed instruction of menu configuration
 - List of possible configurations of submenus items
 - Detailed description of technical approach to achieve having menu configurable.
 
This section explains how the script processes solution.json to generate two package configuration files:
be-openimis.json: Contains backend (bePackages) packages.fe-openimis.json: Contains frontend (fePackages) packages.
Each package is transformed to meet specific naming conventions and structure requirements based on its type.
- 
General Format:
Backend packages are listed under thepipkey. Depending on thetype, they are transformed as follows:- 
Type:
pip{ "name": "menu", "pip": "openimis-be-menu==v1.8.0" } - 
Type:
github{ "name": "core", "pip": "git+https://github.com/openimis/openimis-be-core_py.git@develop#egg=openimis-be-core" } 
 - 
 - 
Naming Rules:
- For 
pippackages, the fullopenimis-be-<module_name>pattern is retained. - For 
githubpackages, theopenimis-be-prefix and_pysuffix are removed, leaving only<module_name>inname. 
 - For 
 
- 
General Format:
Frontend packages are listed under thenpmkey. Depending on thetype, they are transformed as follows:- 
Type:
npm{ "name": "CoreModule", "npm": "@openimis/fe-core@>=v1.7.1" } - 
Type:
github{ "name": "GrievanceSocialProtectionModule", "npm": "@openimis/fe-grievance_social_protection@https://github.com/openimis/openimis-fe-grievance_social_protection_js#develop" } 
 - 
 - 
Naming Rules:
- The 
namekey is converted to PascalCase with theModulesuffix (e.g.,GrievanceSocialProtectionModule). - For 
npmpackages,_jssuffixes are removed. - For 
githubpackages, URLs follow the patternopenimis/openimis-<module_name>_js. 
 - The 
 
{
  "packages": [
    {
      "name": "menu",
      "pip": "openimis-be-menu==v1.8.0"
    },
    {
      "name": "core",
      "pip": "git+https://github.com/openimis/openimis-be-core_py.git@develop#egg=openimis-be-core"
    }
  ]
}{
  "packages": [
    {
      "name": "CoreModule",
      "npm": "@openimis/fe-core@>=v1.7.1"
    },
    {
      "name": "GrievanceSocialProtectionModule",
      "npm": "@openimis/fe-grievance_social_protection@https://github.com/openimis/openimis-fe-grievance_social_protection_js#develop"
    }
  ]
}The build_solution.py script processes multiple JSON module files to generate a consolidated roles file:
generated-roles.json – Contains merged roles with unique permissions.
- Extract roles from each module's JSON file.
 - Merge duplicate role names, ensuring unique permissions are combined.
 - Map permissions using 
permissions_map.jsonto include bothcodeandnamefields. - Sort permissions within each role for better readability.
 - Save results to 
generated-roles.json. 
Each module file should contain a roles section like this:
{
  "roles": [
    {
      "roleName": "LOCAL Administrator",
      "code": "local_admin",
      "permissions": [
        "individual.read_individual",
        "individual.create_individual",
        "individual.update_individual",
        "individual.delete_individual"
      ]
    }
  ]
}- If multiple modules define the same 
roleName, their permissions are merged (no duplicates). - Roles retain the same 
codefrom their original definitions. - The final output consolidates all role definitions into a single 
generated-roles.jsonfile. - Permissions are mapped to include 
codeandnameusingpermissions_map.json. 
{
  "roles": [
    {
      "roleName": "LOCAL Administrator",
      "code": "local_admin",
      "permissions": [
        "grievance.create_grievance",
        "grievance.update_grievance"
      ]
    }
  ]
}{
  "roles": [
    {
      "roleName": "LOCAL Administrator",
      "code": "local_admin",
      "permissions": [
        "grievance.delete_grievance",
        "grievance.read_grievance"
      ]
    }
  ]
}{
  "roles": [
    {
      "roleName": "LOCAL Administrator",
      "code": "local_admin",
      "permissions": [
        {
          "name": "grievance.create_grievance",
          "code": "127001"
        },
        {
          "name": "grievance.update_grievance",
          "code": "127002"
        },
        {
          "name": "grievance.delete_grievance",
          "code": "127003"
        },
        {
          "name": "grievance.read_grievance",
          "code": "127000"
        }
      ]
    }
  ]
}- 
Load Modules
- Extract roles from each module’s JSON file.
 - Each module contains a list of roles with 
roleName,code, and associatedpermissions. 
 - 
Merge Roles
- If the same 
roleNameappears in multiple modules, their permissions are combined. - Duplicates within the permission lists are removed.
 - The 
coderemains the same as defined in the original modules. 
 - If the same 
 - 
Map Permissions
- Each permission string is replaced with a dictionary containing:
"name": The original permission string."code": The mapped value frompermissions_map.json.
 - If no mapping exists, the permission remains unchanged.
 
 - Each permission string is replaced with a dictionary containing:
 - 
Generate Output
- The final list of merged roles is structured into a single JSON object.
 - The processed roles are saved in 
generated-roles.json. 
 
The processed roles are saved in:
generated-roles.json
This file consolidates all roles across modules, ensuring a structured and non-duplicated set of permissions. Each role in the final output contains:
roleName: The name of the role.code: The unique code identifier for the role.permissions: A merged list of permissions from all modules, each containing:name: The permission name.code: The mapped permission code frompermissions_map.json.
The script supports Docker service configuration through a service.json file. This allows defining service solutions, environment files, and Compose YAML file structures. The script generates compose.yml as output, ensuring correct Docker service configuration.
The service.json file should be structured as an array of service definitions, where each entry contains:
path: The Compose YAML file path.env_file: A list of environment files required for the service.
[
    {
        "path": "compose.base.yml",
        "env_file": [
            ".env",
            ".env.redis",
            ".env.openSearch"
        ]
    },
    {
        "path": "compose.${DB_DEFAULT:-postgresql}.yml",
        "env_file": [
            ".env.database"
        ]
    },
    {
        "path": "compose.openSearch.yml",
        "env_file": []
    },
    {
        "path": "compose.cache.yml",
        "env_file": [
            ".env",
            ".env.redis"
        ]
    }
]After processing service.json, the script generates compose.yml, ensuring correct formatting and indentation.
include:
  - path: compose.base.yml
    env_file:
      - .env
      - .env.redis
      - .env.openSearch
  - path: compose.${DB_DEFAULT:-postgresql}.yml
    env_file:
      - .env.database
  - path: compose.openSearch.yml
    env_file: []
  - path: compose.cache.yml
    env_file:
      - .env
      - .env.redis- Reads 
service.json– Extracts service definitions, includingpathandenv_file. - Formats the data – Ensures proper indentation and list formatting for YAML output.
 - Generates 
compose.yml– Writes the structured YAML file for Docker Compose. 
Ensure service.json is present, then execute:
python build_solution.pyThis will generate generated-services.yml in the same directory.
This document provides guidelines on processing Role and RoleRight fixtures for data initialization, ensuring foreign key relationships are properly resolved when using Django fixtures.
To load a standard fixture (e.g., role.json) containing predefined Role data, use:
python manage.py loaddata role.jsonThis command loads role data into the database.
Since RoleRight references Role via a foreign key, but fixtures may store relationships using a natural key (e.g., uuid, name), we use a custom command to resolve and replace these references with actual database IDs.
This command allows loading fixtures while resolving foreign key references using a specified field.
python manage.py load_fixture_foreign_key <fixture_file> <field_name><fixture_file>: Path to the fixture file (e.g.,fixtures/core/roles-right.json)<field_name>: The field to use as the natural key for resolving foreign keys (e.g.,uuid,name)
python manage.py load_fixture_foreign_key fixtures/core/roles-right.json uuidThis command:
- Reads the fixture file.
 - Looks up foreign key references in the related model (e.g., 
Role). - Replaces the natural key field (e.g., 
uuid) with the actual primary key (id). - Loads the modified fixture into the database.
 
- Ensure that the related objects exist in the database before loading fixtures that reference them.
 - The command supports multiple fields as natural keys (e.g., 
uuid,name, etc.), as specified by the user. 
For other fixtures, the standard Django loaddata command can be used:
python manage.py loaddata <fixture_file>For example:
python manage.py loaddata fixtures/core/users.jsonThis ensures the fixture data is loaded directly into the database.