Compare commits
16 Commits
main
...
1281517b2f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1281517b2f | ||
|
|
7a22648429 | ||
|
|
7b1ce9cc5a | ||
|
|
c815374cf1 | ||
|
|
dd6c41b382 | ||
|
|
0a75c2291b | ||
|
|
55365df5f9 | ||
|
|
e4d11de009 | ||
|
|
516b9def3d | ||
|
|
20214b4402 | ||
|
|
fa8b1b98f1 | ||
|
|
7c3d5d49c8 | ||
|
|
333360f9f0 | ||
|
|
4b27955be9 | ||
|
|
a91afa60d3 | ||
|
|
a1508daf24 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1 @@
|
||||
final_404_addons
|
||||
migration.log
|
||||
|
||||
4
13.0/upgrade.sh
Executable file
4
13.0/upgrade.sh
Executable 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
4
14.0/upgrade.sh
Executable 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
4
15.0/upgrade.sh
Executable 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
4
16.0/upgrade.sh
Executable 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
4
17.0/upgrade.sh
Executable 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
6
18.0/post_upgrade.sh
Executable 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
20
18.0/pre_upgrade.sh
Executable 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
4
18.0/upgrade.sh
Executable 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
453
README.md
@@ -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.
|
||||
|
||||
51
finalize_db.sh
Executable file
51
finalize_db.sh
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/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;
|
||||
|
||||
-- Reset website templates to their original state.
|
||||
-- Views with arch_fs (file source) that have been customized (arch_db not null)
|
||||
-- are reset to use the file version, EXCEPT for actual website pages which
|
||||
-- contain user content that must be preserved.
|
||||
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);
|
||||
|
||||
-- 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"
|
||||
|
||||
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"
|
||||
|
||||
# Uninstall obsolete 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
|
||||
@@ -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 rsync; do
|
||||
if ! command -v "$cmd" &>/dev/null; then
|
||||
missing+=("$cmd")
|
||||
fi
|
||||
@@ -85,98 +82,15 @@ copy_filestore() {
|
||||
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 DATASTORE_PATH FILESTORE_SUBPATH
|
||||
export -f log_info log_warn log_error log_step confirm_or_exit
|
||||
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
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -52,7 +52,7 @@ 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
|
||||
)
|
||||
@@ -60,34 +60,11 @@ echo "Retrieve missing addons..."
|
||||
missing_addons=$(query_postgres_container "$SQL_MISSING_ADDONS" "$DB_NAME")
|
||||
|
||||
log_step "ADD-ONS CHECK"
|
||||
classify_missing_addons "$missing_addons"
|
||||
echo "Installed add-ons not available in final Odoo version:"
|
||||
echo "$missing_addons"
|
||||
confirm_or_exit "Do you accept to migrate with these add-ons still installed?"
|
||||
|
||||
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
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PYTHON_SCRIPT="${SCRIPT_DIR}/lib/python/check_views.py"
|
||||
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"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
166
upgrade.sh
166
upgrade.sh
@@ -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,78 +18,29 @@ 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
|
||||
|
||||
readarray -t postgres_containers < <(docker ps --format '{{.Names}}' | grep postgres || true)
|
||||
|
||||
if [[ ${#postgres_containers[@]} -eq 0 ]]; then
|
||||
@@ -115,13 +59,8 @@ 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 +72,52 @@ 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
|
||||
readarray -t versions < <(seq $((ORIGIN_VERSION + 1)) "$FINAL_VERSION")
|
||||
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_NAME" "$FINALE_SERVICE_NAME"
|
||||
|
||||
|
||||
log_step "UPGRADE PROCESS"
|
||||
@@ -220,16 +125,15 @@ 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"
|
||||
"${SCRIPT_DIR}/${version}.0/pre_upgrade.sh"
|
||||
"${SCRIPT_DIR}/${version}.0/upgrade.sh"
|
||||
"${SCRIPT_DIR}/${version}.0/post_upgrade.sh"
|
||||
|
||||
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}"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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!"
|
||||
@@ -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!"
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user