# Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import fields, models, api, _ from odoo.exceptions import UserError, RedirectWarning from odoo.tools.float_utils import float_repr, float_round from odoo.tools import html2plaintext, plaintext2html from odoo.tools.sql import column_exists, create_column from datetime import datetime from . import afip_errors import re import logging import base64 import json from markupsafe import Markup from requests.exceptions import RequestException, Timeout, ConnectionError, HTTPError _logger = logging.getLogger(__name__) WS_DATE_FORMAT = {'wsfe': '%Y%m%d', 'wsfex': '%Y%m%d', 'wsbfe': '%Y%m%d'} class AccountMove(models.Model): _inherit = "account.move" def _auto_init(self): if not column_exists(self.env.cr, "account_move", "l10n_ar_fce_transmission_type"): # Create the column to avoid computation during installation # Default value is set to NULL because it is initiated that way create_column(self.env.cr, "account_move", "l10n_ar_fce_transmission_type", "varchar") return super()._auto_init() l10n_ar_afip_auth_mode = fields.Selection([('CAE', 'CAE'), ('CAI', 'CAI'), ('CAEA', 'CAEA')], string='AFIP Authorization Mode', copy=False, help="This is the type of AFIP Authorization, depending on the way that the invoice is created" " the mode will change:\n\n" " * CAE (Electronic Authorization Code): Means that is an electronic invoice. If you validate a customer invoice" " this field will be autofill with CAE option. Also, if you trying to verify in AFIP an electronic vendor bill" " you can set this option\n" " * CAI (Printed Authorization Code): Means that is a pre-printed invoice. With this option set you can" " register and verify in AFIP pre-printed vendor bills\n" " * CAEA (Anticipated Electronic Authorization Code): Means that is an electronic invoice. This kind of invoices" " are generated using a pre ganerated code by AFIP for companies that have a massive invoicing by month so they" " can pre process all the invoices of the fortnight in one operation with one unique CAEA. Select this option" " only when verifying in AFIP a vendor bill that have CAEA (invoices with CAEA will not have CAE)") l10n_ar_afip_auth_code = fields.Char('Authorization Code', copy=False, size=24, help="Argentina: authorization code given by AFIP after electronic invoice is created and valid.") l10n_ar_afip_auth_code_due = fields.Date('Authorization Due date', copy=False, help="Argentina: The Due Date of the Invoice given by AFIP.") l10n_ar_afip_qr_code = fields.Char(compute='_compute_l10n_ar_afip_qr_code', string='AFIP QR Code', help='This QR code is mandatory by the AFIP in the electronic invoices when this ones are printed.') # electronic invoice fields l10n_ar_afip_xml_request = fields.Text(string='XML Request', copy=False, readonly=True, groups="base.group_system") l10n_ar_afip_xml_response = fields.Text(string='XML Response', copy=False, readonly=True, groups="base.group_system") l10n_ar_afip_result = fields.Selection([('A', 'Accepted in AFIP'), ('O', 'Accepted in AFIP with Observations')], 'Result', copy=False, help="Argentina: Result of the electronic invoice request to the AFIP web service.", tracking=True) l10n_ar_afip_ws = fields.Selection(related="journal_id.l10n_ar_afip_ws") # fields used to check invoice is valid on AFIP l10n_ar_afip_verification_type = fields.Selection( [('not_available', 'Not Available'), ('available', 'Available'), ('required', 'Required')], compute='_compute_l10n_ar_afip_verification_type') l10n_ar_afip_verification_result = fields.Selection([('A', 'Approved'), ('O', 'Observed'), ('R', 'Rejected')], string='AFIP Verification result', copy=False, readonly=True) # FCE related fields l10n_ar_afip_fce_is_cancellation = fields.Boolean(string='FCE: Is Cancellation?', copy=False, help='Argentina: When informing a MiPyMEs (FCE) debit/credit notes in AFIP it is required to send information about whether the' ' original document has been explicitly rejected by the buyer. More information here' ' http://www.afip.gob.ar/facturadecreditoelectronica/preguntasFrecuentes/emisor-factura.asp') l10n_ar_fce_transmission_type = fields.Selection( [('SCA', 'SCA - TRANSFERENCIA AL SISTEMA DE CIRCULACION ABIERTA'), ('ADC', 'ADC - AGENTE DE DEPOSITO COLECTIVO')], string='FCE: Transmission Option', compute="_compute_l10n_ar_fce_transmission_type", store=True, readonly=False, help="This field only need to be set when you are reporting a MiPyME FCE documents. Default value can be set in the Accouting Settings") # Compute methods @api.depends('l10n_ar_afip_result') def _compute_show_reset_to_draft_button(self): """ EXTENDS 'account.move' When the AFIP approved the move, don't show the reset to draft button """ super()._compute_show_reset_to_draft_button() self.filtered(lambda move: move.l10n_ar_afip_result == "A").show_reset_to_draft_button = False @api.depends('l10n_latam_document_type_id') def _compute_l10n_ar_fce_transmission_type(self): """ Automatically set the default value on the l10n_ar_fce_transmission_type field if the invoice is a mipyme one with the default value set in the company """ mipyme_fce_docs = self.filtered(lambda x: x.country_code == 'AR' and x._is_mipyme_fce()) for rec in mipyme_fce_docs.filtered(lambda x: not x.l10n_ar_fce_transmission_type): if rec.company_id.l10n_ar_fce_transmission_type: rec.l10n_ar_fce_transmission_type = rec.company_id.l10n_ar_fce_transmission_type remaining = self - mipyme_fce_docs remaining.l10n_ar_fce_transmission_type = False @api.depends('l10n_ar_afip_auth_code') def _compute_l10n_ar_afip_qr_code(self): """ Method that generates the QR code with the electronic invoice info taking into account RG 4291 """ with_qr_code = self.filtered(lambda x: x.l10n_ar_afip_auth_mode in ['CAE', 'CAEA'] and x.l10n_ar_afip_auth_code) for rec in with_qr_code: data = { 'ver': 1, 'fecha': str(rec.invoice_date), 'cuit': int(rec.company_id.partner_id.l10n_ar_vat), 'ptoVta': rec.journal_id.l10n_ar_afip_pos_number, 'tipoCmp': int(rec.l10n_latam_document_type_id.code), 'nroCmp': int(self._l10n_ar_get_document_number_parts( rec.l10n_latam_document_number, rec.l10n_latam_document_type_id.code)['invoice_number']), 'importe': float_round(rec.amount_total, precision_digits=2, rounding_method='DOWN'), 'moneda': rec.currency_id.l10n_ar_afip_code, 'ctz': float_round(rec.l10n_ar_currency_rate, precision_digits=6, rounding_method='DOWN'), 'tipoCodAut': 'E' if rec.l10n_ar_afip_auth_mode == 'CAE' else 'A', 'codAut': int(rec.l10n_ar_afip_auth_code), } commercial_partner_id = rec.commercial_partner_id if commercial_partner_id.country_id and commercial_partner_id.country_id.code != 'AR': nro_doc_rec = int( commercial_partner_id.country_id.l10n_ar_legal_entity_vat if commercial_partner_id.is_company else commercial_partner_id.country_id.l10n_ar_natural_vat) else: nro_doc_rec = commercial_partner_id._get_id_number_sanitize() or False data.update({'nroDocRec': nro_doc_rec or 0}) if commercial_partner_id.l10n_latam_identification_type_id: data.update({'tipoDocRec': int(rec._get_partner_code_id(commercial_partner_id))}) # For more info go to https://www.afip.gob.ar/fe/qr/especificaciones.asp rec.l10n_ar_afip_qr_code = 'https://www.afip.gob.ar/fe/qr/?p=%s' % base64.b64encode(json.dumps( data).encode()).decode('ascii') remaining = self - with_qr_code remaining.l10n_ar_afip_qr_code = False @api.depends('l10n_latam_document_type_id', 'company_id') def _compute_l10n_ar_afip_verification_type(self): """ Method that tell us if the invoice/vendor bill can be verified in AFIP """ verify_codes = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "15", "19", "20", "21", "49", "51", "52", "53", "54", "60", "61", "63", "64"] available_to_verify = self.filtered( lambda x: x.l10n_latam_document_type_id and x.l10n_latam_document_type_id.code in verify_codes) for rec in available_to_verify: rec.l10n_ar_afip_verification_type = rec.company_id.l10n_ar_afip_verification_type remaining = self - available_to_verify remaining.l10n_ar_afip_verification_type = 'not_available' # Buttons def _is_dummy_afip_validation(self): self.ensure_one() return self.company_id._get_environment_type() == 'testing' and \ not self.company_id.sudo().l10n_ar_afip_ws_crt or not self.company_id.sudo().l10n_ar_afip_ws_key def _post(self, soft=True): """ After validate the invoice we then validate in AFIP. The last thing we do is request the cae because if an error occurs after CAE requested, the invoice has been already validated on AFIP """ ar_invoices = self.filtered(lambda x: x.is_invoice() and x.company_id.account_fiscal_country_id.code == "AR") sale_ar_invoices = ar_invoices.filtered(lambda x: x.move_type in ['out_invoice', 'out_refund']) # Verify only Vendor bills (only when verification is configured as 'required') (ar_invoices - sale_ar_invoices)._l10n_ar_check_afip_auth_verify_required() # Send invoices to AFIP and get the return info ar_edi_invoices = ar_invoices.filtered(lambda x: x.journal_id.l10n_ar_afip_ws) validated = error_invoice = self.env['account.move'] for inv in ar_edi_invoices: # If we are on testing environment and we don't have certificates we validate only locally. # This is useful when duplicating the production database for training purpose or others if inv._is_dummy_afip_validation() and not inv.l10n_ar_afip_auth_code: inv._dummy_afip_validation() validated += super(AccountMove, inv)._post(soft=soft) continue client, auth, transport = inv.company_id._l10n_ar_get_connection(inv.journal_id.l10n_ar_afip_ws)._get_client(return_transport=True) validated += super(AccountMove, inv)._post(soft=soft) return_info = inv._l10n_ar_do_afip_ws_request_cae(client, auth, transport) if return_info: error_invoice = inv validated -= inv break # If we get CAE from AFIP then we make commit because we need to save the information returned by AFIP # in Odoo for consistency, this way if an error ocurrs later in another invoice we will have the ones # correctly validated in AFIP in Odoo (CAE, Result, xml response/request). if not self.env.context.get('l10n_ar_invoice_skip_commit'): self._cr.commit() if error_invoice: if error_invoice.exists(): msg = _('We couldn\'t validate the document "%s" (Draft Invoice *%s) in AFIP', error_invoice.partner_id.name, error_invoice.id) else: msg = _('We couldn\'t validate the invoice in AFIP.') msg += _('This is what we get:\n%s\n\nPlease make the required corrections and try again', return_info) # if we've already validate any invoice, we've commit and we want to inform which invoices were validated # which one were not and the detail of the error we get. This ins neccesary because is not usual to have a # raise with changes commited on databases if validated: unprocess = self - validated - error_invoice msg = _( """Some documents where validated in AFIP but as we have an error with one document the batch validation was stopped * These documents were validated: %(validate_invoices)s * These documents weren\'t validated: %(invalide_invoices)s """, validate_invoices="\n * ".join(validated.mapped('name')), invalide_invoices="\n * ".join([ _("%s: %r amount %s", item.display_name, item.partner_id.name, item.amount_total_signed) for item in unprocess ]) ) raise UserError(msg) return validated + super(AccountMove, self - ar_edi_invoices)._post(soft=soft) def l10n_ar_verify_on_afip(self): """ This method let us to connect to AFIP using WSCDC webservice to verify if a vendor bill is valid on AFIP """ for inv in self: if not inv.l10n_ar_afip_auth_mode or not inv.l10n_ar_afip_auth_code: raise UserError(_('Please set AFIP Authorization Mode and Code to continue!')) # get Issuer and Receptor depending on the document type issuer, receptor = (inv.commercial_partner_id, inv.company_id.partner_id) \ if inv.move_type in ['in_invoice', 'in_refund'] else (inv.company_id.partner_id, inv.commercial_partner_id) issuer_vat = issuer.ensure_vat() receptor_identification_code = receptor.l10n_latam_identification_type_id.l10n_ar_afip_code or '99' receptor_id_number = (receptor_identification_code and str(receptor._get_id_number_sanitize())) if inv.l10n_latam_document_type_id.l10n_ar_letter in ['A', 'M'] and receptor_identification_code != '80' or not receptor_id_number: raise UserError(_('For type A and M documents the receiver identification is mandatory and should be VAT')) document_parts = self._l10n_ar_get_document_number_parts(inv.l10n_latam_document_number, inv.l10n_latam_document_type_id.code) if not document_parts['point_of_sale'] or not document_parts['invoice_number']: raise UserError(_('Point of sale and document number are required!')) if not inv.l10n_latam_document_type_id.code: raise UserError(_('No document type selected or document type is not available for validation!')) if not inv.invoice_date: raise UserError(_('Invoice Date is required!')) connection = self.company_id._l10n_ar_get_connection('wscdc') client, auth = connection._get_client() response = client.service.ComprobanteConstatar(auth, { 'CbteModo': inv.l10n_ar_afip_auth_mode, 'CuitEmisor': issuer_vat, 'PtoVta': document_parts['point_of_sale'], 'CbteTipo': inv.l10n_latam_document_type_id.code, 'CbteNro': document_parts['invoice_number'], 'CbteFch': inv.invoice_date.strftime('%Y%m%d'), 'ImpTotal': float_repr(inv.amount_total, precision_digits=2), 'CodAutorizacion': inv.l10n_ar_afip_auth_code, 'DocTipoReceptor': receptor_identification_code, 'DocNroReceptor': receptor_id_number}) inv.write({'l10n_ar_afip_verification_result': response.Resultado}) if response.Observaciones or response.Errors: inv.message_post(body=_('AFIP authorization verification result: %s%s', response.Observaciones, response.Errors)) # Main methods def _l10n_ar_do_afip_ws_request_cae(self, client, auth, transport): """ Submits the invoice information to AFIP and gets a response of AFIP in return. If we receive a positive response from AFIP then validate the invoice and save the returned information in the corresponding invoice fields: * CAE number (Authorization Electronic Code) * Authorization Type * XML Request * XML Response * Result (Approved, Aproved with Observations) NOTE: If there are observations we leave a message in the invoice message chart with the observation. If there are errors it means that the invoice has been Rejected by AFIP and we raise an user error with the processed info about the error and some hint about how to solve it. The invoice is not valided. """ for inv in self.filtered(lambda x: x.journal_id.l10n_ar_afip_ws and not x.l10n_ar_afip_auth_code): afip_ws = inv.journal_id.l10n_ar_afip_ws errors = obs = events = '' request_data = False return_codes = [] values = {} # We need to call a different method for every webservice type and assemble the returned errors if they exist if afip_ws == 'wsfe': ws_method = 'FECAESolicitar' request_data = inv.wsfe_get_cae_request(client) self._ws_verify_request_data(client, auth, ws_method, request_data) response = client.service[ws_method](auth, request_data) if response.FeDetResp: result = response.FeDetResp.FECAEDetResponse[0] if result.Observaciones: obs = ''.join(['\n* Code %s: %s' % (ob.Code, ob.Msg) for ob in result.Observaciones.Obs]) return_codes += [str(ob.Code) for ob in result.Observaciones.Obs] if result.Resultado == 'A': values = {'l10n_ar_afip_auth_mode': 'CAE', 'l10n_ar_afip_auth_code': result.CAE and str(result.CAE) or "", 'l10n_ar_afip_auth_code_due': datetime.strptime(result.CAEFchVto, '%Y%m%d').date(), 'l10n_ar_afip_result': result.Resultado} if response.Events: events = ''.join(['\n* Code %s: %s' % (evt.Code, evt.Msg) for evt in response.Events.Evt]) return_codes += [str(evt.Code) for evt in response.Events.Evt] if response.Errors: errors = ''.join(['\n* Code %s: %s' % (err.Code, err.Msg) for err in response.Errors.Err]) return_codes += [str(err.Code) for err in response.Errors.Err] # Manage 10016 error origin if '10016' in return_codes: try: client2, _auth2, _transport2 = inv.company_id._l10n_ar_get_connection(inv.journal_id.l10n_ar_afip_ws)._get_client(return_transport=True) last_number_afip = self.journal_id._l10n_ar_get_afip_last_invoice_number(self.l10n_latam_document_type_id) response2 = client2.service.FECompConsultar(auth, { 'CbteTipo': self.l10n_latam_document_type_id.code, 'CbteNro': last_number_afip, 'PtoVta': self.journal_id.l10n_ar_afip_pos_number}) odoo_current_invoice_dict = request_data['FeDetReq'][0]['FECAEDetRequest'] odoo_current_invoice_number = odoo_current_invoice_dict['CbteDesde'] # verify if the invoice that is being validated in Odoo has lower date than the last one registered in afip last_afip_inv_date = response2.ResultGet.CbteFch odoo_current_invoice_date = odoo_current_invoice_dict['CbteFch'] except (Timeout, ConnectionError, RequestException, HTTPError, KeyError): return_codes.extend(['10016-1', '10016-2']) else: if last_afip_inv_date > odoo_current_invoice_date: return_codes.remove('10016') return_codes.append('10016-1') # verify if the invoice that is being validated in Odoo follows the sequence of the last invoice registered in afip # if the sequence is reset (number 00000001) then is not necessary to know the last afip number because number 00000000 does # not exists elif odoo_current_invoice_number <= last_number_afip and odoo_current_invoice_number != 1: return_codes.remove('10016') return_codes.append('10016-2') elif afip_ws == 'wsfex': ws_method = 'FEXAuthorize' last_id = client.service.FEXGetLast_ID(auth).FEXResultGet.Id request_data = inv.wsfex_get_cae_request(last_id+1, client) self._ws_verify_request_data(client, auth, ws_method, request_data) response = client.service[ws_method](auth, request_data) result = response.FEXResultAuth if response.FEXErr.ErrCode != 0 or response.FEXErr.ErrMsg != 'OK': errors = '\n* Code %s: %s' % (response.FEXErr.ErrCode, response.FEXErr.ErrMsg) return_codes += [str(response.FEXErr.ErrCode)] if response.FEXEvents.EventCode != 0 or response.FEXEvents.EventMsg != 'Ok': events = '\n* Code %s: %s' % (response.FEXEvents.EventCode, response.FEXEvents.EventMsg) return_codes += [str(response.FEXEvents.EventCode)] if result: if result.Motivos_Obs: obs = '\n* Code ???: %s' % result.Motivos_Obs return_codes += [result.Motivos_Obs] if result.Reproceso == 'S': return_codes += ['reprocess'] if result.Resultado != 'A': if not errors: return_codes += ['rejected'] else: values = {'l10n_ar_afip_auth_mode': 'CAE', 'l10n_ar_afip_auth_code': result.Cae, 'l10n_ar_afip_auth_code_due': datetime.strptime(result.Fch_venc_Cae, '%Y%m%d').date(), 'l10n_ar_afip_result': result.Resultado} elif afip_ws == 'wsbfe': ws_method = 'BFEAuthorize' last_id = client.service.BFEGetLast_ID(auth).BFEResultGet.Id request_data = inv.wsbfe_get_cae_request(last_id + 1, client) self._ws_verify_request_data(client, auth, ws_method, request_data) response = client.service[ws_method](auth, request_data) result = response.BFEResultAuth if response.BFEErr.ErrCode != 0 or response.BFEErr.ErrMsg != 'OK': errors = '\n* Code %s: %s' % (response.BFEErr.ErrCode, response.BFEErr.ErrMsg) return_codes += [str(response.BFEErr.ErrCode)] if response.BFEEvents.EventCode != 0 or response.BFEEvents.EventMsg: events = '\n* Code %s: %s' % (response.BFEEvents.EventCode, response.BFEEvents.EventMsg) if result.Obs: obs = result.Obs return_codes += [result.Obs] if result.Reproceso == 'S': return_codes += ['reprocess'] if result.Resultado != 'A': if not errors: return_codes += ['rejected'] else: values = {'l10n_ar_afip_auth_code': result.Cae, 'l10n_ar_afip_auth_mode': 'CAE', 'l10n_ar_afip_result': result.Resultado if not obs else 'O', 'l10n_ar_afip_auth_code_due': datetime.strptime(result.Fch_venc_Cae, '%Y%m%d').date()} return_info = inv._prepare_return_msg(afip_ws, errors, obs, events, return_codes) afip_result = values.get('l10n_ar_afip_result') xml_response, xml_request = transport.xml_response, transport.xml_request if afip_result not in ['A', 'O']: if not self.env.context.get('l10n_ar_invoice_skip_commit'): self.env.cr.rollback() if inv.exists(): # Only save the xml_request/xml_response fields if the invoice exists. # It is possible that the invoice will rollback as well e.g. when it is automatically created: # * creating credit note with full reconcile option # * creating/validating an invoice from subscription/sales inv.sudo().write({'l10n_ar_afip_xml_request': xml_request, 'l10n_ar_afip_xml_response': xml_response}) if not self.env.context.get('l10n_ar_invoice_skip_commit'): self.env.cr.commit() return return_info values.update(l10n_ar_afip_xml_request=xml_request, l10n_ar_afip_xml_response=xml_response) inv.sudo().write(values) if return_info: inv.message_post(body=Markup('

%s%s

') % (_('AFIP Messages'), plaintext2html(return_info, 'em'))) # Helpers def _dummy_afip_validation(self): """ Only when we want to skip AFIP validation in testing environment. Fill the AFIP fields with dummy values in order to continue with the invoice validation without passing to AFIP validations """ self.write({'l10n_ar_afip_auth_mode': 'CAE', 'l10n_ar_afip_auth_code': '68448767638166', 'l10n_ar_afip_auth_code_due': self.invoice_date, 'l10n_ar_afip_result': ''}) self.message_post(body=_('Invoice validated locally because it is in a testing environment without testing certificate/keys')) def _l10n_ar_check_afip_auth_verify_required(self): """ If the company has set "Verify vendor bills: Required". it will check if the vendor bill has been verified in AFIP, if not will try to verify them. If the invoice is sucessfully verified in AFIP (result is Approved or Observations) then will let to continue with the post of the bill, if not then will raise an expection that will stop the post. """ verification_missing = self.filtered( lambda x: x.move_type in ['in_invoice', 'in_refund'] and x.l10n_ar_afip_verification_type == 'required' and x.l10n_latam_document_type_id.country_id.code == "AR" and x.l10n_ar_afip_verification_result not in ['A', 'O']) try: verification_missing.l10n_ar_verify_on_afip() except Exception as error: _logger.error(repr(error)) still_missing = verification_missing.filtered(lambda x: x.l10n_ar_afip_verification_result not in ['A', 'O']) if still_missing: if len(still_missing) > 1: raise UserError(_( 'We can not post these vendor bills in Odoo because the ' 'AFIP verification fail: %s\nPlease verify in AFIP ' 'manually and review the bill chatter for more information', '\n * '.join(still_missing.mapped('display_name')))) raise UserError(_( 'We can not post this vendor bill in Odoo because the AFIP ' 'verification fail: %s\nPlease verify in AFIP manually and ' 'review the bill chatter for more information', still_missing.display_name)) def _is_mipyme_fce(self): """ True of False if the invoice is a mipyme document """ self.ensure_one() return int(self.l10n_latam_document_type_id.code) in [201, 206, 211] def _is_mipyme_fce_refund(self): """ True of False if the invoice is a mipyme document """ self.ensure_one() return int(self.l10n_latam_document_type_id.code) in [202, 203, 207, 208, 212, 213] def _due_payment_date(self): """ Due payment date only informed when concept = "services" or when invoice of type mipyme_fce """ if self.l10n_ar_afip_concept != '1' and not self._is_mipyme_fce_refund() or self._is_mipyme_fce(): return self.invoice_date_due or self.invoice_date return False def _service_dates(self): """ Service start and end date only set when concept is ony type "service" """ if self.l10n_ar_afip_concept != '1': return self.l10n_ar_afip_service_start, self.l10n_ar_afip_service_end return False, False def _found_related_invoice(self): """ List related invoice information to fill associated voucher key for AFIP (CbtesAsoc). NOTE: for now we only get related document for debit and credit notes because, for eg, an invoice can not be related to another one, and that happens if you choose the modify option of the credit note wizard A mapping of which documents can be reported as related documents would be a better solution """ self.ensure_one() if self.l10n_latam_document_type_id.internal_type == 'credit_note': return self.reversed_entry_id elif self.l10n_latam_document_type_id.internal_type == 'debit_note': return self.debit_origin_id else: return self.browse() def _get_tributes(self): """ Applies on wsfe web service """ res = [] not_vat_taxes = self.line_ids.filtered(lambda x: x.tax_line_id and x.tax_line_id.tax_group_id.l10n_ar_tribute_afip_code) for tribute in not_vat_taxes: base_imp = sum(self.invoice_line_ids.filtered(lambda x: x.tax_ids.filtered( lambda y: y.tax_group_id.l10n_ar_tribute_afip_code == tribute.tax_line_id.tax_group_id.l10n_ar_tribute_afip_code)).mapped( 'price_subtotal')) res.append({'Id': tribute.tax_line_id.tax_group_id.l10n_ar_tribute_afip_code, 'Alic': 0, 'Desc': tribute.tax_line_id.tax_group_id.name, 'BaseImp': float_repr(base_imp, precision_digits=2), 'Importe': float_repr(abs(tribute.amount_currency), precision_digits=2)}) return res if res else None def _get_related_invoice_data(self): """ Applies on wsfe and wsfex web services """ self.ensure_one() res = {} related_inv = self._found_related_invoice() afip_ws = self.journal_id.l10n_ar_afip_ws if not related_inv: return res # WSBFE_1035 We should only send CbtesAsoc if the invoice to validate has any of the next doc type codes if afip_ws == 'wsbfe' and \ int(self.l10n_latam_document_type_id.code) not in [1, 2, 3, 6, 7, 8, 91, 201, 202, 203, 206, 207, 208]: return res wskey = {'wsfe': {'type': 'Tipo', 'pos_number': 'PtoVta', 'number': 'Nro', 'cuit': 'Cuit', 'date': 'CbteFch'}, 'wsbfe': {'type': 'Tipo_cbte', 'pos_number': 'Punto_vta', 'number': 'Cbte_nro', 'cuit': 'Cuit', 'date': 'Fecha_cbte'}, 'wsfex': {'type': 'Cbte_tipo', 'pos_number': 'Cbte_punto_vta', 'number': 'Cbte_nro', 'cuit': 'Cbte_cuit'}} res.update({wskey[afip_ws]['type']: related_inv.l10n_latam_document_type_id.code, wskey[afip_ws]['pos_number']: related_inv.journal_id.l10n_ar_afip_pos_number, wskey[afip_ws]['number']: self._l10n_ar_get_document_number_parts( related_inv.l10n_latam_document_number, related_inv.l10n_latam_document_type_id.code)['invoice_number']}) # WSFE_10151 send cuit of the issuer if type mipyme refund if self._is_mipyme_fce_refund() or afip_ws == 'wsfex': res.update({wskey[afip_ws]['cuit']: related_inv.company_id.partner_id._get_id_number_sanitize()}) # WSFE_10158 send orignal invoice date on an mipyme document if afip_ws == 'wsfe' and (self._is_mipyme_fce() or self._is_mipyme_fce_refund()): res.update({wskey[afip_ws]['date']: related_inv.invoice_date.strftime(WS_DATE_FORMAT[afip_ws])}) return res def _get_line_details(self): """ Used only in wsbfe and wsfex """ self.ensure_one() details = [] afip_ws = self.journal_id.l10n_ar_afip_ws for line in self.invoice_line_ids.filtered(lambda x: x.display_type not in ('line_section', 'line_note')): # Unit of measure of the product if it sale in a unit of measures different from has been purchase if not line.product_uom_id.l10n_ar_afip_code: raise UserError(_('No AFIP code in %s UOM', line.product_uom_id.name)) Pro_umed = line.product_uom_id.l10n_ar_afip_code values = { 'Pro_ds': line.name, 'Pro_qty': line.quantity, 'Pro_umed': Pro_umed, 'Pro_precio_uni': line.price_unit, } # We compute bonus by substracting theoretical minus amount bonus = line.discount and \ float_repr(line.price_unit * line.quantity - line.price_subtotal, precision_digits=2) or 0.0 if afip_ws == 'wsbfe': if not line.product_id.uom_id.l10n_ar_afip_code: raise UserError(_('No AFIP code in %s UOM', line.product_id.uom_id.name)) vat_tax = line.tax_ids.filtered(lambda x: x.tax_group_id.l10n_ar_vat_afip_code) vat_taxes_amounts = vat_tax.compute_all( line.price_unit, self.currency_id, line.quantity, product=line.product_id, partner=self.partner_id, fixed_multiplicator=line.move_id.direction_sign, ) line.product_id.product_tmpl_id._check_l10n_ar_ncm_code() values.update({'Pro_codigo_ncm': line.product_id.l10n_ar_ncm_code or '', 'Imp_bonif': bonus, 'Iva_id': vat_tax.tax_group_id.l10n_ar_vat_afip_code, 'Imp_total': vat_taxes_amounts['total_included']}) elif afip_ws == 'wsfex': if Pro_umed != ['97', '99', '00']: if line._get_downpayment_lines(): Pro_umed = '97' elif line.price_unit < 0: Pro_umed = '99' if Pro_umed in ['97', '99', '00']: values = { 'Pro_ds': line.name, 'Pro_umed': Pro_umed, 'Pro_total_item': line.price_unit, 'Pro_qty': 0, 'Pro_precio_uni': 0, 'Pro_bonificacion': 0, } values.update({'Pro_codigo': line.product_id.default_code or '', 'Pro_total_item': float_repr(line.price_subtotal, precision_digits=2), 'Pro_bonificacion': bonus}) details.append(values) return details def _get_optionals_data(self): optionals = [] # We add CBU to electronic credit invoice if self._is_mipyme_fce() and self.partner_bank_id.acc_type == 'cbu': optionals.append({'Id': 2101, 'Valor': self.partner_bank_id.acc_number}) # We add FCE Is cancellation value only for refund documents if self._is_mipyme_fce_refund(): optionals.append({'Id': 22, 'Valor': self.l10n_ar_afip_fce_is_cancellation and 'S' or 'N'}) transmission_type = self.l10n_ar_fce_transmission_type if self._is_mipyme_fce() and transmission_type: optionals.append({'Id': 27, 'Valor': transmission_type}) return optionals def _get_partner_code_id(self, partner): """ Return the AFIP code of the identification type of the partner. If not identification type and if the partner responsibility is Final Consumer return AFIP it_Sigd identification type (Sin Categoria / Venta Global) """ partner_id_code = partner.l10n_latam_identification_type_id.l10n_ar_afip_code if partner_id_code: return partner_id_code final_consumer = self.env.ref('l10n_ar.res_CF') if partner.l10n_ar_afip_responsibility_type_id == final_consumer: return '99' return partner_id_code def _prepare_return_msg(self, afip_ws, errors, obs, events, return_codes): self.ensure_one() msg = '' if any([errors, obs, events]): if errors: msg += '\n' + _('AFIP Validation Error') + ': %s' % errors if obs and obs != ' ': msg += '\n' + _('AFIP Validation Observation') + ': %s' % obs if events: msg += '\n' + _('AFIP Validation Event') + ': %s' % events hint_msgs = [] for code in return_codes: fix = afip_errors._hint_msg(code, afip_ws) if fix: hint_msgs.append(fix) if hint_msgs: msg += '\n\n' + _('HINT') + ':\n\n * ' + '\n * '.join(hint_msgs) return msg def _ws_verify_request_data(self, client, auth, ws_method, request_data): """ Validate that all the request data sent is ok """ try: client._Client__obj.create_message(client._Client__obj.service, ws_method, auth, request_data) except Exception as error: raise UserError(repr(error)) # Prepare Request Data for webservices def wsfe_get_cae_request(self, client=None): self.ensure_one() partner_id_code = self._get_partner_code_id(self.commercial_partner_id) invoice_number = self._l10n_ar_get_document_number_parts( self.l10n_latam_document_number, self.l10n_latam_document_type_id.code)['invoice_number'] amounts = self._l10n_ar_get_amounts() due_payment_date = self._due_payment_date() service_start, service_end = self._service_dates() related_invoices = self._get_related_invoice_data() vat_items = self._get_vat() for item in vat_items: if 'BaseImp' in item and 'Importe' in item: item['BaseImp'] = float_repr(item['BaseImp'], precision_digits=2) item['Importe'] = float_repr(item['Importe'], precision_digits=2) vat = partner_id_code and self.commercial_partner_id._get_id_number_sanitize() tributes = self._get_tributes() optionals = self._get_optionals_data() ArrayOfAlicIva = client.get_type('ns0:ArrayOfAlicIva') ArrayOfTributo = client.get_type('ns0:ArrayOfTributo') ArrayOfCbteAsoc = client.get_type('ns0:ArrayOfCbteAsoc') ArrayOfOpcional = client.get_type('ns0:ArrayOfOpcional') if self.l10n_latam_document_type_id.code == '6' and ( self.commercial_partner_id.l10n_ar_afip_responsibility_type_id == self.env.ref('l10n_ar.res_EXT') or self.commercial_partner_id.country_id.code not in ['AR', False]): vat = self.get_vat_country() res = {'FeCabReq': { 'CantReg': 1, 'PtoVta': self.journal_id.l10n_ar_afip_pos_number, 'CbteTipo': self.l10n_latam_document_type_id.code}, 'FeDetReq': [{'FECAEDetRequest': { 'Concepto': int(self.l10n_ar_afip_concept), 'DocTipo': partner_id_code or 0, 'DocNro': vat and int(vat) or 0, 'CbteDesde': invoice_number, 'CbteHasta': invoice_number, 'CbteFch': self.invoice_date.strftime(WS_DATE_FORMAT['wsfe']), 'ImpTotal': float_repr(self.amount_total, precision_digits=2), 'ImpTotConc': float_repr(amounts['vat_untaxed_base_amount'], precision_digits=2), # Not Taxed VAT 'ImpNeto': float_repr(amounts['vat_taxable_amount'], precision_digits=2), 'ImpOpEx': float_repr(amounts['vat_exempt_base_amount'], precision_digits=2), 'ImpTrib': float_repr(amounts['not_vat_taxes_amount'], precision_digits=2), 'ImpIVA': float_repr(amounts['vat_amount'], precision_digits=2), # Service dates are only informed when AFIP Concept is (2,3) 'FchServDesde': service_start.strftime(WS_DATE_FORMAT['wsfe']) if service_start else False, 'FchServHasta': service_end.strftime(WS_DATE_FORMAT['wsfe']) if service_end else False, 'FchVtoPago': due_payment_date.strftime(WS_DATE_FORMAT['wsfe']) if due_payment_date else False, 'MonId': self.currency_id.l10n_ar_afip_code, 'MonCotiz': float_repr(self.l10n_ar_currency_rate, precision_digits=6), 'CbtesAsoc': ArrayOfCbteAsoc([related_invoices]) if related_invoices else None, 'Iva': ArrayOfAlicIva(vat_items) if vat_items else None, 'Tributos': ArrayOfTributo(tributes) if tributes else None, 'Opcionales': ArrayOfOpcional(optionals) if optionals else None, 'Compradores': None}}]} return res def get_vat_country(self): """ CUIT PAIS: Is default VAT(CUIT) that AFIP define per country to identify a foreign country partner, We have 3 CUIT PAIS per contry: one for legal entities, one for natural person and others. Returns (int) number CUIT PAIS of the related partner, Is not CUIT PAIS then return 0 """ vat_country = 0 partner = self.commercial_partner_id if partner.country_id.code not in ['AR', False]: vat_country = partner.country_id.l10n_ar_legal_entity_vat if partner.is_company \ else partner.country_id.l10n_ar_natural_vat return vat_country def wsfex_get_cae_request(self, last_id, client): if not self.commercial_partner_id.country_id: raise UserError(_('For WS "%s" country is required on partner', self.journal_id.l10n_ar_afip_ws)) elif not self.commercial_partner_id.country_id.code: raise UserError(_('For WS "%s" country code is mandatory country: %s', self.journal_id.l10n_ar_afip_ws, self.commercial_partner_id.country_id.name)) elif not self.commercial_partner_id.country_id.l10n_ar_afip_code: hint_msg = afip_errors._hint_msg('country_afip_code', self.journal_id.l10n_ar_afip_ws) msg = _('For "%s" WS the afip code country is mandatory: %s', self.journal_id.l10n_ar_afip_ws, self.commercial_partner_id.country_id.name) if hint_msg: msg += '\n\n' + hint_msg raise RedirectWarning(msg, self.env.ref('l10n_ar_edi.action_help_afip').id, _('Go to AFIP page')) related_invoices = self._get_related_invoice_data() ArrayOfItem = client.get_type('ns0:ArrayOfItem') ArrayOfCmp_asoc = client.get_type('ns0:ArrayOfCmp_asoc') res = {'Id': last_id, 'Fecha_cbte': self.invoice_date.strftime(WS_DATE_FORMAT['wsfex']), 'Cbte_Tipo': self.l10n_latam_document_type_id.code, 'Punto_vta': self.journal_id.l10n_ar_afip_pos_number, 'Cbte_nro': self._l10n_ar_get_document_number_parts( self.l10n_latam_document_number, self.l10n_latam_document_type_id.code)['invoice_number'], 'Tipo_expo': int(self.l10n_ar_afip_concept), 'permisos': None, 'Dst_cmp': self.commercial_partner_id.country_id.l10n_ar_afip_code, 'Cliente': self.commercial_partner_id.name, 'Domicilio_cliente': " - ".join([ self.commercial_partner_id.name or '', self.commercial_partner_id.street or '', self.commercial_partner_id.street2 or '', self.commercial_partner_id.zip or '', self.commercial_partner_id.city or '']), 'Id_impositivo': self.commercial_partner_id.vat or "", 'Cuit_pais_cliente': self.get_vat_country(), 'Moneda_Id': self.currency_id.l10n_ar_afip_code, 'Moneda_ctz': float_repr(self.l10n_ar_currency_rate, precision_digits=6), 'Obs_comerciales': self.invoice_payment_term_id.name if self.invoice_payment_term_id else None, 'Imp_total': float_repr(self.amount_total, precision_digits=2), 'Obs': html2plaintext(self.narration), 'Forma_pago': self.invoice_payment_term_id.name if self.invoice_payment_term_id else None, 'Idioma_cbte': 1, # invoice language: spanish / espaƱol 'Incoterms': self.invoice_incoterm_id.code if self.invoice_incoterm_id else None, # incoterms_ds only admit max 20 characters admite 'Incoterms_Ds': self.invoice_incoterm_id.name[:20] if self.invoice_incoterm_id and self.invoice_incoterm_id.name else None, # Is required only when afip concept = 1 (Products/Exportation) and if doc code = 19, for all the rest we # pass empty string. At the moment we do not have feature to manage permission Id or send 'S' 'Permiso_existente': "N" if int(self.l10n_latam_document_type_id.code) == 19 and int(self.l10n_ar_afip_concept) == 1 else "", 'Items': ArrayOfItem(self._get_line_details()), 'Cmps_asoc': ArrayOfCmp_asoc([related_invoices]) if related_invoices else None} # 1671 Report fecha_pago with format YYYMMDD # 1672 Is required only doc_type 19. concept (2,4) # 1673 If doc_type != 19 should not be reported. # 1674 doc_type 19 concept (2,4). date should be >= invoice date payment_date = datetime.strftime(self.invoice_date_due, WS_DATE_FORMAT['wsfex']) \ if int(self.l10n_latam_document_type_id.code) == 19 and int(self.l10n_ar_afip_concept) in [2, 4] and self.invoice_date_due else '' if payment_date: res.update({'Fecha_pago': payment_date}) return res def wsbfe_get_cae_request(self, last_id, client=None): partner_id_code = self._get_partner_code_id(self.commercial_partner_id) amounts = self._l10n_ar_get_amounts() related_invoices = self._get_related_invoice_data() ArrayOfItem = client.get_type('ns0:ArrayOfItem') ArrayOfCbteAsoc = client.get_type('ns0:ArrayOfCbteAsoc') vat = partner_id_code and self.commercial_partner_id._get_id_number_sanitize() res = {'Id': last_id, 'Tipo_doc': int(partner_id_code) or 0, 'Nro_doc': vat and int(vat) or 0, 'Zona': 1, # National (the only one returned by AFIP) 'Tipo_cbte': int(self.l10n_latam_document_type_id.code), 'Punto_vta': int(self.journal_id.l10n_ar_afip_pos_number), 'Cbte_nro': self._l10n_ar_get_document_number_parts( self.l10n_latam_document_number, self.l10n_latam_document_type_id.code)['invoice_number'], 'Imp_total': float_round(self.amount_total, precision_digits=2), 'Imp_tot_conc': float_round(amounts['vat_untaxed_base_amount'], precision_digits=2), # Not Taxed VAT 'Imp_neto': float_round(amounts['vat_taxable_amount'], precision_digits=2), 'Impto_liq': amounts['vat_amount'], 'Impto_liq_rni': 0.0, # "no categorizado / responsable no inscripto " figure is not used anymore 'Imp_op_ex': float_round(amounts['vat_exempt_base_amount'], precision_digits=2), 'Imp_perc': amounts['vat_perc_amount'] + amounts['profits_perc_amount'] + amounts['other_perc_amount'], 'Imp_iibb': amounts['iibb_perc_amount'], 'Imp_perc_mun': amounts['mun_perc_amount'], 'Imp_internos': amounts['intern_tax_amount'] + amounts['other_taxes_amount'], 'Imp_moneda_Id': self.currency_id.l10n_ar_afip_code, 'Imp_moneda_ctz': float_repr(self.l10n_ar_currency_rate, precision_digits=6), 'Fecha_cbte': self.invoice_date.strftime(WS_DATE_FORMAT['wsbfe']), 'CbtesAsoc': ArrayOfCbteAsoc([related_invoices]) if related_invoices else None, 'Items': ArrayOfItem(self._get_line_details())} if self.l10n_latam_document_type_id.code in ['201', '206']: # WS4900 res.update({'Fecha_vto_pago': self._due_payment_date().strftime(WS_DATE_FORMAT['wsbfe'])}) optionals = self._get_optionals_data() if optionals: ArrayOfOpcional = client.get_type('ns0:ArrayOfOpcional') res.update({'Opcionales': ArrayOfOpcional(optionals)}) return res def _is_argentina_electronic_invoice(self): return bool(self.journal_id.l10n_latam_use_documents and self.env.company.account_fiscal_country_id.code == "AR" and self.journal_id.l10n_ar_afip_ws) def _get_last_sequence_from_afip(self): """ This method is called to return the highest number for electronic invoices, it will try to connect to AFIP only if it is necessary (when we are validating the invoice and need to set the document number)""" last_number = 0 if self._is_dummy_afip_validation() or self.l10n_latam_document_number \ else self.journal_id._l10n_ar_get_afip_last_invoice_number(self.l10n_latam_document_type_id) return "%s %05d-%08d" % (self.l10n_latam_document_type_id.doc_code_prefix, self.journal_id.l10n_ar_afip_pos_number, last_number) def _get_last_sequence(self, relaxed=False, with_prefix=None): """ For argentina electronic invoice, if there is not sequence already then consult the last number from AFIP @return: string with the sequence, something like 'FA-A 00001-00000011' """ res = super()._get_last_sequence(relaxed=relaxed, with_prefix=with_prefix) if not res and self._is_argentina_electronic_invoice() and self.l10n_latam_document_type_id: res = self._get_last_sequence_from_afip() return res