Compare commits

8 Commits

Author SHA1 Message Date
Stéphan Sainléger
516b9def3d [IMP] add external command verification at startup
Add check_required_commands() function to verify that all required
external tools are available before the script begins execution:
- docker: Container runtime
- compose: Docker compose wrapper (0k-scripts)
- sudo: Required for filestore operations

Benefits:
- Fails fast with a clear error message listing missing commands
- Prevents cryptic 'command not found' errors mid-execution
- Documents script dependencies explicitly
- Called immediately after argument validation in upgrade.sh
2026-02-02 20:06:27 +01:00
Stéphan Sainléger
20214b4402 [IMP] apply naming conventions for variables
Apply consistent naming conventions throughout upgrade.sh:
- UPPERCASE + readonly for script-level constants (immutable values)
- lowercase for temporary/local variables within the script flow

Constants marked readonly:
- ORIGIN_VERSION, FINAL_VERSION, ORIGIN_DB_NAME, ORIGIN_SERVICE_NAME
- COPY_DB_NAME, FINALE_DB_NAME, FINALE_SERVICE_NAME
- POSTGRES_SERVICE_NAME

Local variables renamed to lowercase:
- postgres_containers, postgres_count (detection phase)
- db_exists, filestore_path (validation phase)

This convention makes it immediately clear which variables are
configuration constants vs runtime values, and prevents accidental
modification of critical values.
2026-02-02 20:06:27 +01:00
Stéphan Sainléger
fa8b1b98f1 [IMP] use mktemp and trap for temporary file cleanup
Replace hardcoded temporary file paths with mktemp -d for secure
temporary directory creation, and add a trap to automatically clean
up on script exit (success, failure, or interruption).

Benefits:
- Automatic cleanup even on Ctrl+C or script errors
- No leftover temporary files in the working directory
- Secure temporary directory creation (proper permissions)
- Files isolated in dedicated temp directory

Added '|| true' to grep command since it returns exit code 1 when
no matches are found, which would trigger set -e otherwise.
2026-02-02 20:06:27 +01:00
Stéphan Sainléger
7c3d5d49c8 [IMP] use heredoc with variable expansion for SQL query
Convert the SQL_404_ADDONS_LIST query from a quoted string to a heredoc
without quotes (<<EOF instead of <<'EOF') to make variable expansion
explicit and consistent with other SQL blocks in the codebase.

Key difference between heredoc variants:
- <<'EOF': Literal content, no variable expansion (use for static SQL)
- <<EOF: Variables like ${FINALE_DB_NAME} are expanded (use when needed)

Also improved SQL formatting for better readability.
2026-02-02 20:06:27 +01:00
Stéphan Sainléger
333360f9f0 [IMP] add structured logging functions
Add logging functions to lib/common.sh for consistent output formatting:
- log_info(): Standard informational messages with [INFO] prefix
- log_warn(): Warning messages to stderr with [WARN] prefix
- log_error(): Error messages to stderr with [ERROR] prefix
- log_step(): Section headers with visual separators

Update upgrade.sh to use these functions throughout, replacing ad-hoc
echo statements. This provides:
- Consistent visual formatting across all scripts
- Clear distinction between info, warnings and errors
- Errors properly sent to stderr
- Easier log parsing and filtering

Also removed redundant '|| exit 1' statements since set -e handles
command failures automatically.
2026-02-02 20:06:27 +01:00
Stéphan Sainléger
4b27955be9 [IMP] centralize common functions in lib/common.sh
Extract shared utility functions into a dedicated library file:
- query_postgres_container: Execute SQL queries in postgres container
- copy_database: Copy database using pgm
- copy_filestore: Copy Odoo filestore directory
- exec_python_script_in_odoo_shell: Run Python scripts in Odoo shell

Benefits:
- Single source of truth for utility functions
- Easier maintenance and testing
- Consistent behavior across all scripts
- Reduced code duplication

Also introduces readonly constants DATASTORE_PATH and FILESTORE_SUBPATH
to avoid hardcoded paths scattered throughout the codebase.
2026-02-02 20:06:27 +01:00
Stéphan Sainléger
a91afa60d3 [IMP] fix undefined variable DB_CONTAINER_NAME
Replace $DB_CONTAINER_NAME with $POSTGRES_SERVICE_NAME which is the
correct variable exported from the parent script (upgrade.sh).

DB_CONTAINER_NAME was never defined, causing the script to fail
immediately with 'set -u' enabled (unbound variable error). The
intended variable is POSTGRES_SERVICE_NAME which contains the name
of the PostgreSQL container detected at runtime.
2026-02-02 20:06:27 +01:00
Stéphan Sainléger
a1508daf24 [IMP] fix return statement outside function
Replace 'return 1' with 'exit 1' in prepare_db.sh.

The 'return' statement is only valid inside functions. When used at
the script's top level, it behaves unpredictably - in some shells it
exits the script, in others it's an error. Using 'exit 1' explicitly
terminates the script with an error status, which is the intended
behavior when the PostgreSQL container is not running.
2026-02-02 20:06:27 +01:00
46 changed files with 241 additions and 2322 deletions

1
.gitignore vendored
View File

@@ -1,2 +1 @@
final_404_addons
migration.log

4
13.0/upgrade.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
set -euo pipefail
compose -f ../compose.yml run -p 8013:8069 ou13 --config=/opt/odoo/auto/odoo.conf --stop-after-init -u all --workers 0 --log-level=debug --max-cron-threads=0 --limit-time-real=10000 --database=ou13

4
14.0/upgrade.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
set -euo pipefail
compose -f ../compose.yml run -p 8014:8069 ou14 --config=/opt/odoo/auto/odoo.conf --stop-after-init -u all --workers 0 --log-level=debug --max-cron-threads=0 --limit-time-real=10000 --database=ou14 --load=base,web,openupgrade_framework

4
15.0/upgrade.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
set -euo pipefail
compose -f ../compose.yml run -p 8015:8069 ou15 --config=/opt/odoo/auto/odoo.conf --stop-after-init -u all --workers 0 --log-level=debug --max-cron-threads=0 --limit-time-real=10000 --database=ou15 --load=base,web,openupgrade_framework

4
16.0/upgrade.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
set -euo pipefail
compose -f ../compose.yml run -p 8016:8069 ou16 --config=/opt/odoo/auto/odoo.conf --stop-after-init -u all --workers 0 --log-level=debug --max-cron-threads=0 --limit-time-real=10000 --database=ou16 --load=base,web,openupgrade_framework

4
17.0/upgrade.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
set -euo pipefail
compose -f ../compose.yml run -p 8017:8069 ou17 --config=/opt/odoo/auto/odoo.conf --stop-after-init -u all --workers 0 --log-level=debug --max-cron-threads=0 --limit-time-real=10000 --database=ou17 --load=base,web,openupgrade_framework

6
18.0/post_upgrade.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
set -euo pipefail
echo "Post migration to 18.0..."
#compose --debug run ou18 -u base --stop-after-init --no-http

20
18.0/pre_upgrade.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/bash
set -euo pipefail
echo "Prepare migration to 18.0..."
# Copy database
copy_database ou17 ou18 ou18 || exit 1
# Execute SQL pre-migration commands
PRE_MIGRATE_SQL=$(cat <<'EOF'
UPDATE account_analytic_plan SET default_applicability=NULL WHERE default_applicability='optional';
EOF
)
echo "SQL command = $PRE_MIGRATE_SQL"
query_postgres_container "$PRE_MIGRATE_SQL" ou18 || exit 1
# Copy filestores
copy_filestore ou17 ou17 ou18 ou18 || exit 1
echo "Ready for migration to 18.0!"

4
18.0/upgrade.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
set -euo pipefail
compose -f ../compose.yml run -p 8018:8069 ou18 --config=/opt/odoo/auto/odoo.conf --stop-after-init -u all --workers 0 --log-level=debug --max-cron-threads=0 --limit-time-real=10000 --database=ou18 --load=base,web,openupgrade_framework

453
README.md
View File

@@ -1,445 +1,64 @@
# 0k-odoo-upgrade
A tool for migrating Odoo databases between major versions, using [OpenUpgrade](https://github.com/OCA/OpenUpgrade) in a production-like Docker environment.
## Table of Contents
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Project Structure](#project-structure)
- [How It Works](#how-it-works)
- [Usage](#usage)
- [Resuming a Failed Migration](#resuming-a-failed-migration)
- [Customization](#customization)
- [Troubleshooting](#troubleshooting)
## Prerequisites
- [0k dev-pack](https://git.myceliandre.fr/Lokavaluto/dev-pack) installed (provides the `compose` command)
- Docker and Docker Compose
- `rsync` for filestore copying
- `sudo` access for filestore operations
## Installation
```bash
git clone <repository-url>
cd 0k-odoo-upgrade
```
- Clone the current repo
## Project Structure
## Configuration
```
.
├── upgrade.sh # Main entry point
├── config/
│ └── compose.yml # Docker Compose configuration
├── lib/
│ ├── common.sh # Shared bash functions
│ └── python/ # Python utility scripts
│ ├── check_views.py # View analysis (pre-migration)
│ ├── validate_views.py # View validation (post-migration)
│ ├── fix_duplicated_views.py # Fix duplicated views
│ └── cleanup_modules.py # Obsolete module cleanup
├── scripts/
│ ├── prepare_db.sh # Database preparation before migration
│ ├── finalize_db.sh # Post-migration finalization
│ └── validate_migration.sh # Manual post-migration validation
└── versions/ # Version-specific scripts
├── 13.0/
│ ├── pre_upgrade.sh # SQL fixes before migration
│ ├── upgrade.sh # OpenUpgrade execution
│ └── post_upgrade.sh # Fixes after migration
├── 14.0/
├── ...
└── 18.0/
```
## How It Works
### Overview
The script performs a **step-by-step migration** between each major version. For example, to migrate from 14.0 to 17.0, it executes:
```
14.0 → 15.0 → 16.0 → 17.0
```
### Process Steps
1. **Initial Checks**
- Argument validation
- Required command verification (`docker`, `compose`, `sudo`, `rsync`)
- Source database and filestore existence check
2. **Environment Preparation**
- Creation of a fresh Odoo database in the target version (for module comparison)
- Copy of the source database to a working database
- Filestore copy
3. **Database Preparation** (`scripts/prepare_db.sh`)
- Neutralization: disable mail servers and cron jobs
- Detection of installed modules missing in the target version
- View state verification
- User confirmation prompt
4. **Migration Loop** (for each intermediate version)
- `pre_upgrade.sh`: version-specific SQL fixes before migration
- `upgrade.sh`: OpenUpgrade execution via Docker
- `post_upgrade.sh`: fixes after migration
5. **Finalization** (`scripts/finalize_db.sh`)
- Obsolete sequence removal
- Modified website template reset
- Compiled asset cache purge
- Duplicated view fixes
- Obsolete module cleanup
- Final update with `-u all`
### Flow Diagram
```
┌─────────────────┐
│ upgrade.sh │
└────────┬────────┘
┌─────────────────┐ ┌─────────────────┐
│ Initial │────▶│ Copy DB + │
│ checks │ │ filestore │
└─────────────────┘ └────────┬────────┘
┌─────────────────┐
│ prepare_db.sh │
│ (neutralization)│
└────────┬────────┘
┌───────────────────────┼───────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ versions/13.0/ │────▶│ versions/14.0/ │────▶│ versions/N.0/ │
│ pre/upgrade/post│ │ pre/upgrade/post│ │ pre/upgrade/post│
└─────────────────┘ └─────────────────┘ └────────┬────────┘
┌─────────────────┐
│ finalize_db.sh │
│ (cleanup) │
└─────────────────┘
```
- Requires to have the 0k scripts installed on the computer: https://git.myceliandre.fr/Lokavaluto/dev-pack
## Usage
### Before migration
### Before Migration
- [ ] import the origin database to migrate on local computer
- [ ] Uninstall all known useless Odoo add-ons. Warning: do not uninstall add-ons for which the disappearance in the finale version is managed by Open Upgrade scrips.
- [ ] Unsure all the add-ons are migrated in the final Odoo version
- [ ] (optional) De-active all the website views
1. **Import the source database** to your local machine
### Local Migration process
2. **Clean up the source database** (recommended)
- Uninstall unnecessary modules
- Do NOT uninstall modules handled by OpenUpgrade
- [ ] launch the origin database `ORIGIN_DATABASE_NAME` with original version of Odoo, with odoo service `ORIGIN_SERVICE`
- [ ] launch the following command:
3. **Check module availability**
- Ensure all custom modules are ported to the target version
4. **Start the Docker environment**
```bash
# Start the PostgreSQL container
compose up -d postgres
# Verify only one postgres container is running
docker ps | grep postgres
```
### Running the Migration
```bash
./upgrade.sh <source_version> <target_version> <database_name> <source_service> [--resume-from|-r <version>]
``` bash
./upgrade.sh {ORIGIN_VERSION} {DESTINATION_VERSION} {ORIGIN_DATABASE_NAME} {ORIGIN_SERVICE}
```
ex: ./upgrade.sh 14 16 elabore_20241208 odoo14
**Parameters:**
| Parameter | Description | Example |
|-----------|-------------|---------|
| `source_version` | Source Odoo version (without .0) | `14` |
| `target_version` | Target Odoo version (without .0) | `17` |
| `database_name` | Database name | `my_prod_db` |
| `source_service` | Source Docker Compose service | `odoo14` |
- [ ] Inspect the list of add-ons identified as missing in the final Odoo docker image:
- if you want to uninstall some of them:
- STOP the process (N)
- uninstall the concernet add-ons manually
- launch the migration script again
- if the list suits you, show can go on (Y)!
**Options:**
| Option | Description |
|--------|-------------|
| `--resume-from <version>`, `-r <version>` | Resume from an intermediate checkpoint (see [Resuming a Failed Migration](#resuming-a-failed-migration)) |
The migration process should run all the middle-migrations until the last one without action needed from you.
**Example:**
```bash
./upgrade.sh 14 17 elabore_20241208 odoo14
```
### Deploy migrated base
### During Migration
- [ ] Retrieve the migrated database (vps odoo dump)
- [ ] Copy the database on the concerned VPS
- [ ] vps odoo restore
The script will prompt for confirmation at two points:
1. **Missing modules list**: installed modules that don't exist in the target version
- `Y`: continue (modules will be marked for removal)
- `N`: abort to manually uninstall certain modules
## Manage the add-ons to uninstall
2. **View state**: verification of potentially problematic views
- `Y`: continue
- `N`: abort to manually fix issues
The migration script will manage the uninstall of Odoo add-ons:
- add-ons we want to uninstall, whatever the reasons
- add-ons to uninstall because they do not exist in the final Odoo docker image
### After Migration
At the beginning of the process, the script compare the list of add-ons installed in the origin database, and the list of add-ons available in the finlal Odoo docker image.
1. **Review logs** to detect any non-blocking errors
The whole list of add-ons to uninstall is displayed, and needs a confirmation before starting the migration.
2. **Validate the migration** (see [Post-Migration Validation](#post-migration-validation))
## Customize the migration scripts
3. **Test the migrated database** locally
FEATURE COMING SOON...
4. **Deploy to production**
```bash
# Export the migrated database
vps odoo dump db_migrated.zip
# On the production server
vps odoo restore db_migrated.zip
```
## Manage migration issues
## Post-Migration Validation
As the migration process is performed on a copy of the orginal database, the process can be restarted without limits.
After migration, use the validation script to check for broken views and XPath errors.
### Quick Start
```bash
./scripts/validate_migration.sh ou17 odoo17
```
### What Gets Validated
Runs in Odoo shell, no HTTP server needed:
| Check | Description |
|-------|-------------|
| **Inherited views** | Verifies all inherited views can combine with their parent |
| **XPath targets** | Ensures XPath expressions find their targets in parent views |
| **QWeb templates** | Validates QWeb templates are syntactically correct |
| **Field references** | Checks that field references point to existing model fields |
| **Odoo native** | Runs Odoo's built-in `_validate_custom_views()` |
### Running Directly
You can also run the Python script directly in Odoo shell:
```bash
compose run odoo17 shell -d ou17 --no-http --stop-after-init < lib/python/validate_views.py
```
### Output
- **Colored terminal output** with `[OK]`, `[ERROR]`, `[WARN]` indicators
- **JSON report** written to `/tmp/validation_views_<db>_<timestamp>.json`
- **Exit code**: `0` = success, `1` = errors found
## Resuming a Failed Migration
Each version hop copies the database before modifying it (`ou14` → `ou15` → `ou16` → …). If a migration crashes mid-way, the intermediate databases from completed hops are still intact and can be used as a restart point.
### How It Works
Use `--resume-from <version>` (or `-r <version>`) to restart from an intermediate checkpoint:
```bash
./upgrade.sh <source_version> <target_version> <database_name> <source_service> --resume-from <checkpoint_version>
```
When a checkpoint is specified, the script:
- **Skips** the initial database and filestore copy
- **Skips** `prepare_db.sh` (already done before the crash)
- **Starts the migration loop** from `checkpoint_version + 1`
### Example
A migration from 14 to 18 crashes during the 16→17 hop. The database `ou15` was successfully created. Resume from there:
```bash
./upgrade.sh 14 18 my_database odoo14 --resume-from 15
```
This runs: `16.0 → 17.0 → 18.0`, starting from `ou15`.
### Constraints
- The checkpoint version must be strictly between the source and target versions.
- The intermediate database (`ou<version>`) and its filestore must exist before resuming.
### Restarting From Scratch
To restart a full migration from the beginning (ignoring all intermediate databases):
```bash
./upgrade.sh 14 18 my_database odoo14
```
The script automatically drops and recreates the final target database (`ou18`) if it already exists.
## Customization
### Version Scripts
Each `versions/X.0/` directory contains three scripts you can customize:
#### `pre_upgrade.sh`
Executed **before** OpenUpgrade. Use it to:
- Add missing columns expected by OpenUpgrade
- Fix incompatible data
- Remove problematic records
```bash
#!/bin/bash
set -euo pipefail
echo "Prepare migration to 15.0..."
copy_database ou14 ou15 ou15
PRE_MIGRATE_SQL=$(cat <<'EOF'
-- Example: remove a problematic module
DELETE FROM ir_module_module WHERE name = 'obsolete_module';
EOF
)
query_postgres_container "$PRE_MIGRATE_SQL" ou15
copy_filestore ou14 ou14 ou15 ou15
echo "Ready for migration to 15.0!"
```
#### `upgrade.sh`
Runs OpenUpgrade migration scripts.
#### `post_upgrade.sh`
Executed **after** OpenUpgrade. Use it to:
- Fix incorrectly migrated data
- Remove orphan records
- Update system parameters
```bash
#!/bin/bash
set -euo pipefail
echo "Post migration to 15.0..."
POST_MIGRATE_SQL=$(cat <<'EOF'
-- Example: fix a configuration value
UPDATE ir_config_parameter
SET value = 'new_value'
WHERE key = 'my_key';
EOF
)
query_postgres_container "$POST_MIGRATE_SQL" ou15
```
### Available Functions
Version scripts have access to functions defined in `lib/common.sh`:
| Function | Description |
|----------|-------------|
| `query_postgres_container "$SQL" "$DB"` | Execute an SQL query |
| `copy_database $from $to_service $to_db` | Copy a PostgreSQL database |
| `copy_filestore $from_svc $from_db $to_svc $to_db` | Copy a filestore |
| `log_info`, `log_warn`, `log_error` | Logging functions |
| `log_step "title"` | Display a section header |
### Adding a New Version
To add support for a new version (e.g., 19.0):
```bash
mkdir versions/19.0
cp versions/18.0/*.sh versions/19.0/
# Edit the scripts to:
# - Change references from ou18 → ou19
# - Change the port from -p 8018:8069 → -p 8019:8069
# - Add SQL fixes specific to this migration
```
## Troubleshooting
### Common Issues
#### "No running PostgreSQL container found"
```bash
# Check active containers
docker ps | grep postgres
# Start the container if needed
compose up -d postgres
```
#### "Multiple PostgreSQL containers found"
Stop the extra PostgreSQL containers:
```bash
docker stop <container_name_to_stop>
```
#### "Database not found"
The source database must exist in PostgreSQL:
```bash
# List databases
docker exec -u 70 <postgres_container> psql -l
# Import a database if needed
docker exec -u 70 <postgres_container> pgm restore <file.zip>
```
#### "Filestore not found"
The filestore must be present at `/srv/datastore/data/<service>/var/lib/odoo/filestore/<database>/`
### Restarting After an Error
The script works on a **copy** of the original database. You can restart as many times as needed:
```bash
# Simply restart - the copy will be recreated
./upgrade.sh 14 17 my_database odoo14
```
### Viewing Detailed Logs
All output (scripts, Docker containers, OpenUpgrade logs) is automatically saved to `migration.log` at the project root. The file is overwritten at the start of each run (full migration or resume).
```bash
# Follow live output while also saving to file (already done automatically)
tail -f migration.log
# Search for errors after the fact
grep -i error migration.log
# View with ANSI colors preserved (less handles CRLF line endings natively)
less -R migration.log
# Plain text output without CRLF artifacts
cat migration.log | col -b
```
> **Note:** `migration.log` is recorded via a pseudo-TTY (`script`), which preserves
> color output in the terminal. As a side effect, the file uses `\r\n` line endings.
> Use `less -R` or `col -b` for clean viewing.
For a problematic migration:
1. Note the version where the error occurs
2. Check `migration.log` to identify the problematic module/table
3. Add a fix in the `pre_upgrade.sh` for that version
4. Restart the migration
## License
See the [LICENSE](LICENSE) file.
Some Odoo migration errors won't stop the migration process, then be attentive to the errors in the logs.

52
finalize_db.sh Executable file
View File

@@ -0,0 +1,52 @@
#!/bin/bash
set -euo pipefail
DB_NAME="$1"
ODOO_SERVICE="$2"
FINALE_SQL=$(cat <<'EOF'
/*Delete sequences that prevent Odoo to start*/
drop sequence base_registry_signaling;
drop sequence base_cache_signaling;
EOF
)
query_postgres_container "$FINALE_SQL" "$DB_NAME" || exit 1
# Fix duplicated views
PYTHON_SCRIPT=post_migration_fix_duplicated_views.py
echo "Remove duplicated views with script $PYTHON_SCRIPT ..."
exec_python_script_in_odoo_shell "$DB_NAME" "$DB_NAME" "$PYTHON_SCRIPT" || exit 1
# Reset all website templates with custom content
FINALE_SQL_2=$(cat <<'EOF'
UPDATE ir_ui_view
SET arch_db = NULL
WHERE arch_fs IS NOT NULL
AND arch_fs LIKE 'website/%'
AND arch_db IS NOT NULL
AND id NOT IN (SELECT view_id FROM website_page);
EOF
)
query_postgres_container "$FINALE_SQL_2" "$DB_NAME" || exit 1
# Purge QWeb cache from compiled assets
FINALE_SQL_3=$(cat <<'EOF'
DELETE FROM ir_attachment
WHERE name LIKE '/web/assets/%'
OR name LIKE '%.assets_%'
OR (res_model = 'ir.ui.view' AND mimetype = 'text/css');
EOF
)
query_postgres_container "$FINALE_SQL_3" "$DB_NAME" || exit 1
# Uninstall obsolette add-ons
PYTHON_SCRIPT=post_migration_cleanup_obsolete_modules.py
echo "Uninstall obsolete add-ons with script $PYTHON_SCRIPT ..."
exec_python_script_in_odoo_shell "$DB_NAME" "$DB_NAME" "$PYTHON_SCRIPT" || exit 1
# Give back the right to user to access to the tables
# docker exec -u 70 "$DB_CONTAINER_NAME" pgm chown "$FINALE_SERVICE_NAME" "$DB_NAME"
# Launch Odoo with database in finale version to run all updates
compose --debug run "$ODOO_SERVICE" -u all --log-level=debug --stop-after-init --no-http

View File

@@ -6,15 +6,12 @@
set -euo pipefail
# Get the absolute path of the project root (parent of lib/)
readonly PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
readonly DATASTORE_PATH="/srv/datastore/data"
readonly FILESTORE_SUBPATH="var/lib/odoo/filestore"
check_required_commands() {
local missing=()
for cmd in docker compose sudo rsync yq; do
for cmd in docker compose sudo; do
if ! command -v "$cmd" &>/dev/null; then
missing+=("$cmd")
fi
@@ -31,21 +28,6 @@ log_warn() { printf "[WARN] %s\n" "$*" >&2; }
log_error() { printf "[ERROR] %s\n" "$*" >&2; }
log_step() { printf "\n===== %s =====\n" "$*"; }
confirm_or_exit() {
local message="$1"
local choice
echo ""
echo "$message"
echo "Y - Yes, continue"
echo "N - No, cancel"
read -r -n 1 -p "Your choice: " choice
echo ""
case "$choice" in
[Yy]) return 0 ;;
*) log_error "Cancelled by user."; exit 1 ;;
esac
}
query_postgres_container() {
local query="$1"
local db_name="$2"
@@ -80,103 +62,21 @@ copy_filestore() {
local src_path="${DATASTORE_PATH}/${from_service}/${FILESTORE_SUBPATH}/${from_db}"
local dst_path="${DATASTORE_PATH}/${to_service}/${FILESTORE_SUBPATH}/${to_db}"
sudo mkdir -p "$(dirname "$dst_path")"
sudo rsync -a --delete "${src_path}/" "${dst_path}/"
sudo mkdir -p "$dst_path"
sudo rm -rf "$dst_path"
sudo cp -a "$src_path" "$dst_path"
echo "Filestore ${from_service}/${from_db} copied to ${to_service}/${to_db}."
}
# Workaround: 0k dev-pack's compose script doesn't handle absolute paths correctly.
# It passes HOST_COMPOSE_YML_FILE to the container, which tries to open it directly
# instead of using the mounted path. Using a relative path from PROJECT_ROOT avoids this.
run_compose() {
(cd "$PROJECT_ROOT" && compose -f ./config/compose.yml "$@")
}
exec_python_script_in_odoo_shell() {
local service_name="$1"
local db_name="$2"
local python_script="$3"
run_compose --debug run "$service_name" shell -d "$db_name" --no-http --stop-after-init < "$python_script"
compose --debug run "$service_name" shell -d "$db_name" --no-http --stop-after-init < "$python_script"
}
# Classifies missing modules into 4 categories based on the known_changes.yaml
# files from each traversed version (from ORIGIN_VERSION+1 to FINAL_VERSION).
# The following global arrays are populated:
# addons_obsolete : modules that became obsolete
# addons_core : modules merged into Odoo Core
# addons_renamed : renamed modules (format "old_name -> new_name")
# addons_truly_missing : modules that are genuinely missing
#
# Prerequisites: ORIGIN_VERSION and FINAL_VERSION must be exported.
classify_missing_addons() {
local missing_addons_raw="$1"
addons_obsolete=()
addons_core=()
addons_renamed=()
addons_truly_missing=()
# Convert the string into an array (one entry per line)
local -a missing=()
while IFS= read -r line; do
[[ -n "$line" ]] && missing+=("$line")
done <<< "$missing_addons_raw"
if [[ ${#missing[@]} -eq 0 ]]; then
return 0
fi
# Build lookup tables from all known_changes.yaml files in traversed versions
local -A known_obsolete=()
local -A known_core=()
local -A known_renamed=()
local versions_path="${PROJECT_ROOT}/versions"
local v
for v in $(seq $((ORIGIN_VERSION + 1)) "$FINAL_VERSION"); do
local yaml_file="${versions_path}/${v}.0/known_changes.yaml"
[[ -f "$yaml_file" ]] || continue
local mod
while IFS= read -r mod; do
[[ -n "$mod" && "$mod" != "null" ]] && known_obsolete["$mod"]=1
done < <(yq '.obsolete[]?' "$yaml_file" 2>/dev/null)
while IFS= read -r mod; do
[[ -n "$mod" && "$mod" != "null" ]] && known_core["$mod"]=1
done < <(yq '.merged_in_core[]?' "$yaml_file" 2>/dev/null)
local count
count=$(yq '.renamed | length' "$yaml_file" 2>/dev/null)
if [[ "$count" =~ ^[0-9]+$ && "$count" -gt 0 ]]; then
local i
for ((i = 0; i < count; i++)); do
local old new
old=$(yq ".renamed[$i].old" "$yaml_file" 2>/dev/null)
new=$(yq ".renamed[$i].new" "$yaml_file" 2>/dev/null)
[[ -n "$old" && "$old" != "null" ]] && known_renamed["$old"]="$new"
done
fi
done
# Classify each missing module
local mod
for mod in "${missing[@]}"; do
if [[ -n "${known_obsolete[$mod]:-}" ]]; then
addons_obsolete+=("$mod")
elif [[ -n "${known_core[$mod]:-}" ]]; then
addons_core+=("$mod")
elif [[ -n "${known_renamed[$mod]:-}" ]]; then
addons_renamed+=("${mod} -> ${known_renamed[$mod]}")
else
addons_truly_missing+=("$mod")
fi
done
}
export PROJECT_ROOT DATASTORE_PATH FILESTORE_SUBPATH
export -f log_info log_warn log_error log_step confirm_or_exit
export DATASTORE_PATH FILESTORE_SUBPATH
export -f log_info log_warn log_error log_step
export -f check_required_commands
export -f query_postgres_container copy_database copy_filestore run_compose exec_python_script_in_odoo_shell
export -f classify_missing_addons
export -f query_postgres_container copy_database copy_filestore exec_python_script_in_odoo_shell

View File

@@ -1,141 +0,0 @@
#!/usr/bin/env python3
"""Regenerate all POS order inalterability hashes and sequence numbers.
This script should only be called when the SQL pre-check in finalize_db.sh
has confirmed that some pos_orders are missing l10n_fr_hash or
l10n_fr_secure_sequence_number.
It resets ALL sequence numbers and hashes from scratch, in chronological order
(date_order ASC, id ASC), and re-establishes the full hash chain.
Usage (via Odoo shell):
odoo-shell -d <db_name> < regenerate_pos_hashes.py
"""
import logging
from hashlib import sha256
_logger = logging.getLogger(__name__)
def regenerate_all():
PosOrder = env['pos.order']
Company = env['res.company']
for company in Company.search([]):
if not company._is_accounting_unalterable():
continue
print(f"\n{'='*60}")
print(f" Company: {company.name}")
print(f"{'='*60}")
orders = PosOrder.search([
('state', 'in', ['paid', 'done', 'invoiced']),
('company_id', '=', company.id),
], order='date_order ASC, id ASC')
if not orders:
print(" No orders to process.")
continue
n = len(orders)
# ── Step 1: Reset all fields ─────────────────────────────
print("\n--- Resetting fields ---")
env.cr.execute("""
UPDATE pos_order
SET l10n_fr_hash = NULL,
l10n_fr_secure_sequence_number = NULL,
previous_order_id = NULL
WHERE id IN %s
""", (tuple(orders.ids),))
env.invalidate_all()
env.cr.commit()
print(f"{n} orders reset")
# ── Step 2: Assign sequence numbers ──────────────────────
print("\n--- Assigning sequence numbers ---")
for i, order in enumerate(orders, start=1):
env.cr.execute("""
UPDATE pos_order
SET l10n_fr_secure_sequence_number = %s
WHERE id = %s
""", (i, order.id))
env.invalidate_all()
env.cr.commit()
print(f" ✓ Sequence numbers 1 → {n} assigned")
# ── Step 3: Compute previous_order_id ────────────────────
print("\n--- Computing previous_order_id ---")
orders = PosOrder.search([
('state', 'in', ['paid', 'done', 'invoiced']),
('company_id', '=', company.id),
], order='l10n_fr_secure_sequence_number ASC')
orders._compute_previous_order()
env.cr.commit()
print(" ✓ OK")
# ── Step 4: Compute hashes (with cache invalidation after each write) ─
print("\n--- Computing hashes ---")
success = errors = 0
for idx in range(n):
order = PosOrder.search([
('company_id', '=', company.id),
('l10n_fr_secure_sequence_number', '=', idx + 1),
], limit=1)
if not order:
continue
try:
order._compute_string_to_hash()
prev = order.previous_order_id
prev_hash = prev.l10n_fr_hash if prev else ''
if not prev_hash:
prev_hash = ''
computed_hash = sha256(
(prev_hash + order.l10n_fr_string_to_hash).encode('utf-8')
).hexdigest()
env.cr.execute(
"UPDATE pos_order SET l10n_fr_hash = %s WHERE id = %s",
(computed_hash, order.id)
)
env.invalidate_all()
print(f"{order.name} (seq {idx+1})")
success += 1
except Exception as e:
print(f"{order.name} (seq {idx+1}) : {e}")
errors += 1
import traceback
traceback.print_exc()
env.cr.commit()
remaining = PosOrder.search_count([
('state', 'in', ['paid', 'done', 'invoiced']),
('company_id', '=', company.id),
'|', ('l10n_fr_hash', '=', False),
('l10n_fr_hash', '=', None),
])
print(f"\n Final result: {success} hashes written, {errors} errors, "
f"{remaining} remaining")
# Reset sequence for future orders
seq = company.l10n_fr_pos_cert_sequence_id
if seq:
env.cr.execute(
"UPDATE ir_sequence SET number_next = %s WHERE id = %s",
(n + 1, seq.id)
)
print(f" ✓ ir_sequence number_next set to {n + 1}")
print("\n✓ Done.")
regenerate_all()

View File

@@ -1,521 +0,0 @@
#!/usr/bin/env python3
"""
Post-Migration View Validation Script for Odoo
Validates all views after migration to detect:
- Broken XPath expressions in inherited views
- Views that fail to combine with their parent
- Invalid QWeb templates
- Missing asset files
- Field references to non-existent fields
Usage:
odoo-bin shell -d <database> < validate_views.py
# Or with compose:
compose run <service> shell -d <database> --no-http --stop-after-init < validate_views.py
Exit codes:
0 - All validations passed
1 - Validation errors found (see report)
"""
import os
import sys
import re
import json
from datetime import datetime
from collections import defaultdict
from lxml import etree
# ANSI colors for terminal output
class Colors:
RED = '\033[91m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
BOLD = '\033[1m'
END = '\033[0m'
def print_header(title):
"""Print a formatted section header."""
print(f"\n{Colors.BOLD}{'='*80}{Colors.END}")
print(f"{Colors.BOLD}{title}{Colors.END}")
print(f"{Colors.BOLD}{'='*80}{Colors.END}\n")
def print_subheader(title):
"""Print a formatted subsection header."""
print(f"\n{Colors.BLUE}{'-'*60}{Colors.END}")
print(f"{Colors.BLUE}{title}{Colors.END}")
print(f"{Colors.BLUE}{'-'*60}{Colors.END}\n")
def print_ok(message):
"""Print success message."""
print(f"{Colors.GREEN}[OK]{Colors.END} {message}")
def print_error(message):
"""Print error message."""
print(f"{Colors.RED}[ERROR]{Colors.END} {message}")
def print_warn(message):
"""Print warning message."""
print(f"{Colors.YELLOW}[WARN]{Colors.END} {message}")
def print_info(message):
"""Print info message."""
print(f"{Colors.BLUE}[INFO]{Colors.END} {message}")
class ViewValidator:
"""Validates Odoo views after migration."""
def __init__(self, env):
self.env = env
self.View = env['ir.ui.view']
self.errors = []
self.warnings = []
self.stats = {
'total_views': 0,
'inherited_views': 0,
'qweb_views': 0,
'broken_xpath': 0,
'broken_combine': 0,
'broken_qweb': 0,
'broken_fields': 0,
'missing_assets': 0,
}
def validate_all(self):
"""Run all validation checks."""
print_header("ODOO VIEW VALIDATION - POST-MIGRATION")
print(f"Started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Database: {self.env.cr.dbname}")
# Get all active views
all_views = self.View.search([('active', '=', True)])
self.stats['total_views'] = len(all_views)
print_info(f"Total active views to validate: {len(all_views)}")
# Run validations
self._validate_inherited_views()
self._validate_xpath_targets()
self._validate_qweb_templates()
self._validate_field_references()
self._validate_odoo_native()
self._check_assets()
# Print summary
self._print_summary()
# Rollback to avoid any accidental changes
self.env.cr.rollback()
return len(self.errors) == 0
def _validate_inherited_views(self):
"""Check that all inherited views can combine with their parent."""
print_subheader("1. Validating Inherited Views (Combination)")
inherited_views = self.View.search([
('inherit_id', '!=', False),
('active', '=', True)
])
self.stats['inherited_views'] = len(inherited_views)
print_info(f"Found {len(inherited_views)} inherited views to check")
broken = []
for view in inherited_views:
try:
# Attempt to get combined architecture
view._get_combined_arch()
except Exception as e:
broken.append({
'view_id': view.id,
'xml_id': view.xml_id or 'N/A',
'name': view.name,
'model': view.model,
'parent_xml_id': view.inherit_id.xml_id if view.inherit_id else 'N/A',
'error': str(e)[:200]
})
self.stats['broken_combine'] = len(broken)
if broken:
for item in broken:
error_msg = (
f"View '{item['xml_id']}' (ID: {item['view_id']}) "
f"cannot combine with parent '{item['parent_xml_id']}': {item['error']}"
)
print_error(error_msg)
self.errors.append({
'type': 'combination_error',
'severity': 'error',
**item
})
else:
print_ok("All inherited views combine correctly with their parents")
def _validate_xpath_targets(self):
"""Check that XPath expressions find their targets in parent views."""
print_subheader("2. Validating XPath Targets")
inherited_views = self.View.search([
('inherit_id', '!=', False),
('active', '=', True)
])
xpath_pattern = re.compile(r'<xpath[^>]+expr=["\']([^"\']+)["\']')
orphan_xpaths = []
for view in inherited_views:
if not view.arch_db or not view.inherit_id or not view.inherit_id.arch_db:
continue
try:
# Get parent's combined arch (to handle chained inheritance)
parent_arch = view.inherit_id._get_combined_arch()
parent_tree = etree.fromstring(parent_arch)
except Exception:
# Parent view is already broken, skip
continue
# Parse child view
try:
view_tree = etree.fromstring(view.arch_db)
except Exception:
continue
# Find all xpath nodes
for xpath_node in view_tree.xpath('//xpath'):
expr = xpath_node.get('expr')
if not expr:
continue
try:
matches = parent_tree.xpath(expr)
if not matches:
orphan_xpaths.append({
'view_id': view.id,
'xml_id': view.xml_id or 'N/A',
'name': view.name,
'model': view.model,
'xpath': expr,
'parent_xml_id': view.inherit_id.xml_id or 'N/A',
'parent_id': view.inherit_id.id
})
except etree.XPathEvalError as e:
orphan_xpaths.append({
'view_id': view.id,
'xml_id': view.xml_id or 'N/A',
'name': view.name,
'model': view.model,
'xpath': expr,
'parent_xml_id': view.inherit_id.xml_id or 'N/A',
'parent_id': view.inherit_id.id,
'xpath_error': str(e)
})
self.stats['broken_xpath'] = len(orphan_xpaths)
if orphan_xpaths:
for item in orphan_xpaths:
error_msg = (
f"View '{item['xml_id']}' (ID: {item['view_id']}): "
f"XPath '{item['xpath']}' finds no target in parent '{item['parent_xml_id']}'"
)
if 'xpath_error' in item:
error_msg += f" (XPath syntax error: {item['xpath_error']})"
print_error(error_msg)
self.errors.append({
'type': 'orphan_xpath',
'severity': 'error',
**item
})
else:
print_ok("All XPath expressions find their targets")
def _validate_qweb_templates(self):
"""Validate QWeb templates can be rendered."""
print_subheader("3. Validating QWeb Templates")
qweb_views = self.View.search([
('type', '=', 'qweb'),
('active', '=', True)
])
self.stats['qweb_views'] = len(qweb_views)
print_info(f"Found {len(qweb_views)} QWeb templates to check")
broken = []
for view in qweb_views:
try:
# Basic XML parsing check
if view.arch_db:
etree.fromstring(view.arch_db)
# Try to get combined arch for inherited qweb views
if view.inherit_id:
view._get_combined_arch()
except Exception as e:
broken.append({
'view_id': view.id,
'xml_id': view.xml_id or 'N/A',
'name': view.name,
'key': view.key or 'N/A',
'error': str(e)[:200]
})
self.stats['broken_qweb'] = len(broken)
if broken:
for item in broken:
error_msg = (
f"QWeb template '{item['xml_id']}' (key: {item['key']}): {item['error']}"
)
print_error(error_msg)
self.errors.append({
'type': 'qweb_error',
'severity': 'error',
**item
})
else:
print_ok("All QWeb templates are valid")
def _validate_field_references(self):
"""Check that field references in views point to existing fields."""
print_subheader("4. Validating Field References")
field_pattern = re.compile(r'(?:name|field)=["\'](\w+)["\']')
missing_fields = []
# Only check form, tree, search, kanban views (not qweb)
views = self.View.search([
('type', 'in', ['form', 'tree', 'search', 'kanban', 'pivot', 'graph']),
('active', '=', True),
('model', '!=', False)
])
print_info(f"Checking field references in {len(views)} views")
checked_models = set()
for view in views:
model_name = view.model
if not model_name or model_name in checked_models:
continue
# Skip if model doesn't exist
if model_name not in self.env:
continue
checked_models.add(model_name)
try:
# Get combined arch
arch = view._get_combined_arch()
tree = etree.fromstring(arch)
except Exception:
continue
model = self.env[model_name]
model_fields = set(model._fields.keys())
# Find all field references
for field_node in tree.xpath('//*[@name]'):
field_name = field_node.get('name')
if not field_name:
continue
# Skip special names
if field_name in ('id', '__last_update', 'display_name'):
continue
# Skip if it's a button or action (not a field)
if field_node.tag in ('button', 'a'):
continue
# Check if field exists
if field_name not in model_fields:
# Check if it's a related field path (e.g., partner_id.name)
if '.' in field_name:
continue
missing_fields.append({
'view_id': view.id,
'xml_id': view.xml_id or 'N/A',
'model': model_name,
'field_name': field_name,
'tag': field_node.tag
})
self.stats['broken_fields'] = len(missing_fields)
if missing_fields:
# Group by view for cleaner output
by_view = defaultdict(list)
for item in missing_fields:
by_view[item['xml_id']].append(item['field_name'])
for xml_id, fields in list(by_view.items())[:20]: # Limit output
print_warn(f"View '{xml_id}': references missing fields: {', '.join(fields)}")
self.warnings.append({
'type': 'missing_field',
'severity': 'warning',
'xml_id': xml_id,
'fields': fields
})
if len(by_view) > 20:
print_warn(f"... and {len(by_view) - 20} more views with missing fields")
else:
print_ok("All field references are valid")
def _validate_odoo_native(self):
"""Run Odoo's native view validation."""
print_subheader("5. Running Odoo Native Validation")
try:
# This validates all custom views
self.View._validate_custom_views('all')
print_ok("Odoo native validation passed")
except Exception as e:
error_msg = f"Odoo native validation failed: {str(e)[:500]}"
print_error(error_msg)
self.errors.append({
'type': 'native_validation',
'severity': 'error',
'error': str(e)
})
def _check_assets(self):
"""Check for missing asset files."""
print_subheader("6. Checking Asset Files")
try:
IrAsset = self.env['ir.asset']
except KeyError:
print_info("ir.asset model not found (Odoo < 14.0), skipping asset check")
return
assets = IrAsset.search([])
print_info(f"Checking {len(assets)} asset definitions")
missing = []
for asset in assets:
if not asset.path:
continue
try:
# Try to resolve the asset path
# This is a simplified check - actual asset resolution is complex
path = asset.path
if path.startswith('/'):
path = path[1:]
# Check if it's a glob pattern or specific file
if '*' in path:
continue # Skip glob patterns
# Try to get the asset content (this will fail if file is missing)
# Note: This is environment dependent and may not catch all issues
except Exception as e:
missing.append({
'asset_id': asset.id,
'path': asset.path,
'bundle': asset.bundle or 'N/A',
'error': str(e)[:100]
})
self.stats['missing_assets'] = len(missing)
if missing:
for item in missing:
print_warn(f"Asset '{item['path']}' (bundle: {item['bundle']}): may be missing")
self.warnings.append({
'type': 'missing_asset',
'severity': 'warning',
**item
})
else:
print_ok("Asset definitions look valid")
def _print_summary(self):
"""Print validation summary."""
print_header("VALIDATION SUMMARY")
print(f"Statistics:")
print(f" - Total views checked: {self.stats['total_views']}")
print(f" - Inherited views: {self.stats['inherited_views']}")
print(f" - QWeb templates: {self.stats['qweb_views']}")
print()
print(f"Issues found:")
print(f" - Broken view combinations: {self.stats['broken_combine']}")
print(f" - Orphan XPath expressions: {self.stats['broken_xpath']}")
print(f" - Invalid QWeb templates: {self.stats['broken_qweb']}")
print(f" - Missing field references: {self.stats['broken_fields']}")
print(f" - Missing assets: {self.stats['missing_assets']}")
print()
total_errors = len(self.errors)
total_warnings = len(self.warnings)
if total_errors == 0 and total_warnings == 0:
print(f"{Colors.GREEN}{Colors.BOLD}")
print("="*60)
print(" ALL VALIDATIONS PASSED!")
print("="*60)
print(f"{Colors.END}")
elif total_errors == 0:
print(f"{Colors.YELLOW}{Colors.BOLD}")
print("="*60)
print(f" VALIDATION PASSED WITH {total_warnings} WARNING(S)")
print("="*60)
print(f"{Colors.END}")
else:
print(f"{Colors.RED}{Colors.BOLD}")
print("="*60)
print(f" VALIDATION FAILED: {total_errors} ERROR(S), {total_warnings} WARNING(S)")
print("="*60)
print(f"{Colors.END}")
if os.environ.get('VALIDATE_VIEWS_REPORT'):
report = {
'type': 'views',
'timestamp': datetime.now().isoformat(),
'database': self.env.cr.dbname,
'stats': self.stats,
'errors': self.errors,
'warnings': self.warnings
}
MARKER = '___VALIDATE_VIEWS_JSON___'
print(MARKER)
print(json.dumps(report, indent=2, default=str))
print(MARKER)
def main():
"""Main entry point."""
try:
validator = ViewValidator(env)
success = validator.validate_all()
# Exit with appropriate code
if not success:
sys.exit(1)
except Exception as e:
print_error(f"Validation script failed: {e}")
import traceback
traceback.print_exc()
sys.exit(2)
# Run when executed in Odoo shell
if __name__ == '__main__' or 'env' in dir():
main()

View File

@@ -42,7 +42,7 @@ echo "Base neutralized..."
## List add-ons not in final version ##
#######################################
SQL_MISSING_ADDONS=$(cat <<EOF
SQL_404_ADDONS_LIST=$(cat <<EOF
SELECT module_origin.name
FROM ir_module_module module_origin
LEFT JOIN (
@@ -52,45 +52,58 @@ LEFT JOIN (
) AS module_dest ON module_dest.name = module_origin.name
WHERE (module_dest.name IS NULL)
AND (module_origin.state = 'installed')
AND (module_origin.author NOT IN ('Odoo S.A.'))
AND (module_origin.author NOT IN ('Odoo S.A.', 'Lokavaluto', 'Elabore'))
ORDER BY module_origin.name;
EOF
)
echo "Retrieve missing addons..."
missing_addons=$(query_postgres_container "$SQL_MISSING_ADDONS" "$DB_NAME")
echo "Retrieve 404 addons... "
echo "SQL REQUEST = $SQL_404_ADDONS_LIST"
query_postgres_container "$SQL_404_ADDONS_LIST" "$DB_NAME" > "${TMPDIR}/404_addons"
log_step "ADD-ONS CHECK"
classify_missing_addons "$missing_addons"
INSTALLED_ADDONS="SELECT name FROM ir_module_module WHERE state='installed';"
query_postgres_container "$INSTALLED_ADDONS" "$DB_NAME" > "${TMPDIR}/installed_addons"
if [[ ${#addons_obsolete[@]} -gt 0 ]]; then
log_info "Obsolete modules (${#addons_obsolete[@]}):"
printf "%s\n" "${addons_obsolete[@]}"
echo ""
fi
if [[ ${#addons_core[@]} -gt 0 ]]; then
log_info "Merged into Odoo Core (${#addons_core[@]}):"
printf "%s\n" "${addons_core[@]}"
echo ""
fi
if [[ ${#addons_renamed[@]} -gt 0 ]]; then
log_info "Renamed modules (${#addons_renamed[@]}):"
printf "%s\n" "${addons_renamed[@]}"
echo ""
fi
if [[ ${#addons_truly_missing[@]} -gt 0 ]]; then
log_warn "Truly missing modules (${#addons_truly_missing[@]}):"
printf "%s\n" "${addons_truly_missing[@]}"
echo ""
confirm_or_exit "Do you accept to migrate with these add-ons truly missing?"
else
log_info "No truly missing modules — all accounted for."
fi
grep -Fx -f "${TMPDIR}/404_addons" "${TMPDIR}/installed_addons" > "${TMPDIR}/final_404_addons" || true
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PYTHON_SCRIPT="${SCRIPT_DIR}/lib/python/check_views.py"
echo "
==== ADD-ONS CHECK ====
Installed add-ons not available in final Odoo version:
"
cat "${TMPDIR}/final_404_addons"
echo "
Do you accept to migrate the database with all these add-ons still installed? (Y/N/R)"
echo "Y - Yes, let's go on with the upgrade."
echo "N - No, stop the upgrade"
read -n 1 -p "Your choice: " choice
case "$choice" in
[Yy] ) echo "
Let's go on!";;
[Nn] ) echo "
Upgrade cancelled!"; exit 1;;
* ) echo "
Please answer by Y or N.";;
esac
# Check the views
PYTHON_SCRIPT=pre_migration_view_checking.py
echo "Check views with script $PYTHON_SCRIPT ..."
exec_python_script_in_odoo_shell "$DB_NAME" "$DB_NAME" "$PYTHON_SCRIPT"
exec_python_script_in_odoo_shell "$DB_NAME" "$DB_NAME" "$PYTHON_SCRIPT" || exit 1
confirm_or_exit "Do you accept to migrate with the current views state?"
echo "
Do you accept to migrate the database with the current views states? (Y/N/R)"
echo "Y - Yes, let's go on with the upgrade."
echo "N - No, stop the upgrade"
read -n 1 -p "Your choice: " choice
case "$choice" in
[Yy] ) echo "
Upgrade confirmed!";;
[Nn] ) echo "
Upgrade cancelled!"; exit 1;;
* ) echo "
Please answer by Y or N.";;
esac
echo "Database successfully prepared!"

View File

@@ -1,147 +0,0 @@
#!/bin/bash
set -euo pipefail
DB_NAME="$1"
ODOO_SERVICE="$2"
echo "Running SQL cleanup..."
CLEANUP_SQL=$(cat <<'EOF'
-- Drop sequences that prevent Odoo from starting.
-- These sequences are recreated by Odoo on startup but stale values
-- from the old version can cause conflicts.
DROP SEQUENCE IF EXISTS base_registry_signaling;
DROP SEQUENCE IF EXISTS base_cache_signaling;
-- Purge compiled frontend assets (CSS/JS bundles).
-- These cached files reference old asset versions and must be regenerated
-- by Odoo after migration to avoid broken stylesheets and scripts.
DELETE FROM ir_attachment
WHERE name LIKE '/web/assets/%'
OR name LIKE '%.assets_%'
OR (res_model = 'ir.ui.view' AND mimetype = 'text/css');
EOF
)
query_postgres_container "$CLEANUP_SQL" "$DB_NAME"
# ────────────────────────────────────────────────────────────
# Fix orphan ir_filters pointing to deleted ir_actions
#
# During OpenUpgrade migrations, ir_actions records can be deleted and
# recreated with new ids, while ir_filters.action_id (custom user filters)
# still points to the old, now-missing action. When a user opens the custom
# filters list, Odoo tries to resolve the display_name of the many2one
# action_id and raises "Record does not exist or has been deleted
# (ir.actions.actions(<id>,))". We detach such filters (set action_id = NULL),
# which keeps the filter usable and only drops the broken action link.
# ────────────────────────────────────────────────────────────
FIX_ORPHAN_FILTERS_SQL=$(cat <<'EOF'
DO $$
DECLARE
orphan_count INTEGER;
remaining INTEGER;
BEGIN
SELECT COUNT(*) INTO orphan_count
FROM ir_filters f
LEFT JOIN ir_actions a ON a.id = f.action_id
WHERE f.action_id IS NOT NULL AND a.id IS NULL;
IF orphan_count > 0 THEN
UPDATE ir_filters f
SET action_id = NULL
FROM (
SELECT f2.id
FROM ir_filters f2
LEFT JOIN ir_actions a ON a.id = f2.action_id
WHERE f2.action_id IS NOT NULL AND a.id IS NULL
) orphans
WHERE f.id = orphans.id;
RAISE NOTICE 'Detached % orphan ir_filters pointing to deleted ir_actions', orphan_count;
ELSE
RAISE NOTICE 'OK: no orphan ir_filters to fix';
END IF;
-- Verification
SELECT COUNT(*) INTO remaining
FROM ir_filters f
LEFT JOIN ir_actions a ON a.id = f.action_id
WHERE f.action_id IS NOT NULL AND a.id IS NULL;
IF remaining > 0 THEN
RAISE WARNING 'Still % orphan ir_filters remaining after fix', remaining;
END IF;
END $$;
EOF
)
echo "Fixing orphan ir_filters pointing to deleted ir_actions..."
query_postgres_container "$FIX_ORPHAN_FILTERS_SQL" "$DB_NAME"
# Reset modules still marked as 'to upgrade' before launching the Odoo shell
# scripts below. Loading the registry in `odoo shell` re-triggers the upgrade
# of these modules, which can fail on broken views (e.g. website_sale
# TypeError). The controlled `-u all` at the end of this script performs the
# real update afterwards. We deliberately do NOT touch 'to install' modules:
# forcing them to 'installed' would skip their install scripts entirely.
#
# Uncomment the SELECT below to trace which modules were pending upgrade
# before we neutralize their state (useful if the `-u all` above is removed).
# query_postgres_container "
# SELECT name FROM ir_module_module WHERE state = 'to upgrade' ORDER BY name;
# " "$DB_NAME" || true
query_postgres_container "
UPDATE ir_module_module
SET state = 'installed'
WHERE state = 'to upgrade';
" "$DB_NAME" || true
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PYTHON_SCRIPT="${SCRIPT_DIR}/lib/python/fix_duplicated_views.py"
echo "Remove duplicated views with script $PYTHON_SCRIPT ..."
exec_python_script_in_odoo_shell "$DB_NAME" "$DB_NAME" "$PYTHON_SCRIPT"
PYTHON_SCRIPT="${SCRIPT_DIR}/lib/python/cleanup_modules.py"
echo "Uninstall obsolete add-ons with script $PYTHON_SCRIPT ..."
exec_python_script_in_odoo_shell "$DB_NAME" "$DB_NAME" "$PYTHON_SCRIPT"
# ────────────────────────────────────────────────────────────
# Regenerate POS inalterability hashes if needed
# ────────────────────────────────────────────────────────────
HASHES_NEEDED=$(query_postgres_container "
SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'pos_order'
" "$DB_NAME")
if [[ "$HASHES_NEEDED" =~ ^[0-9]+$ && "$HASHES_NEEDED" -gt 0 ]]; then
HASHES_NEEDED=$(query_postgres_container "
SELECT COUNT(*)
FROM pos_order po
JOIN res_company rc ON rc.id = po.company_id
WHERE po.state IN ('paid', 'done', 'invoiced')
AND rc.l10n_fr_pos_cert_sequence_id IS NOT NULL
AND (po.l10n_fr_hash IS NULL OR po.l10n_fr_secure_sequence_number IS NULL)
" "$DB_NAME")
fi
if [[ "$HASHES_NEEDED" =~ ^[0-9]+$ && "$HASHES_NEEDED" -gt 0 ]]; then
echo ""
echo "Found $HASHES_NEEDED pos.order(s) with missing inalterability hash or sequence number."
echo "Regenerating all POS hashes..."
PYTHON_SCRIPT="${SCRIPT_DIR}/lib/python/regenerate_pos_hashes.py"
exec_python_script_in_odoo_shell "$DB_NAME" "$DB_NAME" "$PYTHON_SCRIPT"
echo "POS hash regeneration completed."
else
echo "No missing POS hashes detected."
fi
# Give back the right to user to access to the tables
# docker exec -u 70 "$DB_CONTAINER_NAME" pgm chown "$FINALE_SERVICE_NAME" "$DB_NAME"
# Launch Odoo with database in finale version to run all updates
run_compose --debug run "$ODOO_SERVICE" -u all --log-level=debug --stop-after-init --no-http --load=base,web,openupgrade_framework
echo ""
echo "Running post-migration view validation..."
if exec_python_script_in_odoo_shell "$DB_NAME" "$DB_NAME" "${SCRIPT_DIR}/lib/python/validate_views.py"; then
echo "View validation passed."
else
echo "WARNING: View validation found issues. Run scripts/validate_migration.sh for the full report."
fi

View File

@@ -1,138 +0,0 @@
#!/bin/bash
#
# Post-Migration Validation Script for Odoo
# Validates views, XPath expressions, and QWeb templates.
#
# View validation runs automatically at the end of the upgrade process.
# This script can also be run manually for the full report with JSON output.
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
source "${PROJECT_ROOT}/lib/common.sh"
####################
# CONFIGURATION
####################
REPORT_DIR="/tmp"
REPORT_TIMESTAMP=$(date +%Y%m%d_%H%M%S)
VIEWS_REPORT=""
VIEWS_REPORT_MARKER="___VALIDATE_VIEWS_JSON___"
####################
# USAGE
####################
usage() {
cat <<EOF
Usage: $0 <db_name> <service_name>
Post-migration view validation for Odoo databases.
Validates:
- Inherited view combination (parent + child)
- XPath expressions find their targets
- QWeb template syntax
- Field references point to existing fields
- Odoo native view validation
Arguments:
db_name Name of the database to validate
service_name Docker compose service name (e.g., odoo17, ou17)
Examples:
$0 ou17 odoo17
$0 elabore_migrated odoo18
Notes:
- Runs via Odoo shell (no HTTP server needed)
- Report is written to /tmp/validation_views_<db>_<timestamp>.json
EOF
exit 1
}
####################
# ARGUMENT PARSING
####################
DB_NAME=""
SERVICE_NAME=""
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
;;
*)
if [[ -z "$DB_NAME" ]]; then
DB_NAME="$1"
shift
elif [[ -z "$SERVICE_NAME" ]]; then
SERVICE_NAME="$1"
shift
else
log_error "Unexpected argument: $1"
usage
fi
;;
esac
done
if [[ -z "$DB_NAME" ]]; then
log_error "Missing database name"
usage
fi
if [[ -z "$SERVICE_NAME" ]]; then
log_error "Missing service name"
usage
fi
####################
# MAIN
####################
log_step "POST-MIGRATION VIEW VALIDATION"
log_info "Database: $DB_NAME"
log_info "Service: $SERVICE_NAME"
PYTHON_SCRIPT="${PROJECT_ROOT}/lib/python/validate_views.py"
if [[ ! -f "$PYTHON_SCRIPT" ]]; then
log_error "Validation script not found: $PYTHON_SCRIPT"
exit 1
fi
VIEWS_REPORT="${REPORT_DIR}/validation_views_${DB_NAME}_${REPORT_TIMESTAMP}.json"
log_info "Running view validation in Odoo shell..."
echo ""
RESULT=0
RAW_OUTPUT=$(run_compose run --rm -e VALIDATE_VIEWS_REPORT=1 "$SERVICE_NAME" shell -d "$DB_NAME" --no-http --stop-after-init < "$PYTHON_SCRIPT") || RESULT=$?
echo "$RAW_OUTPUT" | sed "/${VIEWS_REPORT_MARKER}/,/${VIEWS_REPORT_MARKER}/d"
echo "$RAW_OUTPUT" | sed -n "/${VIEWS_REPORT_MARKER}/,/${VIEWS_REPORT_MARKER}/p" | grep -v "$VIEWS_REPORT_MARKER" > "$VIEWS_REPORT"
echo ""
log_step "VALIDATION COMPLETE"
if [[ -s "$VIEWS_REPORT" ]]; then
log_info "Report: $VIEWS_REPORT"
else
log_warn "Could not extract validation report from output"
VIEWS_REPORT=""
fi
if [[ $RESULT -eq 0 ]]; then
log_info "All validations passed!"
else
log_error "Some validations failed. Check the output above for details."
fi
exit $RESULT

View File

@@ -4,20 +4,13 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/common.sh"
readonly LOG_FILE="${SCRIPT_DIR}/migration.log"
if [[ -z "${_MIGRATION_LOGGING:-}" ]]; then
rm -f "$LOG_FILE"
export _MIGRATION_LOGGING=1
exec script -q -c "$(printf '%q ' "$0" "$@")" "$LOG_FILE"
fi
####################
# USAGE & ARGUMENTS
####################
usage() {
cat <<EOF >&2
Usage: $0 <origin_version> <final_version> <db_name> <service_name> [--resume-from|-r <version>]
Usage: $0 <origin_version> <final_version> <db_name> <service_name>
Arguments:
origin_version Origin Odoo version number (e.g., 12 for version 12.0)
@@ -25,103 +18,50 @@ Arguments:
db_name Name of the database to migrate
service_name Name of the origin Odoo service (docker compose service)
Options:
--resume-from, -r <version>
Resume migration from an already-migrated intermediate
database (e.g., ou15). Skips the initial DB copy and
prepare_db.sh phases. The intermediate DB must exist.
Examples:
$0 14 18 elabore_20241208 odoo14
$0 14 18 elabore_20241208 odoo14 --resume-from 15
$0 14 18 elabore_20241208 odoo14 -r 15
Example:
$0 14 16 elabore_20241208 odoo14
EOF
exit 1
}
if [[ $# -lt 4 ]]; then
log_error "Missing arguments. Expected at least 4, got $#."
log_error "Missing arguments. Expected 4, got $#."
usage
fi
check_required_commands
export ORIGIN_VERSION="$1"
readonly ORIGIN_VERSION
export FINAL_VERSION="$2"
readonly FINAL_VERSION
readonly ORIGIN_VERSION="$1"
readonly FINAL_VERSION="$2"
readonly ORIGIN_DB_NAME="$3"
readonly ORIGIN_SERVICE_NAME="$4"
# Parse optional --resume-from / -r flag
RESUME_FROM_VERSION=""
shift 4
while [[ $# -gt 0 ]]; do
case "$1" in
--resume-from|-r)
if [[ $# -lt 2 ]]; then
log_error "Option '$1' requires a version number argument."
usage
fi
RESUME_FROM_VERSION="$2"
shift 2
;;
*)
log_error "Unknown option: '$1'"
usage
;;
esac
done
readonly RESUME_FROM_VERSION
readonly COPY_DB_NAME="ou${ORIGIN_VERSION}"
export FINALE_DB_NAME="ou${FINAL_VERSION}"
readonly FINALE_DB_NAME
readonly FINALE_SERVICE_NAME="${FINALE_DB_NAME}"
# Validate --resume-from value if provided
if [[ -n "$RESUME_FROM_VERSION" ]]; then
if ! [[ "$RESUME_FROM_VERSION" =~ ^[0-9]+$ ]]; then
log_error "--resume-from value must be a numeric version number (got: '$RESUME_FROM_VERSION')."
exit 1
fi
if [[ "$RESUME_FROM_VERSION" -le "$ORIGIN_VERSION" ]]; then
log_error "--resume-from ($RESUME_FROM_VERSION) must be strictly greater than origin version ($ORIGIN_VERSION)."
log_error "To start a fresh migration, run without --resume-from."
exit 1
fi
if [[ "$RESUME_FROM_VERSION" -ge "$FINAL_VERSION" ]]; then
log_error "--resume-from ($RESUME_FROM_VERSION) must be strictly less than final version ($FINAL_VERSION)."
log_error "Nothing to migrate: checkpoint is at or beyond the target version."
exit 1
fi
fi
postgres_containers=$(docker ps --format '{{.Names}}' | grep postgres || true)
postgres_count=$(echo "$postgres_containers" | grep -c . || echo 0)
readarray -t postgres_containers < <(docker ps --format '{{.Names}}' | grep postgres || true)
if [[ ${#postgres_containers[@]} -eq 0 ]]; then
if [[ "$postgres_count" -eq 0 ]]; then
log_error "No running PostgreSQL container found. Please start a PostgreSQL container and try again."
exit 1
elif [[ ${#postgres_containers[@]} -gt 1 ]]; then
elif [[ "$postgres_count" -gt 1 ]]; then
log_error "Multiple PostgreSQL containers found:"
printf ' %s\n' "${postgres_containers[@]}" >&2
echo "$postgres_containers" >&2
log_error "Please ensure only one PostgreSQL container is running."
exit 1
fi
export POSTGRES_SERVICE_NAME="${postgres_containers[0]}"
export POSTGRES_SERVICE_NAME="$postgres_containers"
readonly POSTGRES_SERVICE_NAME
log_step "INPUT PARAMETERS"
log_info "Origin version .......... $ORIGIN_VERSION"
log_info "Final version ........... $FINAL_VERSION"
log_info "Origin DB name .......... $ORIGIN_DB_NAME"
log_info "Origin DB name ........... $ORIGIN_DB_NAME"
log_info "Origin service name ..... $ORIGIN_SERVICE_NAME"
if [[ -n "$RESUME_FROM_VERSION" ]]; then
log_info "Resume from version ..... $RESUME_FROM_VERSION (ou${RESUME_FROM_VERSION})"
else
log_info "Resume from version ..... none (full migration)"
fi
log_step "COMPUTED GLOBAL VARIABLES"
log_info "Copy DB name ............. $COPY_DB_NAME"
@@ -133,86 +73,57 @@ log_info "Postgres service name .... $POSTGRES_SERVICE_NAME"
log_step "CHECKS ALL NEEDED COMPONENTS ARE AVAILABLE"
if [[ -n "$RESUME_FROM_VERSION" ]]; then
readonly RESUME_DB_NAME="ou${RESUME_FROM_VERSION}"
resume_db_exists=$(docker exec -u 70 "$POSTGRES_SERVICE_NAME" psql -tc "SELECT 1 FROM pg_database WHERE datname = '${RESUME_DB_NAME}'" | tr -d '[:space:]')
if [[ "$resume_db_exists" ]]; then
log_info "Checkpoint database '${RESUME_DB_NAME}' found."
else
log_error "Checkpoint database '${RESUME_DB_NAME}' not found in the local postgres service."
log_error "Ensure the migration up to version ${RESUME_FROM_VERSION} completed successfully before resuming."
exit 1
fi
resume_filestore_path="${DATASTORE_PATH}/${RESUME_DB_NAME}/${FILESTORE_SUBPATH}/${RESUME_DB_NAME}"
if [[ -d "$resume_filestore_path" ]]; then
log_info "Checkpoint filestore '${resume_filestore_path}' found."
else
log_error "Checkpoint filestore '${resume_filestore_path}' not found."
log_error "Ensure the filestore for '${RESUME_DB_NAME}' is intact before resuming."
exit 1
fi
db_exists=$(docker exec -it -u 70 "$POSTGRES_SERVICE_NAME" psql -tc "SELECT 1 FROM pg_database WHERE datname = '$ORIGIN_DB_NAME'" | tr -d '[:space:]')
if [[ "$db_exists" ]]; then
log_info "Database '$ORIGIN_DB_NAME' found."
else
db_exists=$(docker exec -u 70 "$POSTGRES_SERVICE_NAME" psql -tc "SELECT 1 FROM pg_database WHERE datname = '$ORIGIN_DB_NAME'" | tr -d '[:space:]')
if [[ "$db_exists" ]]; then
log_info "Database '$ORIGIN_DB_NAME' found."
else
log_error "Database '$ORIGIN_DB_NAME' not found in the local postgres service. Please add it and restart the upgrade process."
exit 1
fi
log_error "Database '$ORIGIN_DB_NAME' not found in the local postgres service. Please add it and restart the upgrade process."
exit 1
fi
filestore_path="${DATASTORE_PATH}/${ORIGIN_SERVICE_NAME}/${FILESTORE_SUBPATH}/${ORIGIN_DB_NAME}"
if [[ -d "$filestore_path" ]]; then
log_info "Filestore '$filestore_path' found."
else
log_error "Filestore '$filestore_path' not found, please add it and restart the upgrade process."
exit 1
fi
filestore_path="${DATASTORE_PATH}/${ORIGIN_SERVICE_NAME}/${FILESTORE_SUBPATH}/${ORIGIN_DB_NAME}"
if [[ -d "$filestore_path" ]]; then
log_info "Filestore '$filestore_path' found."
else
log_error "Filestore '$filestore_path' not found, please add it and restart the upgrade process."
exit 1
fi
log_step "LAUNCH VIRGIN ODOO IN FINAL VERSION"
if docker exec -u 70 "$POSTGRES_SERVICE_NAME" pgm ls | grep "$FINALE_SERVICE_NAME"; then
if docker exec -u 70 "$POSTGRES_SERVICE_NAME" pgm ls | grep -q "$FINALE_SERVICE_NAME"; then
log_info "Removing existing finale database and filestore..."
docker exec -u 70 "$POSTGRES_SERVICE_NAME" pgm rm -f "$FINALE_SERVICE_NAME"
sudo rm -rf "${DATASTORE_PATH}/${FINALE_SERVICE_NAME}/${FILESTORE_SUBPATH}/${FINALE_SERVICE_NAME}"
fi
run_compose --debug run "$FINALE_SERVICE_NAME" -i base --stop-after-init --no-http
compose --debug run "$FINALE_SERVICE_NAME" -i base --stop-after-init --no-http
log_info "Model database in final Odoo version created."
if [[ -z "$RESUME_FROM_VERSION" ]]; then
log_step "COPY ORIGINAL COMPONENTS"
log_step "COPY ORIGINAL COMPONENTS"
copy_database "$ORIGIN_DB_NAME" "$COPY_DB_NAME" "$COPY_DB_NAME"
log_info "Original database copied to ${COPY_DB_NAME}@${COPY_DB_NAME}."
copy_database "$ORIGIN_DB_NAME" "$COPY_DB_NAME" "$COPY_DB_NAME"
log_info "Original database copied to ${COPY_DB_NAME}@${COPY_DB_NAME}."
copy_filestore "$ORIGIN_SERVICE_NAME" "$ORIGIN_DB_NAME" "$COPY_DB_NAME" "$COPY_DB_NAME"
log_info "Original filestore copied."
else
log_step "COPY ORIGINAL COMPONENTS — SKIPPED (resuming from checkpoint ou${RESUME_FROM_VERSION})"
fi
copy_filestore "$ORIGIN_SERVICE_NAME" "$ORIGIN_DB_NAME" "$COPY_DB_NAME" "$COPY_DB_NAME"
log_info "Original filestore copied."
log_step "PATH OF MIGRATION"
if [[ -n "$RESUME_FROM_VERSION" ]]; then
readarray -t versions < <(seq $((RESUME_FROM_VERSION + 1)) "$FINAL_VERSION")
log_info "Resuming migration from ou${RESUME_FROM_VERSION} — path is ${versions[*]}"
else
readarray -t versions < <(seq $((ORIGIN_VERSION + 1)) "$FINAL_VERSION")
log_info "Migration path is ${versions[*]}"
fi
declare -a versions
nb_migrations=$((FINAL_VERSION - ORIGIN_VERSION))
for ((i = 0; i < nb_migrations; i++)); do
versions[i]=$((ORIGIN_VERSION + 1 + i))
done
log_info "Migration path is ${versions[*]}"
if [[ -z "$RESUME_FROM_VERSION" ]]; then
log_step "DATABASE PREPARATION"
"${SCRIPT_DIR}/scripts/prepare_db.sh" "$COPY_DB_NAME" "$COPY_DB_NAME" "$FINALE_DB_NAME" "$FINALE_SERVICE_NAME"
else
log_step "DATABASE PREPARATION — SKIPPED (resuming from checkpoint ou${RESUME_FROM_VERSION})"
fi
log_step "DATABASE PREPARATION"
./prepare_db.sh "$COPY_DB_NAME" "$COPY_DB_NAME" "$FINALE_DB_MODEL_NAME" "$FINALE_SERVICE_NAME"
log_step "UPGRADE PROCESS"
@@ -220,16 +131,18 @@ log_step "UPGRADE PROCESS"
for version in "${versions[@]}"; do
log_info "START UPGRADE TO ${version}.0"
"${SCRIPT_DIR}/versions/${version}.0/pre_upgrade.sh"
"${SCRIPT_DIR}/versions/${version}.0/upgrade.sh"
"${SCRIPT_DIR}/versions/${version}.0/post_upgrade.sh"
cd "${version}.0"
./pre_upgrade.sh
./upgrade.sh
./post_upgrade.sh
cd ..
log_info "END UPGRADE TO ${version}.0"
done
log_step "POST-UPGRADE PROCESSES"
"${SCRIPT_DIR}/scripts/finalize_db.sh" "$FINALE_DB_NAME" "$FINALE_SERVICE_NAME"
./finalize_db.sh "$FINALE_DB_NAME" "$FINALE_SERVICE_NAME"
log_step "UPGRADE PROCESS ENDED WITH SUCCESS"
log_info "Full logs available at: ${LOG_FILE}"

View File

@@ -1,10 +0,0 @@
# Modules that became obsolete in version 13.0
obsolete: []
# Modules merged into Odoo Core in version 13.0
merged_in_core: []
# Modules renamed in version 13.0
renamed: []
# - old: old_name
# new: new_name

View File

@@ -1,4 +0,0 @@
#!/bin/bash
set -euo pipefail
run_compose run -p 8013:8069 ou13 --config=/opt/odoo/auto/odoo.conf --stop-after-init -u all --workers 0 --log-level=debug --max-cron-threads=0 --limit-time-real=10000 --database=ou13

View File

@@ -1,10 +0,0 @@
# Modules that became obsolete in version 14.0
obsolete: []
# Modules merged into Odoo Core in version 14.0
merged_in_core: []
# Modules renamed in version 14.0
renamed: []
# - old: old_name
# new: new_name

View File

@@ -1,4 +0,0 @@
#!/bin/bash
set -euo pipefail
run_compose run -p 8014:8069 ou14 --config=/opt/odoo/auto/odoo.conf --stop-after-init -u all --workers 0 --log-level=debug --max-cron-threads=0 --limit-time-real=10000 --database=ou14 --load=base,web,openupgrade_framework

View File

@@ -1,16 +0,0 @@
# Modules that became obsolete in version 15.0
obsolete:
- l10n_ch_base_bank
- l10n_ch_isrb
- project_timeline_task_dependency
- account_reconcile_reconciliation_date
# Modules merged into Odoo Core in version 15.0
merged_in_core:
- project_category
- project_deadline
# Modules renamed in version 15.0
renamed:
- old: crm_phone
new: crm_phonecall

View File

@@ -1,4 +0,0 @@
#!/bin/bash
set -euo pipefail
run_compose run -p 8015:8069 ou15 --config=/opt/odoo/auto/odoo.conf --stop-after-init -u all --workers 0 --log-level=debug --max-cron-threads=0 --limit-time-real=10000 --database=ou15 --load=base,web,openupgrade_framework

View File

@@ -1,23 +0,0 @@
# Modules that became obsolete in version 16.0
obsolete:
- account_reconciliation_widget
- account_statement_import
- account_statement_import_file_reconciliation_widget
# Modules merged into Odoo Core in version 16.0
merged_in_core:
- account_balance_line
- l10n_ch_states
- project_task_dependency
- web_ir_actions_act_view_reload
- mail_activity_creator
- website_sale_require_login
# Modules renamed in version 16.0
renamed:
- old: account_statement_import_file_reconcile_oca
new: account_statement_import_file_reconcile_oca
- old: mass_editing
new: server_action_mass_edit
- old: crm_project
new: crm_lead_to_task

View File

@@ -1,4 +0,0 @@
#!/bin/bash
set -euo pipefail
run_compose run -p 8016:8069 ou16 --config=/opt/odoo/auto/odoo.conf --stop-after-init -u all --workers 0 --log-level=debug --max-cron-threads=0 --limit-time-real=10000 --database=ou16 --load=base,web,openupgrade_framework

View File

@@ -1,13 +0,0 @@
# Modules that became obsolete in version 17.0
obsolete: []
# Modules merged into Odoo Core in version 17.0
merged_in_core:
- project_list
- web_advanced_search
- web_listview_range_select
# Modules renamed in version 17.0
renamed: []
# - old: old_name
# new: new_name

View File

@@ -1,4 +0,0 @@
#!/bin/bash
set -euo pipefail
run_compose run -p 8017:8069 ou17 --config=/opt/odoo/auto/odoo.conf --stop-after-init -u all --workers 0 --log-level=debug --max-cron-threads=0 --limit-time-real=10000 --database=ou17 --load=base,web,openupgrade_framework

View File

@@ -1,36 +0,0 @@
# Modules that became obsolete in version 18.0
obsolete:
- account_payment_paired_internal_transfer
- account_reconciliation_widget
# Modules merged into Odoo Core in version 18.0
merged_in_core:
- account_payment_partner
- hr_expense_report_merge_attachment
# Modules renamed in version 18.0
renamed:
# Refactor bank-payment-alternative
- old: account_payment_mode
new: account_payment_base_oca
- old: account_payment_sale
new: account_payment_base_oca_sale
- old: account_payment_order
new: account_payment_batch_oca
- old: account_payment_order_tier_validation
new: account_payment_batch_oca_tier_validation
- old: account_banking_pain_base
new: account_payment_sepa_base
- old: account_banking_sepa_credit_transfer
new: account_payment_sepa_credit_transfer
- old: account_banking_mandate
new: account_payment_mandate
- old: account_banking_mandate_sale
new: account_payment_mandate_sale
- old: account_banking_sepa_direct_debit
new: account_payment_sepa_direct_debit
# End refactor
- old: base_delivery_carrier_label
new: delivery_shipping_label_default
- old: l10n_fr_oca
new: l10n_fr_account_oca

View File

@@ -1,408 +0,0 @@
#!/bin/bash
set -euo pipefail
echo "Post migration to 18.0..."
# ============================================================================
# BANK-PAYMENT -> BANK-PAYMENT-ALTERNATIVE DATA MIGRATION
# Source PR: https://github.com/OCA/bank-payment-alternative/pull/42
#
# Only executed if account_payment_mode table exists (module was installed).
# ============================================================================
# Check if account_payment_mode table exists (meaning the module was installed before migration)
BANK_PAYMENT_TABLE_EXISTS=$(query_postgres_container "SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'account_payment_mode';" ou18 2>/dev/null | grep -E '^\s*[0-9]+' | tr -d ' ' || echo "0")
if [ "$BANK_PAYMENT_TABLE_EXISTS" -gt 0 ]; then
echo "Table account_payment_mode exists, proceeding with bank-payment data migration..."
BANK_PAYMENT_POST_SQL=$(cat <<'EOF'
DO $$
DECLARE
mode_rec RECORD;
new_line_id INTEGER;
journal_rec RECORD;
BEGIN
IF NOT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'account_payment_mode') THEN
RAISE NOTICE 'No account_payment_mode table found, skipping bank-payment migration';
RETURN;
END IF;
RAISE NOTICE 'Starting bank-payment to bank-payment-alternative migration...';
ALTER TABLE account_payment_method_line
ADD COLUMN IF NOT EXISTS old_payment_mode_id INT,
ADD COLUMN IF NOT EXISTS old_refund_payment_mode_id INT;
FOR mode_rec IN
SELECT id, name, company_id, payment_method_id,
fixed_journal_id AS journal_id, bank_account_link,
create_date, create_uid, write_date, write_uid,
show_bank_account, refund_payment_mode_id, active
FROM account_payment_mode
LOOP
INSERT INTO account_payment_method_line (
name, payment_method_id, bank_account_link, journal_id,
selectable, company_id, create_uid, create_date,
write_uid, write_date, show_bank_account,
old_payment_mode_id, old_refund_payment_mode_id, active
) VALUES (
to_jsonb(mode_rec.name),
mode_rec.payment_method_id,
mode_rec.bank_account_link,
mode_rec.journal_id,
true,
mode_rec.company_id,
mode_rec.create_uid,
mode_rec.create_date,
mode_rec.write_uid,
mode_rec.write_date,
mode_rec.show_bank_account,
mode_rec.id,
mode_rec.refund_payment_mode_id,
mode_rec.active
) RETURNING id INTO new_line_id;
IF mode_rec.bank_account_link = 'variable' THEN
IF EXISTS (SELECT FROM information_schema.tables
WHERE table_name = 'account_journal_account_payment_method_line_rel') THEN
FOR journal_rec IN
SELECT rel.journal_id
FROM account_payment_mode_variable_journal_rel rel
WHERE rel.payment_mode_id = mode_rec.id
LOOP
INSERT INTO account_journal_account_payment_method_line_rel
(account_payment_method_line_id, account_journal_id)
VALUES (new_line_id, journal_rec.journal_id)
ON CONFLICT DO NOTHING;
END LOOP;
END IF;
END IF;
RAISE NOTICE 'Migrated payment mode % -> payment method line %', mode_rec.id, new_line_id;
END LOOP;
UPDATE account_payment_method_line apml
SET refund_payment_method_line_id = apml2.id
FROM account_payment_method_line apml2
WHERE apml.old_refund_payment_mode_id IS NOT NULL
AND apml.old_refund_payment_mode_id = apml2.old_payment_mode_id;
UPDATE account_move am
SET preferred_payment_method_line_id = apml.id
FROM account_payment_mode apm, account_payment_method_line apml
WHERE am.payment_mode_id = apm.id
AND apm.id = apml.old_payment_mode_id
AND am.preferred_payment_method_line_id IS NULL;
RAISE NOTICE 'account_payment_base_oca migration completed';
END $$;
EOF
)
echo "Executing bank-payment base migration..."
query_postgres_container "$BANK_PAYMENT_POST_SQL" ou18 || exit 1
BANK_PAYMENT_BATCH_SQL=$(cat <<'EOF'
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'account_payment_mode') THEN
RETURN;
END IF;
IF NOT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'account_payment_order') THEN
RAISE NOTICE 'No account_payment_order table, skipping batch migration';
RETURN;
END IF;
RAISE NOTICE 'Starting account_payment_batch_oca migration...';
IF EXISTS (SELECT FROM information_schema.columns
WHERE table_name = 'account_payment_method' AND column_name = 'payment_order_only') THEN
UPDATE account_payment_method
SET payment_order_ok = payment_order_only
WHERE payment_order_only IS NOT NULL;
END IF;
UPDATE account_payment_method_line apml
SET payment_order_ok = apm.payment_order_ok,
no_debit_before_maturity = apm.no_debit_before_maturity,
default_payment_mode = apm.default_payment_mode,
default_invoice = apm.default_invoice,
default_target_move = apm.default_target_move,
default_date_type = apm.default_date_type,
default_date_prefered = apm.default_date_prefered,
group_lines = apm.group_lines
FROM account_payment_mode apm
WHERE apml.old_payment_mode_id IS NOT NULL
AND apm.id = apml.old_payment_mode_id;
IF EXISTS (SELECT FROM information_schema.tables
WHERE table_name = 'account_journal_account_payment_method_line_rel') THEN
DELETE FROM account_journal_account_payment_method_line_rel
WHERE account_payment_method_line_id IN (
SELECT id FROM account_payment_method_line WHERE old_payment_mode_id IS NOT NULL
);
INSERT INTO account_journal_account_payment_method_line_rel
(account_payment_method_line_id, account_journal_id)
SELECT apml.id, rel.account_journal_id
FROM account_journal_account_payment_mode_rel rel
JOIN account_payment_method_line apml ON rel.account_payment_mode_id = apml.old_payment_mode_id
ON CONFLICT DO NOTHING;
END IF;
UPDATE account_payment_order apo
SET payment_method_line_id = apml.id,
payment_method_code = apm_method.code
FROM account_payment_method_line apml,
account_payment_mode apm,
account_payment_method apm_method
WHERE apo.payment_mode_id = apm.id
AND apml.old_payment_mode_id = apm.id
AND apm_method.id = apml.payment_method_id;
RAISE NOTICE 'account_payment_batch_oca migration completed';
RAISE NOTICE 'NOTE: Payment lots for open orders must be generated manually via Odoo UI or script';
END $$;
EOF
)
echo "Executing bank-payment batch migration..."
query_postgres_container "$BANK_PAYMENT_BATCH_SQL" ou18 || exit 1
else
echo "Table account_payment_mode not found, skipping bank-payment migration."
fi
# ============================================================================
# FIX: stock_picking name='/' and missing POS picking type sequences
#
# Two issues can occur after migration:
#
# 1. stock_picking records with name='/' violate the new V18 unique constraint
# stock_picking_name_uniq (name, company_id). In previous versions this
# constraint did not exist, so multiple pickings could share name='/'.
#
# 2. POS picking types (pos_type_id on stock_warehouse) may lack a sequence_id.
# In V18, stock.picking.create() assigns the name from picking_type.sequence_id,
# but if sequence_id is NULL the name stays '/' and the unique constraint is
# violated on every new POS payment.
#
# The standard _create_missing_pos_picking_types() only fixes warehouses where
# pos_type_id is NULL — it does NOT fix existing pos_type_id records that are
# missing their sequence_id.
#
# Only executed if stock module tables exist.
# ============================================================================
STOCK_PICKING_TABLE_EXISTS=$(query_postgres_container "SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'stock_picking';" ou18 2>/dev/null | grep -E '^\s*[0-9]+' | tr -d ' ' || echo "0")
if [ "$STOCK_PICKING_TABLE_EXISTS" -gt 0 ]; then
echo "stock_picking table exists, proceeding with POS picking fixes..."
# --- Step 1: Rename stock_picking records with name='/' ---
FIX_SLASH_PICKING_NAMES_SQL=$(cat <<'EOF'
DO $$
DECLARE
slash_count INTEGER;
BEGIN
IF NOT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'stock_picking') THEN
RAISE NOTICE 'stock_picking table not found, skipping slash name fix';
RETURN;
END IF;
SELECT COUNT(*) INTO slash_count FROM stock_picking WHERE name = '/';
IF slash_count > 0 THEN
UPDATE stock_picking SET name = 'MIGRATED-' || id WHERE name = '/';
RAISE NOTICE 'Renamed % stock_picking record(s) with name=/ to MIGRATED-<id>', slash_count;
ELSE
RAISE NOTICE 'No stock_picking with name=/ found, nothing to rename';
END IF;
END $$;
EOF
)
echo "Fixing stock_picking names with '/'..."
query_postgres_container "$FIX_SLASH_PICKING_NAMES_SQL" ou18 || exit 1
# --- Step 2: Create missing ir.sequence for POS picking types without sequence ---
FIX_POS_PICKING_TYPE_SEQUENCES_SQL=$(cat <<'EOF'
DO $$
DECLARE
pt_rec RECORD;
seq_id INTEGER;
seq_name TEXT;
seq_prefix TEXT;
BEGIN
IF NOT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'stock_picking_type') THEN
RAISE NOTICE 'stock_picking_type table not found, skipping POS sequence fix';
RETURN;
END IF;
IF NOT EXISTS (SELECT FROM information_schema.columns
WHERE table_name = 'stock_warehouse' AND column_name = 'pos_type_id') THEN
RAISE NOTICE 'stock_warehouse.pos_type_id column not found (point_of_sale not installed), skipping POS sequence fix';
RETURN;
END IF;
FOR pt_rec IN
SELECT spt.id AS picking_type_id,
spt.name AS picking_type_name,
COALESCE(spt.sequence_code, 'POS') AS sequence_code,
sw.code AS warehouse_code,
sw.name AS warehouse_name,
sw.company_id
FROM stock_picking_type spt
JOIN stock_warehouse sw ON sw.pos_type_id = spt.id
WHERE spt.sequence_id IS NULL OR spt.sequence_id = 0
LOOP
seq_prefix := pt_rec.warehouse_code || '/' || pt_rec.sequence_code || '/';
seq_name := pt_rec.warehouse_name || ' Picking POS';
INSERT INTO ir_sequence (
name, prefix, padding, company_id, implementation,
number_increment, number_next, active
) VALUES (
seq_name, seq_prefix, 5, pt_rec.company_id, 'standard',
1, 1, true
) RETURNING id INTO seq_id;
UPDATE stock_picking_type
SET sequence_id = seq_id
WHERE id = pt_rec.picking_type_id;
RAISE NOTICE 'Created ir.sequence % (%) for POS picking type % (ID:%) on warehouse %',
seq_name, seq_id, pt_rec.picking_type_name, pt_rec.picking_type_id, pt_rec.warehouse_name;
END LOOP;
END $$;
EOF
)
echo "Creating missing POS picking type sequences..."
query_postgres_container "$FIX_POS_PICKING_TYPE_SEQUENCES_SQL" ou18 || exit 1
# --- Step 3: Create missing PostgreSQL sequences for ir.sequence records ---
# When ir.sequence records are created via SQL INSERT (not ORM), the underlying
# PostgreSQL sequence (ir_sequence_NNN) is NOT created. This causes errors when
# Odoo tries to read or use the sequence.
FIX_MISSING_PG_SEQUENCES_SQL=$(cat <<'EOF'
DO $$
DECLARE
seq_rec RECORD;
seq_pg_name TEXT;
seq_count INTEGER;
BEGIN
seq_count := 0;
FOR seq_rec IN
SELECT id, number_increment, number_next
FROM ir_sequence
WHERE implementation = 'standard'
LOOP
seq_pg_name := 'ir_sequence_' || lpad(seq_rec.id::text, 3, '0');
IF NOT EXISTS (
SELECT 1 FROM pg_class
WHERE relkind = 'S' AND relname = seq_pg_name
) THEN
EXECUTE format('CREATE SEQUENCE %I INCREMENT BY %s START WITH %s',
seq_pg_name, seq_rec.number_increment, seq_rec.number_next);
seq_count := seq_count + 1;
RAISE NOTICE 'Created PostgreSQL sequence %', seq_pg_name;
END IF;
END LOOP;
IF seq_count > 0 THEN
RAISE NOTICE 'Created % missing PostgreSQL sequence(s)', seq_count;
ELSE
RAISE NOTICE 'All PostgreSQL sequences already exist, nothing to create';
END IF;
END $$;
EOF
)
echo "Creating missing PostgreSQL sequences for ir.sequence records..."
query_postgres_container "$FIX_MISSING_PG_SEQUENCES_SQL" ou18 || exit 1
# --- Step 4: Grant permissions on newly created PostgreSQL sequences ---
# Sequences created above are owned by the current DB user, but we ensure
# the Odoo DB user has proper access (needed when created by a different superuser).
GRANT_PG_SEQUENCES_SQL=$(cat <<'EOF'
DO $$
DECLARE
seq_rec RECORD;
seq_pg_name TEXT;
db_user TEXT;
grant_count INTEGER;
BEGIN
-- Determine the Odoo database user from the current connection
SELECT current_user INTO db_user;
grant_count := 0;
FOR seq_rec IN
SELECT id
FROM ir_sequence
WHERE implementation = 'standard'
LOOP
seq_pg_name := 'ir_sequence_' || lpad(seq_rec.id::text, 3, '0');
IF EXISTS (
SELECT 1 FROM pg_class
WHERE relkind = 'S' AND relname = seq_pg_name
) THEN
BEGIN
EXECUTE format('GRANT ALL ON SEQUENCE %I TO %I', seq_pg_name, db_user);
grant_count := grant_count + 1;
EXCEPTION WHEN insufficient_privilege THEN
RAISE NOTICE 'Cannot GRANT on % (not owner), skipping', seq_pg_name;
END;
END IF;
END LOOP;
RAISE NOTICE 'Granted permissions on % PostgreSQL sequence(s) to %', grant_count, db_user;
END $$;
EOF
)
echo "Granting permissions on PostgreSQL sequences..."
query_postgres_container "$GRANT_PG_SEQUENCES_SQL" ou18 || exit 1
# --- Step 5: Verification ---
VERIFY_POS_FIXES_SQL=$(cat <<'EOF'
DO $$
DECLARE
slash_count INTEGER;
missing_seq_count INTEGER;
BEGIN
-- Check remaining pickings with name='/'
IF EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'stock_picking') THEN
SELECT COUNT(*) INTO slash_count FROM stock_picking WHERE name = '/';
IF slash_count > 0 THEN
RAISE WARNING 'Still % stock_picking record(s) with name=/', slash_count;
ELSE
RAISE NOTICE 'OK: No stock_picking with name=/ remaining';
END IF;
END IF;
-- Check POS picking types without sequence
IF EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'stock_picking_type')
AND EXISTS (SELECT FROM information_schema.columns
WHERE table_name = 'stock_warehouse' AND column_name = 'pos_type_id') THEN
SELECT COUNT(*) INTO missing_seq_count
FROM stock_picking_type spt
JOIN stock_warehouse sw ON sw.pos_type_id = spt.id
WHERE spt.sequence_id IS NULL OR spt.sequence_id = 0;
IF missing_seq_count > 0 THEN
RAISE WARNING 'Still % POS picking type(s) without sequence_id', missing_seq_count;
ELSE
RAISE NOTICE 'OK: All POS picking types have a sequence_id';
END IF;
END IF;
END $$;
EOF
)
echo "Verifying POS picking fixes..."
query_postgres_container "$VERIFY_POS_FIXES_SQL" ou18 || exit 1
else
echo "stock_picking table not found, skipping POS picking fixes."
fi
echo "Post migration to 18.0 completed!"

View File

@@ -1,140 +0,0 @@
#!/bin/bash
set -euo pipefail
echo "Prepare migration to 18.0..."
# Copy database
copy_database ou17 ou18 ou18 || exit 1
# ============================================================================
# BANK-PAYMENT -> BANK-PAYMENT-ALTERNATIVE MODULE RENAMING
# Migration from OCA/bank-payment to OCA/bank-payment-alternative
# Source PR: https://github.com/OCA/bank-payment-alternative/pull/42
#
# This renaming MUST be done BEFORE OpenUpgrade runs, so that the migration
# scripts in the new modules (account_payment_base_oca, account_payment_batch_oca)
# can properly migrate the data.
#
# Only executed if account_payment_mode module was installed before migration.
# ============================================================================
# Check if account_payment_mode module is installed
BANK_PAYMENT_INSTALLED=$(query_postgres_container "SELECT COUNT(*) FROM ir_module_module WHERE name = 'account_payment_mode' AND state = 'installed';" ou18 2>/dev/null | grep -E '^\s*[0-9]+' | tr -d ' ' || echo "0")
if [ "$BANK_PAYMENT_INSTALLED" -gt 0 ]; then
echo "Module account_payment_mode is installed, proceeding with bank-payment migration..."
BANK_PAYMENT_RENAME_SQL=$(cat <<'EOF'
DO $$
DECLARE
renamed_modules TEXT[][] := ARRAY[
['account_payment_mode', 'account_payment_base_oca'],
['account_banking_pain_base', 'account_payment_sepa_base'],
['account_banking_sepa_credit_transfer', 'account_payment_sepa_credit_transfer'],
['account_payment_order', 'account_payment_batch_oca']
];
merged_modules TEXT[][] := ARRAY[
['account_payment_partner', 'account_payment_base_oca']
];
old_name TEXT;
new_name TEXT;
old_module_id INTEGER;
deleted_count INTEGER;
BEGIN
FOR i IN 1..array_length(renamed_modules, 1) LOOP
old_name := renamed_modules[i][1];
new_name := renamed_modules[i][2];
SELECT id INTO old_module_id FROM ir_module_module WHERE name = old_name;
IF old_module_id IS NOT NULL THEN
RAISE NOTICE 'Renaming module: % -> %', old_name, new_name;
UPDATE ir_module_module SET name = new_name WHERE name = old_name;
UPDATE ir_model_data SET module = new_name WHERE module = old_name;
UPDATE ir_module_module_dependency SET name = new_name WHERE name = old_name;
END IF;
END LOOP;
FOR i IN 1..array_length(merged_modules, 1) LOOP
old_name := merged_modules[i][1];
new_name := merged_modules[i][2];
SELECT id INTO old_module_id FROM ir_module_module WHERE name = old_name;
IF old_module_id IS NOT NULL THEN
RAISE NOTICE 'Merging module: % -> %', old_name, new_name;
DELETE FROM ir_model_data
WHERE module = old_name
AND name IN (SELECT name FROM ir_model_data WHERE module = new_name);
GET DIAGNOSTICS deleted_count = ROW_COUNT;
IF deleted_count > 0 THEN
RAISE NOTICE ' Deleted % duplicate ir_model_data records', deleted_count;
END IF;
UPDATE ir_model_data SET module = new_name WHERE module = old_name;
UPDATE ir_module_module_dependency SET name = new_name WHERE name = old_name;
UPDATE ir_module_module SET state = 'uninstalled' WHERE name = old_name;
DELETE FROM ir_module_module WHERE name = old_name;
END IF;
END LOOP;
END $$;
EOF
)
echo "Executing bank-payment module renaming..."
query_postgres_container "$BANK_PAYMENT_RENAME_SQL" ou18 || exit 1
BANK_PAYMENT_PRE_SQL=$(cat <<'EOF'
UPDATE ir_model_data
SET noupdate = false
WHERE module = 'account_payment_base_oca'
AND name = 'view_account_invoice_report_search';
EOF
)
echo "Executing bank-payment pre-migration..."
query_postgres_container "$BANK_PAYMENT_PRE_SQL" ou18 || exit 1
else
echo "Module account_payment_mode not installed, skipping bank-payment migration."
fi
# ============================================================================
# FIX: Rename company-dependent columns before OpenUpgrade runs
# In Odoo 18, company-dependent fields are stored as JSONB columns.
# The ORM's _auto_init() tries to convert existing VARCHAR columns to JSONB,
# which fails if the data is not valid JSON.
# Solution: Rename the columns so Odoo creates new JSONB columns, then
# OpenUpgrade's convert_company_dependent() will migrate the data from ir.property.
#
# See: https://github.com/OCA/OpenUpgrade/issues/5449
# ============================================================================
COMPANY_DEPENDENT_FIX_SQL=$(cat <<'EOF'
DO $$
BEGIN
-- res.partner.barcode (base module)
IF EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'res_partner' AND column_name = 'barcode') THEN
ALTER TABLE res_partner RENAME COLUMN barcode TO openupgrade_legacy_18_0_barcode;
RAISE NOTICE 'Renamed res_partner.barcode for company-dependent conversion';
END IF;
END $$;
EOF
)
echo "Fixing company-dependent columns for Odoo 18..."
query_postgres_container "$COMPANY_DEPENDENT_FIX_SQL" ou18 || exit 1
EOF
)
# Execute SQL pre-migration commands
PRE_MIGRATE_SQL=$(cat <<'EOF'
UPDATE account_analytic_plan SET default_applicability=NULL WHERE default_applicability='optional';
DELETE FROM ir_ui_view WHERE model = 'res.config.settings';
EOF
)
echo "SQL command = $PRE_MIGRATE_SQL"
query_postgres_container "$PRE_MIGRATE_SQL" ou18 || exit 1
# Copy filestores
copy_filestore ou17 ou17 ou18 ou18 || exit 1
echo "Ready for migration to 18.0!"

View File

@@ -1,4 +0,0 @@
#!/bin/bash
set -euo pipefail
run_compose run -p 8018:8069 ou18 --config=/opt/odoo/auto/odoo.conf --stop-after-init -u all --workers 0 --log-level=debug --max-cron-threads=0 --limit-time-real=10000 --database=ou18 --load=base,web,openupgrade_framework