Master 2154703 l10n be edi bfr
[IMP] uniformized way to import/export EDI documents In details, this is achieved by - Adding a generic framework module to import/export EDI documents - Adapting existing EDI modules to it - Fix PyPDF2 to manage multiple embedded files - Adding E-FFF standard in a separated module (belgian EDI export) - Fix UBL move line price import. Task ID 2154703 closes odoo/odoo#49011 Related: odoo/upgrade#1108 Signed-off-by: Quentin De Paoli (qdp) <qdp@openerp.com>
This commit is contained in:
@@ -546,10 +546,8 @@ class AccountJournal(models.Model):
|
||||
|
||||
invoices = self.env['account.move']
|
||||
for attachment in attachments:
|
||||
invoice = self.env['account.move'].create({})
|
||||
attachment.write({'res_model': 'mail.compose.message'})
|
||||
invoice.message_post(attachment_ids=[attachment.id])
|
||||
invoices += invoice
|
||||
invoices += self._create_invoice_from_single_attachment(attachment)
|
||||
|
||||
action_vals = {
|
||||
'name': _('Generated Documents'),
|
||||
@@ -565,6 +563,16 @@ class AccountJournal(models.Model):
|
||||
action_vals['view_mode'] = 'tree,form'
|
||||
return action_vals
|
||||
|
||||
def _create_invoice_from_single_attachment(self, attachment):
|
||||
""" Creates an invoice and post the attachment. If the related modules
|
||||
are installed, it will trigger OCR or the import from the EDI.
|
||||
|
||||
:returns: the created invoice.
|
||||
"""
|
||||
invoice = self.env['account.move'].create({})
|
||||
invoice.message_post(attachment_ids=[attachment.id])
|
||||
return invoice
|
||||
|
||||
def _create_secure_sequence(self, sequence_fields):
|
||||
"""This function creates a no_gap sequence on each journal in self that will ensure
|
||||
a unique number is given to all posted account.move in such a way that we can always
|
||||
|
||||
@@ -84,7 +84,8 @@ class AccountInvoiceSend(models.TransientModel):
|
||||
|
||||
def _send_email(self):
|
||||
if self.is_email:
|
||||
self.composer_id.send_mail()
|
||||
# with_context : we don't want to reimport the file we just exported.
|
||||
self.composer_id.with_context(no_new_invoice=True).send_mail()
|
||||
if self.env.context.get('mark_invoice_as_sent'):
|
||||
#Salesman send posted invoice, without the right to write
|
||||
#but they should have the right to change this flag
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
from . import wizard
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name' : 'Import/Export Invoices From XML/PDF',
|
||||
'description':"""
|
||||
Electronic Data Interchange
|
||||
=======================================
|
||||
EDI is the electronic interchange of business information using a standardized format.
|
||||
|
||||
This is the base module for import and export of invoices in various EDI formats, and the
|
||||
the transmission of said documents to various parties involved in the exchange (other company,
|
||||
governements, etc.)
|
||||
""",
|
||||
'version' : '1.0',
|
||||
'category': 'Accounting/Accounting',
|
||||
'depends' : ['account'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'wizard/account_invoice_send_views.xml',
|
||||
'views/account_journal_views.xml'
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': True,
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from . import account_move
|
||||
from . import account_journal
|
||||
from . import account_edi_format
|
||||
from . import ir_actions_report
|
||||
from . import ir_attachment
|
||||
from . import mail_template
|
||||
@@ -0,0 +1,300 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.pdf import OdooPdfFileReader, OdooPdfFileWriter
|
||||
|
||||
from lxml import etree
|
||||
import base64
|
||||
import io
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AccountEdiFormat(models.Model):
|
||||
_name = 'account.edi.format'
|
||||
_description = 'EDI format'
|
||||
|
||||
name = fields.Char()
|
||||
code = fields.Char()
|
||||
hide_on_journal = fields.Selection([('import_export', 'Import/Export'), ('import', 'Import Only')], default='import_export', help='used to hide this EDI format on journals')
|
||||
|
||||
_sql_constraints = [
|
||||
('unique_code', 'unique (code)', 'This code already exists')
|
||||
]
|
||||
|
||||
####################################################
|
||||
# Export method to override based on EDI Format
|
||||
####################################################
|
||||
|
||||
def _export_invoice_to_attachment(self, invoice):
|
||||
""" Create the file content representing the invoice.
|
||||
|
||||
:param invoice: the invoice to encode.
|
||||
:returns: a dictionary (values are compatible to create an ir.attachment)
|
||||
* name : the name of the file
|
||||
* datas : the content of the file,
|
||||
* res_model : 'account.move',
|
||||
* res_id: the id of invoice
|
||||
* mimetype : the mimetype of the attachment
|
||||
"""
|
||||
# TO OVERRIDE
|
||||
self.ensure_one()
|
||||
return False
|
||||
|
||||
def _export_invoice_to_embed_to_pdf(self, pdf_content, invoice):
|
||||
""" Create the file content representing the invoice when it's destined
|
||||
to be embed into a pdf.
|
||||
- default: creates the default EDI document (_export_invoice_to_attachment).
|
||||
- Should return False if this EDI format should not be embedded.
|
||||
- Should be overriden only if a specific behavior (for example,
|
||||
include the pdf content inside the file).
|
||||
|
||||
:param pdf_content: the pdf before any EDI format was added.
|
||||
:param invoice: the invoice to add.
|
||||
:returns: a dictionary or False if this EDI format must not be embedded to pdf.
|
||||
* name : the name of the file
|
||||
* datas : the content of the file,
|
||||
* res_model : 'account.move',
|
||||
* res_id: the id of invoice
|
||||
* mimetype : the mimetype of the attachment
|
||||
"""
|
||||
# TO OVERRIDE
|
||||
self.ensure_one()
|
||||
return self._export_invoice_to_attachment(invoice)
|
||||
|
||||
####################################################
|
||||
# Import methods to override based on EDI Format
|
||||
####################################################
|
||||
|
||||
def _create_invoice_from_xml_tree(self, filename, tree):
|
||||
""" Create a new invoice with the data inside the xml.
|
||||
|
||||
:param filename: The name of the xml.
|
||||
:param tree: The tree of the xml to import.
|
||||
:returns: The created invoice.
|
||||
"""
|
||||
# TO OVERRIDE
|
||||
self.ensure_one()
|
||||
return self.env['account.move']
|
||||
|
||||
def _update_invoice_from_xml_tree(self, filename, tree, invoice):
|
||||
""" Update an existing invoice with the data inside the xml.
|
||||
|
||||
:param filename: The name of the xml.
|
||||
:param tree: The tree of the xml to import.
|
||||
:param invoice: The invoice to update.
|
||||
:returns: The updated invoice.
|
||||
"""
|
||||
# TO OVERRIDE
|
||||
self.ensure_one()
|
||||
return self.env['account.move']
|
||||
|
||||
def _create_invoice_from_pdf_reader(self, filename, reader):
|
||||
""" Create a new invoice with the data inside a pdf.
|
||||
|
||||
:param filename: The name of the pdf.
|
||||
:param reader: The OdooPdfFileReader of the pdf to import.
|
||||
:returns: The created invoice.
|
||||
"""
|
||||
# TO OVERRIDE
|
||||
self.ensure_one()
|
||||
|
||||
return self.env['account.move']
|
||||
|
||||
def _update_invoice_from_pdf_reader(self, filename, reader, invoice):
|
||||
""" Update an existing invoice with the data inside the pdf.
|
||||
|
||||
:param filename: The name of the pdf.
|
||||
:param reader: The OdooPdfFileReader of the pdf to import.
|
||||
:param invoice: The invoice to update.
|
||||
:returns: The updated invoice.
|
||||
"""
|
||||
# TO OVERRIDE
|
||||
self.ensure_one()
|
||||
return self.env['account.move']
|
||||
|
||||
####################################################
|
||||
# Export Internal methods (not meant to be overridden)
|
||||
####################################################
|
||||
|
||||
def _embed_edis_to_pdf(self, pdf_content, invoice):
|
||||
""" Create the EDI document of the invoice and embed it in the pdf_content.
|
||||
|
||||
:param pdf_content: the bytes representing the pdf to add the EDIs to.
|
||||
:param invoice: the invoice to generate the EDI from.
|
||||
:returns: the same pdf_content with the EDI of the invoice embed in it.
|
||||
"""
|
||||
attachments = []
|
||||
for edi_format in self:
|
||||
try:
|
||||
vals = edi_format._export_invoice_to_embed_to_pdf(pdf_content, invoice)
|
||||
except:
|
||||
continue
|
||||
if vals:
|
||||
attachments.append(vals)
|
||||
|
||||
if attachments:
|
||||
# Add the attachments to the pdf file
|
||||
reader_buffer = io.BytesIO(pdf_content)
|
||||
reader = OdooPdfFileReader(reader_buffer)
|
||||
writer = OdooPdfFileWriter()
|
||||
writer.cloneReaderDocumentRoot(reader)
|
||||
for vals in attachments:
|
||||
writer.addAttachment(vals['name'], vals['datas'])
|
||||
buffer = io.BytesIO()
|
||||
writer.write(buffer)
|
||||
pdf_content = buffer.getvalue()
|
||||
reader_buffer.close()
|
||||
buffer.close()
|
||||
return pdf_content
|
||||
|
||||
def _create_ir_attachments(self, invoice):
|
||||
""" Create ir.attachment for the EDIs from invoice.
|
||||
|
||||
:param invoice: the invoice to generate the EDI from.
|
||||
:returns: the newly created attachments.
|
||||
"""
|
||||
attachment_vals_list = []
|
||||
for edi_format in self:
|
||||
vals = edi_format._export_invoice_to_attachment(invoice)
|
||||
if vals:
|
||||
vals['datas'] = base64.encodebytes(vals['datas'])
|
||||
vals['edi_format_id'] = edi_format._origin.id
|
||||
attachment_vals_list.append(vals)
|
||||
res = self.env['ir.attachment'].create(attachment_vals_list)
|
||||
invoice.edi_document_ids |= res
|
||||
return res
|
||||
|
||||
####################################################
|
||||
# Import Internal methods (not meant to be overridden)
|
||||
####################################################
|
||||
|
||||
def _decode_xml(self, filename, content):
|
||||
"""Decodes an xml into a list of one dictionary representing an attachment.
|
||||
|
||||
:param filename: The name of the xml.
|
||||
:param attachment: The xml as a string.
|
||||
:returns: A list with a dictionary.
|
||||
* filename: The name of the attachment.
|
||||
* content: The content of the attachment.
|
||||
* type: The type of the attachment.
|
||||
* xml_tree: The tree of the xml if type is xml.
|
||||
* pdf_reader: The pdf_reader if type is pdf.
|
||||
"""
|
||||
to_process = []
|
||||
try:
|
||||
xml_tree = etree.fromstring(content)
|
||||
except Exception as e:
|
||||
_logger.exception("Error when converting the xml content to etree: %s" % e)
|
||||
return to_process
|
||||
if len(xml_tree):
|
||||
to_process.append({
|
||||
'filename': filename,
|
||||
'content': content,
|
||||
'type': 'xml',
|
||||
'xml_tree': xml_tree,
|
||||
})
|
||||
return to_process
|
||||
|
||||
def _decode_pdf(self, filename, content):
|
||||
"""Decodes a pdf and unwrap sub-attachment into a list of dictionary each representing an attachment.
|
||||
|
||||
:param filename: The name of the pdf.
|
||||
:param content: The bytes representing the pdf.
|
||||
:returns: A list of dictionary for each attachment.
|
||||
* filename: The name of the attachment.
|
||||
* content: The content of the attachment.
|
||||
* type: The type of the attachment.
|
||||
* xml_tree: The tree of the xml if type is xml.
|
||||
* pdf_reader: The pdf_reader if type is pdf.
|
||||
"""
|
||||
to_process = []
|
||||
try:
|
||||
buffer = io.BytesIO(content)
|
||||
pdf_reader = OdooPdfFileReader(buffer)
|
||||
except Exception as e:
|
||||
# Malformed pdf
|
||||
_logger.exception("Error when reading the pdf: %s" % e)
|
||||
return to_process
|
||||
|
||||
# Process embedded files.
|
||||
for xml_name, content in pdf_reader.getAttachments():
|
||||
to_process.extend(self._decode_xml(xml_name, content))
|
||||
|
||||
# Process the pdf itself.
|
||||
to_process.append({
|
||||
'filename': filename,
|
||||
'content': content,
|
||||
'type': 'pdf',
|
||||
'pdf_reader': pdf_reader,
|
||||
})
|
||||
|
||||
return to_process
|
||||
|
||||
def _decode_attachment(self, attachment):
|
||||
"""Decodes an ir.attachment and unwrap sub-attachment into a list of dictionary each representing an attachment.
|
||||
|
||||
:param attachment: An ir.attachment record.
|
||||
:returns: A list of dictionary for each attachment.
|
||||
* filename: The name of the attachment.
|
||||
* content: The content of the attachment.
|
||||
* type: The type of the attachment.
|
||||
* xml_tree: The tree of the xml if type is xml.
|
||||
* pdf_reader: The pdf_reader if type is pdf.
|
||||
"""
|
||||
content = base64.b64decode(attachment.with_context(bin_size=False).datas)
|
||||
to_process = []
|
||||
|
||||
if 'pdf' in attachment.mimetype:
|
||||
to_process.extend(self._decode_pdf(attachment.name, content))
|
||||
elif 'xml' in attachment.mimetype:
|
||||
to_process.extend(self._decode_xml(attachment.name, content))
|
||||
|
||||
return to_process
|
||||
|
||||
def _create_invoice_from_attachment(self, attachment):
|
||||
"""Decodes an ir.attachment to create an invoice.
|
||||
|
||||
:param attachment: An ir.attachment record.
|
||||
:returns: The invoice where to import data.
|
||||
"""
|
||||
for file_data in self._decode_attachment(attachment):
|
||||
for edi_format in self:
|
||||
res = False
|
||||
if file_data['type'] == 'xml':
|
||||
res = edi_format._create_invoice_from_xml_tree(file_data['filename'], file_data['xml_tree'])
|
||||
elif file_data['type'] == 'pdf':
|
||||
res = edi_format._create_invoice_from_pdf_reader(file_data['filename'], file_data['pdf_reader'])
|
||||
file_data['pdf_reader'].stream.close()
|
||||
if res:
|
||||
if 'extract_state' in res:
|
||||
# Bypass the OCR to prevent overwriting data when an EDI was succesfully imported.
|
||||
# TODO : remove when we integrate the OCR to the EDI flow.
|
||||
res.write({'extract_state': 'done'})
|
||||
return res
|
||||
return self.env['account.move']
|
||||
|
||||
def _update_invoice_from_attachment(self, attachment, invoice):
|
||||
"""Decodes an ir.attachment to update an invoice.
|
||||
|
||||
:param attachment: An ir.attachment record.
|
||||
:returns: The invoice where to import data.
|
||||
"""
|
||||
for file_data in self._decode_attachment(attachment):
|
||||
for edi_format in self:
|
||||
res = False
|
||||
if file_data['type'] == 'xml':
|
||||
res = edi_format._update_invoice_from_xml_tree(file_data['filename'], file_data['xml_tree'], invoice)
|
||||
elif file_data['type'] == 'pdf':
|
||||
res = edi_format._update_invoice_from_pdf_reader(file_data['filename'], file_data['pdf_reader'], invoice)
|
||||
file_data['pdf_reader'].stream.close()
|
||||
if res:
|
||||
if 'extract_state' in res:
|
||||
# Bypass the OCR to prevent overwriting data when an EDI was succesfully imported.
|
||||
# TODO : remove when we integrate the OCR to the EDI flow.
|
||||
res.write({'extract_state': 'done'})
|
||||
return res
|
||||
return self.env['account.move']
|
||||
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class AccountJournal(models.Model):
|
||||
_inherit = 'account.journal'
|
||||
|
||||
edi_format_ids = fields.Many2many(comodel_name='account.edi.format',
|
||||
string='Electronic invoicing',
|
||||
help='Send XML/EDI invoices',
|
||||
domain="[('hide_on_journal', '=', 'import_export')]")
|
||||
|
||||
def _create_invoice_from_single_attachment(self, attachment):
|
||||
invoice = self.env['account.edi.format'].search([])._create_invoice_from_attachment(attachment)
|
||||
if invoice:
|
||||
# with_context: we don't want to import the attachment since the invoice was just created from it.
|
||||
invoice.with_context(no_new_invoice=True).message_post(attachment_ids=[attachment.id])
|
||||
return invoice
|
||||
return super()._create_invoice_from_single_attachment(attachment)
|
||||
@@ -0,0 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models, fields
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
edi_document_ids = fields.One2many('ir.attachment', 'res_id', domain=[('res_model', '=', 'account.move'), ('edi_format_id', '!=', False)])
|
||||
|
||||
def post(self):
|
||||
# OVERRIDE
|
||||
# Generate the electronic documents for the move.
|
||||
existing_attachments = self.env['ir.attachment'].search([
|
||||
('res_model', '=', 'account.move'),
|
||||
('res_id', 'in', self.ids),
|
||||
('edi_format_id', 'in', self.journal_id.edi_format_ids.ids)])
|
||||
existing_attachments.unlink()
|
||||
res = super(AccountMove, self).post()
|
||||
for move in self:
|
||||
move.journal_id.edi_format_ids._create_ir_attachments(move)
|
||||
|
||||
return res
|
||||
|
||||
@api.returns('mail.message', lambda value: value.id)
|
||||
def message_post(self, **kwargs):
|
||||
# OVERRIDE
|
||||
# When posting a message, analyse the attachment to check if it is an EDI document and update the invoice
|
||||
# with the data.
|
||||
res = super(AccountMove, self).message_post(**kwargs)
|
||||
|
||||
if len(self) != 1 or self.env.context.get('no_new_invoice') or not self.is_invoice(include_receipts=True):
|
||||
return res
|
||||
|
||||
attachments = self.env['ir.attachment'].browse(kwargs.get('attachment_ids', []))
|
||||
odoobot = self.env.ref('base.partner_root')
|
||||
if attachments and self.state != 'draft':
|
||||
self.message_post(body='The invoice is not a draft, it was not updated from the attachment.',
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
author_id=odoobot.id)
|
||||
return res
|
||||
if attachments and self.line_ids:
|
||||
self.message_post(body='The invoice already contains lines, it was not updated from the attachment.',
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
author_id=odoobot.id)
|
||||
return res
|
||||
|
||||
for attachment in attachments:
|
||||
invoice = self.env['account.edi.format'].search([])._update_invoice_from_attachment(attachment, self)
|
||||
break
|
||||
|
||||
return res
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class IrActionsReport(models.Model):
|
||||
_inherit = 'ir.actions.report'
|
||||
|
||||
def _post_pdf(self, save_in_attachment, pdf_content=None, res_ids=None):
|
||||
# OVERRIDE to embed some EDI documents inside the PDF.
|
||||
if self.model == 'account.move' and res_ids and len(res_ids) == 1 and pdf_content:
|
||||
invoice = self.env['account.move'].browse(res_ids)
|
||||
if invoice.is_sale_document() and invoice.state != 'draft':
|
||||
pdf_content = invoice.journal_id.edi_format_ids._embed_edis_to_pdf(pdf_content, invoice)
|
||||
|
||||
return super(IrActionsReport, self)._post_pdf(save_in_attachment, pdf_content=pdf_content, res_ids=res_ids)
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class IrAttachment(models.Model):
|
||||
_inherit = 'ir.attachment'
|
||||
|
||||
edi_format_id = fields.Many2one('account.edi.format')
|
||||
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class MailTemplate(models.Model):
|
||||
_inherit = "mail.template"
|
||||
|
||||
def generate_email(self, res_ids, fields):
|
||||
res = super().generate_email(res_ids, fields)
|
||||
|
||||
multi_mode = True
|
||||
if isinstance(res_ids, int):
|
||||
res_ids = [res_ids]
|
||||
multi_mode = False
|
||||
|
||||
if self.model != 'account.move':
|
||||
return res
|
||||
|
||||
existing_attachments = self.env['ir.attachment'].search([
|
||||
('res_model', '=', 'account.move'),
|
||||
('res_id', 'in', res_ids),
|
||||
('edi_format_id', '!=', False)])
|
||||
|
||||
for record in self.env[self.model].browse(res_ids):
|
||||
available_formats = existing_attachments.filtered(lambda a: a.res_id == record.id)
|
||||
missing_formats = record.journal_id.edi_format_ids.filtered(lambda f: f._origin.id not in available_formats.edi_format_id.ids)
|
||||
new_attachments = missing_formats._create_ir_attachments(record)
|
||||
available_formats |= new_attachments
|
||||
(res[record.id] if multi_mode else res).setdefault('attachments', []).extend([(a.name, a.datas) for a in available_formats])
|
||||
|
||||
return res
|
||||
@@ -0,0 +1,2 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_account_edi_format_readonly,account.edi.format,model_account_edi_format,,1,0,0,0
|
||||
|
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="view_account_journal_form_inherited" model="ir.ui.view">
|
||||
<field name="name">account.journal.form.inherited</field>
|
||||
<field name="model">account.journal</field>
|
||||
<field name="inherit_id" ref="account.view_account_journal_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//page[@name='advanced_settings']/group" position="inside">
|
||||
<group string="Electronic Data Interchange" >
|
||||
<field name="edi_format_ids" widget="many2many_tags"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1 @@
|
||||
from . import account_invoice_send
|
||||
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
import base64
|
||||
|
||||
|
||||
class AccountInvoiceSend(models.TransientModel):
|
||||
_inherit = 'account.invoice.send'
|
||||
_description = 'Account Invoice Send'
|
||||
|
||||
edi_format_ids = fields.Many2many(related='invoice_ids.journal_id.edi_format_ids', string="Electronic invoicing")
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="account_invoice_send_inherit_account_wizard_form">
|
||||
<field name="name">account.invoice.send.form.inherited.edi</field>
|
||||
<field name="model">account.invoice.send</field>
|
||||
<field name="inherit_id" ref="account.account_invoice_send_wizard_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='mail_form']" position='before'>
|
||||
<div name="edi_format_option">
|
||||
<group>
|
||||
<label for="edi_format_ids"/>
|
||||
<div class="oe_inline">
|
||||
<field name="edi_format_ids"
|
||||
nolabel="1"
|
||||
widget="many2many_tags"
|
||||
placeholder="You can set default electronic invoicing formats on the journal"
|
||||
attrs="{'invisible':[('edi_format_ids', '=', [])]}"/>
|
||||
<div attrs="{'invisible':[('edi_format_ids', '!=', [])]}">
|
||||
<i>Please, set electronic invoicing formats on the journal</i>
|
||||
</div>
|
||||
</div>
|
||||
</group>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
@@ -1,10 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name' : 'Import Bills/Invoices From XML',
|
||||
'name' : 'Import/Export invoices with Factur-X',
|
||||
'version' : '1.0',
|
||||
'category': 'Accounting/Accounting',
|
||||
'depends' : ['account'],
|
||||
'depends' : ['account_edi'],
|
||||
'data': [
|
||||
'data/account_edi_data.xml',
|
||||
'data/facturx_templates.xml',
|
||||
],
|
||||
'installable': True,
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="edi_facturx_1_0_05" model="account.edi.format">
|
||||
<field name="name">Factur-X (FR)</field>
|
||||
<field name="code">facturx_1_0_05</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
+4
-4
@@ -133,7 +133,7 @@
|
||||
<t t-set="line_counter" t-value="0"/>
|
||||
<t t-foreach="record.invoice_line_ids.filtered(lambda l: not l.display_type)" t-as="line">
|
||||
<t t-set="line_counter" t-value="line_counter + 1"/>
|
||||
<t t-call="account_facturx.account_invoice_line_facturx_export"/>
|
||||
<t t-call="account_edi_facturx.account_invoice_line_facturx_export"/>
|
||||
</t>
|
||||
|
||||
<!-- Partners. -->
|
||||
@@ -141,7 +141,7 @@
|
||||
<!-- Seller. -->
|
||||
<ram:SellerTradeParty>
|
||||
<!-- Address. -->
|
||||
<t t-call="account_facturx.account_invoice_partner_facturx_export">
|
||||
<t t-call="account_edi_facturx.account_invoice_partner_facturx_export">
|
||||
<t t-set="partner" t-value="record.company_id.partner_id"/>
|
||||
</t>
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
<!-- Customer. -->
|
||||
<ram:BuyerTradeParty>
|
||||
<!-- Address. -->
|
||||
<t t-call="account_facturx.account_invoice_partner_facturx_export">
|
||||
<t t-call="account_edi_facturx.account_invoice_partner_facturx_export">
|
||||
<t t-set="partner" t-value="record.commercial_partner_id"/>
|
||||
</t>
|
||||
|
||||
@@ -174,7 +174,7 @@
|
||||
<ram:ApplicableHeaderTradeDelivery>
|
||||
<ram:ShipToTradeParty
|
||||
t-if="'partner_shipping_id' in record._fields and record.partner_shipping_id">
|
||||
<t t-call="account_facturx.account_invoice_partner_facturx_export">
|
||||
<t t-call="account_edi_facturx.account_invoice_partner_facturx_export">
|
||||
<t t-set="partner" t-value="record.partner_shipping_id"/>
|
||||
</t>
|
||||
</ram:ShipToTradeParty>
|
||||
@@ -0,0 +1 @@
|
||||
from . import account_edi_format
|
||||
+56
-128
@@ -10,23 +10,22 @@ from lxml import etree
|
||||
from PyPDF2 import PdfFileReader
|
||||
|
||||
import io
|
||||
import base64
|
||||
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DEFAULT_FACTURX_DATE_FORMAT = '%Y%m%d'
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
class AccountEdiFormat(models.Model):
|
||||
_inherit = 'account.edi.format'
|
||||
|
||||
def _export_as_facturx_xml(self):
|
||||
''' Create the Factur-x xml file content.
|
||||
:return: The XML content as str.
|
||||
'''
|
||||
def _export_invoice_to_attachment(self, invoice):
|
||||
self.ensure_one()
|
||||
if self.code != 'facturx_1_0_05':
|
||||
return super()._export_invoice_to_attachment(invoice)
|
||||
|
||||
def format_date(dt):
|
||||
# Format the date in the Factur-x standard.
|
||||
@@ -39,35 +38,59 @@ class AccountMove(models.Model):
|
||||
|
||||
# Create file content.
|
||||
template_values = {
|
||||
'record': self,
|
||||
'record': invoice,
|
||||
'format_date': format_date,
|
||||
'format_monetary': format_monetary,
|
||||
}
|
||||
content = self.env.ref('account_facturx.account_invoice_facturx_export')._render(template_values)
|
||||
return b"<?xml version='1.0' encoding='UTF-8'?>" + content
|
||||
xml_content = b"<?xml version='1.0' encoding='UTF-8'?>"
|
||||
xml_content += self.env.ref('account_edi_facturx.account_invoice_facturx_export')._render(template_values)
|
||||
xml_name = '%s_facturx.xml' % (invoice.name.replace('/', '_'))
|
||||
return {'name': xml_name,
|
||||
'datas': xml_content,
|
||||
'res_model': 'account.move',
|
||||
'res_id': invoice._origin.id,
|
||||
'mimetype': 'application/xml'
|
||||
}
|
||||
|
||||
def _import_facturx_invoice(self, tree):
|
||||
''' Extract invoice values from the Factur-x xml tree passed as parameter.
|
||||
def _is_facturx(self, filename, tree):
|
||||
return self.code == 'facturx_1_0_05' and tree.tag == '{urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100}CrossIndustryInvoice'
|
||||
|
||||
def _create_invoice_from_xml_tree(self, filename, tree):
|
||||
self.ensure_one()
|
||||
if self._is_facturx(filename, tree):
|
||||
return self._import_facturx(tree, self.env['account_move'])
|
||||
return super()._create_invoice_from_xml_tree(filename, tree)
|
||||
|
||||
def _update_invoice_from_xml_tree(self, filename, tree, invoice):
|
||||
self.ensure_one()
|
||||
if self._is_facturx(filename, tree):
|
||||
return self._import_facturx(tree, invoice)
|
||||
return super()._update_invoice_from_xml_tree(filename, tree, invoice)
|
||||
|
||||
def _import_facturx(self, tree, invoice):
|
||||
""" Decodes a factur-x invoice into an invoice.
|
||||
|
||||
:param tree: the factur-x tree to decode.
|
||||
:param invoice: the invoice to update or an empty recordset.
|
||||
:returns: the invoice where the factur-x data was imported.
|
||||
"""
|
||||
|
||||
:param tree: The tree of the Factur-x xml file.
|
||||
:return: A dictionary containing account.invoice values to create/update it.
|
||||
'''
|
||||
amount_total_import = None
|
||||
|
||||
default_type = False
|
||||
if self._context.get('default_journal_id'):
|
||||
default_move_type = False
|
||||
if invoice._context.get('default_journal_id'):
|
||||
journal = self.env['account.journal'].browse(self.env.context['default_journal_id'])
|
||||
default_type = 'out_invoice' if journal.type == 'sale' else 'in_invoice'
|
||||
elif self._context.get('default_move_type'):
|
||||
default_type = self._context['default_move_type']
|
||||
elif self.move_type in self.env['account.move'].get_invoice_types(include_receipts=True):
|
||||
default_move_type = 'out_invoice' if journal.type == 'sale' else 'in_invoice'
|
||||
elif invoice._context.get('default_move_type'):
|
||||
default_move_type = self._context['default_move_type']
|
||||
elif invoice.move_type in self.env['account.move'].get_invoice_types(include_receipts=True):
|
||||
# in case an attachment is saved on a draft invoice previously created, we might
|
||||
# have lost the default value in context but the type was already set
|
||||
default_type = self.move_type
|
||||
default_move_type = invoice.move_type
|
||||
|
||||
if not default_type:
|
||||
if not default_move_type:
|
||||
raise UserError(_("No information about the journal or the type of invoice is passed"))
|
||||
if default_type == 'entry':
|
||||
if default_move_type == 'entry':
|
||||
return
|
||||
|
||||
# Total amount.
|
||||
@@ -82,31 +105,31 @@ class AccountMove(models.Model):
|
||||
elements = tree.xpath('//rsm:ExchangedDocument/ram:TypeCode', namespaces=tree.nsmap)
|
||||
type_code = elements[0].text
|
||||
|
||||
default_type.replace('_refund', '_invoice')
|
||||
default_move_type.replace('_refund', '_invoice')
|
||||
if type_code == '381':
|
||||
default_type = 'out_refund' if default_type == 'out_invoice' else 'in_refund'
|
||||
default_move_type = 'out_refund' if default_move_type == 'out_invoice' else 'in_refund'
|
||||
refund_sign = -1
|
||||
else:
|
||||
# Handle 'b' refund mode.
|
||||
if total_amount < 0:
|
||||
default_type = 'out_refund' if default_type == 'out_invoice' else 'in_refund'
|
||||
refund_sign = -1 if 'refund' in default_type else 1
|
||||
default_move_type = 'out_refund' if default_move_type == 'out_invoice' else 'in_refund'
|
||||
refund_sign = -1 if 'refund' in default_move_type else 1
|
||||
|
||||
# Write the type as the journal entry is already created.
|
||||
self.move_type = default_type
|
||||
invoice.move_type = default_move_type
|
||||
|
||||
# self could be a single record (editing) or be empty (new).
|
||||
with Form(self.with_context(default_move_type=default_type)) as invoice_form:
|
||||
with Form(invoice.with_context(default_move_type=default_move_type)) as invoice_form:
|
||||
# Partner (first step to avoid warning 'Warning! You must first select a partner.').
|
||||
partner_type = invoice_form.journal_id.type == 'purchase' and 'SellerTradeParty' or 'BuyerTradeParty'
|
||||
elements = tree.xpath('//ram:'+partner_type+'/ram:SpecifiedTaxRegistration/ram:ID', namespaces=tree.nsmap)
|
||||
elements = tree.xpath('//ram:' + partner_type + '/ram:SpecifiedTaxRegistration/ram:ID', namespaces=tree.nsmap)
|
||||
partner = elements and self.env['res.partner'].search([('vat', '=', elements[0].text)], limit=1)
|
||||
if not partner:
|
||||
elements = tree.xpath('//ram:'+partner_type+'/ram:Name', namespaces=tree.nsmap)
|
||||
elements = tree.xpath('//ram:' + partner_type + '/ram:Name', namespaces=tree.nsmap)
|
||||
partner_name = elements and elements[0].text
|
||||
partner = elements and self.env['res.partner'].search([('name', 'ilike', partner_name)], limit=1)
|
||||
if not partner:
|
||||
elements = tree.xpath('//ram:'+partner_type+'//ram:URIID[@schemeID=\'SMTP\']', namespaces=tree.nsmap)
|
||||
elements = tree.xpath('//ram:' + partner_type + '//ram:URIID[@schemeID=\'SMTP\']', namespaces=tree.nsmap)
|
||||
partner = elements and self.env['res.partner'].search([('email', '=', elements[0].text)], limit=1)
|
||||
if partner:
|
||||
invoice_form.partner_id = partner
|
||||
@@ -223,98 +246,3 @@ class AccountMove(models.Model):
|
||||
invoice_line_form.price_unit = amount_total_import
|
||||
|
||||
return invoice_form.save()
|
||||
|
||||
@api.returns('mail.message', lambda value: value.id)
|
||||
def message_post(self, **kwargs):
|
||||
# OVERRIDE
|
||||
# /!\ 'default_res_id' in self._context is used to don't process attachment when using a form view.
|
||||
res = super(AccountMove, self).message_post(**kwargs)
|
||||
|
||||
if not self.env.context.get('no_new_invoice') and len(self) == 1 and self.state == 'draft' and (
|
||||
self.env.context.get('default_move_type', self.move_type) in self.env['account.move'].get_invoice_types(include_receipts=True)
|
||||
or self.env['account.journal'].browse(self.env.context.get('default_journal_id')).type in ('sale', 'purchase')
|
||||
):
|
||||
for attachment in self.env['ir.attachment'].browse(kwargs.get('attachment_ids', [])):
|
||||
self._create_invoice_from_attachment(attachment)
|
||||
return res
|
||||
|
||||
def _create_invoice_from_attachment(self, attachment):
|
||||
if 'pdf' in attachment.mimetype:
|
||||
for move in self:
|
||||
move._create_invoice_from_pdf(attachment)
|
||||
if 'xml' in attachment.mimetype:
|
||||
for move in self:
|
||||
move._create_invoice_from_xml(attachment)
|
||||
|
||||
def _create_invoice_from_pdf(self, attachment):
|
||||
content = base64.b64decode(attachment.datas)
|
||||
|
||||
with io.BytesIO(content) as buffer:
|
||||
try:
|
||||
reader = PdfFileReader(buffer)
|
||||
|
||||
# Search for Factur-x embedded file.
|
||||
if reader.trailer['/Root'].get('/Names') and reader.trailer['/Root']['/Names'].get('/EmbeddedFiles'):
|
||||
# N.B: embedded_files looks like:
|
||||
# ['file.xml', {'/Type': '/Filespec', '/F': 'file.xml', '/EF': {'/F': IndirectObject(22, 0)}}]
|
||||
embedded_files = reader.trailer['/Root']['/Names']['/EmbeddedFiles']['/Names']
|
||||
# '[::2]' because it's a list [fn_1, content_1, fn_2, content_2, ..., fn_n, content_2]
|
||||
for filename_obj, content_obj in list(zip(embedded_files, embedded_files[1:]))[::2]:
|
||||
content = content_obj.getObject()['/EF']['/F'].getData()
|
||||
|
||||
if filename_obj == 'factur-x.xml':
|
||||
try:
|
||||
tree = etree.fromstring(content)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
self._import_facturx_invoice(tree)
|
||||
self._remove_ocr_option()
|
||||
buffer.close()
|
||||
except UserError as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
# Malformed pdf
|
||||
_logger.exception(e)
|
||||
|
||||
@api.model
|
||||
def _get_xml_decoders(self):
|
||||
''' List of usable decoders to extract invoice from attachments.
|
||||
|
||||
:return: a list of triplet (xml_type, check_func, decode_func)
|
||||
* xml_type: The format name, e.g 'UBL 2.1'
|
||||
* check_func: A function taking an etree and a file name as parameter and returning a dict:
|
||||
* flag: The etree is part of this format.
|
||||
* error: Error message.
|
||||
* decode_func: A function taking an etree as parameter and returning an invoice record.
|
||||
'''
|
||||
# TO BE OVERWRITTEN
|
||||
return []
|
||||
|
||||
def _create_invoice_from_xml(self, attachment):
|
||||
decoders = self._get_xml_decoders()
|
||||
|
||||
# Convert attachment -> etree
|
||||
content = base64.b64decode(attachment.datas)
|
||||
try:
|
||||
tree = etree.fromstring(content)
|
||||
except Exception:
|
||||
_logger.exception('The xml file is badly formatted : {}'.format(attachment.name))
|
||||
|
||||
for xml_type, check_func, decode_func in decoders:
|
||||
check_res = check_func(tree, attachment.name)
|
||||
|
||||
if check_res.get('flag') and not check_res.get('error'):
|
||||
invoice_ids = decode_func(tree)
|
||||
if invoice_ids:
|
||||
invoice_ids._remove_ocr_option()
|
||||
break
|
||||
|
||||
try:
|
||||
return invoice_ids
|
||||
except UnboundLocalError:
|
||||
_logger.exception('No decoder was found for the xml file: {}. The file is badly formatted, not supported or the decoder is not installed'.format(attachment.name))
|
||||
|
||||
def _remove_ocr_option(self):
|
||||
if 'extract_state' in self:
|
||||
self.write({'extract_state': 'done'})
|
||||
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name' : 'Import/Export invoices with generic UBL',
|
||||
'version' : '1.0',
|
||||
'category': 'Accounting/Accounting',
|
||||
'depends' : ['account_edi'],
|
||||
'data': [
|
||||
'data/ubl_templates.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': True,
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!--
|
||||
Render a res.partner record to be added to the UBL xml document.
|
||||
-->
|
||||
<template id="export_ubl_invoice_partner">
|
||||
<cac:Party
|
||||
xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
||||
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
||||
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
||||
<cbc:WebsiteURI
|
||||
t-if="partner.website"
|
||||
t-esc="partner.website"/>
|
||||
<cac:PartyName>
|
||||
<cbc:Name t-esc="partner.name"/>
|
||||
</cac:PartyName>
|
||||
<cac:Language t-if="partner.lang">
|
||||
<cbc:LocaleCode t-esc="partner.lang"/>
|
||||
</cac:Language>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName
|
||||
t-if="partner.street"
|
||||
t-esc="partner.street"/>
|
||||
<cbc:AdditionalStreetName
|
||||
t-if="partner.street2"
|
||||
t-esc="partner.street2"/>
|
||||
<cbc:CityName
|
||||
t-if="partner.city"
|
||||
t-esc="partner.city"/>
|
||||
<cbc:PostalZone
|
||||
t-if="partner.zip"
|
||||
t-esc="partner.zip"/>
|
||||
<cbc:CountrySubentity
|
||||
t-if="partner.state_id"
|
||||
t-esc="partner.state_id.name"/>
|
||||
<cbc:CountrySubentityCode
|
||||
t-if="partner.state_id"
|
||||
t-esc="partner.state_id.code"/>
|
||||
<cac:Country
|
||||
t-if="partner.country_id">
|
||||
<cbc:IdentificationCode t-esc="partner.country_id.code"/>
|
||||
<cbc:Name t-esc="partner.country_id.name"/>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
<cac:PartyTaxScheme t-if="partner.vat">
|
||||
<cbc:RegistrationName t-esc="partner.name"/>
|
||||
<cbc:CompanyID t-esc="partner.vat"/>
|
||||
<cac:TaxScheme>
|
||||
<cbc:ID
|
||||
schemeID="UN/ECE 5153"
|
||||
schemeAgencyID="6"
|
||||
>VAT</cbc:ID>
|
||||
</cac:TaxScheme>
|
||||
</cac:PartyTaxScheme>
|
||||
<cac:Contact>
|
||||
<cbc:Name t-esc="partner.name"/>
|
||||
<cbc:Telephone
|
||||
t-if="partner.phone"
|
||||
t-esc="partner.phone"/>
|
||||
<cbc:ElectronicMail
|
||||
t-if="partner.email"
|
||||
t-esc="partner.email"/>
|
||||
</cac:Contact>
|
||||
</cac:Party>
|
||||
</template>
|
||||
|
||||
<!--
|
||||
Render an invoice's line to be added to the UBL xml document.
|
||||
-->
|
||||
<template id="export_ubl_invoice_line">
|
||||
<cac:InvoiceLine
|
||||
xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
||||
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
||||
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
||||
<cbc:ID t-esc="line._origin.id"/>
|
||||
<cbc:Note t-if="line.discount" t-esc="_('Discount (%s %)') % line.discount"/>
|
||||
<cbc:InvoicedQuantity t-esc="line.quantity"/>
|
||||
<cbc:LineExtensionAmount
|
||||
t-att-currencyID="line.currency_id.name or invoice.currency_id.name"
|
||||
t-esc="format_monetary(line.price_subtotal)"/>
|
||||
<cac:TaxTotal
|
||||
t-foreach="line.tax_ids.compute_all(line.price_unit, quantity=line.quantity, product=line.product_id, partner=invoice.partner_id)['taxes']"
|
||||
t-as="tax">
|
||||
<cbc:TaxAmount
|
||||
t-att-currencyID="line.currency_id.name or invoice.currency_id.name"
|
||||
t-esc="format_monetary(tax['amount'])"/>
|
||||
</cac:TaxTotal>
|
||||
<cac:Item>
|
||||
<cbc:Description t-esc="line.name.replace('\n', ', ')"/>
|
||||
<cbc:Name t-esc="line.product_id.name"/>
|
||||
<cac:SellersItemIdentification t-if="line.product_id.default_code">
|
||||
<cbc:ID t-esc="line.product_id.default_code"/>
|
||||
</cac:SellersItemIdentification>
|
||||
</cac:Item>
|
||||
<cac:Price>
|
||||
<cbc:PriceAmount
|
||||
t-att-currencyID="line.currency_id.name or invoice.currency_id.name"
|
||||
t-esc="format_monetary(line.price_unit)"/>
|
||||
</cac:Price>
|
||||
</cac:InvoiceLine>
|
||||
</template>
|
||||
|
||||
<!--
|
||||
Render an invoice to be added to the UBL xml document.
|
||||
-->
|
||||
<template id="export_ubl_invoice">
|
||||
<Invoice
|
||||
xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
||||
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
||||
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
||||
<cbc:UBLVersionID t-esc="ubl_version"/>
|
||||
<cbc:ID t-esc="invoice.name"/>
|
||||
<cbc:IssueDate t-esc="invoice.invoice_date"/>
|
||||
<cbc:InvoiceTypeCode t-esc="type_code"/>
|
||||
<cbc:Note t-esc="invoice.narration"/>
|
||||
<cbc:DocumentCurrencyCode t-esc="invoice.currency_id.name"/>
|
||||
<cac:AccountingSupplierParty t-call="account_edi_ubl.export_ubl_invoice_partner">
|
||||
<t t-set="partner" t-value="invoice.company_id.partner_id.commercial_partner_id"/>
|
||||
</cac:AccountingSupplierParty>
|
||||
<cac:AccountingCustomerParty t-call="account_edi_ubl.export_ubl_invoice_partner">
|
||||
<t t-set="partner" t-value="invoice.commercial_partner_id"/>
|
||||
</cac:AccountingCustomerParty>
|
||||
<cac:PaymentMeans>
|
||||
<cbc:PaymentMeansCode listID="UN/ECE 4461" t-esc="payment_means_code"/>
|
||||
<cbc:PaymentDueDate t-esc="invoice.invoice_date_due"/>
|
||||
<cac:PayeeFinancialAccount t-if="invoice.journal_id.bank_account_id">
|
||||
<cbc:ID schemeName="IBAN" t-esc="invoice.journal_id.bank_account_id.acc_number"/>
|
||||
<cac:FinancialInstitutionBranch t-if="invoice.journal_id.bank_account_id.bank_bic">
|
||||
<cac:FinancialInstitution>
|
||||
<cbc:ID schemeName="BIC" t-esc="invoice.journal_id.bank_account_id.bank_bic"/>
|
||||
</cac:FinancialInstitution>
|
||||
</cac:FinancialInstitutionBranch>
|
||||
</cac:PayeeFinancialAccount>
|
||||
</cac:PaymentMeans>
|
||||
<cac:PaymentTerms t-if="invoice.invoice_payment_term_id">
|
||||
<cbc:Note t-esc="invoice.invoice_payment_term_id.name"/>
|
||||
</cac:PaymentTerms>
|
||||
<cac:TaxTotal t-if="not invoice.currency_id.is_zero(invoice.amount_tax)">
|
||||
<cbc:TaxAmount
|
||||
t-att-currencyID="invoice.currency_id.name"
|
||||
t-esc="format_monetary(invoice.amount_tax)"/>
|
||||
</cac:TaxTotal>
|
||||
<cac:LegalMonetaryTotal>
|
||||
<cbc:LineExtensionAmount
|
||||
t-att-currencyID="invoice.currency_id.name"
|
||||
t-esc="format_monetary(invoice.amount_untaxed)"/>
|
||||
<cbc:TaxExclusiveAmount
|
||||
t-att-currencyID="invoice.currency_id.name"
|
||||
t-esc="format_monetary(invoice.amount_untaxed)"/>
|
||||
<cbc:TaxInclusiveAmount
|
||||
t-att-currencyID="invoice.currency_id.name"
|
||||
t-esc="format_monetary(invoice.amount_total)"/>
|
||||
<cbc:PrepaidAmount
|
||||
t-att-currencyID="invoice.currency_id.name"
|
||||
t-esc="format_monetary(invoice.amount_total - invoice.amount_residual)"/>
|
||||
<cbc:PayableAmount
|
||||
t-att-currencyID="invoice.currency_id.name"
|
||||
t-esc="format_monetary(invoice.amount_residual)"/>
|
||||
</cac:LegalMonetaryTotal>
|
||||
<t t-call="account_edi_ubl.export_ubl_invoice_line"
|
||||
t-foreach="invoice.invoice_line_ids"
|
||||
t-as="line"/>
|
||||
</Invoice>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,2 @@
|
||||
from . import account_move
|
||||
from . import account_edi_format
|
||||
+58
-52
@@ -1,52 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import api, models, fields, tools, _
|
||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, float_repr
|
||||
from odoo.tests.common import Form
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.osv import expression
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
class AccountEdiFormat(models.Model):
|
||||
_inherit = 'account.edi.format'
|
||||
|
||||
@api.model
|
||||
def _get_ubl_namespaces(self, tree):
|
||||
''' If the namespace is declared with xmlns='...', the namespaces map contains the 'None' key that causes an
|
||||
TypeError: empty namespace prefix is not supported in XPath
|
||||
Then, we need to remap arbitrarily this key.
|
||||
def _import_ubl(self, tree, invoice):
|
||||
""" Decodes an UBL invoice into an invoice.
|
||||
|
||||
:param tree: An instance of etree.
|
||||
:return: The namespaces map without 'None' key.
|
||||
'''
|
||||
namespaces = tree.nsmap
|
||||
namespaces['inv'] = namespaces.pop(None)
|
||||
return namespaces
|
||||
:param tree: the UBL tree to decode.
|
||||
:param invoice: the invoice to update or an empty recordset.
|
||||
:returns: the invoice where the UBL data was imported.
|
||||
"""
|
||||
|
||||
@api.model
|
||||
def _detect_ubl_2_1(self, tree, file_name):
|
||||
# Quick check the tree looks like an UBL 2.1 file.
|
||||
flag = tree.tag == '{urn:oasis:names:specification:ubl:schema:xsd:Invoice-2}Invoice'
|
||||
error = None
|
||||
def _get_ubl_namespaces():
|
||||
''' If the namespace is declared with xmlns='...', the namespaces map contains the 'None' key that causes an
|
||||
TypeError: empty namespace prefix is not supported in XPath
|
||||
Then, we need to remap arbitrarily this key.
|
||||
|
||||
return {'flag': flag, 'error': error}
|
||||
:param tree: An instance of etree.
|
||||
:return: The namespaces map without 'None' key.
|
||||
'''
|
||||
namespaces = tree.nsmap
|
||||
namespaces['inv'] = namespaces.pop(None)
|
||||
return namespaces
|
||||
|
||||
@api.model
|
||||
def _decode_ubl_2_1(self, tree):
|
||||
self.ensure_one()
|
||||
namespaces = self._get_ubl_namespaces(tree)
|
||||
namespaces = _get_ubl_namespaces()
|
||||
if not invoice:
|
||||
invoice = self.env['account.move'].create({})
|
||||
|
||||
elements = tree.xpath('//cbc:InvoiceTypeCode', namespaces=namespaces)
|
||||
if elements:
|
||||
type_code = elements[0].text
|
||||
type = 'in_refund' if type_code == '381' else 'in_invoice'
|
||||
move_type = 'in_refund' if type_code == '381' else 'in_invoice'
|
||||
else:
|
||||
type = 'in_invoice'
|
||||
move_type = 'in_invoice'
|
||||
|
||||
default_journal = self.with_context(default_move_type=type)._get_default_journal()
|
||||
default_journal = invoice.with_context(default_move_type=move_type)._get_default_journal()
|
||||
|
||||
with Form(self.with_context(default_move_type=type, default_journal_id=default_journal.id)) as invoice_form:
|
||||
with Form(invoice.with_context(default_move_type=move_type, default_journal_id=default_journal.id)) as invoice_form:
|
||||
# Reference
|
||||
elements = tree.xpath('//cbc:ID', namespaces=namespaces)
|
||||
if elements:
|
||||
@@ -87,6 +90,8 @@ class AccountMove(models.Model):
|
||||
if elements:
|
||||
partner_name = elements[0].text
|
||||
domains.append([('name', 'ilike', partner_name)])
|
||||
else:
|
||||
partner_name = ''
|
||||
elements = partner_element.xpath('//cac:AccountingSupplierParty/cac:Party//cbc:Telephone', namespaces=namespaces)
|
||||
if elements:
|
||||
partner_telephone = elements[0].text
|
||||
@@ -104,6 +109,7 @@ class AccountMove(models.Model):
|
||||
partner = self.env['res.partner'].search(expression.OR(domains), limit=1)
|
||||
if partner:
|
||||
invoice_form.partner_id = partner
|
||||
partner_name = partner.name
|
||||
else:
|
||||
invoice_form.partner_id = self.env['res.partner']
|
||||
|
||||
@@ -116,33 +122,18 @@ class AccountMove(models.Model):
|
||||
if attachment_name and attachment_data:
|
||||
attachments |= self.env['ir.attachment'].create({
|
||||
'name': attachment_name[0].text,
|
||||
'res_id': self.id,
|
||||
'res_id': invoice.id,
|
||||
'res_model': 'account.move',
|
||||
'datas': attachment_data[0].text,
|
||||
'type': 'binary',
|
||||
})
|
||||
if attachments:
|
||||
self.with_context(no_new_invoice=True).message_post(attachment_ids=attachments.ids)
|
||||
invoice.with_context(no_new_invoice=True).message_post(attachment_ids=attachments.ids)
|
||||
|
||||
# Lines
|
||||
lines_elements = tree.xpath('//cac:InvoiceLine', namespaces=namespaces)
|
||||
for eline in lines_elements:
|
||||
with invoice_form.invoice_line_ids.new() as invoice_line_form:
|
||||
# Quantity
|
||||
elements = eline.xpath('cbc:InvoicedQuantity', namespaces=namespaces)
|
||||
quantity = elements and float(elements[0].text) or 1.0
|
||||
invoice_line_form.quantity = quantity
|
||||
|
||||
# Price Unit
|
||||
elements = eline.xpath('cac:Price/cbc:PriceAmount', namespaces=namespaces)
|
||||
invoice_line_form.price_unit = elements and float(elements[0].text) or 0.0
|
||||
|
||||
# Name
|
||||
elements = eline.xpath('cac:Item/cbc:Description', namespaces=namespaces)
|
||||
invoice_line_form.name = elements and elements[0].text or ''
|
||||
invoice_line_form.name = invoice_line_form.name.replace('%month%', str(fields.Date.to_date(invoice_form.invoice_date).month)) # TODO: full name in locale
|
||||
invoice_line_form.name = invoice_line_form.name.replace('%year%', str(fields.Date.to_date(invoice_form.invoice_date).year))
|
||||
|
||||
# Product
|
||||
elements = eline.xpath('cac:Item/cac:SellersItemIdentification/cbc:ID', namespaces=namespaces)
|
||||
domains = []
|
||||
@@ -158,6 +149,27 @@ class AccountMove(models.Model):
|
||||
if product:
|
||||
invoice_line_form.product_id = product
|
||||
|
||||
# Quantity
|
||||
elements = eline.xpath('cbc:InvoicedQuantity', namespaces=namespaces)
|
||||
quantity = elements and float(elements[0].text) or 1.0
|
||||
invoice_line_form.quantity = quantity
|
||||
|
||||
# Price Unit
|
||||
elements = eline.xpath('cac:Price/cbc:PriceAmount', namespaces=namespaces)
|
||||
price_unit = elements and float(elements[0].text) or 0.0
|
||||
elements = eline.xpath('cbc:LineExtensionAmount', namespaces=namespaces)
|
||||
line_extension_amount = elements and float(elements[0].text) or 0.0
|
||||
invoice_line_form.price_unit = price_unit or line_extension_amount / invoice_line_form.quantity or 0.0
|
||||
|
||||
# Name
|
||||
elements = eline.xpath('cac:Item/cbc:Description', namespaces=namespaces)
|
||||
if elements and elements[0].text:
|
||||
invoice_line_form.name = elements[0].text
|
||||
invoice_line_form.name = invoice_line_form.name.replace('%month%', str(fields.Date.to_date(invoice_form.invoice_date).month)) # TODO: full name in locale
|
||||
invoice_line_form.name = invoice_line_form.name.replace('%year%', str(fields.Date.to_date(invoice_form.invoice_date).year))
|
||||
else:
|
||||
invoice_line_form.name = "%s (%s)" % (partner_name or '', invoice_form.invoice_date)
|
||||
|
||||
# Taxes
|
||||
taxes_elements = eline.xpath('cac:TaxTotal/cac:TaxSubtotal', namespaces=namespaces)
|
||||
invoice_line_form.tax_ids.clear()
|
||||
@@ -173,9 +185,3 @@ class AccountMove(models.Model):
|
||||
invoice_line_form.tax_ids.add(tax)
|
||||
|
||||
return invoice_form.save()
|
||||
|
||||
@api.model
|
||||
def _get_xml_decoders(self):
|
||||
# Override
|
||||
ubl_decoders = [('UBL 2.1', self._detect_ubl_2_1, self._decode_ubl_2_1)]
|
||||
return super(AccountMove, self)._get_xml_decoders() + ubl_decoders
|
||||
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import models
|
||||
from odoo.tools import float_repr
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
def _get_ubl_values(self):
|
||||
def format_monetary(amount):
|
||||
# Format the monetary values to avoid trailing decimals (e.g. 90.85000000000001).
|
||||
return float_repr(amount, self.currency_id.decimal_places)
|
||||
|
||||
return {
|
||||
'invoice': self,
|
||||
'ubl_version': 2.1,
|
||||
'type_code': 380 if self.move_type == 'out_invoice' else 381,
|
||||
'payment_means_code': 42 if self.journal_id.bank_account_id else 31,
|
||||
'format_monetary': format_monetary,
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~13.2\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-02-13 07:24+0000\n"
|
||||
"PO-Revision-Date: 2020-02-13 07:24+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:0
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr ""
|
||||
@@ -1,55 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
# Translators:
|
||||
# Mustafa Rawi <mustafa@cubexco.com>, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Last-Translator: Mustafa Rawi <mustafa@cubexco.com>, 2019\n"
|
||||
"Language-Team: Arabic (https://www.transifex.com/odoo/teams/41243/ar/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: ar\n"
|
||||
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr "القيود اليومية"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr "إجراء التقرير"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr ""
|
||||
@@ -1,55 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
# Translators:
|
||||
# Martin Trigaux, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Last-Translator: Martin Trigaux, 2019\n"
|
||||
"Language-Team: Czech (https://www.transifex.com/odoo/teams/41243/cs/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: cs\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr "Položky deníku"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr ""
|
||||
@@ -1,55 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
# Translators:
|
||||
# Ejner Sønniksen <ejner@vkdata.dk>, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Last-Translator: Ejner Sønniksen <ejner@vkdata.dk>, 2019\n"
|
||||
"Language-Team: Danish (https://www.transifex.com/odoo/teams/41243/da/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: da\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr "Journalposter"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr ""
|
||||
@@ -1,60 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
# Translators:
|
||||
# Martin Trigaux, 2019
|
||||
# Leon Grill <leg@odoo.com>, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Last-Translator: Leon Grill <leg@odoo.com>, 2019\n"
|
||||
"Language-Team: German (https://www.transifex.com/odoo/teams/41243/de/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: de\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr "Buchungssätze"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
"Es wurde kein Decoder für die Xml-Datei gefunden: {}. Die Datei ist schlecht"
|
||||
" formatiert, nicht unterstützt oder der Decoder ist nicht installiert"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
"Es werden keine Informationen über das Journal oder die Art der Rechnung "
|
||||
"übergeben"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr "Die Xml-Datei ist schlecht formatiert : {}"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr "urn:cen.eu:en16931:2017"
|
||||
@@ -1,59 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
# Translators:
|
||||
# Martin Trigaux, 2019
|
||||
# Pedro M. Baeza <pedro.baeza@gmail.com>, 2019
|
||||
# John Guardado <jgu@odoo.com>, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Last-Translator: John Guardado <jgu@odoo.com>, 2019\n"
|
||||
"Language-Team: Spanish (https://www.transifex.com/odoo/teams/41243/es/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: es\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr "Asientos contables"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
"No se encontró ningún decodificador para el archivo xml: {}. El archivo está"
|
||||
" mal formateado, no es compatible o el decodificador no está instalado."
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr "No hay información sobre el diario ni sobre el tipo de factura."
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr "Acción de informe"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr "El archivo xml está mal formateado : {}"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr "urn:cen.eu:en16931:2017"
|
||||
@@ -1,59 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
# Translators:
|
||||
# Martin Trigaux, 2019
|
||||
# Cécile Collart <cco@odoo.com>, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Last-Translator: Cécile Collart <cco@odoo.com>, 2019\n"
|
||||
"Language-Team: French (https://www.transifex.com/odoo/teams/41243/fr/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: fr\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr "Pièces comptables"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
"Aucun décodeur n'a été trouvé pour le fichier XML: {}. Le fichier est mal "
|
||||
"formaté, non supporté ou le décodeur n'est pas installé"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
"Aucune information sur le journal ou le type de facture n'est transmise"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr "Signaler l'action"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr "Le fichier XML est mal formaté: {}"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr "urn:cen.eu:en16931:2017"
|
||||
@@ -1,55 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
# Translators:
|
||||
# דודי מלכה <Dudimalka6@gmail.com>, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Last-Translator: דודי מלכה <Dudimalka6@gmail.com>, 2019\n"
|
||||
"Language-Team: Hebrew (https://www.transifex.com/odoo/teams/41243/he/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: he\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr "רשומות יומן"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr "דוח פעולה"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr ""
|
||||
@@ -1,55 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
# Translators:
|
||||
# Martin Trigaux, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Last-Translator: Martin Trigaux, 2019\n"
|
||||
"Language-Team: Croatian (https://www.transifex.com/odoo/teams/41243/hr/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: hr\n"
|
||||
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr "Temeljnice"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr ""
|
||||
@@ -1,55 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
# Translators:
|
||||
# Martin Trigaux, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Last-Translator: Martin Trigaux, 2019\n"
|
||||
"Language-Team: Hungarian (https://www.transifex.com/odoo/teams/41243/hu/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: hu\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr "Könyvelési tételek"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr ""
|
||||
@@ -1,55 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
# Translators:
|
||||
# Sergio Zanchetta <primes2h@gmail.com>, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Last-Translator: Sergio Zanchetta <primes2h@gmail.com>, 2019\n"
|
||||
"Language-Team: Italian (https://www.transifex.com/odoo/teams/41243/it/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: it\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr "Registrazioni contabili"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr "Azione resoconto"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr "urn:cen.eu:en16931:2017"
|
||||
@@ -1,51 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Language-Team: Luxembourgish (https://www.transifex.com/odoo/teams/41243/lb/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: lb\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr ""
|
||||
@@ -1,55 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
# Translators:
|
||||
# Linas Versada <linaskrisiukenas@gmail.com>, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Last-Translator: Linas Versada <linaskrisiukenas@gmail.com>, 2019\n"
|
||||
"Language-Team: Lithuanian (https://www.transifex.com/odoo/teams/41243/lt/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: lt\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr "Žurnalo įrašai"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr ""
|
||||
@@ -1,55 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
# Translators:
|
||||
# Baskhuu Lodoikhuu <baskhuujacara@gmail.com>, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Last-Translator: Baskhuu Lodoikhuu <baskhuujacara@gmail.com>, 2019\n"
|
||||
"Language-Team: Mongolian (https://www.transifex.com/odoo/teams/41243/mn/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: mn\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr "Ажил гүйлгээ"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr ""
|
||||
@@ -1,55 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
# Translators:
|
||||
# Martin Trigaux, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Last-Translator: Martin Trigaux, 2019\n"
|
||||
"Language-Team: Norwegian Bokmål (https://www.transifex.com/odoo/teams/41243/nb/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: nb\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr "Journalregistreringer"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr ""
|
||||
@@ -1,59 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
# Translators:
|
||||
# Martin Trigaux, 2019
|
||||
# Yenthe Van Ginneken <yenthespam@gmail.com>, 2019
|
||||
# Gunther Clauwaert <gclauwae@hotmail.com>, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Last-Translator: Gunther Clauwaert <gclauwae@hotmail.com>, 2019\n"
|
||||
"Language-Team: Dutch (https://www.transifex.com/odoo/teams/41243/nl/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: nl\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr "Boekingen"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
"Er werd geen decoder voor de xml file: {} gevonden. De file is slecht "
|
||||
"geformatteerd, niet ondersteunt of de decoder is niet geïnstalleerd"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr "Rapport actie"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr "Het XML bestand is slecht geformatteerd : {}"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr "urn:cen.eu:en16931:2017"
|
||||
@@ -1,55 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
# Translators:
|
||||
# Martin Trigaux, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Last-Translator: Martin Trigaux, 2019\n"
|
||||
"Language-Team: Polish (https://www.transifex.com/odoo/teams/41243/pl/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: pl\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr "Zapisy księgowe"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr ""
|
||||
@@ -1,51 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Language-Team: Portuguese (https://www.transifex.com/odoo/teams/41243/pt/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: pt\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr ""
|
||||
@@ -1,55 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
# Translators:
|
||||
# Martin Trigaux, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Last-Translator: Martin Trigaux, 2019\n"
|
||||
"Language-Team: Portuguese (Brazil) (https://www.transifex.com/odoo/teams/41243/pt_BR/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: pt_BR\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr "Lançamentos de Diário"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr ""
|
||||
@@ -1,51 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Language-Team: Romanian (https://www.transifex.com/odoo/teams/41243/ro/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: ro\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr ""
|
||||
@@ -1,55 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
# Translators:
|
||||
# Martin Trigaux, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Last-Translator: Martin Trigaux, 2019\n"
|
||||
"Language-Team: Russian (https://www.transifex.com/odoo/teams/41243/ru/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: ru\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr "Записи журнала"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr ""
|
||||
@@ -1,55 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
# Translators:
|
||||
# Murat Kaplan <muratk@projetgrup.com>, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Last-Translator: Murat Kaplan <muratk@projetgrup.com>, 2019\n"
|
||||
"Language-Team: Turkish (https://www.transifex.com/odoo/teams/41243/tr/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: tr\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr "Yevmiye Kayıtları"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr ""
|
||||
@@ -1,58 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
# Translators:
|
||||
# Martin Trigaux, 2019
|
||||
# Alina Lisnenko <alinasemeniuk1@gmail.com>, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Last-Translator: Alina Lisnenko <alinasemeniuk1@gmail.com>, 2019\n"
|
||||
"Language-Team: Ukrainian (https://www.transifex.com/odoo/teams/41243/uk/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: uk\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr "Записи в журналі"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
"Не знайдено декодер для xml фалу: {}. Файл погано відформатований, не "
|
||||
"підтримується або декодер не встановлено"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr "Пропущено інформацію про журнал або тип рахунку"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr "Дія звіту"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr "Файл xml погано відформатовано : {}"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr "urn:cen.eu:en16931:2017"
|
||||
@@ -1,56 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
# Translators:
|
||||
# Nancy Momoland <thanhnguyen.icsc@gmail.com>, 2019
|
||||
# Chinh Chinh <trinhttp@trobz.com>, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Last-Translator: Chinh Chinh <trinhttp@trobz.com>, 2019\n"
|
||||
"Language-Team: Vietnamese (https://www.transifex.com/odoo/teams/41243/vi/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: vi\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr "Bút toán phát sinh"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr "Báo cáo hành động"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr ""
|
||||
@@ -1,56 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
# Translators:
|
||||
# Jeanphy <hzh0292@qq.com>, 2019
|
||||
# niulin lnc. <admin@niulin.net>, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Last-Translator: niulin lnc. <admin@niulin.net>, 2019\n"
|
||||
"Language-Team: Chinese (China) (https://www.transifex.com/odoo/teams/41243/zh_CN/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: zh_CN\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr "日记账分录"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr "报告动作"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr ""
|
||||
@@ -1,56 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_facturx
|
||||
#
|
||||
# Translators:
|
||||
# Martin Trigaux, 2019
|
||||
# 敬雲 林 <chingyun@yuanchih-consult.com>, 2019
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~12.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-08-12 11:32+0000\n"
|
||||
"PO-Revision-Date: 2019-08-26 09:08+0000\n"
|
||||
"Last-Translator: 敬雲 林 <chingyun@yuanchih-consult.com>, 2019\n"
|
||||
"Language-Team: Chinese (Taiwan) (https://www.transifex.com/odoo/teams/41243/zh_TW/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: zh_TW\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_account_move
|
||||
msgid "Journal Entries"
|
||||
msgstr "日記帳分錄"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:328
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No decoder was found for the xml file: {}. The file is badly formatted, not "
|
||||
"supported or the decoder is not installed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:65
|
||||
#, python-format
|
||||
msgid "No information about the journal or the type of invoice is passed"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model:ir.model,name:account_facturx.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr "報告動作"
|
||||
|
||||
#. module: account_facturx
|
||||
#: code:addons/account_facturx/models/account_move.py:314
|
||||
#, python-format
|
||||
msgid "The xml file is badly formatted : {}"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_facturx
|
||||
#: model_terms:ir.ui.view,arch_db:account_facturx.account_invoice_facturx_export
|
||||
msgid "urn:cen.eu:en16931:2017"
|
||||
msgstr ""
|
||||
@@ -1,4 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from . import account_move
|
||||
from . import ir_actions_report
|
||||
@@ -1,32 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
from PyPDF2 import PdfFileWriter, PdfFileReader
|
||||
|
||||
import io
|
||||
|
||||
|
||||
class IrActionsReport(models.Model):
|
||||
_inherit = 'ir.actions.report'
|
||||
|
||||
def _post_pdf(self, save_in_attachment, pdf_content=None, res_ids=None):
|
||||
# OVERRIDE
|
||||
if self.model == 'account.move' and res_ids and len(res_ids) == 1:
|
||||
invoice = self.env['account.move'].browse(res_ids)
|
||||
if invoice.is_sale_document() and invoice.state != 'draft':
|
||||
xml_content = invoice._export_as_facturx_xml()
|
||||
|
||||
# Add attachment.
|
||||
reader_buffer = io.BytesIO(pdf_content)
|
||||
reader = PdfFileReader(reader_buffer)
|
||||
writer = PdfFileWriter()
|
||||
writer.cloneReaderDocumentRoot(reader)
|
||||
writer.addAttachment('factur-x.xml', xml_content)
|
||||
buffer = io.BytesIO()
|
||||
writer.write(buffer)
|
||||
pdf_content = buffer.getvalue()
|
||||
|
||||
reader_buffer.close()
|
||||
buffer.close()
|
||||
return super(IrActionsReport, self)._post_pdf(save_in_attachment, pdf_content=pdf_content, res_ids=res_ids)
|
||||
@@ -11,8 +11,9 @@ invoices. The UBL standard became the `ISO/IEC 19845
|
||||
(cf the `official announce <http://www.prweb.com/releases/2016/01/prweb13186919.htm>`_).
|
||||
Belgian e-invoicing uses the UBL 2.0 using the e-fff protocol.
|
||||
""",
|
||||
'depends': ['account', 'l10n_be', 'account_facturx'],
|
||||
'depends': ['l10n_be', 'account_edi'],
|
||||
'data': [
|
||||
'data/account_edi_data.xml'
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': True,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="edi_efff_1" model="account.edi.format">
|
||||
<field name="name">E-FFF (BE)</field>
|
||||
<field name="code">efff_1</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from . import account_invoice
|
||||
from . import account_edi_format
|
||||
from . import account_move
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, models, fields
|
||||
from odoo.tools import float_repr
|
||||
|
||||
|
||||
class AccountEdiFormat(models.Model):
|
||||
_inherit = 'account.edi.format'
|
||||
|
||||
def _is_efff(self, filename, tree):
|
||||
return self.code == 'efff_1' and tree.tag == '{urn:oasis:names:specification:ubl:schema:xsd:Invoice-2}Invoice'
|
||||
|
||||
def _create_invoice_from_xml_tree(self, filename, tree):
|
||||
self.ensure_one()
|
||||
if self._is_efff(filename, tree):
|
||||
return self._import_ubl(tree, self.env['account.move'])
|
||||
return super()._create_invoice_from_xml_tree(filename, tree)
|
||||
|
||||
def _update_invoice_from_xml_tree(self, filename, tree, invoice):
|
||||
self.ensure_one()
|
||||
if self._is_efff(filename, tree):
|
||||
return self._import_ubl(tree, invoice)
|
||||
return super()._update_invoice_from_xml_tree(filename, tree, invoice)
|
||||
|
||||
def _export_invoice_to_attachment(self, invoice):
|
||||
self.ensure_one()
|
||||
if self.code != 'efff_1':
|
||||
return super()._export_invoice_to_attachment(invoice)
|
||||
|
||||
# Create file content.
|
||||
xml_content = b"<?xml version='1.0' encoding='UTF-8'?>"
|
||||
xml_content += self.env.ref('account_edi_ubl.export_ubl_invoice')._render(invoice._get_ubl_values())
|
||||
vat = invoice.company_id.partner_id.commercial_partner_id.vat
|
||||
xml_name = 'efff-%s%s%s.xml' % (vat or '', '-' if vat else '', invoice.name.replace('/', '_')) # official naming convention
|
||||
return {'name': xml_name,
|
||||
'datas': xml_content,
|
||||
'res_model': 'account.move',
|
||||
'res_id': invoice._origin.id,
|
||||
'mimetype': 'application/xml'
|
||||
}
|
||||
|
||||
def _export_invoice_to_embed_to_pdf(self, pdf_content, invoice):
|
||||
self.ensure_one()
|
||||
if self.code != 'efff_1':
|
||||
return super()._export_invoice_to_embed_to_pdf(pdf_content, invoice)
|
||||
return False # ubl must not be embedded to PDF.
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
def _get_ubl_values(self):
|
||||
values = super(AccountMove, self)._get_ubl_values()
|
||||
|
||||
# E-fff uses ubl_version 2.0, account_edi_ubl supports ubl_version 2.1 but generates 2.0 UBL
|
||||
# so we only need to override the version to be compatible with E-FFF
|
||||
values['ubl_version'] = 2.0
|
||||
|
||||
return values
|
||||
@@ -25,8 +25,11 @@ class TestUBL(common.TransactionCase):
|
||||
'res_model': 'account.move',
|
||||
})
|
||||
|
||||
invoice_count = len(self.env['account.move'].search([]))
|
||||
|
||||
invoice.message_post(attachment_ids=[attachment_id.id])
|
||||
|
||||
self.assertEqual(len(self.env['account.move'].search([])), invoice_count)
|
||||
self.assertEqual(invoice.amount_total, 666.50)
|
||||
self.assertEqual(invoice.amount_tax, 115.67)
|
||||
self.assertEqual(invoice.partner_id, self.partner_id)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
'depends': [
|
||||
'l10n_it',
|
||||
'fetchmail',
|
||||
'account_edi'
|
||||
],
|
||||
'author': 'Odoo',
|
||||
'description': """
|
||||
@@ -16,6 +17,7 @@ E-invoice implementation
|
||||
'website': 'http://www.odoo.com/',
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/account_edi_data.xml',
|
||||
'data/invoice_it_template.xml',
|
||||
'views/l10n_it_view.xml',
|
||||
],
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="edi_fatturaPA" model="account.edi.format">
|
||||
<field name="name">Fattura PA (IT)</field>
|
||||
<field name="code">fattura_pa</field>
|
||||
<field name="hide_on_journal">import</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -4,5 +4,6 @@
|
||||
from . import res_partner
|
||||
from . import res_company
|
||||
from . import account_invoice
|
||||
from . import account_edi_format
|
||||
from . import ir_mail_server
|
||||
from . import ddt
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models, fields, _
|
||||
from odoo.tests.common import Form
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import float_repr
|
||||
|
||||
import re
|
||||
from datetime import date, datetime
|
||||
import logging
|
||||
import base64
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_FACTUR_ITALIAN_DATE_FORMAT = '%Y-%m-%d'
|
||||
|
||||
|
||||
class AccountEdiFormat(models.Model):
|
||||
_inherit = 'account.edi.format'
|
||||
|
||||
def _check_filename_is_fattura_pa(self, filename):
|
||||
return re.search("([A-Z]{2}[A-Za-z0-9]{2,28}_[A-Za-z0-9]{0,5}.(xml.p7m|xml))", filename)
|
||||
|
||||
def _is_fattura_pa(self, filename, tree):
|
||||
return self.code == 'fattura_pa' and self._check_filename_is_fattura_pa(filename)
|
||||
|
||||
def _create_invoice_from_xml_tree(self, filename, tree):
|
||||
self.ensure_one()
|
||||
if self._is_fattura_pa(filename, tree):
|
||||
return self._import_fattura_pa(tree, self.env['account.move'])
|
||||
return super()._create_invoice_from_xml_tree(filename, tree)
|
||||
|
||||
def _update_invoice_from_xml_tree(self, filename, tree, invoice):
|
||||
self.ensure_one()
|
||||
if self._is_fattura_pa(filename, tree):
|
||||
if len(tree.xpath('//FatturaElettronicaBody')) > 1:
|
||||
invoice.message_post(body='The attachment contains multiple invoices, this invoice was not updated from it.',
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
author_id=self.env.ref('base.partner_root').id)
|
||||
else:
|
||||
return self._import_fattura_pa(tree, invoice)
|
||||
return super()._update_invoice_from_xml_tree(filename, tree, invoice)
|
||||
|
||||
def _import_fattura_pa(self, tree, invoice):
|
||||
""" Decodes a fattura_pa invoice into an invoice.
|
||||
|
||||
:param tree: the fattura_pa tree to decode.
|
||||
:param invoice: the invoice to update or an empty recordset.
|
||||
:returns: the invoice where the fattura_pa data was imported.
|
||||
"""
|
||||
invoices = self.env['account.move']
|
||||
first_run = True
|
||||
|
||||
# possible to have multiple invoices in the case of an invoice batch, the batch itself is repeated for every invoice of the batch
|
||||
for body_tree in tree.xpath('//FatturaElettronicaBody'):
|
||||
if not first_run or not invoice:
|
||||
# make sure all the iterations create a new invoice record (except the first which could have already created one)
|
||||
invoice = self.env['account.move']
|
||||
first_run = False
|
||||
|
||||
elements = tree.xpath('//DatiGeneraliDocumento/TipoDocumento')
|
||||
if elements and elements[0].text and elements[0].text == 'TD01':
|
||||
self_ctx = invoice.with_context(default_move_type='in_invoice')
|
||||
elif elements and elements[0].text and elements[0].text == 'TD04':
|
||||
self_ctx = invoice.with_context(default_move_type='in_refund')
|
||||
else:
|
||||
_logger.info(_('Document type not managed: %s.') % (elements[0].text))
|
||||
|
||||
# type must be present in the context to get the right behavior of the _default_journal method (account.move).
|
||||
# journal_id must be present in the context to get the right behavior of the _default_account method (account.move.line).
|
||||
|
||||
elements = tree.xpath('//CessionarioCommittente//IdCodice')
|
||||
company = elements and self.env['res.company'].search([('vat', 'ilike', elements[0].text)], limit=1)
|
||||
if not company:
|
||||
elements = tree.xpath('//CessionarioCommittente//CodiceFiscale')
|
||||
company = elements and self.env['res.company'].search([('l10n_it_codice_fiscale', 'ilike', elements[0].text)], limit=1)
|
||||
|
||||
if company:
|
||||
self_ctx = self_ctx.with_context(company_id=company.id)
|
||||
else:
|
||||
company = self.env.company
|
||||
if elements:
|
||||
_logger.info(_('Company not found with codice fiscale: %s. The company\'s user is set by default.') % elements[0].text)
|
||||
else:
|
||||
_logger.info(_('Company not found. The company\'s user is set by default.'))
|
||||
|
||||
if not self.env.is_superuser():
|
||||
if self.env.company != company:
|
||||
raise UserError(_("You can only import invoice concern your current company: %s") % self.env.company.display_name)
|
||||
|
||||
# Refund type.
|
||||
# TD01 == invoice
|
||||
# TD02 == advance/down payment on invoice
|
||||
# TD03 == advance/down payment on fee
|
||||
# TD04 == credit note
|
||||
# TD05 == debit note
|
||||
# TD06 == fee
|
||||
elements = tree.xpath('//DatiGeneraliDocumento/TipoDocumento')
|
||||
if elements and elements[0].text and elements[0].text == 'TD01':
|
||||
move_type = 'in_invoice'
|
||||
elif elements and elements[0].text and elements[0].text == 'TD04':
|
||||
move_type = 'in_refund'
|
||||
# move could be a single record (editing) or be empty (new).
|
||||
with Form(invoice.with_context(default_move_type=move_type)) as invoice_form:
|
||||
message_to_log = []
|
||||
|
||||
# Partner (first step to avoid warning 'Warning! You must first select a partner.'). <1.2>
|
||||
elements = tree.xpath('//CedentePrestatore//IdCodice')
|
||||
partner = elements and self.env['res.partner'].search(['&', ('vat', 'ilike', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1)
|
||||
if not partner:
|
||||
elements = tree.xpath('//CedentePrestatore//CodiceFiscale')
|
||||
partner = elements and self.env['res.partner'].search(['&', ('l10n_it_codice_fiscale', '=', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1)
|
||||
if not partner:
|
||||
elements = tree.xpath('//DatiTrasmissione//Email')
|
||||
partner = elements and self.env['res.partner'].search(['&', '|', ('email', '=', elements[0].text), ('l10n_it_pec_email', '=', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1)
|
||||
if partner:
|
||||
invoice_form.partner_id = partner
|
||||
else:
|
||||
message_to_log.append("%s<br/>%s" % (
|
||||
_("Vendor not found, useful informations from XML file:"),
|
||||
invoice._compose_info_message(
|
||||
tree, './/CedentePrestatore')))
|
||||
|
||||
# Numbering attributed by the transmitter. <1.1.2>
|
||||
elements = tree.xpath('//ProgressivoInvio')
|
||||
if elements:
|
||||
invoice_form.payment_reference = elements[0].text
|
||||
|
||||
elements = body_tree.xpath('.//DatiGeneraliDocumento//Numero')
|
||||
if elements:
|
||||
invoice_form.ref = elements[0].text
|
||||
|
||||
# Currency. <2.1.1.2>
|
||||
elements = body_tree.xpath('.//DatiGeneraliDocumento/Divisa')
|
||||
if elements:
|
||||
currency_str = elements[0].text
|
||||
currency = self.env.ref('base.%s' % currency_str.upper(), raise_if_not_found=False)
|
||||
if currency != self.env.company.currency_id and currency.active:
|
||||
invoice_form.currency_id = currency
|
||||
|
||||
# Date. <2.1.1.3>
|
||||
elements = body_tree.xpath('.//DatiGeneraliDocumento/Data')
|
||||
if elements:
|
||||
date_str = elements[0].text
|
||||
date_obj = datetime.strptime(date_str, DEFAULT_FACTUR_ITALIAN_DATE_FORMAT)
|
||||
invoice_form.invoice_date = date_obj.strftime(DEFAULT_FACTUR_ITALIAN_DATE_FORMAT)
|
||||
|
||||
# Dati Bollo. <2.1.1.6>
|
||||
elements = body_tree.xpath('.//DatiGeneraliDocumento/DatiBollo/ImportoBollo')
|
||||
if elements:
|
||||
invoice_form.l10n_it_stamp_duty = float(elements[0].text)
|
||||
|
||||
# List of all amount discount (will be add after all article to avoid to have a negative sum)
|
||||
discount_list = []
|
||||
percentage_global_discount = 1.0
|
||||
|
||||
# Global discount. <2.1.1.8>
|
||||
discount_elements = body_tree.xpath('.//DatiGeneraliDocumento/ScontoMaggiorazione')
|
||||
total_discount_amount = 0.0
|
||||
if discount_elements:
|
||||
for discount_element in discount_elements:
|
||||
discount_line = discount_element.xpath('.//Tipo')
|
||||
discount_sign = -1
|
||||
if discount_line and discount_line[0].text == 'SC':
|
||||
discount_sign = 1
|
||||
discount_percentage = discount_element.xpath('.//Percentuale')
|
||||
if discount_percentage and discount_percentage[0].text:
|
||||
percentage_global_discount *= 1 - float(discount_percentage[0].text)/100 * discount_sign
|
||||
|
||||
discount_amount_text = discount_element.xpath('.//Importo')
|
||||
if discount_amount_text and discount_amount_text[0].text:
|
||||
discount_amount = float(discount_amount_text[0].text) * discount_sign * -1
|
||||
discount = {}
|
||||
discount["seq"] = 0
|
||||
|
||||
if discount_amount < 0:
|
||||
discount["name"] = _('GLOBAL DISCOUNT')
|
||||
else:
|
||||
discount["name"] = _('GLOBAL EXTRA CHARGE')
|
||||
discount["amount"] = discount_amount
|
||||
discount["tax"] = []
|
||||
discount_list.append(discount)
|
||||
|
||||
# Comment. <2.1.1.11>
|
||||
elements = body_tree.xpath('.//DatiGeneraliDocumento//Causale')
|
||||
for element in elements:
|
||||
invoice_form.narration = '%s%s\n' % (invoice_form.narration or '', element.text)
|
||||
|
||||
# Informations relative to the purchase order, the contract, the agreement,
|
||||
# the reception phase or invoices previously transmitted
|
||||
# <2.1.2> - <2.1.6>
|
||||
for document_type in ['DatiOrdineAcquisto', 'DatiContratto', 'DatiConvenzione', 'DatiRicezione', 'DatiFattureCollegate']:
|
||||
elements = body_tree.xpath('.//DatiGenerali/' + document_type)
|
||||
if elements:
|
||||
for element in elements:
|
||||
message_to_log.append("%s %s<br/>%s" % (document_type, _("from XML file:"),
|
||||
invoice._compose_info_message(element, '.')))
|
||||
|
||||
# Dati DDT. <2.1.8>
|
||||
elements = body_tree.xpath('.//DatiGenerali/DatiDDT')
|
||||
if elements:
|
||||
message_to_log.append("%s<br/>%s" % (
|
||||
_("Transport informations from XML file:"),
|
||||
invoice._compose_info_message(body_tree, './/DatiGenerali/DatiDDT')))
|
||||
|
||||
# Due date. <2.4.2.5>
|
||||
elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento/DataScadenzaPagamento')
|
||||
if elements:
|
||||
date_str = elements[0].text
|
||||
date_obj = datetime.strptime(date_str, DEFAULT_FACTUR_ITALIAN_DATE_FORMAT)
|
||||
invoice_form.invoice_date_due = fields.Date.to_string(date_obj)
|
||||
|
||||
# Total amount. <2.4.2.6>
|
||||
elements = body_tree.xpath('.//ImportoPagamento')
|
||||
amount_total_import = 0
|
||||
for element in elements:
|
||||
amount_total_import += float(element.text)
|
||||
if amount_total_import:
|
||||
message_to_log.append(_("Total amount from the XML File: %s") % (
|
||||
amount_total_import))
|
||||
|
||||
# Bank account. <2.4.2.13>
|
||||
if invoice_form.move_type not in ('out_invoice', 'in_refund'):
|
||||
elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento/IBAN')
|
||||
if elements:
|
||||
if invoice_form.partner_id and invoice_form.partner_id.commercial_partner_id:
|
||||
bank = self.env['res.partner.bank'].search([
|
||||
('acc_number', '=', elements[0].text),
|
||||
('partner_id.id', '=', invoice_form.partner_id.commercial_partner_id.id)
|
||||
])
|
||||
else:
|
||||
bank = self.env['res.partner.bank'].search([('acc_number', '=', elements[0].text)])
|
||||
if bank:
|
||||
invoice_form.partner_bank_id = bank
|
||||
else:
|
||||
message_to_log.append("%s<br/>%s" % (
|
||||
_("Bank account not found, useful informations from XML file:"),
|
||||
invoice._compose_multi_info_message(
|
||||
body_tree, ['.//DatiPagamento//Beneficiario',
|
||||
'.//DatiPagamento//IstitutoFinanziario',
|
||||
'.//DatiPagamento//IBAN',
|
||||
'.//DatiPagamento//ABI',
|
||||
'.//DatiPagamento//CAB',
|
||||
'.//DatiPagamento//BIC',
|
||||
'.//DatiPagamento//ModalitaPagamento'])))
|
||||
else:
|
||||
elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento')
|
||||
if elements:
|
||||
message_to_log.append("%s<br/>%s" % (
|
||||
_("Bank account not found, useful informations from XML file:"),
|
||||
self._compose_info_message(body_tree, './/DatiPagamento')))
|
||||
|
||||
# Invoice lines. <2.2.1>
|
||||
elements = body_tree.xpath('.//DettaglioLinee')
|
||||
if elements:
|
||||
for element in elements:
|
||||
with invoice_form.invoice_line_ids.new() as invoice_line_form:
|
||||
|
||||
# Sequence.
|
||||
line_elements = element.xpath('.//NumeroLinea')
|
||||
if line_elements:
|
||||
invoice_line_form.sequence = int(line_elements[0].text) * 2
|
||||
|
||||
# Product.
|
||||
line_elements = element.xpath('.//Descrizione')
|
||||
if line_elements:
|
||||
invoice_line_form.name = " ".join(line_elements[0].text.split())
|
||||
|
||||
elements_code = element.xpath('.//CodiceArticolo')
|
||||
if elements_code:
|
||||
for element_code in elements_code:
|
||||
type_code = element_code.xpath('.//CodiceTipo')[0]
|
||||
code = element_code.xpath('.//CodiceValore')[0]
|
||||
if type_code.text == 'EAN':
|
||||
product = self.env['product.product'].search([('barcode', '=', code.text)])
|
||||
if product:
|
||||
invoice_line_form.product_id = product
|
||||
break
|
||||
if partner:
|
||||
product_supplier = self.env['product.supplierinfo'].search([('name', '=', partner.id), ('product_code', '=', code.text)])
|
||||
if product_supplier and product_supplier.product_id:
|
||||
invoice_line_form.product_id = product_supplier.product_id
|
||||
break
|
||||
if not invoice_line_form.product_id:
|
||||
for element_code in elements_code:
|
||||
code = element_code.xpath('.//CodiceValore')[0]
|
||||
product = self.env['product.product'].search([('default_code', '=', code.text)])
|
||||
if product:
|
||||
invoice_line_form.product_id = product
|
||||
break
|
||||
|
||||
# Price Unit.
|
||||
line_elements = element.xpath('.//PrezzoUnitario')
|
||||
if line_elements:
|
||||
invoice_line_form.price_unit = float(line_elements[0].text)
|
||||
|
||||
# Quantity.
|
||||
line_elements = element.xpath('.//Quantita')
|
||||
if line_elements:
|
||||
invoice_line_form.quantity = float(line_elements[0].text)
|
||||
else:
|
||||
invoice_line_form.quantity = 1
|
||||
|
||||
# Taxes
|
||||
tax_element = element.xpath('.//AliquotaIVA')
|
||||
natura_element = element.xpath('.//Natura')
|
||||
invoice_line_form.tax_ids.clear()
|
||||
if tax_element and tax_element[0].text:
|
||||
percentage = float(tax_element[0].text)
|
||||
if natura_element and natura_element[0].text:
|
||||
l10n_it_kind_exoneration = natura_element[0].text
|
||||
tax = self.env['account.tax'].search([
|
||||
('company_id', '=', invoice_form.company_id.id),
|
||||
('amount_type', '=', 'percent'),
|
||||
('type_tax_use', '=', 'purchase'),
|
||||
('amount', '=', percentage),
|
||||
('l10n_it_kind_exoneration', '=', l10n_it_kind_exoneration),
|
||||
], limit=1)
|
||||
else:
|
||||
tax = self.env['account.tax'].search([
|
||||
('company_id', '=', invoice_form.company_id.id),
|
||||
('amount_type', '=', 'percent'),
|
||||
('type_tax_use', '=', 'purchase'),
|
||||
('amount', '=', percentage),
|
||||
], limit=1)
|
||||
l10n_it_kind_exoneration = ''
|
||||
|
||||
if tax:
|
||||
invoice_line_form.tax_ids.add(tax)
|
||||
else:
|
||||
if l10n_it_kind_exoneration:
|
||||
message_to_log.append(_("Tax not found with percentage: %s and exoneration %s for the article: %s") % (
|
||||
percentage,
|
||||
l10n_it_kind_exoneration,
|
||||
invoice_line_form.name))
|
||||
else:
|
||||
message_to_log.append(_("Tax not found with percentage: %s for the article: %s") % (
|
||||
percentage,
|
||||
invoice_line_form.name))
|
||||
|
||||
# Discount in cascade mode.
|
||||
# if 3 discounts : -10% -50€ -20%
|
||||
# the result must be : (((price -10%)-50€) -20%)
|
||||
# Generic form : (((price -P1%)-A1€) -P2%)
|
||||
# It will be split in two parts: fix amount and pourcent amount
|
||||
# example: (((((price - A1€) -P2%) -A3€) -A4€) -P5€)
|
||||
# pourcent: 1-(1-P2)*(1-P5)
|
||||
# fix amount: A1*(1-P2)*(1-P5)+A3*(1-P5)+A4*(1-P5) (we must take account of all
|
||||
# percentage present after the fix amount)
|
||||
line_elements = element.xpath('.//ScontoMaggiorazione')
|
||||
total_discount_amount = 0.0
|
||||
total_discount_percentage = percentage_global_discount
|
||||
if line_elements:
|
||||
for line_element in line_elements:
|
||||
discount_line = line_element.xpath('.//Tipo')
|
||||
discount_sign = -1
|
||||
if discount_line and discount_line[0].text == 'SC':
|
||||
discount_sign = 1
|
||||
discount_percentage = line_element.xpath('.//Percentuale')
|
||||
if discount_percentage and discount_percentage[0].text:
|
||||
pourcentage_actual = 1 - float(discount_percentage[0].text)/100 * discount_sign
|
||||
total_discount_percentage *= pourcentage_actual
|
||||
total_discount_amount *= pourcentage_actual
|
||||
|
||||
discount_amount = line_element.xpath('.//Importo')
|
||||
if discount_amount and discount_amount[0].text:
|
||||
total_discount_amount += float(discount_amount[0].text) * discount_sign * -1
|
||||
|
||||
# Save amount discount.
|
||||
if total_discount_amount != 0:
|
||||
discount = {}
|
||||
discount["seq"] = invoice_line_form.sequence + 1
|
||||
|
||||
if total_discount_amount < 0:
|
||||
discount["name"] = _('DISCOUNT: ') + invoice_line_form.name
|
||||
else:
|
||||
discount["name"] = _('EXTRA CHARGE: ') + invoice_line_form.name
|
||||
discount["amount"] = total_discount_amount
|
||||
discount["tax"] = []
|
||||
for tax in invoice_line_form.tax_ids:
|
||||
discount["tax"].append(tax)
|
||||
discount_list.append(discount)
|
||||
invoice_line_form.discount = (1 - total_discount_percentage) * 100
|
||||
|
||||
# Apply amount discount.
|
||||
for discount in discount_list:
|
||||
with invoice_form.invoice_line_ids.new() as invoice_line_form_discount:
|
||||
invoice_line_form_discount.tax_ids.clear()
|
||||
invoice_line_form_discount.sequence = discount["seq"]
|
||||
invoice_line_form_discount.name = discount["name"]
|
||||
invoice_line_form_discount.price_unit = discount["amount"]
|
||||
|
||||
new_invoice = invoice_form.save()
|
||||
new_invoice.l10n_it_send_state = "other"
|
||||
|
||||
elements = body_tree.xpath('.//Allegati')
|
||||
if elements:
|
||||
for element in elements:
|
||||
name_attachment = element.xpath('.//NomeAttachment')[0].text
|
||||
attachment_64 = str.encode(element.xpath('.//Attachment')[0].text)
|
||||
attachment_64 = self.env['ir.attachment'].create({
|
||||
'name': name_attachment,
|
||||
'datas': attachment_64,
|
||||
'type': 'binary',
|
||||
})
|
||||
|
||||
# default_res_id is had to context to avoid facturx to import his content
|
||||
# no_new_invoice to prevent from looping on the message_post that would create a new invoice without it
|
||||
new_invoice.with_context(no_new_invoice=True, default_res_id=new_invoice.id).message_post(
|
||||
body=(_("Attachment from XML")),
|
||||
attachment_ids=[attachment_64.id]
|
||||
)
|
||||
|
||||
for message in message_to_log:
|
||||
new_invoice.message_post(body=message)
|
||||
|
||||
invoices += new_invoice
|
||||
return invoices
|
||||
@@ -43,9 +43,17 @@ class AccountMove(models.Model):
|
||||
|
||||
l10n_it_ddt_id = fields.Many2one('l10n_it.ddt', string='DDT', readonly=True, states={'draft': [('readonly', False)]}, copy=False)
|
||||
|
||||
l10n_it_einvoice_name = fields.Char(readonly=True, copy=False)
|
||||
l10n_it_einvoice_name = fields.Char(compute='_compute_l10n_it_einvoice')
|
||||
|
||||
l10n_it_einvoice_id = fields.Many2one('ir.attachment', string="Electronic invoice", copy=False)
|
||||
l10n_it_einvoice_id = fields.Many2one('ir.attachment', string="Electronic invoice", compute='_compute_l10n_it_einvoice')
|
||||
|
||||
@api.depends('edi_document_ids')
|
||||
def _compute_l10n_it_einvoice(self):
|
||||
fattura_pa = self.env.ref('l10n_it_edi.edi_fatturaPA')
|
||||
for invoice in self:
|
||||
einvoice = invoice.edi_document_ids.filtered(lambda d: d.edi_format_id == fattura_pa)
|
||||
invoice.l10n_it_einvoice_id = einvoice
|
||||
invoice.l10n_it_einvoice_name = einvoice.name
|
||||
|
||||
def post(self):
|
||||
# OVERRIDE
|
||||
@@ -158,17 +166,17 @@ class AccountMove(models.Model):
|
||||
'codice': invoice.company_id.l10n_it_codice_fiscale,
|
||||
'progressive_number': progressive_number.zfill(5),
|
||||
}
|
||||
invoice.l10n_it_einvoice_name = report_name
|
||||
|
||||
data = b"<?xml version='1.0' encoding='UTF-8'?>" + invoice._export_as_xml()
|
||||
description = _('Italian invoice: %s') % invoice.move_type
|
||||
invoice.l10n_it_einvoice_id = self.env['ir.attachment'].create({
|
||||
invoice.edi_document_ids = self.env['ir.attachment'].create({
|
||||
'name': report_name,
|
||||
'res_id': invoice.id,
|
||||
'res_model': invoice._name,
|
||||
'datas': base64.encodebytes(data),
|
||||
'description': description,
|
||||
'type': 'binary',
|
||||
'edi_format_id': self.env.ref('l10n_it_edi.edi_fatturaPA').id
|
||||
})
|
||||
|
||||
invoice.message_post(
|
||||
@@ -276,8 +284,8 @@ class AccountMove(models.Model):
|
||||
raise UserError(_("%s isn't in a right state. It must be in a 'Not yet send' or 'Invalid' state.") % (self.display_name))
|
||||
|
||||
message = self.env['mail.message'].create({
|
||||
'subject': _('Sending file: %s') % (self.l10n_it_einvoice_id.name),
|
||||
'body': _('Sending file: %s to ES: %s') % (self.l10n_it_einvoice_id.name, self.env.company.l10n_it_address_recipient_fatturapa),
|
||||
'subject': _('Sending file: %s') % (self.l10n_it_einvoice_name),
|
||||
'body': _('Sending file: %s to ES: %s') % (self.l10n_it_einvoice_name, self.env.company.l10n_it_address_recipient_fatturapa),
|
||||
'author_id': self.env.user.partner_id.id,
|
||||
'email_from': self.env.company.l10n_it_address_send_fatturapa,
|
||||
'reply_to': self.env.company.l10n_it_address_send_fatturapa,
|
||||
@@ -301,382 +309,6 @@ class AccountMove(models.Model):
|
||||
)
|
||||
self.l10n_it_send_state = 'invalid'
|
||||
|
||||
def _import_xml_invoice(self, tree):
|
||||
''' Extract invoice values from the E-Invoice xml tree passed as parameter.
|
||||
|
||||
:param content: The tree of the xml file.
|
||||
:return: A dictionary containing account.invoice values to create/update it.
|
||||
'''
|
||||
|
||||
invoices = self.env['account.move']
|
||||
multi = False
|
||||
|
||||
# possible to have multiple invoices in the case of an invoice batch, the batch itself is repeated for every invoice of the batch
|
||||
for body_tree in tree.xpath('//FatturaElettronicaBody'):
|
||||
if multi:
|
||||
# make sure all the iterations create a new invoice record (except the first which could have already created one)
|
||||
self = self.env['account.move']
|
||||
multi = True
|
||||
|
||||
elements = tree.xpath('//DatiGeneraliDocumento/TipoDocumento')
|
||||
if elements and elements[0].text and elements[0].text == 'TD01':
|
||||
self_ctx = self.with_context(default_move_type='in_invoice')
|
||||
elif elements and elements[0].text and elements[0].text == 'TD04':
|
||||
self_ctx = self.with_context(default_move_type='in_refund')
|
||||
else:
|
||||
_logger.info(_('Document type not managed: %s.') % (elements[0].text))
|
||||
|
||||
# type must be present in the context to get the right behavior of the _default_journal method (account.move).
|
||||
# journal_id must be present in the context to get the right behavior of the _default_account method (account.move.line).
|
||||
|
||||
elements = tree.xpath('//CessionarioCommittente//IdCodice')
|
||||
company = elements and self.env['res.company'].search([('vat', 'ilike', elements[0].text)], limit=1)
|
||||
if not company:
|
||||
elements = tree.xpath('//CessionarioCommittente//CodiceFiscale')
|
||||
company = elements and self.env['res.company'].search([('l10n_it_codice_fiscale', 'ilike', elements[0].text)], limit=1)
|
||||
|
||||
if company:
|
||||
self_ctx = self_ctx.with_context(company_id=company.id)
|
||||
else:
|
||||
company = self.env.company
|
||||
if elements:
|
||||
_logger.info(_('Company not found with codice fiscale: %s. The company\'s user is set by default.') % elements[0].text)
|
||||
else:
|
||||
_logger.info(_('Company not found. The company\'s user is set by default.'))
|
||||
|
||||
if not self.env.is_superuser():
|
||||
if self.env.company != company:
|
||||
raise UserError(_("You can only import invoice concern your current company: %s") % self.env.company.display_name)
|
||||
|
||||
# Refund type.
|
||||
# TD01 == invoice
|
||||
# TD02 == advance/down payment on invoice
|
||||
# TD03 == advance/down payment on fee
|
||||
# TD04 == credit note
|
||||
# TD05 == debit note
|
||||
# TD06 == fee
|
||||
elements = tree.xpath('//DatiGeneraliDocumento/TipoDocumento')
|
||||
if elements and elements[0].text and elements[0].text == 'TD01':
|
||||
type = 'in_invoice'
|
||||
elif elements and elements[0].text and elements[0].text == 'TD04':
|
||||
type = 'in_refund'
|
||||
# self could be a single record (editing) or be empty (new).
|
||||
with Form(self.with_context(default_move_type=type)) as invoice_form:
|
||||
message_to_log = []
|
||||
|
||||
# Partner (first step to avoid warning 'Warning! You must first select a partner.'). <1.2>
|
||||
elements = tree.xpath('//CedentePrestatore//IdCodice')
|
||||
partner = elements and self.env['res.partner'].search(['&', ('vat', 'ilike', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1)
|
||||
if not partner:
|
||||
elements = tree.xpath('//CedentePrestatore//CodiceFiscale')
|
||||
partner = elements and self.env['res.partner'].search(['&', ('l10n_it_codice_fiscale', '=', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1)
|
||||
if not partner:
|
||||
elements = tree.xpath('//DatiTrasmissione//Email')
|
||||
partner = elements and self.env['res.partner'].search(['&', '|', ('email', '=', elements[0].text), ('l10n_it_pec_email', '=', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False)], limit=1)
|
||||
if partner:
|
||||
invoice_form.partner_id = partner
|
||||
else:
|
||||
message_to_log.append("%s<br/>%s" % (
|
||||
_("Vendor not found, useful informations from XML file:"),
|
||||
self._compose_info_message(
|
||||
tree, './/CedentePrestatore')))
|
||||
|
||||
# Numbering attributed by the transmitter. <1.1.2>
|
||||
elements = tree.xpath('//ProgressivoInvio')
|
||||
if elements:
|
||||
invoice_form.payment_reference = elements[0].text
|
||||
|
||||
elements = body_tree.xpath('.//DatiGeneraliDocumento//Numero')
|
||||
if elements:
|
||||
invoice_form.ref = elements[0].text
|
||||
|
||||
# Currency. <2.1.1.2>
|
||||
elements = body_tree.xpath('.//DatiGeneraliDocumento/Divisa')
|
||||
if elements:
|
||||
currency_str = elements[0].text
|
||||
currency = self.env.ref('base.%s' % currency_str.upper(), raise_if_not_found=False)
|
||||
if currency != self.env.company.currency_id and currency.active:
|
||||
invoice_form.currency_id = currency
|
||||
|
||||
# Date. <2.1.1.3>
|
||||
elements = body_tree.xpath('.//DatiGeneraliDocumento/Data')
|
||||
if elements:
|
||||
date_str = elements[0].text
|
||||
date_obj = datetime.strptime(date_str, DEFAULT_FACTUR_ITALIAN_DATE_FORMAT)
|
||||
invoice_form.invoice_date = date_obj.strftime(DEFAULT_FACTUR_ITALIAN_DATE_FORMAT)
|
||||
|
||||
# Dati Bollo. <2.1.1.6>
|
||||
elements = body_tree.xpath('.//DatiGeneraliDocumento/DatiBollo/ImportoBollo')
|
||||
if elements:
|
||||
invoice_form.l10n_it_stamp_duty = float(elements[0].text)
|
||||
|
||||
# List of all amount discount (will be add after all article to avoid to have a negative sum)
|
||||
discount_list = []
|
||||
percentage_global_discount = 1.0
|
||||
|
||||
# Global discount. <2.1.1.8>
|
||||
discount_elements = body_tree.xpath('.//DatiGeneraliDocumento/ScontoMaggiorazione')
|
||||
total_discount_amount = 0.0
|
||||
if discount_elements:
|
||||
for discount_element in discount_elements:
|
||||
discount_line = discount_element.xpath('.//Tipo')
|
||||
discount_sign = -1
|
||||
if discount_line and discount_line[0].text == 'SC':
|
||||
discount_sign = 1
|
||||
discount_percentage = discount_element.xpath('.//Percentuale')
|
||||
if discount_percentage and discount_percentage[0].text:
|
||||
percentage_global_discount *= 1 - float(discount_percentage[0].text)/100 * discount_sign
|
||||
|
||||
discount_amount_text = discount_element.xpath('.//Importo')
|
||||
if discount_amount_text and discount_amount_text[0].text:
|
||||
discount_amount = float(discount_amount_text[0].text) * discount_sign * -1
|
||||
discount = {}
|
||||
discount["seq"] = 0
|
||||
|
||||
if discount_amount < 0:
|
||||
discount["name"] = _('GLOBAL DISCOUNT')
|
||||
else:
|
||||
discount["name"] = _('GLOBAL EXTRA CHARGE')
|
||||
discount["amount"] = discount_amount
|
||||
discount["tax"] = []
|
||||
discount_list.append(discount)
|
||||
|
||||
# Comment. <2.1.1.11>
|
||||
elements = body_tree.xpath('.//DatiGeneraliDocumento//Causale')
|
||||
for element in elements:
|
||||
invoice_form.narration = '%s%s\n' % (invoice_form.narration or '', element.text)
|
||||
|
||||
# Informations relative to the purchase order, the contract, the agreement,
|
||||
# the reception phase or invoices previously transmitted
|
||||
# <2.1.2> - <2.1.6>
|
||||
for document_type in ['DatiOrdineAcquisto', 'DatiContratto', 'DatiConvenzione', 'DatiRicezione', 'DatiFattureCollegate']:
|
||||
elements = body_tree.xpath('.//DatiGenerali/' + document_type)
|
||||
if elements:
|
||||
for element in elements:
|
||||
message_to_log.append("%s %s<br/>%s" % (document_type, _("from XML file:"),
|
||||
self._compose_info_message(element, '.')))
|
||||
|
||||
# Dati DDT. <2.1.8>
|
||||
elements = body_tree.xpath('.//DatiGenerali/DatiDDT')
|
||||
if elements:
|
||||
message_to_log.append("%s<br/>%s" % (
|
||||
_("Transport informations from XML file:"),
|
||||
self._compose_info_message(body_tree, './/DatiGenerali/DatiDDT')))
|
||||
|
||||
# Due date. <2.4.2.5>
|
||||
elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento/DataScadenzaPagamento')
|
||||
if elements:
|
||||
date_str = elements[0].text
|
||||
date_obj = datetime.strptime(date_str, DEFAULT_FACTUR_ITALIAN_DATE_FORMAT)
|
||||
invoice_form.invoice_date_due = fields.Date.to_string(date_obj)
|
||||
|
||||
# Total amount. <2.4.2.6>
|
||||
elements = body_tree.xpath('.//ImportoPagamento')
|
||||
amount_total_import = 0
|
||||
for element in elements:
|
||||
amount_total_import += float(element.text)
|
||||
if amount_total_import:
|
||||
message_to_log.append(_("Total amount from the XML File: %s") % (
|
||||
amount_total_import))
|
||||
|
||||
# Bank account. <2.4.2.13>
|
||||
if invoice_form.type not in ('out_invoice', 'in_refund'):
|
||||
elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento/IBAN')
|
||||
if elements:
|
||||
if invoice_form.partner_id and invoice_form.partner_id.commercial_partner_id:
|
||||
bank = self.env['res.partner.bank'].search([
|
||||
('acc_number', '=', elements[0].text),
|
||||
('partner_id.id', '=', invoice_form.partner_id.commercial_partner_id.id)
|
||||
])
|
||||
else:
|
||||
bank = self.env['res.partner.bank'].search([('acc_number', '=', elements[0].text)])
|
||||
if bank:
|
||||
invoice_form.partner_bank_id = bank
|
||||
else:
|
||||
message_to_log.append("%s<br/>%s" % (
|
||||
_("Bank account not found, useful informations from XML file:"),
|
||||
self._compose_multi_info_message(
|
||||
body_tree, ['.//DatiPagamento//Beneficiario',
|
||||
'.//DatiPagamento//IstitutoFinanziario',
|
||||
'.//DatiPagamento//IBAN',
|
||||
'.//DatiPagamento//ABI',
|
||||
'.//DatiPagamento//CAB',
|
||||
'.//DatiPagamento//BIC',
|
||||
'.//DatiPagamento//ModalitaPagamento'])))
|
||||
else:
|
||||
elements = body_tree.xpath('.//DatiPagamento/DettaglioPagamento')
|
||||
if elements:
|
||||
message_to_log.append("%s<br/>%s" % (
|
||||
_("Bank account not found, useful informations from XML file:"),
|
||||
self._compose_info_message(body_tree, './/DatiPagamento')))
|
||||
|
||||
# Invoice lines. <2.2.1>
|
||||
elements = body_tree.xpath('.//DettaglioLinee')
|
||||
if elements:
|
||||
for element in elements:
|
||||
with invoice_form.invoice_line_ids.new() as invoice_line_form:
|
||||
|
||||
# Sequence.
|
||||
line_elements = element.xpath('.//NumeroLinea')
|
||||
if line_elements:
|
||||
invoice_line_form.sequence = int(line_elements[0].text) * 2
|
||||
|
||||
# Product.
|
||||
line_elements = element.xpath('.//Descrizione')
|
||||
if line_elements:
|
||||
invoice_line_form.name = " ".join(line_elements[0].text.split())
|
||||
|
||||
elements_code = element.xpath('.//CodiceArticolo')
|
||||
if elements_code:
|
||||
for element_code in elements_code:
|
||||
type_code = element_code.xpath('.//CodiceTipo')[0]
|
||||
code = element_code.xpath('.//CodiceValore')[0]
|
||||
if type_code.text == 'EAN':
|
||||
product = self.env['product.product'].search([('barcode', '=', code.text)])
|
||||
if product:
|
||||
invoice_line_form.product_id = product
|
||||
break
|
||||
if partner:
|
||||
product_supplier = self.env['product.supplierinfo'].search([('name', '=', partner.id), ('product_code', '=', code.text)])
|
||||
if product_supplier and product_supplier.product_id:
|
||||
invoice_line_form.product_id = product_supplier.product_id
|
||||
break
|
||||
if not invoice_line_form.product_id:
|
||||
for element_code in elements_code:
|
||||
code = element_code.xpath('.//CodiceValore')[0]
|
||||
product = self.env['product.product'].search([('default_code', '=', code.text)])
|
||||
if product:
|
||||
invoice_line_form.product_id = product
|
||||
break
|
||||
|
||||
# Price Unit.
|
||||
line_elements = element.xpath('.//PrezzoUnitario')
|
||||
if line_elements:
|
||||
invoice_line_form.price_unit = float(line_elements[0].text)
|
||||
|
||||
# Quantity.
|
||||
line_elements = element.xpath('.//Quantita')
|
||||
if line_elements:
|
||||
invoice_line_form.quantity = float(line_elements[0].text)
|
||||
else:
|
||||
invoice_line_form.quantity = 1
|
||||
|
||||
# Taxes
|
||||
tax_element = element.xpath('.//AliquotaIVA')
|
||||
natura_element = element.xpath('.//Natura')
|
||||
invoice_line_form.tax_ids.clear()
|
||||
if tax_element and tax_element[0].text:
|
||||
percentage = float(tax_element[0].text)
|
||||
if natura_element and natura_element[0].text:
|
||||
l10n_it_kind_exoneration = natura_element[0].text
|
||||
tax = self.env['account.tax'].search([
|
||||
('company_id', '=', invoice_form.company_id.id),
|
||||
('amount_type', '=', 'percent'),
|
||||
('type_tax_use', '=', 'purchase'),
|
||||
('amount', '=', percentage),
|
||||
('l10n_it_kind_exoneration', '=', l10n_it_kind_exoneration),
|
||||
], limit=1)
|
||||
else:
|
||||
tax = self.env['account.tax'].search([
|
||||
('company_id', '=', invoice_form.company_id.id),
|
||||
('amount_type', '=', 'percent'),
|
||||
('type_tax_use', '=', 'purchase'),
|
||||
('amount', '=', percentage),
|
||||
], limit=1)
|
||||
l10n_it_kind_exoneration = ''
|
||||
|
||||
if tax:
|
||||
invoice_line_form.tax_ids.add(tax)
|
||||
else:
|
||||
if l10n_it_kind_exoneration:
|
||||
message_to_log.append(_("Tax not found with percentage: %s and exoneration %s for the article: %s") % (
|
||||
percentage,
|
||||
l10n_it_kind_exoneration,
|
||||
invoice_line_form.name))
|
||||
else:
|
||||
message_to_log.append(_("Tax not found with percentage: %s for the article: %s") % (
|
||||
percentage,
|
||||
invoice_line_form.name))
|
||||
|
||||
# Discount in cascade mode.
|
||||
# if 3 discounts : -10% -50€ -20%
|
||||
# the result must be : (((price -10%)-50€) -20%)
|
||||
# Generic form : (((price -P1%)-A1€) -P2%)
|
||||
# It will be split in two parts: fix amount and pourcent amount
|
||||
# example: (((((price - A1€) -P2%) -A3€) -A4€) -P5€)
|
||||
# pourcent: 1-(1-P2)*(1-P5)
|
||||
# fix amount: A1*(1-P2)*(1-P5)+A3*(1-P5)+A4*(1-P5) (we must take account of all
|
||||
# percentage present after the fix amount)
|
||||
line_elements = element.xpath('.//ScontoMaggiorazione')
|
||||
total_discount_amount = 0.0
|
||||
total_discount_percentage = percentage_global_discount
|
||||
if line_elements:
|
||||
for line_element in line_elements:
|
||||
discount_line = line_element.xpath('.//Tipo')
|
||||
discount_sign = -1
|
||||
if discount_line and discount_line[0].text == 'SC':
|
||||
discount_sign = 1
|
||||
discount_percentage = line_element.xpath('.//Percentuale')
|
||||
if discount_percentage and discount_percentage[0].text:
|
||||
pourcentage_actual = 1 - float(discount_percentage[0].text)/100 * discount_sign
|
||||
total_discount_percentage *= pourcentage_actual
|
||||
total_discount_amount *= pourcentage_actual
|
||||
|
||||
discount_amount = line_element.xpath('.//Importo')
|
||||
if discount_amount and discount_amount[0].text:
|
||||
total_discount_amount += float(discount_amount[0].text) * discount_sign * -1
|
||||
|
||||
# Save amount discount.
|
||||
if total_discount_amount != 0:
|
||||
discount = {}
|
||||
discount["seq"] = invoice_line_form.sequence + 1
|
||||
|
||||
if total_discount_amount < 0:
|
||||
discount["name"] = _('DISCOUNT: ') + invoice_line_form.name
|
||||
else:
|
||||
discount["name"] = _('EXTRA CHARGE: ') + invoice_line_form.name
|
||||
discount["amount"] = total_discount_amount
|
||||
discount["tax"] = []
|
||||
for tax in invoice_line_form.tax_ids:
|
||||
discount["tax"].append(tax)
|
||||
discount_list.append(discount)
|
||||
invoice_line_form.discount = (1 - total_discount_percentage) * 100
|
||||
|
||||
# Apply amount discount.
|
||||
for discount in discount_list:
|
||||
with invoice_form.invoice_line_ids.new() as invoice_line_form_discount:
|
||||
invoice_line_form_discount.tax_ids.clear()
|
||||
invoice_line_form_discount.sequence = discount["seq"]
|
||||
invoice_line_form_discount.name = discount["name"]
|
||||
invoice_line_form_discount.price_unit = discount["amount"]
|
||||
|
||||
new_invoice = invoice_form.save()
|
||||
new_invoice.l10n_it_send_state = "other"
|
||||
|
||||
elements = body_tree.xpath('.//Allegati')
|
||||
if elements:
|
||||
for element in elements:
|
||||
name_attachment = element.xpath('.//NomeAttachment')[0].text
|
||||
attachment_64 = str.encode(element.xpath('.//Attachment')[0].text)
|
||||
attachment_64 = self.env['ir.attachment'].create({
|
||||
'name': name_attachment,
|
||||
'datas': attachment_64,
|
||||
'type': 'binary',
|
||||
})
|
||||
|
||||
# default_res_id is had to context to avoid facturx to import his content
|
||||
# no_new_invoice to prevent from looping on the message_post that would create a new invoice without it
|
||||
new_invoice.with_context(no_new_invoice=True, default_res_id=new_invoice.id).message_post(
|
||||
body=(_("Attachment from XML")),
|
||||
attachment_ids=[attachment_64.id]
|
||||
)
|
||||
|
||||
for message in message_to_log:
|
||||
new_invoice.message_post(body=message)
|
||||
|
||||
invoices += new_invoice
|
||||
return invoices
|
||||
|
||||
def _compose_info_message(self, tree, element_tags):
|
||||
output_str = ""
|
||||
elements = tree.xpath(element_tags)
|
||||
@@ -703,20 +335,6 @@ class AccountMove(models.Model):
|
||||
output_str += "<li>%s: %s</li>" % (element.tag, text)
|
||||
return output_str + "</ul>"
|
||||
|
||||
@api.model
|
||||
def _get_xml_decoders(self):
|
||||
# Override
|
||||
ubl_decoders = [('Italy', self._detect_italy_edi, self._import_xml_invoice)]
|
||||
return super(AccountMove, self)._get_xml_decoders() + ubl_decoders
|
||||
|
||||
@api.model
|
||||
def _detect_italy_edi(self, tree, file_name):
|
||||
# Quick check the file name looks like the one of an Italian EDI XML.
|
||||
flag = re.search("([A-Z]{2}[A-Za-z0-9]{2,28}_[A-Za-z0-9]{0,5}.(xml.p7m|xml))", file_name)
|
||||
error = None
|
||||
|
||||
return {'flag': flag, 'error': error}
|
||||
|
||||
class AccountTax(models.Model):
|
||||
_name = "account.tax"
|
||||
_inherit = "account.tax"
|
||||
|
||||
@@ -140,7 +140,7 @@ class FetchmailServer(models.Model):
|
||||
except Exception:
|
||||
raise UserError(_('The xml file is badly formatted : {}').format(att_name))
|
||||
|
||||
invoice = self.env['account.move']._import_xml_invoice(tree)
|
||||
invoice = self.env.ref('l10n_it_edi.edi_fatturaPA')._create_invoice_from_xml_tree(att_name, tree)
|
||||
invoice.l10n_it_send_state = "new"
|
||||
invoice.source_email = from_address
|
||||
self._cr.commit()
|
||||
|
||||
+84
-8
@@ -1,25 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import io
|
||||
import PyPDF2
|
||||
from PyPDF2 import PdfFileWriter, PdfFileReader
|
||||
from PyPDF2.generic import DictionaryObject, DecodedStreamObject, NameObject, createStringObject, ArrayObject
|
||||
from PyPDF2.utils import b_
|
||||
from datetime import datetime
|
||||
|
||||
class BrandedFileWriter(PyPDF2.PdfFileWriter):
|
||||
import io
|
||||
import hashlib
|
||||
|
||||
|
||||
DEFAULT_PDF_DATETIME_FORMAT = "D:%Y%m%d%H%M%S+00'00'"
|
||||
|
||||
|
||||
class BrandedFileWriter(PdfFileWriter):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.addMetadata({
|
||||
'/Creator': "Odoo",
|
||||
'/Producer': "Odoo",
|
||||
})
|
||||
PyPDF2.PdfFileWriter = BrandedFileWriter
|
||||
|
||||
|
||||
PdfFileWriter = BrandedFileWriter
|
||||
|
||||
|
||||
def merge_pdf(pdf_data):
|
||||
''' Merge a collection of PDF documents in one
|
||||
:param list pdf_data: a list of PDF datastrings
|
||||
:return: a unique merged PDF datastring
|
||||
'''
|
||||
writer = PyPDF2.PdfFileWriter()
|
||||
writer = PdfFileWriter()
|
||||
for document in pdf_data:
|
||||
reader = PyPDF2.PdfFileReader(io.BytesIO(document), strict=False)
|
||||
reader = PdfFileReader(io.BytesIO(document), strict=False)
|
||||
for page in range(0, reader.getNumPages()):
|
||||
writer.addPage(reader.getPage(page))
|
||||
with io.BytesIO() as _buffer:
|
||||
@@ -32,8 +44,8 @@ def rotate_pdf(pdf):
|
||||
:param pdf: a PDF to rotate
|
||||
:return: a PDF rotated
|
||||
'''
|
||||
writer = PyPDF2.PdfFileWriter()
|
||||
reader = PyPDF2.PdfFileReader(io.BytesIO(pdf), strict=False)
|
||||
writer = PdfFileWriter()
|
||||
reader = PdfFileReader(io.BytesIO(pdf), strict=False)
|
||||
for page in range(0, reader.getNumPages()):
|
||||
page = reader.getPage(page)
|
||||
page.rotateClockwise(90)
|
||||
@@ -41,3 +53,67 @@ def rotate_pdf(pdf):
|
||||
with io.BytesIO() as _buffer:
|
||||
writer.write(_buffer)
|
||||
return _buffer.getvalue()
|
||||
|
||||
|
||||
class OdooPdfFileReader(PdfFileReader):
|
||||
# OVERRIDE of PdfFileReader to add the management of multiple embedded files.
|
||||
|
||||
def getAttachments(self):
|
||||
if "/Names" not in self.trailer["/Root"]:
|
||||
return []
|
||||
for i in range(0, len(self.trailer["/Root"]["/Names"]["/EmbeddedFiles"]["/Names"]), 2):
|
||||
attachment = self.trailer["/Root"]["/Names"]["/EmbeddedFiles"]["/Names"][i+1].getObject()
|
||||
yield (attachment["/F"], attachment["/EF"]["/F"].getObject().getData())
|
||||
|
||||
|
||||
class OdooPdfFileWriter(PdfFileWriter):
|
||||
# OVERRIDE of PdfFileWriter to add the management of multiple embedded files.
|
||||
|
||||
def _create_attachment_object(self, attachment):
|
||||
''' Create a PyPdf2.generic object representing an embedded file.
|
||||
|
||||
:param attachment: A dictionary containing:
|
||||
* filename: The name of the file to embed (require).
|
||||
* content: The content of the file encoded in base64 (require).
|
||||
:return:
|
||||
'''
|
||||
file_entry = DecodedStreamObject()
|
||||
file_entry.setData(attachment['content'])
|
||||
file_entry.update({
|
||||
NameObject("/Type"): NameObject("/EmbeddedFile"),
|
||||
NameObject("/Params"):
|
||||
DictionaryObject({
|
||||
NameObject('/CheckSum'): createStringObject(hashlib.md5(attachment['content']).hexdigest()),
|
||||
NameObject('/ModDate'): createStringObject(datetime.now().strftime(DEFAULT_PDF_DATETIME_FORMAT)),
|
||||
NameObject('/Size'): NameObject(str(len(attachment['content']))),
|
||||
}),
|
||||
})
|
||||
if attachment.get('subtype'):
|
||||
file_entry.update({
|
||||
NameObject("/Subtype"): NameObject(attachment['subtype']),
|
||||
})
|
||||
file_entry_object = self._addObject(file_entry)
|
||||
filename_object = createStringObject(attachment['filename'])
|
||||
filespec_object = DictionaryObject({
|
||||
NameObject("/AFRelationship"): NameObject("/Data"),
|
||||
NameObject("/Type"): NameObject("/Filespec"),
|
||||
NameObject("/F"): filename_object,
|
||||
NameObject("/EF"):
|
||||
DictionaryObject({
|
||||
NameObject("/F"): file_entry_object,
|
||||
NameObject('/UF'): file_entry_object,
|
||||
}),
|
||||
NameObject("/UF"): filename_object,
|
||||
})
|
||||
if attachment.get('description'):
|
||||
filespec_object.update({NameObject("/Desc"): createStringObject(attachment['description'])})
|
||||
return self._addObject(filespec_object)
|
||||
|
||||
def addAttachment(self, fname, fdata):
|
||||
# OVERRIDE of the AddAttachment method to allow appending attachemnts when some already exist
|
||||
if self._root_object.get('/Names') and self._root_object['/Names'].get('/EmbeddedFiles'):
|
||||
attachments = self._root_object["/Names"]["/EmbeddedFiles"]["/Names"]
|
||||
new_attachment = self._create_attachment_object({'filename': fname, 'content': fdata})
|
||||
attachments.extend([new_attachment.getObject()['/F'], new_attachment])
|
||||
else:
|
||||
super().addAttachment(fname, fdata)
|
||||
|
||||
Reference in New Issue
Block a user