tweaks for import

This commit is contained in:
Florian du Garage Num 2025-08-26 11:56:43 +02:00
parent 95ad2b2855
commit 8f0d5775a6
9 changed files with 534 additions and 0 deletions

255
gn_import/README.md Normal file
View File

@ -0,0 +1,255 @@
# GN Import
If you migrate your data from one instance to another, you might need to restore attachment files as well.
Export and import the list, with their external id and filename. Import using filename as url.
Copy the files from old instance's filestore folder to new one's, keeping arborescency intact.
Install this module and create an action server calling to records.action_upload_file().
Now you have a contextuel action on file attachments: "Réimporter le fichier".
It will use path for filestore and filename to get the file and import it as base64 content.
## How to migrate
In both old and new instance, activate Dev Mode from Paramètres / Paramètres généraux (tout en bas de la page)
### Exports from new instance
Some exports from the new instance because some ids are generated during initial configuration
Before importing records from old instance, you will have to use these data to replace some records ids in the data to export.
#### Export des taxes (account.tax) et des lignes de répartition des taxes (account.tax.repartition.line)
Dans la nouvelle instance, exporter les Taxes (Facturation / Configuration / Comptabilité / Taxes)
| Nom | Nom dev_mode |
|------|--------------|
| ID externe | id |
| Répartition pour les factures/Taxe | invoice_repartition_line_ids/tax_id |
| Répartition pour les factures/Basé sur | invoice_repartition_line_ids/repartition_type |
| Répartition pour les factures/ID externe | invoice_repartition_line_ids/id |
| Répartition pour les factures de remboursement/Taxes | invoice_repartition_line_ids/tax_id |
| Répartition pour les factures de remboursement/Basé sur | invoice_repartition_line_ids/tax_id |
| Répartition pour les factures de remboursement/ID externe | invoice_repartition_line_ids/tax_id |
#### Export de account.journal
Dans la nouvelle instance, exporter les journaux (facturation / Configuration / Comptabilité Journaux)
| Nom | Nom dev_mode |
|------|---------------|
| id externe | id |
| Compte de profit | profit_account_id |
| Compte des pertes | loss_account_id |
| Compte par défaut | default_account_id |
| Compte créancier/débiteur | receivable_account_id |
| Compte d'attent | suspense_account_id |
| Modes de paiement entrant/Compte de paiement | inbound_payment_method_line_ids/payment_account_id |
| Modes de paiement sortant/Compte de paiement | outbound_payment_method_line_ids/payment_account_id |
### Migration
# Migrate res.partner
- replace ids in column ID externe (id): attention à bas.main_partner et base.parten_admin
- Configurer le compte client et fournisseur
| Nom à extraire | Nom à extraire (dev_mode) | Nom de colonne à modifier | Enregistrements à modifier |
| ---------------------------------- | -------------------------------- | ----------------------------- | ---------------------------- |
| ID externe | id | | |
| Actif | active | | |
| Code postal | zip | | |
| Contact/ID | child_ids/id | | |
| Email | email | | |
| Employé | employee | | |
| Forme juridique/Titre | partner_company_type/name | Forme juridique | Respecter la casse |
| Informations additionnelles | additional_info | | |
| Mobile | mobile | | |
| Mode de facturation | invoicing_mode | Invoicing Mode | |
| Nom | name | | |
| Prénom | first_name | | |
| Nom de famille | last_name | | |
| Nom de société | company_name | | |
| Notes | comment | | |
| N° TVA | vat | | |
| Pays | country_id | | |
| Poste de travail | function | Poste | |
| Rue | street | | |
| Rue 2 | street2 | | |
| Référence | ref | | |
| SIRET | siret | | |
| Titre/ID | title/id | | |
| Type d'adresse | type | | |
| Type de société | company_type | | |
| Téléphone | phone | | |
| Téléphone / Mobile | phone_mobile_search | Téléphone fixe/Mobile | |
| Ville | city | | |
| Compte client / Code | property_account_receivable/code | Compte client | |
| Compte fournisseur / Code | property_account_payable/code | Compte fournisseur | |
#### Migrate res.users
- Supprimer la ligne de l'utilisateur base.admin
- Identifiant
- Langue
- Nom
- Fuseau horaire
- Id externe
- Nom de famille
- Prénom
- Partenaire associé | partner_id/ID
### Migrate account.product
! Remplacer les Ids des taxes
! Pour type de produit: Consommable -> consu ; Service -> service ; possibilité d'utiliser comb |
| Nom | Nom (mode développeur) | Nouveau nom de colonne | Changements à faire |
| ------ | ---------------------------------------------------------- | ----------------------------- | ---------------------------- |
| Actif | active | | |
| Catégorie de produit/ID externe | categ_id/id | | |
| Compte de charges/code | property_account_expense_id/code | Compte des charges | |
| Compte des revenus/code | property_account_income_id/code | Compte des revenus | |
| Coût | standard_price | | |
| Créer à la commande | service_tracking | | |
| Description | description | | |
| Description achat | description_purchase | Description des achats | |
| Description vente | description_sale | Description vente | |
| Doit avoir une date de début et une date de fin | must_have_dates | | |
| Nom | name | | |
| Nom d'affichage | display_name | | |
| Peut être acheté | purchase_ok | Achats | |
| Peut être inséré dans une note de frais | can_be_expensed | Dépenses | |
| Peut être vendu | sale_ok | Ventes | |
| Politique de contrôle | purchase_method | | |
| Politique de facturation | invoice_policy | | |
| Politique de facturation du service | service_policy | | |
| Référence interne | default_code | | |
| Séquence | sequence | | |
| Taxes fournisseurs/Nom de la taxe | supplier_taxes_id/name | | Supprimer avant import, après remplacement des ids |
| Taxes fournisseurs/ID externe | supplier_taxes_id/id | Taxes d'achat/ID externe | à remplacer |
| Taxes à la vente/Nom de la taxe | taxes_id/name | | Supprimer avant import, après remplacement des ids |
| Taxes à la vente/ID externe | taxes_id/id | | à remplacer |
| Type de produit | detailed_type | | |
| UdM achat/ID externe | uom_po_ id/id | Unité d'achat/Id externe | |
| Unité de mesure/ID externe | uom_id/id | Unité de mesure/ID externe | |
| ID externe | id | | |
| Produit/ID externe | product_variant_id/id | | |
#### Export account.move
On fait un premier import de account.move, en excluant les colonnes qui provoquent un recalcul du montant (débit, crédit, taxes, produit...)
Le deuxième import prend en compte toutes les colonnes.
| Nom | Nom (dev mode) | Ne pas importer | Dès le 1er import | Nom de colonne pour import | Remplacements |
|----------|----------------------------------------------------------- | ----------------| -------------------| ----------------------------- | --------------------- |
| ID | id | | X | | |
| Extourne/ID | reversal_move_id/id | | X | | |
| Type | move_type | | X | | |
| Statut | state | | X | | |
| Date d'échéance | invoice_date_due | | X | | |
| Date | date | | X | | |
| Date de facturation | invoice_date | | X | | |
| Journal/ID | journal_id/id | | X | | à remplacer |
| Journal/Nom du journal | journal_id/name | | X | | à supprimer |
| Numéro | name | | X | | |
| Référence | ref | | X | | |
| Référence du paiement | payment_ref | | X | | |
| Partenaire/ID | partner_id/id | | X | | |
| Partenaire/Nom | partner_id/name | | X | | |
| Paiements/ID | id | | X | | |
| Paiements/ID | id | | X | | |
| Bon de commande / ID | id | | X | | |
| Note de frais/ID | | X | | |
| Note de frais/ID | | X | | |
| Écritures comptables/ID | line_ids/id | | X | | |
| Écritures comptables/Type d'affichage | line_ids/display_type | | X | | sur ttes les lignes |
| Écritures comptables/Pièce comptable/ID | line_ids/move_id/id | X | | | |
| Écritures comptables/Compte/Code | line_ids/account_id/code | | X | Écritures comptables/Compte | à tester |
| Écritures comptables/Libellé | line_ids/name | | X | | |
| Écritures comptables/Date d'échéance | line_ids/date_maturity | | X | | |
| Écritures comptables/Date de début | line_ids/start_date | | X | | |
| Écritures comptables/Date de fin | line_ids/end_date | | X | | |
| Écritures comptables/Ligne d'avoir/ID | line_ids/refund_line_ids/id | | X | | |
| Écritures comptables/Partenaire/ID | line_ids/partner_id/id | | x | | à vérifier |
| Écritures comptables/Unité de mesure/ID | line_ids/product_uom_id/id | | x | | |
| Écritures comptables/Débit | line_ids/debit | | | | |
| Écritures comptables/Crédit | line_ids/credit | | | | |
| Écritures comptables/Produit/ID | line_ids/product_id/id | | | | |
| Écritures comptables/Prix unitaire | line_ids/price_unit | | | | |
| Écritures comptables/Quantité | line_ids/quantity | | | | |
| Écritures comptables/Remise % | line_ids/discount | | | | |
| Écritures comptables/Remise fixe | line_ids/discount_fixed | | | | |
| Écritures comptables/Taxes/ID | line_ids/tax_ids/id | | | | à remplacer |
| Écritures comptables/Taxes | line_ids/tax_ids/id | X | | | à supprimer |
| Écritures comptables/Ligne de répartition de la taxe d'origine/ID | line_ids/tax_repartition_line_id/id | | | | à remplacer |
| Écritures comptables/Ligne de répartition de la taxe d'origine/Taxe |line_ids/tax_repartition_line_id/tax_id | X | | | à supprimer |
| Écritures comptables/Ligne de répartition de la taxe d'origine/Basé sur | line_ids/tax_repartition_line_id/repartition_type | X | | | à supprimer |
| Écritures comptables/Ligne de répartition de la taxe d'origine/Type de documents | line_ids/tax_repartition_line_id/document_type | X | | |à supprimer |
### Migrate bank statements
On fait 2 imports successifs:
- le 1er avec aucune colonne concernant les lignes de relevé, c'est à dire qu'on garde uniquement id, name, balance_end_real, balance_start
- un 2e pour lequel on import à partir des lignes de relevé, plutôt que les relevés eux même. Pour ça on clique sur le bouton "Nouveau" sur la page des relevé, pour accéder à la page des Transactions, à partir duquel on peut importer le modèle account.bank.statement.line.
| Nom | Nom (dev mode) | Colonne Import 1 | Colonne Import 2 | Modification |
| -------- |---------------------------------------|--------------------------|--------------------------|--------------------------|
| ID externe | id | Id externe | - | |
| Référence | name | Référence | - | |
| Solde final | balance_end_real | Solde final | - | |
| Solde initial | balance_start | Solde initial | - | |
| Lignes de relevé/ID externe | line_ids/id | | ID externe | |
| Lignes de relevé / Relevé/ID externe (line_ids/statement_id/id) | | Relevé/ID externe | |
| Lignes de relevé/Pièce comptable/ID externe | line_ids/move_id/id | - | Pièce comptable/ID externe | |
| Lignes de relevé/Séquence | line_ids/sequence | - | Séquence | |
| Lignes de relevé/Date | line_ids/date | - | Date | |
| Lignes de relevé/Libellé | line_ids/payment_ref | - | Libellé | |
| Lignes de relevé/Montant | line_ids/amount | - | Montant | |
| Lignes de relevé/Partenaire/ID externe | line_ids/partner_id/id | - | Partenaire/ID externe | à vérifier |
| Lignes de relevé/Journal/ID externe | line_ids/journal_id/id | - | Journal/ID externe | Remplacer |
| Lignes de relevé/Journal/Nom du journal | line_ids/journal_id/name | - | - | Supprimer |
### Migrate account.payment
1. Export des paiements fournisseurs et import avec les mêmes données
| Nom | Nom (dev_mode) | colonne | Actions |
|-------------|------------------------------------|--------------|---------------------|
| ID externe | id | ID externe | |
| Journal/ID externe | journal_id/id | | remplacer les ids |
| Clients/Fournisseurs/ID | partner_id/id | Clients/Fournisseurs/ID externe | |
| Type de partenaire | partner_type | | |
| Paiement de virement interne jumelé/ID externe | paired_internal_transfer_payment_id/id | | |
| Date | date | | |
| Type de paiement | payment_type | | |
| Référence | ref | | Mémo | |
| Référence de paiement | payment_ref | | |
| Montant | amount | | | |
| Compte en suspens/Code | outstanding_account_id | Compte en suspens | |
| Compte de destination/Code | destination_account_id | Compte de destination | |
| Pièce comptable/ID externe | move_id/id | | |
| Mode de paiement/ID externe | payment_method_line_id/id | | remplacer les ids |
| MOde de paiement/Journal | payment_method_line_id/journal_id | | à supprimer, sert à déterminer l'id |
| Mode de paiement/Fournisseur de paiement | payment_method_line_id/payment_provider_id | | à supprimer |
| Mode de paiement/Nom | payment_method_line_id/name | | à supprimer |
| Mode de paiement/Type | payment_method_line_id/type | | à supprimer |
### Migrate sale.order
### Migrate purchase.order
### Import account.move state
| Nom | Nom (dev_mode) |
|-------------|------------------------------------|
| ID | id |
| Statut | state |
### Migrate reconcile
Export as account.move.line and import as account.move
| Nom | Nom (dev_mode) | Nom de colonne |
|-------------|------------------------------------|-----------------------------|
| Écritures comptables/ID externe | line_ids/id | Écritures comptables/ID externe |
| Écritures comptables / Débits lettrés / ID externe | line_ids/matched_debit_ids/id | |
| Écritures comptables / Crédits lettrés / ID externe | line_ids/matched_credit_ids/id | |
## Changelog
- v0.0.1

2
gn_import/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from . import models

13
gn_import/__manifest__.py Normal file
View File

@ -0,0 +1,13 @@
{
"name": "GN - Import Utils",
"version": "18.0.0.0.1",
"category": "Utils",
"summary": "Reimport attachment files as base64 content after manual migration, allows tax lines import",
"author": "Le Garage Numérique",
"maintainers": ["makayabou"],
"website": "https://git.legaragenumerique.fr",
"depends": [
"account",
],
"license": "LGPL-3",
}

View File

@ -0,0 +1,5 @@
from . import file_uploader
from . import statement_line
from . import account_move
from . import account_move_line
from . import account_payment

View File

@ -0,0 +1,134 @@
from odoo import models, fields, api
from odoo.exceptions import UserError, ValidationError
import json
import base64
import traceback
import logging
_logger = logging.getLogger(__name__)
class AccountMove(models.Model):
_inherit = 'account.move'
move_type = fields.Selection(
selection=[
('entry', 'Journal Entry'),
('out_invoice', 'Customer Invoice'),
('out_refund', 'Customer Credit Note'),
('in_invoice', 'Vendor Bill'),
('in_refund', 'Vendor Credit Note'),
('out_receipt', 'Sales Receipt'),
('in_receipt', 'Purchase Receipt'),
],
string='Type',
required=True,
readonly=False, #We need readonly=False for import
tracking=True,
change_default=True,
index=True,
default="entry",
)
state = fields.Selection(
selection=[
('draft', 'Draft'),
('posted', 'Posted'),
('cancel', 'Cancelled'),
],
string='Status',
required=True,
readonly=False, #We need readonly=False for import
copy=False,
tracking=True,
default='draft',
)
def load(self, import_fields, merged_data):
_logger.debug("load account move with import_fields: %s", import_fields) #, merged_data)
reconciliations_idx = [index for index, field in enumerate(import_fields) if field and "matched" in field]
if len(reconciliations_idx) > 0:
try:
_logger.debug("Reconciliation fields list: %s", reconciliations_idx)
mapping = {}
for num, row in enumerate(merged_data):
line_id_col_id = [index for index, field in enumerate(import_fields) if field and field == "line_ids/id"][0]
line_id = row[line_id_col_id]
partial_debit_col_ids = [index for index, field in enumerate(import_fields) if field and field == "line_ids/matched_debit_ids/id"]
partial_debit = row[partial_debit_col_ids[0]] if partial_debit_col_ids and row[partial_debit_col_ids[0]] else False
_logger.debug("partial debit for line %s: %s", line_id, partial_debit)
partial_credit_col_ids = [index for index, field in enumerate(import_fields) if field and field == "line_ids/matched_credit_ids/id"]
partial_credit = row[partial_credit_col_ids[0]] if partial_credit_col_ids and row[partial_credit_col_ids[0]] else False
_logger.debug("partial credit for line %s: %s", line_id, partial_credit)
if partial_debit:
mapping.setdefault(partial_debit, []).append(line_id)
if partial_credit:
mapping.setdefault(partial_credit, []).append(line_id)
_logger.debug("mapping: %", mapping)
errors = {"lines": [], "err": []}
for partial, lines_list in mapping.items():
try:
lines_ids = [self.env['ir.model.data']._xmlid_to_res_id(line, raise_if_not_found=True) for line in lines_list]
lines_subset = self.env['account.move.line'].browse(lines_ids)
line_details = " et ".join(
f"[{line.account_id.code} - {line.name} - {line.move_id.name}]"
for line in lines_subset
)
_logger.debug("reconcile %s", line_details)
lines_subset.with_context(
skip_invoice_sync=False,
skip_invoice_line_sync=True
).reconcile()
except Exception as e:
_logger.error(f"Error processing reconciliation: {str(e)}")
_logger.error("Full traceback:\n%s", traceback.format_exc())
errors.lines.append(line_details)
errors.err.append(e)
return {
'ids': None,
'messages': [
{
'type': 'error',
'message': f"Error processing reconciliation between lines {[line.move_id.name for line in lines_subset]}. Error: {e}",
'record': False,
'rows': {'from': 1, 'to': 1},
}
],
'nextrow': 0,
'name': []
}
if errors:
return {
'ids': None,
'messages': [
{
'type': 'error',
'message': f"Error processing reconciliation between lines {[line_detail for line_detail in errors.lines]}. Errors: {[err for err in errors.err]}",
'record': False,
'rows': {'from': 1, 'to': 1},
}
],
'nextrow': 0,
'name': []
}
return {
'ids': [],
'messages': [],
'nextrow': 0,
'name': []
}
except Exception as e:
_logger.error(f"Error while trying to reconciliate: {e}")
_logger.error("Full traceback:\n%s", traceback.format_exc())
return {
'ids': None,
'messages': [
{
'type': 'error',
'message': f"Error while trying to reconciliate: {e}",
'record': False,
'rows': {'from': 1, 'to': 1},
}
],
'nextrow': 0,
'name': []
}
return super().load(import_fields, merged_data)

View File

@ -0,0 +1,23 @@
from odoo import models, fields, api
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
tax_repartition_line_id = fields.Many2one(
comodel_name='account.tax.repartition.line',
string="Originator Tax Distribution Line",
ondelete='restrict',
readonly=False, #We need readonly=False for import
check_company=True,
help="Tax distribution line that caused the creation of this move line, if any")
matched_debit_ids = fields.One2many(
comodel_name='account.partial.reconcile', inverse_name='credit_move_id',
string='Matched Debits',
readonly=False,
help='Debit journal items that are matched with this journal item.',
)
matched_credit_ids = fields.One2many(
comodel_name='account.partial.reconcile', inverse_name='debit_move_id',
string='Matched Credits',
readonly=False,
help='Credit journal items that are matched with this journal item.',
)

View File

@ -0,0 +1,25 @@
from odoo import models, fields, api
from odoo.exceptions import UserError
import json
import base64
import logging
_logger = logging.getLogger(__name__)
class AccountPayment(models.Model):
_inherit = 'account.payment'
def load(self, import_fields, merged_data):
_logger.debug("Custom load for account.payment with import_fields: %s", import_fields)
if "move_id/id" not in import_fields:
return super().load(import_fields, merged_data)
self = self.with_context(import_payment=True)
_logger.debug("call load for account_payment with context import_payment: %s", self.env.context.get("import_payment"))
return super(AccountPayment, self).load(import_fields, merged_data)
def write(self, vals):
_logger.debug("in custom write for payment %s wth import_payment in context: %s", self, self.env.context.get('import_payment'))
if not self.env.context.get('import_payment'):
return super().write(vals)
_logger.debug("call raw write for account_payment")
return models.Model.write(self, vals)

View File

@ -0,0 +1,32 @@
import os
import base64
from odoo import models, fields, api
from odoo.exceptions import UserError
import logging
_logger = logging.getLogger(__name__)
class FileUploader(models.Model):
_inherit = 'ir.attachment'
def action_upload_file(self):
"""
Reads a file from the server path in `url` field and uploads it to `file_field`.
"""
db_name = self._cr.dbname
_logger.info("db name : %s", db_name)
base_path = "/var/lib/odoo/.local/share/Odoo/filestore"
for rec in self:
if not rec.url:
raise UserError(f"No URL provided for record {rec.display_name}")
file_path = os.path.join(base_path, db_name, rec.url)
if not os.path.exists(file_path):
raise UserError(f"File not found: {file_path}")
with open(file_path, 'rb') as f:
file_data = base64.b64encode(f.read())
rec.write({
'type': "binary",
'datas': file_data,
})

View File

@ -0,0 +1,45 @@
import logging
from datetime import datetime
from odoo import models, api, fields
_logger = logging.getLogger(__name__)
class AccountBankStatementLine(models.Model):
_inherit = 'account.bank.statement.line'
move_id = fields.Many2one(
comodel_name='account.move',
auto_join=True,
string='Journal Entry', required=True, readonly=False, #readonly False for import
ondelete='cascade',
index=True,
check_company=True)
#def write(self, vals):
# OVERRIDE
# _logger.debug("in custom write statement line for line with move_id %s" % self.move_id.name)
# res = super(AccountBankStatementLine, self.with_context(skip_account_move_synchronization=True, skip_readonly_check=True)).write(vals)
#self._synchronize_to_moves(set(vals.keys()))
# return res
@api.model_create_multi
def create(self, vals_list):
counterpart_account_ids = []
for vals in vals_list:
_logger.debug("in create custom with vals : %s" % vals)
created_records = self.env[self._name]
if 'amount' not in vals:
vals['amount'] = 0
if vals.get('move_id'):
_logger.debug("get move_id in vals")
move_id = self.env['account.move'].browse(vals['move_id'])
vals['name'] = move_id.name
created_records |= models.Model.create(self, vals)
else:
_logger.debug("no move_id in vals")
created_records |= super(AccountBankStatementLine, self.with_context(is_statement_line=True)).create(vals)
return created_records.with_env(self.env)