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:
Odoo's Mergebot
2020-05-29 14:30:57 +02:00
committed by GitHub
69 changed files with 1496 additions and 2057 deletions
+11 -3
View File
@@ -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
+24
View File
@@ -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,
}
+8
View File
@@ -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)
+55
View File
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 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>
+1
View File
@@ -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>
+1
View File
@@ -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>
@@ -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
@@ -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'})
+1
View File
@@ -0,0 +1 @@
from . import models
+13
View File
@@ -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
@@ -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 ""
-55
View File
@@ -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 ""
-55
View File
@@ -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 ""
-55
View File
@@ -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 ""
-60
View File
@@ -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"
-59
View File
@@ -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"
-59
View File
@@ -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"
-55
View File
@@ -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 ""
-55
View File
@@ -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 ""
-55
View File
@@ -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 ""
-55
View File
@@ -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"
-51
View File
@@ -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 ""
-55
View File
@@ -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 ""
-55
View File
@@ -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 ""
-55
View File
@@ -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 ""
-59
View File
@@ -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"
-55
View File
@@ -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 ""
-51
View File
@@ -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 ""
-55
View File
@@ -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 ""
-51
View File
@@ -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 ""
-55
View File
@@ -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 ""
-55
View File
@@ -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 ""
-58
View File
@@ -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"
-56
View File
@@ -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 ""
-56
View File
@@ -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 ""
-56
View File
@@ -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)
+2 -1
View File
@@ -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>
+3 -2
View File
@@ -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.
+16
View File
@@ -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
+3
View File
@@ -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)
+2
View File
@@ -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>
+1
View File
@@ -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
+14 -396
View File
@@ -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"
+1 -1
View File
@@ -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
View File
@@ -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)