odoo-devops-tools 1.23.0__tar.gz → 1.23.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: odoo-devops-tools
3
- Version: 1.23.0
3
+ Version: 1.23.2
4
4
  Summary: Build reproducible Odoo workspaces from a single project definition.
5
5
  Author: Roman Lacko
6
6
  License-Expression: MIT
@@ -120,7 +120,7 @@ Choose the workflow that fits your environment. Both use the same `odoo-project.
120
120
  Create a new workspace:
121
121
 
122
122
  ```bash
123
- odoo-compose --init-project ./odoo19
123
+ odoo-compose --init ./odoo19
124
124
  ```
125
125
 
126
126
  Start the Docker Compose environment:
@@ -238,6 +238,7 @@ On Unix-like systems:
238
238
  docker/local/scripts/
239
239
  ├── backup-db.sh
240
240
  ├── restore-db.sh
241
+ ├── neutralize-db.sh
241
242
  ├── backup-filestore.sh
242
243
  ├── restore-filestore.sh
243
244
  ├── restic.sh
@@ -281,6 +282,21 @@ Restore a database dump with `pg_restore`:
281
282
  ./docker/local/scripts/restore-db.sh ./odoo-backups/pre-upgrade.dump
282
283
  ```
283
284
 
285
+ Database restores use copy semantics by default, matching `click-odoo-restoredb --copy`.
286
+ After `pg_restore`, Odoo database identity parameters are regenerated so the restored
287
+ database can coexist with the source database:
288
+
289
+ ```bash
290
+ ./docker/local/scripts/restore-db.sh --copy ./odoo-backups/pre-upgrade.dump
291
+ ```
292
+
293
+ Use `--move` only when the restored database replaces the original database and its
294
+ identity must be preserved:
295
+
296
+ ```bash
297
+ ./docker/local/scripts/restore-db.sh --move ./odoo-backups/pre-upgrade.dump
298
+ ```
299
+
284
300
  If the target database already exists, the restore is rejected unless `--force` is used. With `--force`, the existing database is dropped and recreated before the dump is restored. When restoring the database currently used by the Odoo service, stop Odoo first and start it again after the restore:
285
301
 
286
302
  ```bash
@@ -295,7 +311,37 @@ To restore the same dump into another database without replacing the default dat
295
311
  ODOO_DB_NAME=odoo_restore ./docker/local/scripts/restore-db.sh ./odoo-backups/pre-upgrade.dump
296
312
  ```
297
313
 
298
- #### 1.3.3. Filestore backup
314
+ #### 1.3.3. Database neutralization
315
+
316
+ For development or test databases restored from production, run the generated neutralization helper explicitly:
317
+
318
+ ```bash
319
+ ./docker/local/scripts/neutralize-db.sh
320
+ ```
321
+
322
+ Override the target database with `ODOO_DB_NAME` when needed:
323
+
324
+ ```bash
325
+ ODOO_DB_NAME=odoo_test ./docker/local/scripts/neutralize-db.sh
326
+ ```
327
+
328
+ For Odoo 16.0 and newer, the helper uses Odoo's native database neutralization mechanism. This applies the `neutralize.sql` scripts provided by installed Odoo modules, disabling or redirecting production-side effects such as scheduled actions, outgoing email, and supported external integrations.
329
+
330
+ For Odoo 13.0 through 15.0, where the native neutralization framework is not available, the helper applies a deliberately minimal compatibility neutralization through the Odoo ORM: it disables scheduled actions, configured outgoing mail servers, and configured incoming mail servers.
331
+
332
+ Neutralization is intentionally separate from database restore. A typical test/staging restore workflow is therefore:
333
+
334
+ ```bash
335
+ docker compose stop odoo
336
+ ./docker/local/scripts/restore-db.sh --copy ./odoo-backups/production.dump
337
+ ./docker/local/scripts/restore-filestore.sh ./odoo-backups/production-filestore.tar.gz
338
+ ./docker/local/scripts/neutralize-db.sh
339
+ docker compose up -d odoo
340
+ ```
341
+
342
+ `--copy` / `--move` and neutralization serve different purposes: copy/move controls restored database identity, while neutralization controls production side effects.
343
+
344
+ #### 1.3.4. Filestore backup
299
345
 
300
346
  Create a compressed tar archive of the selected database filestore:
301
347
 
@@ -326,7 +372,7 @@ docker compose stop odoo
326
372
  docker compose up -d odoo
327
373
  ```
328
374
 
329
- #### 1.3.4. Filestore restore
375
+ #### 1.3.5. Filestore restore
330
376
 
331
377
  Restore a filestore archive:
332
378
 
@@ -350,7 +396,7 @@ ODOO_DB_NAME=odoo_restore ./docker/local/scripts/restore-filestore.sh ./odoo-bac
350
396
 
351
397
  Database and filestore backups are intentionally separate. This allows either part to be restored independently while still making it possible to create matching database and filestore backups when both are needed.
352
398
 
353
- #### 1.3.5. Restic filestore backup
399
+ #### 1.3.6. Restic filestore backup
354
400
 
355
401
  The generated Docker image also includes `restic`. Restic is an additional option intended especially for large filestores where incremental snapshots and deduplication are useful.
356
402
 
@@ -398,7 +444,7 @@ List or inspect snapshots through the generic wrapper:
398
444
  ./docker/local/scripts/restic.sh check
399
445
  ```
400
446
 
401
- #### 1.3.6. Restic filestore restore
447
+ #### 1.3.7. Restic filestore restore
402
448
 
403
449
  Restore the latest matching filestore snapshot:
404
450
 
@@ -468,7 +514,7 @@ In this workflow, Odoo runs directly on the host using a generated Python virtua
468
514
  Create the workspace with the Odoo source and virtual environment:
469
515
 
470
516
  ```bash
471
- odoo-compose --init-project ./odoo19 --sync-all --create-venv \
517
+ odoo-compose --init ./odoo19 --sync-all --create-venv \
472
518
  --set config:db_host=127.0.0.1 \
473
519
  --set config:db_name=odoo \
474
520
  --set config:db_user=odoo \
@@ -906,7 +952,7 @@ The merged project file is saved as `ROOT/odoo-project.ini`, replacing any exist
906
952
  ### Paths and outputs
907
953
 
908
954
  - `--root ROOT` — workspace root directory. Default: the directory containing a local INI file, or the current working directory for a remote INI or omitted INI. In include mode, the default is the directory of the first local source, or the current working directory when the first source is remote.
909
- - `--init-project [ROOT]` — create `ROOT/odoo-project.ini` from the bundled default template if it does not already exist. `ROOT` is optional; when supplied, it is a shorthand for selecting the workspace root directly, for example `odoo-compose --init-project ./odoo19`. When the optional value is omitted, `--root ROOT` remains supported for backward compatibility. Do not supply both `--init-project ROOT` and `--root ROOT`; the command exits with an error instead of choosing one implicitly. This option is valid only when `INI` is omitted and no `-i/--include` is provided. Existing project files are not overwritten.
955
+ - `--init [ROOT]` — create `ROOT/odoo-project.ini` from the bundled default template if it does not already exist. `ROOT` is optional; when supplied, it is a shorthand for selecting the workspace root directly, for example `odoo-compose --init ./odoo19`. When the optional value is omitted, `--root ROOT` remains supported for backward compatibility. Do not supply both `--init ROOT` and `--root ROOT`; the command exits with an error instead of choosing one implicitly. This option is valid only when `INI` is omitted and no `-i/--include` is provided. Existing project files are not overwritten.
910
956
  - `--include INI`, `-i INI` — include an additional project INI layer; can be repeated. Later layers override earlier layers.
911
957
  - `--extra-var KEY=VALUE`, `-e KEY=VALUE` — override or inject a value in the optional `[vars]` section; can be repeated.
912
958
  - `--set SECTION:KEY=VALUE`, `-S SECTION:KEY=VALUE` — set or override a supported project option; can be repeated. Missing supported sections/options are created automatically. Structured sections (`[virtualenv]`, `[odoo]`, `[addons.<name>]`, and `[docker]`) accept only documented keys; `[config]` remains open to standard Odoo configuration options except `addons_path`.
@@ -977,7 +1023,7 @@ The following sections are supported:
977
1023
 
978
1024
  ### General rules
979
1025
 
980
- - The project file can have any filename when passed explicitly. When `INI` is omitted, `odoo-compose` uses the existing `ROOT/odoo-project.ini`; if it is missing, use `--init-project` to create it explicitly from the bundled default template. Remote INI sources and merged include layers are materialized as `ROOT/odoo-project.ini`.
1026
+ - The project file can have any filename when passed explicitly. When `INI` is omitted, `odoo-compose` uses the existing `ROOT/odoo-project.ini`; if it is missing, use `--init` to create it explicitly from the bundled default template. Remote INI sources and merged include layers are materialized as `ROOT/odoo-project.ini`.
981
1027
  - INI interpolation is supported, so values such as `${odoo:version}` can be reused across sections.
982
1028
  - Multiple INI layers can be composed with `-i/--include`. Later layers override earlier layers; multi-line values are replaced as whole option values, not appended.
983
1029
  - The optional `[vars]` section is useful for reusable values referenced as `${vars:name}`.
@@ -92,7 +92,7 @@ Choose the workflow that fits your environment. Both use the same `odoo-project.
92
92
  Create a new workspace:
93
93
 
94
94
  ```bash
95
- odoo-compose --init-project ./odoo19
95
+ odoo-compose --init ./odoo19
96
96
  ```
97
97
 
98
98
  Start the Docker Compose environment:
@@ -210,6 +210,7 @@ On Unix-like systems:
210
210
  docker/local/scripts/
211
211
  ├── backup-db.sh
212
212
  ├── restore-db.sh
213
+ ├── neutralize-db.sh
213
214
  ├── backup-filestore.sh
214
215
  ├── restore-filestore.sh
215
216
  ├── restic.sh
@@ -253,6 +254,21 @@ Restore a database dump with `pg_restore`:
253
254
  ./docker/local/scripts/restore-db.sh ./odoo-backups/pre-upgrade.dump
254
255
  ```
255
256
 
257
+ Database restores use copy semantics by default, matching `click-odoo-restoredb --copy`.
258
+ After `pg_restore`, Odoo database identity parameters are regenerated so the restored
259
+ database can coexist with the source database:
260
+
261
+ ```bash
262
+ ./docker/local/scripts/restore-db.sh --copy ./odoo-backups/pre-upgrade.dump
263
+ ```
264
+
265
+ Use `--move` only when the restored database replaces the original database and its
266
+ identity must be preserved:
267
+
268
+ ```bash
269
+ ./docker/local/scripts/restore-db.sh --move ./odoo-backups/pre-upgrade.dump
270
+ ```
271
+
256
272
  If the target database already exists, the restore is rejected unless `--force` is used. With `--force`, the existing database is dropped and recreated before the dump is restored. When restoring the database currently used by the Odoo service, stop Odoo first and start it again after the restore:
257
273
 
258
274
  ```bash
@@ -267,7 +283,37 @@ To restore the same dump into another database without replacing the default dat
267
283
  ODOO_DB_NAME=odoo_restore ./docker/local/scripts/restore-db.sh ./odoo-backups/pre-upgrade.dump
268
284
  ```
269
285
 
270
- #### 1.3.3. Filestore backup
286
+ #### 1.3.3. Database neutralization
287
+
288
+ For development or test databases restored from production, run the generated neutralization helper explicitly:
289
+
290
+ ```bash
291
+ ./docker/local/scripts/neutralize-db.sh
292
+ ```
293
+
294
+ Override the target database with `ODOO_DB_NAME` when needed:
295
+
296
+ ```bash
297
+ ODOO_DB_NAME=odoo_test ./docker/local/scripts/neutralize-db.sh
298
+ ```
299
+
300
+ For Odoo 16.0 and newer, the helper uses Odoo's native database neutralization mechanism. This applies the `neutralize.sql` scripts provided by installed Odoo modules, disabling or redirecting production-side effects such as scheduled actions, outgoing email, and supported external integrations.
301
+
302
+ For Odoo 13.0 through 15.0, where the native neutralization framework is not available, the helper applies a deliberately minimal compatibility neutralization through the Odoo ORM: it disables scheduled actions, configured outgoing mail servers, and configured incoming mail servers.
303
+
304
+ Neutralization is intentionally separate from database restore. A typical test/staging restore workflow is therefore:
305
+
306
+ ```bash
307
+ docker compose stop odoo
308
+ ./docker/local/scripts/restore-db.sh --copy ./odoo-backups/production.dump
309
+ ./docker/local/scripts/restore-filestore.sh ./odoo-backups/production-filestore.tar.gz
310
+ ./docker/local/scripts/neutralize-db.sh
311
+ docker compose up -d odoo
312
+ ```
313
+
314
+ `--copy` / `--move` and neutralization serve different purposes: copy/move controls restored database identity, while neutralization controls production side effects.
315
+
316
+ #### 1.3.4. Filestore backup
271
317
 
272
318
  Create a compressed tar archive of the selected database filestore:
273
319
 
@@ -298,7 +344,7 @@ docker compose stop odoo
298
344
  docker compose up -d odoo
299
345
  ```
300
346
 
301
- #### 1.3.4. Filestore restore
347
+ #### 1.3.5. Filestore restore
302
348
 
303
349
  Restore a filestore archive:
304
350
 
@@ -322,7 +368,7 @@ ODOO_DB_NAME=odoo_restore ./docker/local/scripts/restore-filestore.sh ./odoo-bac
322
368
 
323
369
  Database and filestore backups are intentionally separate. This allows either part to be restored independently while still making it possible to create matching database and filestore backups when both are needed.
324
370
 
325
- #### 1.3.5. Restic filestore backup
371
+ #### 1.3.6. Restic filestore backup
326
372
 
327
373
  The generated Docker image also includes `restic`. Restic is an additional option intended especially for large filestores where incremental snapshots and deduplication are useful.
328
374
 
@@ -370,7 +416,7 @@ List or inspect snapshots through the generic wrapper:
370
416
  ./docker/local/scripts/restic.sh check
371
417
  ```
372
418
 
373
- #### 1.3.6. Restic filestore restore
419
+ #### 1.3.7. Restic filestore restore
374
420
 
375
421
  Restore the latest matching filestore snapshot:
376
422
 
@@ -440,7 +486,7 @@ In this workflow, Odoo runs directly on the host using a generated Python virtua
440
486
  Create the workspace with the Odoo source and virtual environment:
441
487
 
442
488
  ```bash
443
- odoo-compose --init-project ./odoo19 --sync-all --create-venv \
489
+ odoo-compose --init ./odoo19 --sync-all --create-venv \
444
490
  --set config:db_host=127.0.0.1 \
445
491
  --set config:db_name=odoo \
446
492
  --set config:db_user=odoo \
@@ -878,7 +924,7 @@ The merged project file is saved as `ROOT/odoo-project.ini`, replacing any exist
878
924
  ### Paths and outputs
879
925
 
880
926
  - `--root ROOT` — workspace root directory. Default: the directory containing a local INI file, or the current working directory for a remote INI or omitted INI. In include mode, the default is the directory of the first local source, or the current working directory when the first source is remote.
881
- - `--init-project [ROOT]` — create `ROOT/odoo-project.ini` from the bundled default template if it does not already exist. `ROOT` is optional; when supplied, it is a shorthand for selecting the workspace root directly, for example `odoo-compose --init-project ./odoo19`. When the optional value is omitted, `--root ROOT` remains supported for backward compatibility. Do not supply both `--init-project ROOT` and `--root ROOT`; the command exits with an error instead of choosing one implicitly. This option is valid only when `INI` is omitted and no `-i/--include` is provided. Existing project files are not overwritten.
927
+ - `--init [ROOT]` — create `ROOT/odoo-project.ini` from the bundled default template if it does not already exist. `ROOT` is optional; when supplied, it is a shorthand for selecting the workspace root directly, for example `odoo-compose --init ./odoo19`. When the optional value is omitted, `--root ROOT` remains supported for backward compatibility. Do not supply both `--init ROOT` and `--root ROOT`; the command exits with an error instead of choosing one implicitly. This option is valid only when `INI` is omitted and no `-i/--include` is provided. Existing project files are not overwritten.
882
928
  - `--include INI`, `-i INI` — include an additional project INI layer; can be repeated. Later layers override earlier layers.
883
929
  - `--extra-var KEY=VALUE`, `-e KEY=VALUE` — override or inject a value in the optional `[vars]` section; can be repeated.
884
930
  - `--set SECTION:KEY=VALUE`, `-S SECTION:KEY=VALUE` — set or override a supported project option; can be repeated. Missing supported sections/options are created automatically. Structured sections (`[virtualenv]`, `[odoo]`, `[addons.<name>]`, and `[docker]`) accept only documented keys; `[config]` remains open to standard Odoo configuration options except `addons_path`.
@@ -949,7 +995,7 @@ The following sections are supported:
949
995
 
950
996
  ### General rules
951
997
 
952
- - The project file can have any filename when passed explicitly. When `INI` is omitted, `odoo-compose` uses the existing `ROOT/odoo-project.ini`; if it is missing, use `--init-project` to create it explicitly from the bundled default template. Remote INI sources and merged include layers are materialized as `ROOT/odoo-project.ini`.
998
+ - The project file can have any filename when passed explicitly. When `INI` is omitted, `odoo-compose` uses the existing `ROOT/odoo-project.ini`; if it is missing, use `--init` to create it explicitly from the bundled default template. Remote INI sources and merged include layers are materialized as `ROOT/odoo-project.ini`.
953
999
  - INI interpolation is supported, so values such as `${odoo:version}` can be reused across sections.
954
1000
  - Multiple INI layers can be composed with `-i/--include`. Later layers override earlier layers; multi-line values are replaced as whole option values, not appended.
955
1001
  - The optional `[vars]` section is useful for reusable values referenced as `${vars:name}`.
@@ -0,0 +1 @@
1
+ __version__ = "1.23.2"
@@ -3024,10 +3024,11 @@ ROOT_DIR="$(cd "${{SCRIPT_DIR}}/../../.." && pwd)"
3024
3024
  DEFAULT_DB_NAME={default_db_name}
3025
3025
  DB_NAME="${{ODOO_DB_NAME:-${{DEFAULT_DB_NAME}}}}"
3026
3026
  FORCE=false
3027
+ RESTORE_MODE=copy
3027
3028
  SOURCE=""
3028
3029
 
3029
3030
  usage() {{
3030
- echo "Usage: $(basename "$0") [--force] BACKUP.dump" >&2
3031
+ echo "Usage: $(basename "$0") [--force] [--copy|--move] BACKUP.dump" >&2
3031
3032
  }}
3032
3033
 
3033
3034
  while [[ $# -gt 0 ]]; do
@@ -3035,6 +3036,12 @@ while [[ $# -gt 0 ]]; do
3035
3036
  --force)
3036
3037
  FORCE=true
3037
3038
  ;;
3039
+ --copy)
3040
+ RESTORE_MODE=copy
3041
+ ;;
3042
+ --move)
3043
+ RESTORE_MODE=move
3044
+ ;;
3038
3045
  -*)
3039
3046
  echo "ERROR: unknown option: $1" >&2
3040
3047
  usage
@@ -3086,11 +3093,86 @@ docker compose exec -T db \
3086
3093
  pg_restore --exit-on-error --no-owner --no-acl -U odoo -d "${{DB_NAME}}" \
3087
3094
  < "${{SOURCE}}"
3088
3095
 
3096
+ if [[ "${{RESTORE_MODE}}" == "copy" ]]; then
3097
+ echo "INFO: Resetting copied Odoo database identity for '${{DB_NAME}}'."
3098
+ printf '%s\n' 'env["ir.config_parameter"].init(force=True)' | \
3099
+ docker compose run --rm --no-deps -T \
3100
+ --entrypoint click-odoo odoo \
3101
+ -c /etc/odoo/odoo.conf \
3102
+ -d "${{DB_NAME}}" \
3103
+ --log-level=error
3104
+
3105
+ docker compose exec -T db \
3106
+ psql -v ON_ERROR_STOP=1 -U odoo -d "${{DB_NAME}}" <<'SQL'
3107
+ DELETE FROM ir_config_parameter
3108
+ WHERE key = 'database.enterprise_code';
3109
+
3110
+ UPDATE ir_config_parameter
3111
+ SET value = 'copy'
3112
+ WHERE key = 'database.expiration_reason'
3113
+ AND value != 'demo';
3114
+
3115
+ UPDATE ir_config_parameter
3116
+ SET value = CURRENT_DATE + INTERVAL '2 month'
3117
+ WHERE key = 'database.expiration_date';
3118
+ SQL
3119
+ else
3120
+ echo "INFO: Preserving Odoo database identity (--move)."
3121
+ fi
3122
+
3089
3123
  echo "INFO: Database restore completed: ${{DB_NAME}}"
3090
3124
  """
3091
3125
  return _write_docker_local_script(layout, "restore-db", content)
3092
3126
 
3093
3127
 
3128
+ def write_docker_neutralize_db_sh(layout: Layout, cfg: ProjectConfig) -> Path:
3129
+ default_db_name = shlex.quote(_docker_default_db_name(cfg))
3130
+ odoo_major_version = _parse_odoo_version(cfg.odoo.version)
3131
+ if odoo_major_version >= 16:
3132
+ neutralize_command = rf"""
3133
+ echo "INFO: Using native Odoo neutralization for Odoo {odoo_major_version}. "
3134
+ docker compose run --rm --no-deps -T \
3135
+ --entrypoint odoo odoo \
3136
+ neutralize \
3137
+ -c /etc/odoo/odoo.conf \
3138
+ -d "${{DB_NAME}}"
3139
+ """
3140
+ else:
3141
+ neutralize_command = rf"""
3142
+ echo "INFO: Using legacy minimal neutralization for Odoo {odoo_major_version}."
3143
+ printf '%s\n' \
3144
+ '[(model.search([("active", "=", True)]).write({{"active": False}})) for model_name in ("ir.cron", "ir.mail_server", "fetchmail.server") if model_name in env for model in (env[model_name],) if "active" in model._fields]; env.cr.commit()' | \
3145
+ docker compose run --rm --no-deps -T \
3146
+ --entrypoint click-odoo odoo \
3147
+ -c /etc/odoo/odoo.conf \
3148
+ -d "${{DB_NAME}}" \
3149
+ --log-level=error
3150
+ """
3151
+
3152
+ content = fr"""#!/usr/bin/env bash
3153
+ set -euo pipefail
3154
+
3155
+ SCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"
3156
+ ROOT_DIR="$(cd "${{SCRIPT_DIR}}/../../.." && pwd)"
3157
+ DEFAULT_DB_NAME={default_db_name}
3158
+ DB_NAME="${{ODOO_DB_NAME:-${{DEFAULT_DB_NAME}}}}"
3159
+
3160
+ cd "${{ROOT_DIR}}"
3161
+
3162
+ DATABASES="$(docker compose exec -T db \
3163
+ psql -U odoo -d postgres -Atqc "SELECT datname FROM pg_database")"
3164
+ if ! grep -Fxq "${{DB_NAME}}" <<< "${{DATABASES}}"; then
3165
+ echo "ERROR: target database '${{DB_NAME}}' does not exist." >&2
3166
+ exit 1
3167
+ fi
3168
+
3169
+ echo "INFO: Neutralizing Odoo database '${{DB_NAME}}'."
3170
+ {neutralize_command}
3171
+ echo "INFO: Database neutralization completed: ${{DB_NAME}}"
3172
+ """
3173
+ return _write_docker_local_script(layout, "neutralize-db", content)
3174
+
3175
+
3094
3176
  def write_docker_backup_filestore_sh(layout: Layout, cfg: ProjectConfig) -> Path:
3095
3177
  default_db_name = shlex.quote(_docker_default_db_name(cfg))
3096
3178
  content = fr"""#!/usr/bin/env bash
@@ -3446,6 +3528,7 @@ if defined ODOO_DB_NAME (
3446
3528
  set "DB_NAME=%DEFAULT_DB_NAME%"
3447
3529
  )
3448
3530
  set "FORCE=false"
3531
+ set "RESTORE_MODE=copy"
3449
3532
  set "SOURCE="
3450
3533
 
3451
3534
  goto parse_args
@@ -3453,6 +3536,8 @@ goto parse_args
3453
3536
  :parse_args
3454
3537
  if "%~1"=="" goto args_done
3455
3538
  if /i "%~1"=="--force" goto force_arg
3539
+ if /i "%~1"=="--copy" goto copy_arg
3540
+ if /i "%~1"=="--move" goto move_arg
3456
3541
  if "%~1:~0,1%"=="-" goto unknown_option
3457
3542
  if defined SOURCE goto usage_error
3458
3543
  set "SOURCE=%~1"
@@ -3464,6 +3549,16 @@ set "FORCE=true"
3464
3549
  shift
3465
3550
  goto parse_args
3466
3551
 
3552
+ :copy_arg
3553
+ set "RESTORE_MODE=copy"
3554
+ shift
3555
+ goto parse_args
3556
+
3557
+ :move_arg
3558
+ set "RESTORE_MODE=move"
3559
+ shift
3560
+ goto parse_args
3561
+
3467
3562
  :args_done
3468
3563
  if not defined SOURCE goto usage_error
3469
3564
  if not exist "%SOURCE%" (
@@ -3495,6 +3590,18 @@ if errorlevel 1 exit /b 1
3495
3590
  docker compose exec -T db pg_restore --exit-on-error --no-owner --no-acl -U odoo -d "%DB_NAME%" < "%SOURCE%"
3496
3591
  if errorlevel 1 exit /b 1
3497
3592
 
3593
+ if /i "%RESTORE_MODE%"=="move" goto restore_done
3594
+
3595
+ echo INFO: Resetting copied Odoo database identity for '%DB_NAME%'.
3596
+ echo env["ir.config_parameter"].init(force=True^) | docker compose run --rm --no-deps -T --entrypoint click-odoo odoo -c /etc/odoo/odoo.conf -d "%DB_NAME%" --log-level=error
3597
+ if errorlevel 1 exit /b 1
3598
+
3599
+ docker compose exec -T db psql -v ON_ERROR_STOP=1 -U odoo -d "%DB_NAME%" -c "DELETE FROM ir_config_parameter WHERE key = 'database.enterprise_code'; UPDATE ir_config_parameter SET value = 'copy' WHERE key = 'database.expiration_reason' AND value != 'demo'; UPDATE ir_config_parameter SET value = CURRENT_DATE + INTERVAL '2 month' WHERE key = 'database.expiration_date';"
3600
+ if errorlevel 1 exit /b 1
3601
+ goto restore_done
3602
+
3603
+ :restore_done
3604
+ if /i "%RESTORE_MODE%"=="move" echo INFO: Preserving Odoo database identity (--move).
3498
3605
  echo INFO: Database restore completed: %DB_NAME%
3499
3606
  endlocal
3500
3607
  exit /b 0
@@ -3504,12 +3611,60 @@ echo ERROR: unknown option: %~1
3504
3611
  goto usage_error
3505
3612
 
3506
3613
  :usage_error
3507
- echo Usage: %~nx0 [--force] BACKUP.dump
3614
+ echo Usage: %~nx0 [--force] [--copy^|--move] BACKUP.dump
3508
3615
  exit /b 2
3509
3616
  """
3510
3617
  return _write_docker_local_script(layout, "restore-db", content, ext="bat")
3511
3618
 
3512
3619
 
3620
+ def write_docker_neutralize_db_bat(layout: Layout, cfg: ProjectConfig) -> Path:
3621
+ default_db_name = _docker_default_db_name(cfg).replace("%", "%%")
3622
+ odoo_major_version = _parse_odoo_version(cfg.odoo.version)
3623
+ if odoo_major_version >= 16:
3624
+ neutralize_command = rf"""echo INFO: Using native Odoo neutralization for Odoo {odoo_major_version}.
3625
+ docker compose run --rm --no-deps -T --entrypoint odoo odoo neutralize -c /etc/odoo/odoo.conf -d "%DB_NAME%"
3626
+ if errorlevel 1 exit /b 1
3627
+ """
3628
+ else:
3629
+ neutralize_command = rf"""echo INFO: Using legacy minimal neutralization for Odoo {odoo_major_version}.
3630
+ echo [(model.search([("active", "=", True)]).write({{"active": False}})) for model_name in ("ir.cron", "ir.mail_server", "fetchmail.server") if model_name in env for model in (env[model_name],) if "active" in model._fields]; env.cr.commit() | docker compose run --rm --no-deps -T --entrypoint click-odoo odoo -c /etc/odoo/odoo.conf -d "%DB_NAME%" --log-level=error
3631
+ if errorlevel 1 exit /b 1
3632
+ """
3633
+
3634
+ content = rf"""@echo off
3635
+ setlocal
3636
+
3637
+ set "SCRIPT_DIR=%~dp0"
3638
+ if "%SCRIPT_DIR:~-1%"=="\" set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
3639
+ for %%I in ("%SCRIPT_DIR%\..\..\..") do set "ROOT_DIR=%%~fI"
3640
+ set "DEFAULT_DB_NAME={default_db_name}"
3641
+ if defined ODOO_DB_NAME (
3642
+ set "DB_NAME=%ODOO_DB_NAME%"
3643
+ ) else (
3644
+ set "DB_NAME=%DEFAULT_DB_NAME%"
3645
+ )
3646
+
3647
+ cd /d "%ROOT_DIR%" || exit /b 1
3648
+
3649
+ set "DB_EXISTS=false"
3650
+ for /f "usebackq delims=" %%D in (`docker compose exec -T db psql -U odoo -d postgres -Atqc "SELECT datname FROM pg_database"`) do (
3651
+ if "%%D"=="%DB_NAME%" set "DB_EXISTS=true"
3652
+ )
3653
+
3654
+ if not "%DB_EXISTS%"=="true" (
3655
+ echo ERROR: target database '%DB_NAME%' does not exist.
3656
+ exit /b 1
3657
+ )
3658
+
3659
+ echo INFO: Neutralizing Odoo database '%DB_NAME%'.
3660
+ {neutralize_command}
3661
+ echo INFO: Database neutralization completed: %DB_NAME%
3662
+ endlocal
3663
+ exit /b 0
3664
+ """
3665
+ return _write_docker_local_script(layout, "neutralize-db", content, ext="bat")
3666
+
3667
+
3513
3668
  def write_docker_backup_filestore_bat(layout: Layout, cfg: ProjectConfig) -> Path:
3514
3669
  default_db_name = _docker_default_db_name(cfg).replace("%", "%%")
3515
3670
  content = rf"""@echo off
@@ -3874,6 +4029,7 @@ def write_docker_local_scripts(layout: Layout, cfg: ProjectConfig) -> dict[str,
3874
4029
  return {
3875
4030
  "backup_db": write_docker_backup_db_bat(layout, cfg),
3876
4031
  "restore_db": write_docker_restore_db_bat(layout, cfg),
4032
+ "neutralize_db": write_docker_neutralize_db_bat(layout, cfg),
3877
4033
  "backup_filestore": write_docker_backup_filestore_bat(layout, cfg),
3878
4034
  "restore_filestore": write_docker_restore_filestore_bat(layout, cfg),
3879
4035
  "restic": write_docker_restic_bat(layout),
@@ -3884,6 +4040,7 @@ def write_docker_local_scripts(layout: Layout, cfg: ProjectConfig) -> dict[str,
3884
4040
  return {
3885
4041
  "backup_db": write_docker_backup_db_sh(layout, cfg),
3886
4042
  "restore_db": write_docker_restore_db_sh(layout, cfg),
4043
+ "neutralize_db": write_docker_neutralize_db_sh(layout, cfg),
3887
4044
  "backup_filestore": write_docker_backup_filestore_sh(layout, cfg),
3888
4045
  "restore_filestore": write_docker_restore_filestore_sh(layout, cfg),
3889
4046
  "restic": write_docker_restic_sh(layout),
@@ -6090,11 +6247,11 @@ def build_parser() -> argparse.ArgumentParser:
6090
6247
  Examples:
6091
6248
 
6092
6249
  Creating a workspace:
6093
- odoo-compose --init-project ./odoo18-workspace --sync-all --create-venv \\
6250
+ odoo-compose --init ./odoo18-workspace --sync-all --create-venv \\
6094
6251
  --set odoo:version=18.0
6095
6252
 
6096
6253
  Creating a workspace with explicit database settings:
6097
- odoo-compose --init-project ./odoo18-workspace --sync-all --create-venv \\
6254
+ odoo-compose --init ./odoo18-workspace --sync-all --create-venv \\
6098
6255
  --set odoo:version=18.0 \\
6099
6256
  --set config:db_host=127.0.0.1 \\
6100
6257
  --set config:db_name=odoo \\
@@ -6111,7 +6268,7 @@ Examples:
6111
6268
  odoo-compose git+https://github.com/lck/odoo-devops-tools.git//examples/odoo18-minimal.ini?ref=main --sync-all --create-venv
6112
6269
 
6113
6270
  Preparing local Docker development:
6114
- odoo-compose --init-project ./odoo19 --sync-addons
6271
+ odoo-compose --init ./odoo19 --sync-addons
6115
6272
 
6116
6273
  Generating a deploy Docker build context:
6117
6274
  odoo-compose /path/to/odoo-project.ini --sync-addons --create-docker-deploy
@@ -6179,7 +6336,9 @@ Examples:
6179
6336
  )
6180
6337
 
6181
6338
  parser.add_argument(
6339
+ "--init",
6182
6340
  "--init-project",
6341
+ dest="init_project",
6183
6342
  nargs="?",
6184
6343
  const="",
6185
6344
  default=None,
@@ -6188,7 +6347,7 @@ Examples:
6188
6347
  "Create ROOT/odoo-project.ini from the bundled default template if it does not already exist. "
6189
6348
  "An optional ROOT can be supplied directly as shorthand for --root. "
6190
6349
  "If ROOT is supplied here, --root must not also be used. "
6191
- "Without --init-project, an existing ROOT/odoo-project.ini is required."
6350
+ "Without --init, an existing ROOT/odoo-project.ini is required."
6192
6351
  ),
6193
6352
  )
6194
6353
 
@@ -6435,12 +6594,12 @@ def main() -> None:
6435
6594
 
6436
6595
  if init_project_root_raw and args.root:
6437
6596
  parser.error(
6438
- "workspace ROOT was specified both as --init-project ROOT and --root; "
6597
+ "workspace ROOT was specified both as --init ROOT and --root; "
6439
6598
  "use only one of them."
6440
6599
  )
6441
6600
 
6442
6601
  if bool(getattr(args, 'show_last_run', False)) and init_project:
6443
- parser.error("--init-project cannot be used together with --show-last-run.")
6602
+ parser.error("--init cannot be used together with --show-last-run.")
6444
6603
  if bool(getattr(args, 'show_last_run', False)) and getattr(args, 'create_from_bundle', None):
6445
6604
  parser.error("--create-from-bundle cannot be used together with --show-last-run.")
6446
6605
  if bool(getattr(args, 'show_last_run', False)) and getattr(args, 'create_bundle', None) is not None:
@@ -6499,7 +6658,7 @@ def main() -> None:
6499
6658
  if getattr(args, 'include_inis', None):
6500
6659
  conflicting_options.append("--include")
6501
6660
  if init_project:
6502
- conflicting_options.append("--init-project")
6661
+ conflicting_options.append("--init")
6503
6662
  if args.all or args.odoo or args.addons:
6504
6663
  conflicting_options.append("--sync-*")
6505
6664
  if bool(getattr(args, 'create_venv', False)) or create_venv_from_wheelhouse:
@@ -6580,14 +6739,14 @@ def main() -> None:
6580
6739
  project_ini_status: Optional[str] = None
6581
6740
 
6582
6741
  if init_project and not implicit_ini:
6583
- parser.error("--init-project can only be used when INI is omitted and no -i/--include is provided.")
6742
+ parser.error("--init can only be used when INI is omitted and no -i/--include is provided.")
6584
6743
 
6585
6744
  if implicit_ini:
6586
6745
  if init_project_root_raw:
6587
6746
  root_override = _validate_root_override(
6588
6747
  parser,
6589
6748
  init_project_root_raw,
6590
- option_name="--init-project ROOT",
6749
+ option_name="--init ROOT",
6591
6750
  )
6592
6751
  elif args.root:
6593
6752
  root_override = _validate_root_override(parser, args.root)
@@ -6624,7 +6783,7 @@ def main() -> None:
6624
6783
  project_ini_status = f"used existing ROOT/{_DEFAULT_PROJECT_INI_NAME}"
6625
6784
  _logger.info("Project INI already exists, leaving it in place: %s", ini_path)
6626
6785
  elif not ini_path.is_file():
6627
- suggested_cmd = ["odoo-compose", "--init-project"]
6786
+ suggested_cmd = ["odoo-compose", "--init"]
6628
6787
  if args.root:
6629
6788
  suggested_cmd.append(str(root_override))
6630
6789
  parser.error(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: odoo-devops-tools
3
- Version: 1.23.0
3
+ Version: 1.23.2
4
4
  Summary: Build reproducible Odoo workspaces from a single project definition.
5
5
  Author: Roman Lacko
6
6
  License-Expression: MIT
@@ -120,7 +120,7 @@ Choose the workflow that fits your environment. Both use the same `odoo-project.
120
120
  Create a new workspace:
121
121
 
122
122
  ```bash
123
- odoo-compose --init-project ./odoo19
123
+ odoo-compose --init ./odoo19
124
124
  ```
125
125
 
126
126
  Start the Docker Compose environment:
@@ -238,6 +238,7 @@ On Unix-like systems:
238
238
  docker/local/scripts/
239
239
  ├── backup-db.sh
240
240
  ├── restore-db.sh
241
+ ├── neutralize-db.sh
241
242
  ├── backup-filestore.sh
242
243
  ├── restore-filestore.sh
243
244
  ├── restic.sh
@@ -281,6 +282,21 @@ Restore a database dump with `pg_restore`:
281
282
  ./docker/local/scripts/restore-db.sh ./odoo-backups/pre-upgrade.dump
282
283
  ```
283
284
 
285
+ Database restores use copy semantics by default, matching `click-odoo-restoredb --copy`.
286
+ After `pg_restore`, Odoo database identity parameters are regenerated so the restored
287
+ database can coexist with the source database:
288
+
289
+ ```bash
290
+ ./docker/local/scripts/restore-db.sh --copy ./odoo-backups/pre-upgrade.dump
291
+ ```
292
+
293
+ Use `--move` only when the restored database replaces the original database and its
294
+ identity must be preserved:
295
+
296
+ ```bash
297
+ ./docker/local/scripts/restore-db.sh --move ./odoo-backups/pre-upgrade.dump
298
+ ```
299
+
284
300
  If the target database already exists, the restore is rejected unless `--force` is used. With `--force`, the existing database is dropped and recreated before the dump is restored. When restoring the database currently used by the Odoo service, stop Odoo first and start it again after the restore:
285
301
 
286
302
  ```bash
@@ -295,7 +311,37 @@ To restore the same dump into another database without replacing the default dat
295
311
  ODOO_DB_NAME=odoo_restore ./docker/local/scripts/restore-db.sh ./odoo-backups/pre-upgrade.dump
296
312
  ```
297
313
 
298
- #### 1.3.3. Filestore backup
314
+ #### 1.3.3. Database neutralization
315
+
316
+ For development or test databases restored from production, run the generated neutralization helper explicitly:
317
+
318
+ ```bash
319
+ ./docker/local/scripts/neutralize-db.sh
320
+ ```
321
+
322
+ Override the target database with `ODOO_DB_NAME` when needed:
323
+
324
+ ```bash
325
+ ODOO_DB_NAME=odoo_test ./docker/local/scripts/neutralize-db.sh
326
+ ```
327
+
328
+ For Odoo 16.0 and newer, the helper uses Odoo's native database neutralization mechanism. This applies the `neutralize.sql` scripts provided by installed Odoo modules, disabling or redirecting production-side effects such as scheduled actions, outgoing email, and supported external integrations.
329
+
330
+ For Odoo 13.0 through 15.0, where the native neutralization framework is not available, the helper applies a deliberately minimal compatibility neutralization through the Odoo ORM: it disables scheduled actions, configured outgoing mail servers, and configured incoming mail servers.
331
+
332
+ Neutralization is intentionally separate from database restore. A typical test/staging restore workflow is therefore:
333
+
334
+ ```bash
335
+ docker compose stop odoo
336
+ ./docker/local/scripts/restore-db.sh --copy ./odoo-backups/production.dump
337
+ ./docker/local/scripts/restore-filestore.sh ./odoo-backups/production-filestore.tar.gz
338
+ ./docker/local/scripts/neutralize-db.sh
339
+ docker compose up -d odoo
340
+ ```
341
+
342
+ `--copy` / `--move` and neutralization serve different purposes: copy/move controls restored database identity, while neutralization controls production side effects.
343
+
344
+ #### 1.3.4. Filestore backup
299
345
 
300
346
  Create a compressed tar archive of the selected database filestore:
301
347
 
@@ -326,7 +372,7 @@ docker compose stop odoo
326
372
  docker compose up -d odoo
327
373
  ```
328
374
 
329
- #### 1.3.4. Filestore restore
375
+ #### 1.3.5. Filestore restore
330
376
 
331
377
  Restore a filestore archive:
332
378
 
@@ -350,7 +396,7 @@ ODOO_DB_NAME=odoo_restore ./docker/local/scripts/restore-filestore.sh ./odoo-bac
350
396
 
351
397
  Database and filestore backups are intentionally separate. This allows either part to be restored independently while still making it possible to create matching database and filestore backups when both are needed.
352
398
 
353
- #### 1.3.5. Restic filestore backup
399
+ #### 1.3.6. Restic filestore backup
354
400
 
355
401
  The generated Docker image also includes `restic`. Restic is an additional option intended especially for large filestores where incremental snapshots and deduplication are useful.
356
402
 
@@ -398,7 +444,7 @@ List or inspect snapshots through the generic wrapper:
398
444
  ./docker/local/scripts/restic.sh check
399
445
  ```
400
446
 
401
- #### 1.3.6. Restic filestore restore
447
+ #### 1.3.7. Restic filestore restore
402
448
 
403
449
  Restore the latest matching filestore snapshot:
404
450
 
@@ -468,7 +514,7 @@ In this workflow, Odoo runs directly on the host using a generated Python virtua
468
514
  Create the workspace with the Odoo source and virtual environment:
469
515
 
470
516
  ```bash
471
- odoo-compose --init-project ./odoo19 --sync-all --create-venv \
517
+ odoo-compose --init ./odoo19 --sync-all --create-venv \
472
518
  --set config:db_host=127.0.0.1 \
473
519
  --set config:db_name=odoo \
474
520
  --set config:db_user=odoo \
@@ -906,7 +952,7 @@ The merged project file is saved as `ROOT/odoo-project.ini`, replacing any exist
906
952
  ### Paths and outputs
907
953
 
908
954
  - `--root ROOT` — workspace root directory. Default: the directory containing a local INI file, or the current working directory for a remote INI or omitted INI. In include mode, the default is the directory of the first local source, or the current working directory when the first source is remote.
909
- - `--init-project [ROOT]` — create `ROOT/odoo-project.ini` from the bundled default template if it does not already exist. `ROOT` is optional; when supplied, it is a shorthand for selecting the workspace root directly, for example `odoo-compose --init-project ./odoo19`. When the optional value is omitted, `--root ROOT` remains supported for backward compatibility. Do not supply both `--init-project ROOT` and `--root ROOT`; the command exits with an error instead of choosing one implicitly. This option is valid only when `INI` is omitted and no `-i/--include` is provided. Existing project files are not overwritten.
955
+ - `--init [ROOT]` — create `ROOT/odoo-project.ini` from the bundled default template if it does not already exist. `ROOT` is optional; when supplied, it is a shorthand for selecting the workspace root directly, for example `odoo-compose --init ./odoo19`. When the optional value is omitted, `--root ROOT` remains supported for backward compatibility. Do not supply both `--init ROOT` and `--root ROOT`; the command exits with an error instead of choosing one implicitly. This option is valid only when `INI` is omitted and no `-i/--include` is provided. Existing project files are not overwritten.
910
956
  - `--include INI`, `-i INI` — include an additional project INI layer; can be repeated. Later layers override earlier layers.
911
957
  - `--extra-var KEY=VALUE`, `-e KEY=VALUE` — override or inject a value in the optional `[vars]` section; can be repeated.
912
958
  - `--set SECTION:KEY=VALUE`, `-S SECTION:KEY=VALUE` — set or override a supported project option; can be repeated. Missing supported sections/options are created automatically. Structured sections (`[virtualenv]`, `[odoo]`, `[addons.<name>]`, and `[docker]`) accept only documented keys; `[config]` remains open to standard Odoo configuration options except `addons_path`.
@@ -977,7 +1023,7 @@ The following sections are supported:
977
1023
 
978
1024
  ### General rules
979
1025
 
980
- - The project file can have any filename when passed explicitly. When `INI` is omitted, `odoo-compose` uses the existing `ROOT/odoo-project.ini`; if it is missing, use `--init-project` to create it explicitly from the bundled default template. Remote INI sources and merged include layers are materialized as `ROOT/odoo-project.ini`.
1026
+ - The project file can have any filename when passed explicitly. When `INI` is omitted, `odoo-compose` uses the existing `ROOT/odoo-project.ini`; if it is missing, use `--init` to create it explicitly from the bundled default template. Remote INI sources and merged include layers are materialized as `ROOT/odoo-project.ini`.
981
1027
  - INI interpolation is supported, so values such as `${odoo:version}` can be reused across sections.
982
1028
  - Multiple INI layers can be composed with `-i/--include`. Later layers override earlier layers; multi-line values are replaced as whole option values, not appended.
983
1029
  - The optional `[vars]` section is useful for reusable values referenced as `${vars:name}`.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "odoo-devops-tools"
7
- version = "1.23.0"
7
+ version = "1.23.2"
8
8
  description = "Build reproducible Odoo workspaces from a single project definition."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1 +0,0 @@
1
- __version__ = "1.23.0"