# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import base64 import logging import re from collections import namedtuple from datetime import datetime from io import BytesIO from markupsafe import Markup from psycopg2 import OperationalError from lxml import etree from odoo import fields, models from odoo.addons.l10n_cl_edi.models.l10n_cl_edi_util import UnexpectedXMLResponse, InvalidToken from odoo.exceptions import UserError from odoo.tools.translate import _ from odoo.tools.float_utils import float_repr _logger = logging.getLogger(__name__) try: import pdf417gen except ImportError: pdf417gen = None _logger.error('Could not import library pdf417gen') class AccountMove(models.Model): _name = 'account.move' _inherit = ['l10n_cl.edi.util', 'account.move'] l10n_cl_sii_barcode = fields.Char( string='SII Barcode', readonly=True, copy=False, help='This XML contains the portion of the DTE XML that should be coded in PDF417 ' 'and printed in the invoice barcode should be present in the printed invoice report to be valid') l10n_cl_dte_status = fields.Selection([ ('not_sent', 'Pending To Be Sent'), ('ask_for_status', 'Ask For Status'), ('accepted', 'Accepted'), ('objected', 'Accepted With Objections'), ('rejected', 'Rejected'), ('cancelled', 'Cancelled'), ('manual', 'Manual'), ], string='SII DTE status', copy=False, tracking=True, help="""Status of sending the DTE to the SII: - Not sent: the DTE has not been sent to SII but it has created. - Ask For Status: The DTE is asking for its status to the SII. - Accepted: The DTE has been accepted by SII. - Accepted With Objections: The DTE has been accepted with objections by SII. - Rejected: The DTE has been rejected by SII. - Cancelled: The DTE has been deleted by the user. - Manual: The DTE is sent manually, i.e.: the DTE will not be sending manually.""") l10n_cl_dte_partner_status = fields.Selection([ ('not_sent', 'Not Sent'), ('sent', 'Sent'), ], string='Partner DTE status', copy=False, readonly=True, help=""" Status of sending the DTE to the partner: - Not sent: the DTE has not been sent to the partner but it has sent to SII. - Sent: The DTE has been sent to the partner.""") l10n_cl_dte_acceptation_status = fields.Selection([ ('received', 'Received'), ('ack_sent', 'Acknowledge Sent'), ('claimed', 'Claimed'), ('accepted', 'Accepted'), ('goods', 'Reception 19983'), ('accepted_goods', 'Accepted and RG 19983'), ], string='DTE Accept status', copy=False, help="""The status of the DTE Acceptation Received: the DTE was received by us for vendor bills, by our customers for customer invoices. Acknowledge Sent: the Acknowledge has been sent to the vendor. Claimed: the DTE was claimed by us for vendor bills, by our customers for customer invoices. Accepted: the DTE was accepted by us for vendor bills, by our customers for customer invoices. Reception 19983: means that the merchandise or services reception has been created and sent. Accepted and RG 19983: means that both the content of the document has been accepted and the merchandise or services reception has been received as well. """) l10n_cl_claim = fields.Selection([ ('ACD', 'Accept the Content of the Document'), ('RCD', 'Claim the Content of the Document'), ('ERM', 'Provide Receipt of Merchandise or Services'), ('RFP', 'Claim for Partial Lack of Merchandise'), ('RFT', 'Claim for Total Lack of Merchandise'), ('NCA', 'Reception of Cancellation that References Document'), ], string='Claim', copy=False, help='The reason why the DTE was accepted or claimed by the customer') l10n_cl_claim_description = fields.Char(string='Claim Detail', readonly=True, copy=False) l10n_cl_sii_send_file = fields.Many2one('ir.attachment', string='SII Send file', copy=False) l10n_cl_dte_file = fields.Many2one('ir.attachment', string='DTE file', copy=False) l10n_cl_sii_send_ident = fields.Text(string='SII Send Identification(Track ID)', copy=False, tracking=True) l10n_cl_journal_point_of_sale_type = fields.Selection(related='journal_id.l10n_cl_point_of_sale_type') l10n_cl_reference_ids = fields.One2many('l10n_cl.account.invoice.reference', 'move_id', string='Reference Records') def button_cancel(self): for record in self.filtered(lambda x: x.company_id.country_id.code == "CL"): # The move cannot be modified once the DTE has been accepted, objected or sent to the SII if record.l10n_cl_dte_status in ['accepted', 'objected']: l10n_cl_dte_status = dict(record._fields['l10n_cl_dte_status']._description_selection( self.env)).get(record.l10n_cl_dte_status) raise UserError(_('This %s is in SII status: %s. It cannot be cancelled. ' 'Instead you should revert it.') % ( record.l10n_latam_document_type_id.name, l10n_cl_dte_status)) elif record.l10n_cl_dte_status == 'ask_for_status': raise UserError(_('This %s is in the intermediate state: \'Ask for Status in the SII\'. ' 'You will be able to cancel it only when the document has reached the state ' 'of rejection. Otherwise, if it were accepted or objected you should revert it ' 'with a suitable document instead of cancelling it.') % record.l10n_latam_document_type_id.name) record.l10n_cl_dte_status = 'cancelled' return super().button_cancel() def button_draft(self): for record in self.filtered(lambda x: x.company_id.country_id.code == "CL"): # The move cannot be modified once the DTE has been accepted, objected or sent to the SII if record.l10n_cl_dte_status in ['accepted', 'objected']: l10n_cl_dte_status = dict(record._fields['l10n_cl_dte_status']._description_selection( self.env)).get(record.l10n_cl_dte_status) raise UserError(_('This %s is in SII status %s. It cannot be reset to draft state. ' 'Instead you should revert it.') % ( record.l10n_latam_document_type_id.name, l10n_cl_dte_status)) elif record.l10n_cl_dte_status == 'ask_for_status': raise UserError(_('This %s is in the intermediate state: \'Ask for Status in the SII\'. ' 'You will be able to reset it to draft only when the document has reached the state ' 'of rejection. Otherwise, if it were accepted or objected you should revert it ' 'with a suitable document instead of cancelling it.') % record.l10n_latam_document_type_id.name) record.l10n_cl_dte_status = None return super().button_draft() def _post(self, soft=True): res = super(AccountMove, self)._post(soft=soft) # Avoid to post a vendor bill with a inactive currency created from the incoming mail for move in self.filtered( lambda x: x.company_id.account_fiscal_country_id.code == "CL" and x.company_id.l10n_cl_dte_service_provider in ['SII', 'SIITEST', 'SIIDEMO'] and x.journal_id.l10n_latam_use_documents): msg_demo = _(' in DEMO mode.') if move.company_id.l10n_cl_dte_service_provider == 'SIIDEMO' else '.' # check if we have the currency active, in order to receive vendor bills correctly. if move.move_type in ['in_invoice', 'in_refund'] and not move.currency_id.active: raise UserError( _('Invoice %s has the currency %s inactive. Please activate the currency and try again.') % ( move.name, move.currency_id.name)) # generation of customer invoices if ((move.move_type in ['out_invoice', 'out_refund'] and move.journal_id.type == 'sale') or (move.move_type in ['in_invoice', 'in_refund'] and move.l10n_latam_document_type_id._is_doc_type_vendor())): if move.journal_id.l10n_cl_point_of_sale_type != 'online' and not move.l10n_latam_document_type_id._is_doc_type_vendor(): move.l10n_cl_dte_status = 'manual' continue move._l10n_cl_edi_post_validation() move._l10n_cl_create_dte() move.l10n_cl_dte_status = 'not_sent' dte_signed, file_name = move._l10n_cl_create_dte_envelope() attachment = self.env['ir.attachment'].create({ 'name': 'SII_{}'.format(file_name), 'res_id': move.id, 'res_model': 'account.move', 'datas': base64.b64encode(dte_signed.encode('ISO-8859-1', 'replace')), 'type': 'binary', }) move.l10n_cl_sii_send_file = attachment.id move.with_context(no_new_invoice=True).message_post( body=_('DTE has been created%s', msg_demo), attachment_ids=attachment.ids) return res def action_reverse(self): for record in self.filtered(lambda x: x.company_id.account_fiscal_country_id.code == "CL"): if record.l10n_cl_dte_status == 'rejected': raise UserError(_('This %s is rejected by SII. Instead of creating a reverse, you should set it to ' 'draft state, correct it and post it again.') % record.l10n_latam_document_type_id.name) return super().action_reverse() def _reverse_moves(self, default_values_list=None, cancel=False): reverse_moves = super(AccountMove, self)._reverse_moves(default_values_list, cancel) # The reverse move lines of the reverse moves created to correct the original text are replaced by # a line with the quantity to 1, the amount to 0 and the original text and correct text as the name # since sii regulations stipulate the option to use this kind of document with an amount_untaxed # and amount_total equal to $0.0 just in order to inform this is only a text correction. # for example, for a bad address or a bad activity description in the originating document. if self._context.get('default_l10n_cl_edi_reference_doc_code') == '2': for move in reverse_moves: move.invoice_line_ids = [[5, 0], [0, 0, { 'account_id': move.journal_id.default_account_id.id, 'name': _('Where it says: %s should say: %s') % ( self._context.get('default_l10n_cl_original_text'), self._context.get('default_l10n_cl_corrected_text')), 'quantity': 1, 'price_unit': 0.0, }, ], ] return reverse_moves def _compute_l10n_latam_document_type(self): """ Extension to ensure the document type is computed based on the partner's taxpayer type and allows the SII, the fiscal authority of Chile, to properly validate the record. """ taxpayer_moves = self.filtered(lambda m: m.partner_id.l10n_cl_sii_taxpayer_type in ['1', '3', '4']) grouped_doc_types = {doc_type.code: doc_type for doc_type in self.l10n_latam_available_document_type_ids._origin} for move in taxpayer_moves: taxpayer_type = move.partner_id.l10n_cl_sii_taxpayer_type if taxpayer_type == '1': move.l10n_latam_document_type_id = grouped_doc_types.get('33', False) elif taxpayer_type == '3': move.l10n_latam_document_type_id = grouped_doc_types.get('39', False) else: move.l10n_latam_document_type_id = grouped_doc_types.get('110', False) super(AccountMove, self - taxpayer_moves)._compute_l10n_latam_document_type() # SII Customer Invoice Buttons def l10n_cl_send_dte_to_sii(self, retry_send=True): """ Send the DTE to the SII. """ try: with self.env.cr.savepoint(flush=False): self.env.cr.execute('SELECT * FROM account_move WHERE id IN %s FOR UPDATE NOWAIT', [tuple(self.ids)]) except OperationalError as e: if e.pgcode == '55P03': if not self.env.context.get('cron_skip_connection_errs'): raise UserError(_('This invoice is being processed already.')) return raise e # To avoid double send on double-click if self.l10n_cl_dte_status != "not_sent": return None _logger.info('Sending DTE for invoice with ID %s (name: %s)', self.id, self.name) digital_signature_sudo = self.company_id.sudo()._get_digital_signature(user_id=self.env.user.id) if self.company_id.l10n_cl_dte_service_provider == 'SIIDEMO': self.message_post(body=_('This DTE has been generated in DEMO Mode. It is considered as accepted and ' 'it won\'t be sent to SII.')) self.l10n_cl_dte_status = 'accepted' return None response = self._send_xml_to_sii( self.company_id.l10n_cl_dte_service_provider, self.company_id.website, self.company_id.vat, self.l10n_cl_sii_send_file.name, base64.b64decode(self.l10n_cl_sii_send_file.datas), digital_signature_sudo ) if not response: return None response_parsed = etree.fromstring(response) self.l10n_cl_sii_send_ident = response_parsed.findtext('TRACKID') sii_response_status = response_parsed.findtext('STATUS') if sii_response_status == '5': digital_signature_sudo.last_token = False _logger.warning('The response status is %s. Clearing the token.', self._l10n_cl_get_sii_reception_status_message(sii_response_status)) if retry_send: _logger.info('Retrying send DTE to SII') self.l10n_cl_send_dte_to_sii(retry_send=False) # cleans the token and keeps the l10n_cl_dte_status until new attempt to connect # would like to resend from here, because we cannot wait till tomorrow to attempt # a new send else: self.l10n_cl_dte_status = 'ask_for_status' if sii_response_status == '0' else 'rejected' self.message_post(body=_('DTE has been sent to SII with response: %s.') % self._l10n_cl_get_sii_reception_status_message(sii_response_status)) def l10n_cl_verify_dte_status(self, send_dte_to_partner=True): digital_signature_sudo = self.company_id.sudo()._get_digital_signature(user_id=self.env.user.id) response = self._get_send_status( self.company_id.l10n_cl_dte_service_provider, self.l10n_cl_sii_send_ident, self._l10n_cl_format_vat(self.company_id.vat), digital_signature_sudo) if not response: self.l10n_cl_dte_status = 'ask_for_status' digital_signature_sudo.last_token = False return None response_parsed = etree.fromstring(response.encode('utf-8')) if response_parsed.findtext('{http://www.sii.cl/XMLSchema}RESP_HDR/ESTADO') in ['001', '002', '003']: digital_signature_sudo.last_token = False _logger.error('Token is invalid.') return try: self.l10n_cl_dte_status = self._analyze_sii_result(response_parsed) except UnexpectedXMLResponse: # The assumption here is that the unexpected input is intermittent, # so we'll retry later. If the same input appears regularly, it should # be handled properly in _analyze_sii_result. _logger.error("Unexpected XML response:\n%s", response) return if self.l10n_cl_dte_status in ['accepted', 'objected']: self.l10n_cl_dte_partner_status = 'not_sent' if send_dte_to_partner: self._l10n_cl_send_dte_to_partner() self.message_post( body=_('Asking for DTE status with response:') + Markup('
  • ESTADO: %s
  • GLOSA: %s
  • NUM_ATENCION: %s
  • ') % ( response_parsed.findtext('{http://www.sii.cl/XMLSchema}RESP_HDR/ESTADO'), response_parsed.findtext('{http://www.sii.cl/XMLSchema}RESP_HDR/GLOSA'), response_parsed.findtext('{http://www.sii.cl/XMLSchema}RESP_HDR/NUM_ATENCION'))) def l10n_cl_verify_claim_status(self): if self.company_id.l10n_cl_dte_service_provider in ['SIITEST', 'SIIDEMO']: raise UserError(_('This feature is not available in certification/test mode')) response = self._get_dte_claim( self.company_id.l10n_cl_dte_service_provider, self.company_id.vat, self.company_id.sudo()._get_digital_signature(user_id=self.env.user.id), self.l10n_latam_document_type_id.code, self.l10n_latam_document_number ) if not response: return None try: events = response['listaEventosDoc'] # listaEventosDoc can be either a list or a CompoundValue (which behaves like but isn't a dict) if not isinstance(events, list): events = [events] # listaEventosDoc can also be falsy and empty, for example if codResp is 16 and the client didn't accept or # claim the invoice yet. In that case we keep checking until it is and keep the response code unset. response_code = False # The purpose of this loop is to check each event has a `codEvento` value. If it doesn't something's wrong # and we log that as an error. We store the value of the last event in l10n_cl_claim, to mark the claim as # successfully parsed. It would make more sense to have it be a list instead of a single value, since a list # of events can be returned, but l10n_cl_claim was defined to only hold a single value and that's not # something we can change in stable. It's currently not used anywhere in the standard code except to check # if we already parsed the claim and if not to retrieve its status from the SII. for event in events: response_code = event['codEvento'] self.l10n_cl_claim = response_code except Exception as error: _logger.error(error) if not self.env.context.get('cron_skip_connection_errs'): self.message_post(body=_('Asking for claim status with response:') + Markup('
    : %s
    ') % response + _('failed due to:') + Markup('
    %s') % error) else: self.message_post(body=_('Asking for claim status with response:') + Markup('
    %s') % response) # SII Vendor Bill Buttons def _l10n_cl_send_dte_reception_status(self, status_type): """ Send to the supplier the acceptance or claim of the bill received. """ response_id = self.env['ir.sequence'].browse(self.env.ref('l10n_cl_edi.response_sequence').id).next_by_id() StatusType = namedtuple( 'StatusType', ['dte_status', 'dte_glosa_status', 'code_rejected', 'email_template', 'response_type', 'env_type']) status_types = { 'accepted': StatusType(0, 'DTE Aceptado OK', None, 'email_template_receipt_commercial_accept', 'response_dte', 'env_resp'), 'goods': StatusType( 1, 'El acuse de recibo que se declara en este acto, de acuerdo a lo dispuesto en la letra b) ' 'del Art. 4, y la letra c) del Art. 5 de la Ley 19.983, acredita que la entrega de ' 'mercaderias o servicio(s) prestado(s) ha(n) sido recibido(s).', None, 'email_template_receipt_goods', 'receipt_dte', 'recep'), 'claimed': StatusType(2, 'DTE Rechazado', -1, 'email_template_claimed_ack', 'response_dte', 'env_resp'), } if not int(self.l10n_latam_document_number): raise UserError(_('Please check the document number')) digital_signature = self.company_id._get_digital_signature(user_id=self.env.user.id) response = self.env['ir.qweb']._render('l10n_cl_edi.%s' % status_types[status_type].response_type, { 'move': self, 'doc_id': f'F{int(self.l10n_latam_document_number)}T{self.l10n_latam_document_type_id.code}', 'format_vat': self._l10n_cl_format_vat, 'time_stamp': self._get_cl_current_strftime(), 'response_id': response_id, 'dte_status': status_types[status_type].dte_status, 'dte_glosa_status': status_types[status_type].dte_glosa_status, 'code_rejected': status_types[status_type].code_rejected, 'signer_rut': digital_signature.subject_serial_number, 'rec_address': f'{self.company_id.street}, {self.company_id.street2} {self.company_id.city}', '__keep_empty_lines': True, }) signed_response = self._sign_full_xml( response, digital_signature, '', status_types[status_type].env_type, self.l10n_latam_document_type_id._is_doc_type_voucher()) if status_type == 'goods': response = self.env['ir.qweb']._render('l10n_cl_edi.envio_receipt_dte', { 'move': self, 'format_vat': self._l10n_cl_format_vat, 'receipt_section': signed_response.replace(Markup(''), ''), 'time_stamp': self._get_cl_current_strftime(), '__keep_empty_lines': True, }) signed_response = self._sign_full_xml( response, digital_signature, '', 'env_recep', self.l10n_latam_document_type_id._is_doc_type_voucher()) dte_attachment = self.env['ir.attachment'].create({ 'name': 'DTE_{}_{}.xml'.format(status_type, self.name), 'res_model': self._name, 'res_id': self.id, 'type': 'binary', 'datas': base64.b64encode(bytes(signed_response, 'utf-8')), }) self.l10n_cl_dte_file = dte_attachment.id # If we are sending a reception of goods or services we must use an envelope and sign it the same # way we do with the invoice (DTE / EnvioDTE) in this case we use the tags: DocumentoRecibo / EnvioRecibos email_template = self.env.ref('l10n_cl_edi.%s' % status_types[status_type].email_template) email_template.send_mail(self.id, force_send=True, email_values={'attachment_ids': [dte_attachment.id]}) def l10n_cl_reprocess_acknowledge(self): if not self.partner_id: raise UserError(_('Please assign a partner before sending the acknowledgement')) try: self._l10n_cl_send_receipt_acknowledgment() except Exception as error: self.message_post(body=str(error)) def _l10n_cl_send_receipt_acknowledgment(self): """ This method sends an xml with the acknowledgement of the reception of the invoice by email to the vendor. """ attch_name = 'DTE_{}.xml'.format(self.l10n_latam_document_number) dte_attachment = self.l10n_cl_dte_file if not dte_attachment: raise Exception(_('DTE attachment not found => %s') % attch_name) xml_dte = base64.b64decode(dte_attachment.datas).decode('utf-8') xml_content = etree.fromstring(xml_dte) response_id = self.env['ir.sequence'].browse(self.env.ref('l10n_cl_edi.response_sequence').id).next_by_id() xml_ack_template = self.env['ir.qweb']._render('l10n_cl_edi.ack_template', { 'move': self, 'format_vat': self._l10n_cl_format_vat, 'get_cl_current_strftime': self._get_cl_current_strftime, 'response_id': response_id, 'nmb_envio': 'RESP_%s' % attch_name, 'envio_dte_id': self._l10n_cl_get_set_dte_id(xml_content), 'digest_value': xml_content.findtext( './/ns1:DigestValue', namespaces={'ns1': 'http://www.w3.org/2000/09/xmldsig#'}), '__keep_empty_lines': True, }) xml_ack_template = xml_ack_template.replace( '<', '<').replace('>', '>').replace('"', '"').replace( '', '') try: digital_signature_sudo = self.company_id.sudo()._get_digital_signature(user_id=self.env.user.id) except Exception: raise Exception(_('There is no signature available to send acknowledge or acceptation of this DTE. ' 'Please setup your digital signature')) xml_ack = self._sign_full_xml(xml_ack_template, digital_signature_sudo, str(response_id), 'env_resp', self.l10n_latam_document_type_id._is_doc_type_voucher()) attachment = self.env['ir.attachment'].create({ 'name': 'receipt_acknowledgment_{}.xml'.format(response_id), 'res_model': self._name, 'res_id': self.id, 'type': 'binary', 'datas': base64.b64encode(bytes(xml_ack, 'utf-8')), }) self.env.ref('l10n_cl_edi.email_template_receipt_ack').send_mail(self.id, force_send=True, email_values={ 'attachment_ids': attachment}) self.l10n_cl_dte_acceptation_status = 'ack_sent' def _l10n_cl_action_response(self, status_type): """ This method is intended to manage the claim/acceptation/receipt of goods for vendor bill """ accepted_statuses = {'accepted_goods', 'accepted', 'goods'} ActionResponse = namedtuple('ActionResponse', ['code', 'category', 'status', 'description']) action_response = { 'accepted': ActionResponse('ACD', _('Claim status'), 'accepted', _('acceptance')), 'goods': ActionResponse('ERM', _('Reception law 19983'), 'received', _('reception of goods or services RG 19.983')), 'claimed': ActionResponse('RCD', _('Claim status'), 'claimed', _('claim')), } if self.company_id.l10n_cl_dte_service_provider == 'SIIDEMO': self.message_post(body=_( 'WARNING: Simulating %s in Demo Mode') % action_response[status_type].description) self.l10n_cl_dte_acceptation_status = 'accepted_goods' if \ self.l10n_cl_dte_acceptation_status in accepted_statuses and status_type in accepted_statuses \ else status_type self._l10n_cl_send_dte_reception_status(status_type) if not self.l10n_latam_document_type_id._is_doc_type_acceptance(): raise UserError(_('The document type with code %s cannot be %s') % (self.l10n_latam_document_type_id.code, action_response[status_type].status)) try: response = self._send_sii_claim_response( self.company_id.l10n_cl_dte_service_provider, self.partner_id.vat, self.company_id.sudo()._get_digital_signature(user_id=self.env.user.id), self.l10n_latam_document_type_id.code, self.l10n_latam_document_number, action_response[status_type].code) except InvalidToken: digital_signature_sudo = self.company_id.sudo()._get_digital_signature(user_id=self.env.user.id) digital_signature_sudo.last_token = None return self.l10n_cl_accept_document() if not response: return None try: cod_response = response['codResp'] description_response = response['descResp'] except Exception as error: _logger.error(error) self.message_post(body=_('Exception error parsing the response: %s') % response) return None if cod_response in [0, 1]: # accepted_goods only when both acceptation of the invoice and receipt of goods have been done self.l10n_cl_dte_acceptation_status = 'accepted_goods' if \ self.l10n_cl_dte_acceptation_status in accepted_statuses and status_type in accepted_statuses \ else status_type self._l10n_cl_send_dte_reception_status(status_type) msg = _('Document %s was accepted with the following response:') % ( action_response[status_type].description) + '
    %s: %s.' % ( cod_response, description_response) if self.company_id.l10n_cl_dte_service_provider == 'SIIDEMO': msg += _(' -- Response simulated in Demo Mode') else: msg = _('Document %s failed with the following response:') % (action_response[status_type].description) + \ Markup('
    %s: %s.') % (cod_response, description_response) if cod_response == 9 and self.company_id.l10n_cl_dte_service_provider == 'SIITEST': msg += Markup(_('

    If you are trying to test %s of documents, you should send this %s as a vendor ' 'to %s before doing the test.')) % ( action_response[status_type].description, self.l10n_latam_document_type_id.name, self.company_id.name) self.message_post(body=msg) def l10n_cl_accept_document(self): self._l10n_cl_action_response('accepted') def l10n_cl_receipt_service_or_merchandise(self): self._l10n_cl_action_response('goods') def l10n_cl_claim_document(self): self._l10n_cl_action_response('claimed') # DTE creation def _l10n_cl_create_dte(self): folio = int(self.l10n_latam_document_number) doc_id_number = 'F{}T{}'.format(folio, self.l10n_latam_document_type_id.code) dte_barcode_xml = self._l10n_cl_get_dte_barcode_xml() self.l10n_cl_sii_barcode = dte_barcode_xml['barcode'] dte = self.env['ir.qweb']._render('l10n_cl_edi.dte_template', { 'move': self, 'format_vat': self._l10n_cl_format_vat, 'get_cl_current_strftime': self._get_cl_current_strftime, 'format_length': self._format_length, 'format_uom': self._format_uom, 'float_repr': float_repr, 'float_rr': self._float_repr_float_round, 'doc_id': doc_id_number, 'caf': self.l10n_latam_document_type_id._get_caf_file(self.company_id.id, int(self.l10n_latam_document_number)), 'amounts': self._l10n_cl_get_amounts(), 'withholdings': self._l10n_cl_get_withholdings(), 'dte': dte_barcode_xml['ted'], '__keep_empty_lines': True, }) digital_signature_sudo = self.company_id.sudo()._get_digital_signature(user_id=self.env.user.id) signed_dte = self._sign_full_xml( dte, digital_signature_sudo, doc_id_number, 'doc', self.l10n_latam_document_type_id._is_doc_type_voucher()) dte_attachment = self.env['ir.attachment'].create({ 'name': 'DTE_{}.xml'.format(self.name), 'res_model': self._name, 'res_id': self.id, 'type': 'binary', 'datas': base64.b64encode(signed_dte.encode('ISO-8859-1', 'replace')) }) self.l10n_cl_dte_file = dte_attachment.id def _l10n_cl_create_partner_dte(self): dte_signed, file_name = self._l10n_cl_create_dte_envelope( '55555555-5' if self.partner_id.l10n_cl_sii_taxpayer_type == '4' else self.partner_id.vat) dte_partner_attachment = self.env['ir.attachment'].create({ 'name': file_name, 'res_model': self._name, 'res_id': self.id, 'type': 'binary', 'datas': base64.b64encode(dte_signed.encode('ISO-8859-1', 'replace')) }) self.with_context(no_new_invoice=True).message_post( body=_('Partner DTE has been generated'), attachment_ids=[dte_partner_attachment.id]) return dte_partner_attachment def _l10n_cl_create_dte_envelope(self, receiver_rut='60803000-K'): file_name = 'F{}T{}.xml'.format(self.l10n_latam_document_number, self.l10n_latam_document_type_id.code) digital_signature_sudo = self.company_id.sudo()._get_digital_signature(user_id=self.env.user.id) template = self.l10n_latam_document_type_id._is_doc_type_voucher() and self.env.ref( 'l10n_cl_edi.envio_boleta') or self.env.ref('l10n_cl_edi.envio_dte') dte = self.l10n_cl_dte_file.raw.decode('ISO-8859-1') dte = Markup(dte.replace('', '')) dte_rendered = self.env['ir.qweb']._render(template.id, { 'move': self, 'RutEmisor': self._l10n_cl_format_vat(self.company_id.vat), 'RutEnvia': digital_signature_sudo.subject_serial_number, 'RutReceptor': receiver_rut, 'FchResol': self.company_id.l10n_cl_dte_resolution_date, 'NroResol': self.company_id.l10n_cl_dte_resolution_number, 'TmstFirmaEnv': self._get_cl_current_strftime(), 'dte': dte, '__keep_empty_lines': True, }) dte_rendered = dte_rendered.replace('', '') dte_signed = self._sign_full_xml( dte_rendered, digital_signature_sudo, 'SetDoc', self.l10n_latam_document_type_id._is_doc_type_voucher() and 'bol' or 'env', self.l10n_latam_document_type_id._is_doc_type_voucher() ) return dte_signed, file_name # DTE sending def _l10n_cl_send_dte_to_partner(self): # We need a DTE with the partner vat as RutReceptor to be sent to the partner dte_partner_attachment = self._l10n_cl_create_partner_dte() self.env.ref('l10n_cl_edi.l10n_cl_edi_email_template_invoice').send_mail( self.id, force_send=True, email_values={'attachment_ids': [dte_partner_attachment.id]}) self.l10n_cl_dte_partner_status = 'sent' self.message_post(body=_('DTE has been sent to the partner')) # Helpers def _l10n_cl_edi_currency_validation(self): # commented to allow validation to true for every case of invoices (national invoices in foreign currency) return True def _l10n_cl_edi_post_validation(self): if (self.l10n_cl_journal_point_of_sale_type == 'online' and not ((self.partner_id.l10n_cl_dte_email or self.commercial_partner_id.l10n_cl_dte_email) and self.company_id.l10n_cl_dte_email) and not self.l10n_latam_document_type_id._is_doc_type_export() and not self.l10n_latam_document_type_id._is_doc_type_ticket()): raise UserError( _('The %s %s has not a DTE email defined. This is mandatory for electronic invoicing.') % (_('partner') if not (self.partner_id.l10n_cl_dte_email or self.commercial_partner_id.l10n_cl_dte_email) else _('company'), self.partner_id.name)) if datetime.strptime(self._get_cl_current_strftime(), '%Y-%m-%dT%H:%M:%S').date() < self.invoice_date: raise UserError( _('The stamp date and time cannot be prior to the invoice issue date and time. TIP: check ' 'in your user preferences if the timezone is "America/Santiago"')) if not self.company_id.l10n_cl_dte_service_provider: raise UserError(_( 'You have not selected an invoicing service provider for your company. ' 'Please go to your company and select one')) if not self.company_id.l10n_cl_activity_description: raise UserError(_( 'Your company has not an activity description configured. This is mandatory for electronic ' 'invoicing. Please go to your company and set the correct one (www.sii.cl - Mi SII)')) if not self.company_id.l10n_cl_company_activity_ids: raise UserError(_( 'There are no activity codes configured in your company. This is mandatory for electronic ' 'invoicing. Please go to your company and set the correct activity codes (www.sii.cl - Mi SII)')) if not self.company_id.l10n_cl_sii_regional_office: raise UserError(_( 'There is no SII Regional Office configured in your company. This is mandatory for electronic ' 'invoicing. Please go to your company and set the regional office, according to your company ' 'address (www.sii.cl - Mi SII)')) if (self.l10n_latam_document_type_id.code not in ['39', '41', '110', '111', '112'] and not (self.partner_id.l10n_cl_activity_description or self.commercial_partner_id.l10n_cl_activity_description)): raise UserError(_( 'There is not an activity description configured in the ' 'customer %s record. This is mandatory for electronic invoicing for this type of ' 'document. Please go to the partner record and set the activity description') % self.partner_id.name) if not self.l10n_latam_document_type_id._is_doc_type_electronic_ticket() and not self.partner_id.street: raise UserError(_( 'There is no address configured in your customer %s record. ' 'This is mandatory for electronic invoicing for this type of document. ' 'Please go to the partner record and set the address') % self.partner_id.name) if (self.l10n_latam_document_type_id.code in ['34', '41', '110', '111', '112'] and self.amount_untaxed != self.amount_total): raise UserError(_('It seems that you are using items with taxes in exempt documents in invoice %s - %s.' ' You must either:\n' ' - Change the document type to a not exempt type.\n' ' - Set an exempt fiscal position to remove taxes automatically.\n' ' - Use products without taxes.\n' ' - Remove taxes from product lines.') % (self.id, self.name)) if self.l10n_latam_document_type_id.code == '33' and self.amount_untaxed == self.amount_total: raise UserError(_('All the items you are billing in invoice %s - %s, have no taxes.\n' ' If you need to bill exempt items you must either use exempt invoice document type (34),' ' or at least one of the items should have vat tax.') % (self.id, self.name)) self._l10n_cl_edi_validate_boletas() def _l10n_cl_edi_validate_boletas(self): if self.l10n_latam_document_type_id.code in ['39', '41']: raise UserError(_('Ticket is not allowed, please contact your administrator to install the ' 'l10n_cl_edi_boletas module')) def _l10n_cl_get_sii_reception_status_message(self, sii_response_status): """ Get the value of the code returns by SII once the DTE has been sent to the SII. """ return { '0': _('Upload OK'), '1': _('Sender Does Not Have Permission To Send'), '2': _('File Size Error (Too Big or Too Small)'), '3': _('Incomplete File (Size <> Parameter size)'), '5': _('Not Authenticated'), '6': _('Company Not Authorized to Send Files'), '7': _('Invalid Schema'), '8': _('Document Signature'), '9': _('System Locked'), 'Otro': _('Internal Error'), }.get(sii_response_status, sii_response_status) def _l10n_cl_normalize_currency_name(self, currency_name): currency_dict = { 'AED': 'DIRHAM', 'ARS': 'PESO', 'AUD': 'DOLAR AUST', 'BOB': 'BOLIVIANO', 'BRL': 'CRUZEIRO REAL', 'CAD': 'DOLAR CAN', 'CHF': 'FRANCO SZ', 'CLP': 'PESO CL', 'CNY': 'RENMINBI', 'COP': 'PESO COL', 'ECS': 'SUCRE', 'EUR': 'EURO', 'GBP': 'LIBRA EST', 'HKD': 'DOLAR HK', 'INR': 'RUPIA', 'JPY': 'YEN', 'MXN': 'PESO MEX', 'NOK': 'CORONA NOR', 'NZD': 'DOLAR NZ', 'PEN': 'NUEVO SOL', 'PYG': 'GUARANI', 'SEK': 'CORONA SC', 'SGD': 'DOLAR SIN', 'TWD': 'DOLAR TAI', 'USD': 'DOLAR USA', 'UYU': 'PESO URUG', 'VEF': 'BOLIVAR', 'ZAR': 'RAND', } return currency_dict.get(currency_name, 'OTRAS MONEDAS') def _l10n_cl_get_dte_barcode_xml(self): """ This method create the "stamp" (timbre). Is the auto-contained information inside the pdf417 barcode, which consists of a reduced xml version of the invoice, containing: issuer, recipient, folio and the first line of the invoice, etc. :return: xml that goes embedded inside the pdf417 code """ dd = self.env['ir.qweb']._render('l10n_cl_edi.dd_template', { 'move': self, 'format_vat': self._l10n_cl_format_vat, 'float_repr': float_repr, 'format_length': self._format_length, 'format_uom': self._format_uom, 'time_stamp': self._get_cl_current_strftime(), 'caf': self.l10n_latam_document_type_id._get_caf_file( self.company_id.id, int(self.l10n_latam_document_number)), 'amounts': self._l10n_cl_get_amounts(), '__keep_empty_lines': True, }) caf_file = self.l10n_latam_document_type_id._get_caf_file( self.company_id.id, int(self.l10n_latam_document_number)) ted = self.env['ir.qweb']._render('l10n_cl_edi.ted_template', { 'dd': dd, 'frmt': self._sign_message(dd.encode('ISO-8859-1', 'replace'), caf_file.findtext('RSASK')), 'stamp': self._get_cl_current_strftime(), '__keep_empty_lines': True, }) return { 'ted': Markup(re.sub(r'\n\s*$', '', ted, flags=re.MULTILINE)), 'barcode': etree.tostring(etree.fromstring(re.sub( r'.*', '', ted), parser=etree.XMLParser(remove_blank_text=True))) } def _l10n_cl_get_reverse_doc_type(self): if self.partner_id.l10n_cl_sii_taxpayer_type == '4' or self.partner_id.country_id.code != "CL": return self.env['l10n_latam.document.type'].search( [('code', '=', '112'), ('country_id.code', '=', "CL")], limit=1) return self.env['l10n_latam.document.type'].search( [('code', '=', '61'), ('country_id.code', '=', "CL")], limit=1) def _l10n_cl_get_comuna_recep(self, recep=True): if self.partner_id._l10n_cl_is_foreign(): if recep: return self._format_length( self.partner_id.state_id.name or self.commercial_partner_id.state_id.name or 'N-A', 20) return self._format_length(self.partner_shipping_id.state_id.name or 'N-A', 20) if self.l10n_latam_document_type_id._is_doc_type_voucher(): return 'N-A' if recep: return self._format_length(self.partner_id.city or self.commercial_partner_id.city, 20) or False return self._format_length(self.partner_shipping_id.city, 20) or False def _l10n_cl_get_set_dte_id(self, xml_content): set_dte = xml_content.find('.//ns0:SetDTE', namespaces={'ns0': 'http://www.sii.cl/SiiDte'}) set_dte_attrb = set_dte and set_dte.attrib or {} return set_dte_attrb.get('ID', '') # Cron methods def _l10n_cl_ask_dte_status(self): for move in self.search([('l10n_cl_dte_status', '=', 'ask_for_status')]): move.l10n_cl_verify_dte_status(send_dte_to_partner=False) self.env.cr.commit() def _l10n_cl_send_dte_to_partner_multi(self): for move in self.search([('l10n_cl_dte_status', '=', 'accepted'), ('l10n_cl_dte_partner_status', '=', 'not_sent'), ('partner_id.country_id.code', '=', "CL")]): _logger.debug('Sending %s DTE to partner' % move.name) if move.partner_id._l10n_cl_is_foreign(): continue move._l10n_cl_send_dte_to_partner() self.env.cr.commit() def _l10n_cl_ask_claim_status(self): for move in self.search([('l10n_cl_dte_acceptation_status', 'in', ['accepted', 'claimed']), ('move_type', 'in', ['out_invoice', 'out_refund']), ('l10n_cl_claim', '=', False)]): if move.company_id.l10n_cl_dte_service_provider in ['SIITEST', 'SIIDEMO']: continue move.l10n_cl_verify_claim_status() self.env.cr.commit() def _pdf417_barcode(self, barcode_data): # This method creates the graphic representation of the barcode barcode_file = BytesIO() if pdf417gen is None: return False bc = pdf417gen.encode(barcode_data, security_level=5, columns=13) image = pdf417gen.render_image(bc, padding=15, scale=1) image.save(barcode_file, 'PNG') data = barcode_file.getvalue() return base64.b64encode(data) # cron jobs def cron_run_sii_workflow(self): """ This method groups all the steps needed to do the SII workflow: 1.- Ask to SII for the status of the DTE sent 2.- Send to the customer the DTE accepted by the SII 3.- Ask the status of the DTE claimed by the customer """ _logger.debug('Starting cron SII workflow') self_skip = self.with_context(cron_skip_connection_errs=True) self_skip._l10n_cl_ask_dte_status() self_skip._l10n_cl_send_dte_to_partner_multi() self_skip._l10n_cl_ask_claim_status() def cron_send_dte_to_sii(self): for record in self.search([('l10n_cl_dte_status', '=', 'not_sent')]): record.with_context(cron_skip_connection_errs=True).l10n_cl_send_dte_to_sii() self.env.cr.commit()