chore: import enterprise addons 19.0 bulk snapshot

This commit is contained in:
1b-agent committed 2026-09-16 10:13:08 +00:00
commit ca4af030d3
39951 files changed
+15570005

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_journal
from . import account_move
from . import soda_account_mapping
from . import res_config_settings
+117
View File
@@ -0,0 +1,117 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import io
from lxml import etree
import re
from odoo import _, fields, models
from odoo.exceptions import UserError
class AccountJournal(models.Model):
_inherit = 'account.journal'
def create_document_from_attachment(self, attachment_ids=None):
# OVERRIDE
journal = self or self.browse(self.env.context.get('default_journal_id'))
if journal.type == 'general':
attachments = self.env['ir.attachment'].browse(attachment_ids or [])
if not attachments:
raise UserError(_("No attachment was provided"))
if all(journal._l10n_be_check_soda_format(attachment) for attachment in attachments):
return journal._l10n_be_parse_soda_file(attachments)
return super().create_document_from_attachment(attachment_ids)
def _l10n_be_check_soda_format(self, attachment):
try:
return (
(attachment.mimetype in ('application/xml', 'text/xml')
# XML files sent by email have text/plain as mimetype
or (attachment.mimetype == 'text/plain' and attachment.name.lower().endswith('.xml')))
and etree.parse(io.BytesIO(attachment.raw)).getroot().tag == 'SocialDocument'
)
except etree.XMLSyntaxError:
return False
def _l10n_be_parse_soda_file(self, attachments, skip_wizard=False, move=None):
self.ensure_one()
# We keep a dict mapping the SODA reference to a dict with a list of `entries` and an `attachment_id`
# {
# 'soda_reference_1': {
# 'entries': [
# {
# 'code': '1200',
# 'name': 'Line Description',
# 'debit': '150.0',
# 'credit': '0.0',
# },
# ...
# ],
# 'attachment_id': 'attachment_id_1',
# },
# ...
# }
soda_files = {}
soda_code_to_name_mapping = {}
errors_to_chatter = set()
for attachment in attachments:
parsed_attachment = etree.parse(io.BytesIO(attachment.raw))
# The document VAT number must match the journal's company's VAT number
journal_company_vat = self.company_id.vat or self.company_id.company_registry
journal_company_vat = journal_company_vat and re.sub("[^0-9]", "", journal_company_vat)
parsed_ent_num = parsed_attachment.find('.//EntNum')
ent_num = parsed_ent_num.text and re.search(r'\d+', parsed_ent_num.text).group()
if ent_num != journal_company_vat:
if len(attachments) == 1:
message = _('The imported document doesn\'t seem to correspond to this company\'s VAT number nor company id')
else:
message = _('The company VAT number found in at least one document doesn\'t seem to correspond to this company\'s VAT number nor company id')
errors_to_chatter.add(message)
# account.move.ref is SocialNumber+SequenceNumber+AccountPeriodYYYY/AccountPeriodmm : check that this move has not already been imported
account_period = parsed_attachment.find('.//AccountPeriod').text
ref = "%s-%s-%s/%s" % (parsed_attachment.find('.//Source').text, parsed_attachment.find('.//SeqNumber').text, account_period[:4], account_period[4:])
existing_move = self.env['account.move'].search([('ref', '=', ref), ('company_id', '=', self.company_id.id)])
if existing_move:
if self.env.context.get('raise_no_imported_file', True):
raise UserError(
_('The entry %(entry)s has already been uploaded (%(existing_entry)s).', entry=ref, existing_entry=existing_move.name))
else:
return
soda_files[ref] = {
'entries': [],
'attachment_id': attachment.id,
'date': parsed_attachment.findtext('.//GenDate') or fields.Date.today().strftime("%Y-%m-%d"),
}
# Retrieve aml's infos
for _idx, elem in enumerate(parsed_attachment.findall('.//Accounting')):
code = elem.find('./Account').text
name = elem.find('./Label').text
soda_files[ref]['entries'].append({
'code': code,
'name': name,
'debit': float(elem.find('./Amount/Debit').text),
'credit': float(elem.find('./Amount/Credit').text),
})
soda_code_to_name_mapping[code] = name
wizard = self.env['soda.import.wizard'].create({
'soda_files': soda_files,
'soda_code_to_name_mapping': soda_code_to_name_mapping,
'company_id': self.company_id.id,
'journal_id': self.id,
})
if skip_wizard:
return wizard._action_save_and_import(existing_move=move)
return {
'name': _('SODA Import'),
'type': 'ir.actions.act_window',
'views': [(False, 'form')],
'view_mode': 'form',
'view_id': self.env.ref('l10n_be_soda.soda_import_wizard_view_form').id,
'res_model': 'soda.import.wizard',
'res_id': wizard.id,
'target': 'new',
'context': {
'errors': list(errors_to_chatter),
},
}
+51
View File
@@ -0,0 +1,51 @@
from odoo import models
class AccountMove(models.Model):
_inherit = 'account.move'
def _get_import_file_type(self, file_data):
""" Identify SODA files. """
# EXTENDS 'account'
if file_data['attachment'] and file_data['xml_tree'] is not None and file_data['xml_tree'].tag == 'SocialDocument':
return 'l10n_be.soda'
return super()._get_import_file_type(file_data)
def _get_edi_decoder(self, file_data, new=False):
# EXTENDS 'account'
if file_data['import_file_type'] == 'l10n_be.soda':
return {
'priority': 20,
'decoder': self._soda_edi_decoder,
}
return super()._get_edi_decoder(file_data, new=new)
def _soda_edi_decoder(self, move, file_data, new=False):
if move.journal_id.type != 'general':
return move.env._("SODA files can only be imported in misc journals.")
if move.invoice_line_ids:
return move._reason_cannot_decode_has_invoice_lines()
move.journal_id._l10n_be_parse_soda_file(file_data['attachment'], skip_wizard=True, move=move)
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
def write(self, vals):
if not self.env.user.has_group('account.group_account_user') \
or 'account_id' not in vals:
return super().write(vals)
for line in self.filtered(lambda l: l.company_id.account_fiscal_country_id.code == 'BE'):
suspense_account = line.company_id.account_journal_suspense_account_id
if line.account_id == suspense_account:
if mapping := self.env['soda.account.mapping'].search([
('company_id', '=', line.company_id.id),
('name', '=', line.name),
'|',
('account_id', '=', False),
('account_id', '=', suspense_account.id),
]):
mapping.account_id = vals['account_id']
return super().write(vals)
@@ -0,0 +1,24 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, _
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
def l10n_be_soda_open_soda_mapping(self):
wizard = self.env['soda.import.wizard'].create({
'soda_files': {},
'company_id': self.env.company.id,
})
return {
'name': _('SODA Mapping'),
'view_id': self.env.ref('l10n_be_soda.soda_import_wizard_view_form').id,
'res_model': 'soda.import.wizard',
'res_id': wizard.id,
'context': {**self.env.context, 'soda_mapping_save_only': True},
'type': 'ir.actions.act_window',
'views': [(False, 'form')],
'view_mode': 'form',
'target': 'new',
}
@@ -0,0 +1,72 @@
from odoo import api, fields, models
class SodaAccountMapping(models.Model):
_name = 'soda.account.mapping'
_description = 'SODA Account Mapping'
_order = 'code, company_id'
_check_company_auto = True
company_id = fields.Many2one('res.company', required=True, default=lambda self: self.env.company)
code = fields.Char('SODA Account', required=True)
name = fields.Char('SODA Label', required=True)
account_id = fields.Many2one(
'account.account',
string='Mapped Account',
compute='_compute_account_id',
readonly=False,
store=True,
check_company=True
)
_code_company_uniq = models.Constraint(
'unique (code, company_id)',
"The code of the SODA account must be unique per company",
)
@api.depends('code', 'company_id')
def _compute_account_id(self):
for mapping in self:
mapping.account_id = self.env['account.account'].with_company(self.company_id).search([
*self.env['account.account']._check_company_domain(self.company_id),
('code', '=like', f'{mapping.code}%'),
], limit=1)
@api.depends('code', 'name')
def _compute_display_name(self):
for mapping in self:
mapping.display_name = f"{mapping.code} {mapping.name}"
@api.model
def find_or_create_mapping_entries(self, soda_code_to_name_mapping, company_id):
"""Find account mappings for the provided SODA codes and/or create new ones when mappings for some SODA codes
do not exist yet.
:param soda_code_to_name_mapping: a dict mapping SODA codes to their names
:param company_id: the company for which to find or create the entries
:return: a recordset of soda.account.mapping entries for the given SODA codes
"""
soda_code_to_name_mapping = soda_code_to_name_mapping or {}
soda_account_codes = list(soda_code_to_name_mapping.keys())
# Find existing account mappings for the provided SODA codes
domain = [*self._check_company_domain(company_id)]
if soda_account_codes:
domain.append(('code', 'in', soda_account_codes))
soda_account_mappings = self.search(domain)
soda_account_mapping_codes = set(soda_account_mappings.mapped('code'))
new_soda_account_mappings = []
for code, name in soda_code_to_name_mapping.items():
# For each SODA code where there's not mapping yet, we create a new one
if code not in soda_account_mapping_codes:
soda_account_mapping_codes.add(code)
new_soda_account_mappings.append({
'code': code,
'name': name or "",
'company_id': company_id.id,
})
soda_account_mappings |= self.create(new_soda_account_mappings)
return soda_account_mappings
def action_unlink(self):
self.unlink()
return self.env['res.config.settings'].l10n_be_soda_open_soda_mapping()