Compare commits

...

3 Commits

Author SHA1 Message Date
Stéphan Sainléger
969414e649 [IMP] detect missing pos.order hashes and regenerate them 2026-05-28 11:45:18 +02:00
Stéphan Sainléger
f9d678508b [IMP] add Odoo addons known changes 2026-05-28 11:45:18 +02:00
Stéphan Sainléger
55218946c5 [NEW] add detection of obsolete, Odoo core integrated or renamed addons 2026-05-28 11:45:18 +02:00
11 changed files with 376 additions and 6 deletions

View File

@@ -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

View 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()

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View 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

View 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

View 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

View 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

View 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

View 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