Compare commits
7 Commits
main
...
d40e8941fb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d40e8941fb | ||
|
|
29dda445a2 | ||
|
|
969414e649 | ||
|
|
f9d678508b | ||
|
|
55218946c5 | ||
|
|
25fff3da5d | ||
|
|
c59439aa8d |
@@ -14,7 +14,7 @@ readonly FILESTORE_SUBPATH="var/lib/odoo/filestore"
|
||||
|
||||
check_required_commands() {
|
||||
local missing=()
|
||||
for cmd in docker compose sudo rsync; do
|
||||
for cmd in docker compose sudo rsync yq; do
|
||||
if ! command -v "$cmd" &>/dev/null; then
|
||||
missing+=("$cmd")
|
||||
fi
|
||||
@@ -100,7 +100,83 @@ exec_python_script_in_odoo_shell() {
|
||||
run_compose --debug run "$service_name" shell -d "$db_name" --no-http --stop-after-init < "$python_script"
|
||||
}
|
||||
|
||||
# Classifies missing modules into 4 categories based on the known_changes.yaml
|
||||
# files from each traversed version (from ORIGIN_VERSION+1 to FINAL_VERSION).
|
||||
# The following global arrays are populated:
|
||||
# addons_obsolete : modules that became obsolete
|
||||
# addons_core : modules merged into Odoo Core
|
||||
# addons_renamed : renamed modules (format "old_name -> new_name")
|
||||
# addons_truly_missing : modules that are genuinely missing
|
||||
#
|
||||
# Prerequisites: ORIGIN_VERSION and FINAL_VERSION must be exported.
|
||||
classify_missing_addons() {
|
||||
local missing_addons_raw="$1"
|
||||
|
||||
addons_obsolete=()
|
||||
addons_core=()
|
||||
addons_renamed=()
|
||||
addons_truly_missing=()
|
||||
|
||||
# Convert the string into an array (one entry per line)
|
||||
local -a missing=()
|
||||
while IFS= read -r line; do
|
||||
[[ -n "$line" ]] && missing+=("$line")
|
||||
done <<< "$missing_addons_raw"
|
||||
|
||||
if [[ ${#missing[@]} -eq 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Build lookup tables from all known_changes.yaml files in traversed versions
|
||||
local -A known_obsolete=()
|
||||
local -A known_core=()
|
||||
local -A known_renamed=()
|
||||
|
||||
local versions_path="${PROJECT_ROOT}/versions"
|
||||
local v
|
||||
for v in $(seq $((ORIGIN_VERSION + 1)) "$FINAL_VERSION"); do
|
||||
local yaml_file="${versions_path}/${v}.0/known_changes.yaml"
|
||||
[[ -f "$yaml_file" ]] || continue
|
||||
|
||||
local mod
|
||||
while IFS= read -r mod; do
|
||||
[[ -n "$mod" && "$mod" != "null" ]] && known_obsolete["$mod"]=1
|
||||
done < <(yq '.obsolete[]?' "$yaml_file" 2>/dev/null)
|
||||
|
||||
while IFS= read -r mod; do
|
||||
[[ -n "$mod" && "$mod" != "null" ]] && known_core["$mod"]=1
|
||||
done < <(yq '.merged_in_core[]?' "$yaml_file" 2>/dev/null)
|
||||
|
||||
local count
|
||||
count=$(yq '.renamed | length' "$yaml_file" 2>/dev/null)
|
||||
if [[ "$count" =~ ^[0-9]+$ && "$count" -gt 0 ]]; then
|
||||
local i
|
||||
for ((i = 0; i < count; i++)); do
|
||||
local old new
|
||||
old=$(yq ".renamed[$i].old" "$yaml_file" 2>/dev/null)
|
||||
new=$(yq ".renamed[$i].new" "$yaml_file" 2>/dev/null)
|
||||
[[ -n "$old" && "$old" != "null" ]] && known_renamed["$old"]="$new"
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
# Classify each missing module
|
||||
local mod
|
||||
for mod in "${missing[@]}"; do
|
||||
if [[ -n "${known_obsolete[$mod]:-}" ]]; then
|
||||
addons_obsolete+=("$mod")
|
||||
elif [[ -n "${known_core[$mod]:-}" ]]; then
|
||||
addons_core+=("$mod")
|
||||
elif [[ -n "${known_renamed[$mod]:-}" ]]; then
|
||||
addons_renamed+=("${mod} -> ${known_renamed[$mod]}")
|
||||
else
|
||||
addons_truly_missing+=("$mod")
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
export PROJECT_ROOT DATASTORE_PATH FILESTORE_SUBPATH
|
||||
export -f log_info log_warn log_error log_step confirm_or_exit
|
||||
export -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
|
||||
|
||||
141
lib/python/regenerate_pos_hashes.py
Normal file
141
lib/python/regenerate_pos_hashes.py
Normal file
@@ -0,0 +1,141 @@
|
||||
#!/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()
|
||||
@@ -44,6 +44,29 @@ 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 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")
|
||||
|
||||
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"
|
||||
|
||||
|
||||
@@ -60,9 +60,31 @@ echo "Retrieve missing addons..."
|
||||
missing_addons=$(query_postgres_container "$SQL_MISSING_ADDONS" "$DB_NAME")
|
||||
|
||||
log_step "ADD-ONS CHECK"
|
||||
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?"
|
||||
classify_missing_addons "$missing_addons"
|
||||
|
||||
if [[ ${#addons_obsolete[@]} -gt 0 ]]; then
|
||||
log_info "Obsolete modules (${#addons_obsolete[@]}):"
|
||||
printf "%s\n" "${addons_obsolete[@]}"
|
||||
echo ""
|
||||
fi
|
||||
if [[ ${#addons_core[@]} -gt 0 ]]; then
|
||||
log_info "Merged into Odoo Core (${#addons_core[@]}):"
|
||||
printf "%s\n" "${addons_core[@]}"
|
||||
echo ""
|
||||
fi
|
||||
if [[ ${#addons_renamed[@]} -gt 0 ]]; then
|
||||
log_info "Renamed modules (${#addons_renamed[@]}):"
|
||||
printf "%s\n" "${addons_renamed[@]}"
|
||||
echo ""
|
||||
fi
|
||||
if [[ ${#addons_truly_missing[@]} -gt 0 ]]; then
|
||||
log_warn "Truly missing modules (${#addons_truly_missing[@]}):"
|
||||
printf "%s\n" "${addons_truly_missing[@]}"
|
||||
echo ""
|
||||
confirm_or_exit "Do you accept to migrate with these add-ons truly missing?"
|
||||
else
|
||||
log_info "No truly missing modules — all accounted for."
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PYTHON_SCRIPT="${SCRIPT_DIR}/lib/python/check_views.py"
|
||||
|
||||
@@ -31,8 +31,10 @@ fi
|
||||
|
||||
check_required_commands
|
||||
|
||||
readonly ORIGIN_VERSION="$1"
|
||||
readonly FINAL_VERSION="$2"
|
||||
export ORIGIN_VERSION="$1"
|
||||
readonly ORIGIN_VERSION
|
||||
export FINAL_VERSION="$2"
|
||||
readonly FINAL_VERSION
|
||||
readonly ORIGIN_DB_NAME="$3"
|
||||
readonly ORIGIN_SERVICE_NAME="$4"
|
||||
|
||||
|
||||
10
versions/13.0/known_changes.yaml
Normal file
10
versions/13.0/known_changes.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
# 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
|
||||
10
versions/14.0/known_changes.yaml
Normal file
10
versions/14.0/known_changes.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
# 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
|
||||
15
versions/15.0/known_changes.yaml
Normal file
15
versions/15.0/known_changes.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
# 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
|
||||
|
||||
# Modules renamed in version 15.0
|
||||
renamed:
|
||||
- old: crm_phone
|
||||
new: crm_phonecall
|
||||
23
versions/16.0/known_changes.yaml
Normal file
23
versions/16.0/known_changes.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
# 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
|
||||
13
versions/17.0/known_changes.yaml
Normal file
13
versions/17.0/known_changes.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
# 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
|
||||
35
versions/18.0/known_changes.yaml
Normal file
35
versions/18.0/known_changes.yaml
Normal file
@@ -0,0 +1,35 @@
|
||||
# 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
|
||||
|
||||
# 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
|
||||
@@ -3,4 +3,406 @@ set -euo pipefail
|
||||
|
||||
echo "Post migration to 18.0..."
|
||||
|
||||
#compose --debug run ou18 -u base --stop-after-init --no-http
|
||||
# ============================================================================
|
||||
# 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!"
|
||||
|
||||
@@ -6,9 +6,129 @@ 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"
|
||||
|
||||
Reference in New Issue
Block a user