diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/l10n_it_edi/models/account_invoice.py | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/l10n_it_edi/models/account_invoice.py')
| -rw-r--r-- | addons/l10n_it_edi/models/account_invoice.py | 317 |
1 files changed, 317 insertions, 0 deletions
diff --git a/addons/l10n_it_edi/models/account_invoice.py b/addons/l10n_it_edi/models/account_invoice.py new file mode 100644 index 00000000..df578e5d --- /dev/null +++ b/addons/l10n_it_edi/models/account_invoice.py @@ -0,0 +1,317 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 +import zipfile +import io +import logging +import re + +from datetime import date, datetime +from lxml import etree + +from odoo import api, fields, models, _ +from odoo.tools import float_repr +from odoo.exceptions import UserError, ValidationError +from odoo.addons.base.models.ir_mail_server import MailDeliveryException +from odoo.tests.common import Form + + +_logger = logging.getLogger(__name__) + +DEFAULT_FACTUR_ITALIAN_DATE_FORMAT = '%Y-%m-%d' + + +class AccountMove(models.Model): + _inherit = 'account.move' + + l10n_it_send_state = fields.Selection([ + ('new', 'New'), + ('other', 'Other'), + ('to_send', 'Not yet send'), + ('sent', 'Sent, waiting for response'), + ('invalid', 'Sent, but invalid'), + ('delivered', 'This invoice is delivered'), + ('delivered_accepted', 'This invoice is delivered and accepted by destinatory'), + ('delivered_refused', 'This invoice is delivered and refused by destinatory'), + ('delivered_expired', 'This invoice is delivered and expired (expiry of the maximum term for communication of acceptance/refusal)'), + ('failed_delivery', 'Delivery impossible, ES certify that it has received the invoice and that the file \ + could not be delivered to the addressee') # ok we must do nothing + ], default='to_send', copy=False) + + l10n_it_stamp_duty = fields.Float(default=0, string="Dati Bollo", readonly=True, states={'draft': [('readonly', False)]}) + + 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(compute='_compute_l10n_it_einvoice') + + l10n_it_einvoice_id = fields.Many2one('ir.attachment', string="Electronic invoice", compute='_compute_l10n_it_einvoice') + + @api.depends('edi_document_ids', 'edi_document_ids.attachment_id') + 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.attachment_id + invoice.l10n_it_einvoice_name = einvoice.attachment_id.name + + def _check_before_xml_exporting(self): + # DEPRECATED use AccountEdiFormat._l10n_it_edi_check_invoice_configuration instead + errors = self.env['account.edi.format']._l10n_it_edi_check_invoice_configuration(self) + if errors: + raise UserError(self.env['account.edi.format']._format_error_message(_("Invalid configuration:"), errors)) + + def invoice_generate_xml(self): + self.ensure_one() + report_name = self.env['account.edi.format']._l10n_it_edi_generate_electronic_invoice_filename(self) + + data = b"<?xml version='1.0' encoding='UTF-8'?>" + self._export_as_xml() + description = _('Italian invoice: %s', self.move_type) + attachment = self.env['ir.attachment'].create({ + 'name': report_name, + 'res_id': self.id, + 'res_model': self._name, + 'datas': base64.encodebytes(data), + 'description': description, + 'type': 'binary', + }) + + self.message_post( + body=(_("E-Invoice is generated on %s by %s") % (fields.Datetime.now(), self.env.user.display_name)) + ) + return {'attachment': attachment} + + def _prepare_fatturapa_export_values(self): + self.ensure_one() + + def format_date(dt): + # Format the date in the italian standard. + dt = dt or datetime.now() + return dt.strftime(DEFAULT_FACTUR_ITALIAN_DATE_FORMAT) + + def format_monetary(number, currency): + # Format the monetary values to avoid trailing decimals (e.g. 90.85000000000001). + return float_repr(number, min(2, currency.decimal_places)) + + def format_numbers(number): + #format number to str with between 2 and 8 decimals (event if it's .00) + number_splited = str(number).split('.') + if len(number_splited) == 1: + return "%.02f" % number + + cents = number_splited[1] + if len(cents) > 8: + return "%.08f" % number + return float_repr(number, max(2, len(cents))) + + def format_numbers_two(number): + #format number to str with 2 (event if it's .00) + return "%.02f" % number + + def discount_type(discount): + return 'SC' if discount > 0 else 'MG' + + def format_phone(number): + if not number: + return False + number = number.replace(' ', '').replace('/', '').replace('.', '') + if len(number) > 4 and len(number) < 13: + return number + return False + + def get_vat_number(vat): + return vat[2:].replace(' ', '') + + def get_vat_country(vat): + return vat[:2].upper() + + def in_eu(partner): + europe = self.env.ref('base.europe', raise_if_not_found=False) + country = partner.country_id + if not europe or not country or country in europe.country_ids: + return True + return False + + formato_trasmissione = "FPR12" + if len(self.commercial_partner_id.l10n_it_pa_index or '1') == 6: + formato_trasmissione = "FPA12" + + if self.move_type == 'out_invoice': + document_type = 'TD01' + elif self.move_type == 'out_refund': + document_type = 'TD04' + else: + document_type = 'TD0X' + + pdf = self.env.ref('account.account_invoices')._render_qweb_pdf(self.id)[0] + pdf = base64.b64encode(pdf) + pdf_name = re.sub(r'\W+', '', self.name) + '.pdf' + + # tax map for 0% taxes which have no tax_line_id + tax_map = dict() + for line in self.line_ids: + for tax in line.tax_ids: + if tax.amount == 0.0: + tax_map[tax] = tax_map.get(tax, 0.0) + line.price_subtotal + + # Create file content. + template_values = { + 'record': self, + 'format_date': format_date, + 'format_monetary': format_monetary, + 'format_numbers': format_numbers, + 'format_numbers_two': format_numbers_two, + 'format_phone': format_phone, + 'discount_type': discount_type, + 'get_vat_number': get_vat_number, + 'get_vat_country': get_vat_country, + 'in_eu': in_eu, + 'abs': abs, + 'formato_trasmissione': formato_trasmissione, + 'document_type': document_type, + 'pdf': pdf, + 'pdf_name': pdf_name, + 'tax_map': tax_map, + } + return template_values + + def _export_as_xml(self): + '''DEPRECATED : this will be moved to AccountEdiFormat in a future version. + Create the xml file content. + :return: The XML content as str. + ''' + template_values = self._prepare_fatturapa_export_values() + content = self.env.ref('l10n_it_edi.account_invoice_it_FatturaPA_export')._render(template_values) + return content + + def _post(self, soft=True): + # OVERRIDE + posted = super()._post(soft=soft) + + for move in posted.filtered(lambda m: m.l10n_it_send_state == 'to_send' and m.move_type == 'out_invoice' and m.company_id.country_id.code == 'IT'): + move.send_pec_mail() + + return posted + + def send_pec_mail(self): + self.ensure_one() + allowed_state = ['to_send', 'invalid'] + + if ( + not self.company_id.l10n_it_mail_pec_server_id + or not self.company_id.l10n_it_mail_pec_server_id.active + or not self.company_id.l10n_it_address_send_fatturapa + ): + self.message_post( + body=(_("Error when sending mail with E-Invoice: Your company must have a mail PEC server and must indicate the mail PEC that will send electronic invoice.")) + ) + self.l10n_it_send_state = 'invalid' + return + + if self.l10n_it_send_state not in allowed_state: + 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_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, + 'mail_server_id': self.env.company.l10n_it_mail_pec_server_id.id, + 'attachment_ids': [(6, 0, self.l10n_it_einvoice_id.ids)], + }) + + mail_fattura = self.env['mail.mail'].sudo().with_context(wo_bounce_return_path=True).create({ + 'mail_message_id': message.id, + 'email_to': self.env.company.l10n_it_address_recipient_fatturapa, + }) + try: + mail_fattura.send(raise_exception=True) + self.message_post( + body=(_("Mail sent on %s by %s") % (fields.Datetime.now(), self.env.user.display_name)) + ) + self.l10n_it_send_state = 'sent' + except MailDeliveryException as error: + self.message_post( + body=(_("Error when sending mail with E-Invoice: %s") % (error.args[0])) + ) + self.l10n_it_send_state = 'invalid' + + def _compose_info_message(self, tree, element_tags): + output_str = "" + elements = tree.xpath(element_tags) + for element in elements: + output_str += "<ul>" + for line in element.iter(): + if line.text: + text = " ".join(line.text.split()) + if text: + output_str += "<li>%s: %s</li>" % (line.tag, text) + output_str += "</ul>" + return output_str + + def _compose_multi_info_message(self, tree, element_tags): + output_str = "<ul>" + + for element_tag in element_tags: + elements = tree.xpath(element_tag) + if not elements: + continue + for element in elements: + text = " ".join(element.text.split()) + if text: + output_str += "<li>%s: %s</li>" % (element.tag, text) + return output_str + "</ul>" + +class AccountTax(models.Model): + _name = "account.tax" + _inherit = "account.tax" + + l10n_it_vat_due_date = fields.Selection([ + ("I", "[I] IVA ad esigibilità immediata"), + ("D", "[D] IVA ad esigibilità differita"), + ("S", "[S] Scissione dei pagamenti")], default="I", string="VAT due date") + + l10n_it_has_exoneration = fields.Boolean(string="Has exoneration of tax (Italy)", help="Tax has a tax exoneration.") + l10n_it_kind_exoneration = fields.Selection(selection=[ + ("N1", "[N1] Escluse ex art. 15"), + ("N2", "[N2] Non soggette"), + ("N2.1", "[N2.1] Non soggette ad IVA ai sensi degli artt. Da 7 a 7-septies del DPR 633/72"), + ("N2.2", "[N2.2] Non soggette – altri casi"), + ("N3", "[N3] Non imponibili"), + ("N3.1", "[N3.1] Non imponibili – esportazioni"), + ("N3.2", "[N3.2] Non imponibili – cessioni intracomunitarie"), + ("N3.3", "[N3.3] Non imponibili – cessioni verso San Marino"), + ("N3.4", "[N3.4] Non imponibili – operazioni assimilate alle cessioni all’esportazione"), + ("N3.5", "[N3.5] Non imponibili – a seguito di dichiarazioni d’intento"), + ("N3.6", "[N3.6] Non imponibili – altre operazioni che non concorrono alla formazione del plafond"), + ("N4", "[N4] Esenti"), + ("N5", "[N5] Regime del margine / IVA non esposta in fattura"), + ("N6", "[N6] Inversione contabile (per le operazioni in reverse charge ovvero nei casi di autofatturazione per acquisti extra UE di servizi ovvero per importazioni di beni nei soli casi previsti)"), + ("N6.1", "[N6.1] Inversione contabile – cessione di rottami e altri materiali di recupero"), + ("N6.2", "[N6.2] Inversione contabile – cessione di oro e argento puro"), + ("N6.3", "[N6.3] Inversione contabile – subappalto nel settore edile"), + ("N6.4", "[N6.4] Inversione contabile – cessione di fabbricati"), + ("N6.5", "[N6.5] Inversione contabile – cessione di telefoni cellulari"), + ("N6.6", "[N6.6] Inversione contabile – cessione di prodotti elettronici"), + ("N6.7", "[N6.7] Inversione contabile – prestazioni comparto edile esettori connessi"), + ("N6.8", "[N6.8] Inversione contabile – operazioni settore energetico"), + ("N6.9", "[N6.9] Inversione contabile – altri casi"), + ("N7", "[N7] IVA assolta in altro stato UE (vendite a distanza ex art. 40 c. 3 e 4 e art. 41 c. 1 lett. b, DL 331/93; prestazione di servizi di telecomunicazioni, tele-radiodiffusione ed elettronici ex art. 7-sexies lett. f, g, art. 74-sexies DPR 633/72)")], + string="Exoneration", + help="Exoneration type", + default="N1") + l10n_it_law_reference = fields.Char(string="Law Reference", size=100) + + @api.constrains('l10n_it_has_exoneration', + 'l10n_it_kind_exoneration', + 'l10n_it_law_reference', + 'amount', + 'l10n_it_vat_due_date') + def _check_exoneration_with_no_tax(self): + for tax in self: + if tax.l10n_it_has_exoneration: + if not tax.l10n_it_kind_exoneration or not tax.l10n_it_law_reference or tax.amount != 0: + raise ValidationError(_("If the tax has exoneration, you must enter a kind of exoneration, a law reference and the amount of the tax must be 0.0.")) + if tax.l10n_it_kind_exoneration == 'N6' and tax.l10n_it_vat_due_date == 'S': + raise UserError(_("'Scissione dei pagamenti' is not compatible with exoneration of kind 'N6'")) |
