Files
enterprise-addons/l10n_ar_edi/models/account_move.py
T

998 lines
57 KiB
Python

# 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 odoo.tools.float_utils import float_compare
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='ARCA Authorization Mode', copy=False,
help="This is the type of ARCA 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 ARCA 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 ARCA 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 ARCA 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 ARCA 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 ARCA 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 ARCA.")
l10n_ar_afip_qr_code = fields.Char(compute='_compute_l10n_ar_afip_qr_code', string='ARCA QR Code',
help='This QR code is mandatory by the ARCA 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 ARCA'), ('O', 'Accepted in ARCA with Observations')], 'Result',
copy=False, help="Argentina: Result of the electronic invoice request to the ARCA 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 ARCA
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='ARCA 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 ARCA 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")
l10n_ar_payment_foreign_currency_default = fields.Selection(related="company_id.l10n_ar_payment_foreign_currency")
l10n_ar_payment_foreign_currency = fields.Selection(
selection=[("Yes", "Yes"), ("No", "No")],
compute="_compute_l10n_ar_payment_foreign_currency",
string="Payment in Foreign Currency")
l10n_ar_currency_code = fields.Char("Currency Code", related="currency_id.name")
@api.depends("currency_id", "line_ids")
def _compute_l10n_ar_payment_foreign_currency(self):
""" Compute that let us know in the payment will be done in Foreign currency
Only applies to sale EDI invoices and depends on the company default value
if company default values is:
* No: all the invoices will be send as Payment Foreign Currency = No
* Yes: all the invoices will be send as Payment Foreign Currency = Yes
* Account's Dependent: Will be Yes or No will depend on the payment lines, if they use an a account we
a forced currency then will send Yes, but for a regular account will be No
"""
for move in self:
if not move.is_sale_document() or move.country_code != 'AR' or not move.journal_id.l10n_ar_afip_ws \
or move.currency_id == move.company_currency_id:
move.l10n_ar_payment_foreign_currency = False
else:
value = move.l10n_ar_payment_foreign_currency_default
if move.l10n_ar_payment_foreign_currency_default == "account":
payment_terms = move.line_ids.filtered(lambda aml: aml.display_type == 'payment_term')
account = payment_terms.account_id[:1]
value = "Yes" if account.currency_id and account.currency_id != move.company_currency_id else "No"
move.l10n_ar_payment_foreign_currency = value
# -------------------------------------------------------------------------
# Compute methods
# -------------------------------------------------------------------------
@api.depends('l10n_ar_afip_result')
def _compute_show_reset_to_draft_button(self):
"""
EXTENDS 'account.move'
When the ARCA 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(1 / rec.invoice_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 ARCA """
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_id
def _post(self, soft=True):
""" After validate the invoice we then validate in ARCA. The last thing we do is request the cae because if an
error occurs after CAE requested, the invoice has been already validated on ARCA """
ar_invoices = self.filtered(lambda x: x.is_invoice() and x.country_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')
if ar_bills := ar_invoices - sale_ar_invoices:
ar_bills._l10n_ar_check_afip_auth_verify_required()
# Send invoices to ARCA 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 ARCA then we make commit because we need to save the information returned by ARCA
# in Odoo for consistency, this way if an error ocurrs later in another invoice we will have the ones
# correctly validated in ARCA in Odoo (CAE, Result, xml response/request).
if not self.env.context.get('l10n_ar_invoice_skip_commit'):
self.env.cr.commit()
if error_invoice:
if error_invoice.exists():
msg = _('We couldn\'t validate the document "%(partner_name)s" (Draft Invoice *%(invoice)s) in ARCA',
partner_name=error_invoice.partner_id.name, invoice=error_invoice.id)
else:
msg = _('We couldn\'t validate the invoice in ARCA.')
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 ARCA 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([
_('%(item)s: "%(partner)s" amount %(amount)s', item=item.display_name, partner=item.partner_id.name, amount=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_edi_get_request_data_verify(self):
self.ensure_one()
if not self.l10n_ar_afip_auth_mode or not self.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 = (self.commercial_partner_id, self.company_id.partner_id) \
if self.move_type in ['in_invoice', 'in_refund'] else (self.company_id.partner_id, self.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 self.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(self.l10n_latam_document_number, self.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 self.l10n_latam_document_type_id.code:
raise UserError(_('No document type selected or document type is not available for validation!'))
if not self.invoice_date:
raise UserError(_('Invoice Date is required!'))
return {
'CbteModo': self.l10n_ar_afip_auth_mode,
'CuitEmisor': issuer_vat,
'PtoVta': document_parts['point_of_sale'],
'CbteTipo': self.l10n_latam_document_type_id.code,
'CbteNro': document_parts['invoice_number'],
'CbteFch': self.invoice_date.strftime('%Y%m%d'),
'ImpTotal': float_repr(self.amount_total, precision_digits=2),
'CodAutorizacion': self.l10n_ar_afip_auth_code,
'DocTipoReceptor': receptor_identification_code,
'DocNroReceptor': receptor_id_number,
}
def l10n_ar_verify_on_afip(self):
""" This method let us to connect to ARCA using WSCDC webservice to verify if a vendor bill is valid on ARCA """
for inv in self:
verfiy_request_data = inv._l10n_ar_edi_get_request_data_verify()
connection = self.company_id._l10n_ar_get_connection('wscdc')
client, auth = connection._get_client()
response = client.service.ComprobanteConstatar(auth, verfiy_request_data)
inv.write({'l10n_ar_afip_verification_result': response.Resultado})
if response.Observaciones or response.Errors:
inv.message_post(body=_('ARCA authorization verification result: %(observations)s%(errors)s', observations=response.Observaciones, errors=response.Errors))
def l10n_ar_check_rate(self):
""" If the user indicates that the payment will be done in foreign currency (option YES) then ARCA force that
the rate used is exactly the same as the last business day (if date in future then do not report the rate)
We alert the user and show them the correct rate and date so they can fixed in the currency config to continue
with the invoice validation """
self.ensure_one()
if self.l10n_ar_payment_foreign_currency == "Yes":
arca_date, arca_rate = self.currency_id._l10n_ar_get_last_business_day_rate(
self.journal_id.l10n_ar_afip_ws, self.invoice_date)
# WSFE 10119 / WSFEX 1667 / WSBFE 1014: Extra Show that the rates are out of allowed margin
min_rate = arca_rate - (arca_rate * 0.02)
max_rate = arca_rate * 400
if not (min_rate <= 1 / self.invoice_currency_rate <= max_rate):
raise UserError(_(
"The currency rate to be reported (%(currency_rate)s) is not valid. It must be between 2%% and 400%% of"
" the official quote (%(min_rate)s - %(max_rate)s)",
currency_rate=float_repr(1 / self.invoice_currency_rate, precision_digits=3),
min_rate=float_repr(min_rate, precision_digits=3),
max_rate=float_repr(max_rate, precision_digits=3),
))
# WSFE 10038 / WSFEX 1604
if self.l10n_ar_payment_foreign_currency == "Yes" and float_compare(
1 / self.invoice_currency_rate, arca_rate, precision_digits=3) != 0:
raise UserError(_(
"The rate to be reported (%(currency_rate)s) differs from that of ARCA Remember that if you pay"
" in foreign currency you must use the same rate of the last business day of ARCA (%(arca_rate)s - %(arca_date)s)",
currency_rate=float_repr(1 / self.invoice_currency_rate, precision_digits=3),
arca_rate=arca_rate,
arca_date=arca_date,
))
# -------------------------------------------------------------------------
# Main methods
# -------------------------------------------------------------------------
def _l10n_ar_do_afip_ws_request_cae(self, client, auth, transport):
""" Submits the invoice information to ARCA and gets a response of ARCA in return.
If we receive a positive response from ARCA 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 ARCA 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 = ''
return_codes = []
values = {}
self.l10n_ar_check_rate()
# 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('<p><b>%s%s</b></p>') % (_('ARCA Messages'), plaintext2html(return_info, 'em')))
# -------------------------------------------------------------------------
# Helpers
# -------------------------------------------------------------------------
def _dummy_afip_validation(self):
""" Only when we want to skip ARCA validation in testing environment. Fill the ARCA fields with dummy values in
order to continue with the invoice validation without passing to ARCA validations
"""
self.sudo().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 ARCA, if not will try to verify them.
If the invoice is sucessfully verified in ARCA (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 UserError as error:
_logger.warning(error)
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 '
'ARCA verification fail: %s\nPlease verify in ARCA '
'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 ARCA '
'verification fail: %s\nPlease verify in ARCA 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 ARCA (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, base_lines=None):
""" Applies on wsfe web service """
res = []
base_lines = base_lines or []
def tax_grouping_by_tribute_afip_code(_arg_base_line, arg_tax_data):
arg_tax_data = arg_tax_data or {'tax': self.env['account.tax']}
return {
'tribute_afip_code': arg_tax_data['tax'].tax_group_id.l10n_ar_tribute_afip_code,
'group_name': arg_tax_data['tax'].tax_group_id.name,
}
base_lines_aggregated_values = self.env['account.tax']._aggregate_base_lines_tax_details(base_lines, tax_grouping_by_tribute_afip_code)
aggregated_tax_details = self.env['account.tax']._aggregate_base_lines_aggregated_values(base_lines_aggregated_values)
for grouping_key, values in aggregated_tax_details.items():
if grouping_key['tribute_afip_code']:
res.append({
'Id': grouping_key['tribute_afip_code'],
'Alic': 0,
'Desc': grouping_key['group_name'],
'BaseImp': float_round(values['base_amount_currency'], precision_digits=2),
'Importe': float_round(values['tax_amount_currency'], precision_digits=2),
})
return res
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, base_lines=None):
""" Used only in wsbfe and wsfex """
self.ensure_one()
details = []
afip_ws = self.journal_id.l10n_ar_afip_ws
uom_precision_digits = min(self.env['decimal.precision'].precision_get('Product Unit of Measure'), 2)
price_precision_digits = min(self.env['decimal.precision'].precision_get('Product Price'), 3)
for base_line in base_lines:
line = base_line['record']
# 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 ARCA code in %s UOM', line.product_uom_id.name))
product_afip_code = line.product_uom_id.l10n_ar_afip_code
unit_price_truncated_amount = float_repr(line.price_unit, precision_digits=price_precision_digits)
values = {
'Pro_ds': line.name,
'Pro_qty': float_repr(base_line['quantity'], precision_digits=uom_precision_digits),
'Pro_umed': product_afip_code,
'Pro_precio_uni': unit_price_truncated_amount,
}
# We compute bonus by subtracting theoretical minus amount
bonus_amount = (
base_line['discount'] and
(float(unit_price_truncated_amount) * base_line['quantity'] - base_line['tax_details']['raw_total_excluded_currency']) or
0.0
)
if afip_ws == 'wsbfe':
if not line.product_id.uom_id.l10n_ar_afip_code:
raise UserError(_('No ARCA 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)
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': float_repr(bonus_amount, precision_digits=2),
'Iva_id': vat_tax.tax_group_id.l10n_ar_vat_afip_code,
'Imp_total': float_repr(base_line['tax_details']['total_included_currency'], precision_digits=2),
})
elif afip_ws == 'wsfex':
if product_afip_code not in ('97', '99', '00'):
if line._get_downpayment_lines():
product_afip_code = '97'
elif line.price_unit < 0:
product_afip_code = '99'
if product_afip_code in ('97', '99', '00'):
values.update({
'Pro_qty': '0.00',
'Pro_precio_uni': '0.00',
})
values.update({
'Pro_codigo': line.product_id.default_code or '',
'Pro_total_item': float_repr(
base_line['tax_details']['total_excluded_currency'] + base_line['tax_details']['delta_total_excluded_currency'],
precision_digits=2,
),
'Pro_bonificacion': float_repr(bonus_amount, precision_digits=2),
})
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 ARCA code of the identification type of the partner.
If not identification type and if the partner responsibility is Final Consumer return
ARCA it_Sigd identification type (Sin Categoria / Venta Global)
"""
partner_id_code = partner.l10n_latam_identification_type_id.l10n_ar_afip_code
final_consumer = self.env.ref('l10n_ar.res_CF')
partner_number = partner._get_id_number_sanitize()
if partner.l10n_ar_afip_responsibility_type_id == final_consumer and not partner_number:
return '99'
if partner_id_code:
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]):
messages = []
if errors:
messages.append(_("ARCA Validation Error: %(error)s", error=errors))
if obs and obs != " ":
messages.append(_("ARCA Validation Observation: %(obs)s", obs=obs))
if events:
messages.append(_("ARCA Validation Event: %(event)s", event=events))
hint_msgs = []
for code in return_codes:
fix = afip_errors._hint_msg(self.env, code, afip_ws)
if fix:
hint_msgs.append(fix)
if hint_msgs:
messages.append("\n\n" + _("HINT") + ":\n\n * " + "\n * ".join(hint_msgs))
msg = "\n".join(messages)
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))
def _l10n_ar_check_final_consumer_doc_type(self, vat):
self.ensure_one()
partner = self.commercial_partner_id
if (
partner.country_id.code == 'AR'
and partner.l10n_ar_afip_responsibility_type_id == self.env.ref('l10n_ar.res_CF')
and not str(vat).isdigit()
):
raise UserError(_(
'For Argentinean contacts with AFIP Responsibility Type "Consumidor Final", the identification type must be DNI or CUIL/CUIT.'
))
# -------------------------------------------------------------------------
# Prepare Request Data for webservices
# -------------------------------------------------------------------------
def wsfe_get_cae_request(self, client=None):
self.ensure_one()
base_lines, _tax_lines = self._get_rounded_base_and_tax_lines()
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(base_lines=base_lines)
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(base_lines=base_lines)
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(base_lines=base_lines)
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 in ['6', '7', '8'] 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()
self._l10n_ar_check_final_consumer_doc_type(vat)
res = {
'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 ARCA 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(1 / self.invoice_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,
'CondicionIVAReceptorId': self.partner_id.l10n_ar_afip_responsibility_type_id.code,
'Compradores': None,
}
if res.get('MonId') != 'PES': # WSFE 10241
# if currency date in future then do not send MonCotiz
res['CanMisMonExt'] = {"Yes": "S", "No": "N"}.get(self.l10n_ar_payment_foreign_currency)
if self.l10n_ar_payment_foreign_currency == "Yes" and self.invoice_date > fields.Date.context_today(self):
res.pop('MonCotiz')
return {
'FeCabReq': {
'CantReg': 1,
'PtoVta': self.journal_id.l10n_ar_afip_pos_number,
'CbteTipo': self.l10n_latam_document_type_id.code,
},
'FeDetReq': [{'FECAEDetRequest': res}],
}
def get_vat_country(self):
""" CUIT PAIS: Is default VAT(CUIT) that ARCA 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 or 0
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 "%(ws)s" country code is mandatory country: %(country)s', ws=self.journal_id.l10n_ar_afip_ws,
country=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(self.env, 'country_afip_code', self.journal_id.l10n_ar_afip_ws)
msg = _('For "%(ws)s" WS the afip code country is mandatory: %(country)s', ws=self.journal_id.l10n_ar_afip_ws,
country=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 ARCA page'))
related_invoices = self._get_related_invoice_data()
base_lines, _tax_lines = self._get_rounded_base_and_tax_lines()
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(1 / self.invoice_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(base_lines=base_lines)),
'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})
if res.get("Moneda_Id") != "PES" and res.get("Cbte_Tipo") == "19":
res['CanMisMonExt'] = {"Yes": "S", "No": "N"}.get(self.l10n_ar_payment_foreign_currency)
return res
def wsbfe_get_cae_request(self, last_id, client=None):
partner_id_code = self._get_partner_code_id(self.commercial_partner_id)
base_lines, _tax_lines = self._get_rounded_base_and_tax_lines()
amounts = self._l10n_ar_get_amounts(base_lines=base_lines)
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()
self._l10n_ar_check_final_consumer_doc_type(vat)
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 ARCA)
'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_repr(self.amount_total, precision_digits=2),
'Imp_tot_conc': float_repr(amounts['vat_untaxed_base_amount'], precision_digits=2), # Not Taxed VAT
'Imp_neto': float_repr(amounts['vat_taxable_amount'], precision_digits=2),
'Impto_liq': float_repr(amounts['vat_amount'], precision_digits=2),
'Impto_liq_rni': float_repr(0.0, precision_digits=2), # "no categorizado / responsable no inscripto " figure is not used anymore
'Imp_op_ex': float_repr(amounts['vat_exempt_base_amount'], precision_digits=2),
'Imp_perc': float_repr(amounts['vat_perc_amount'] + amounts['profits_perc_amount'] + amounts['other_perc_amount'], precision_digits=2),
'Imp_iibb': float_repr(amounts['iibb_perc_amount'], precision_digits=2),
'Imp_perc_mun': float_repr(amounts['mun_perc_amount'], precision_digits=2),
'Imp_internos': float_repr(amounts['intern_tax_amount'] + amounts['other_taxes_amount'], precision_digits=2),
'Imp_moneda_Id': self.currency_id.l10n_ar_afip_code,
'Imp_moneda_ctz': float_repr(1 / self.invoice_currency_rate, precision_digits=6),
'Fecha_cbte': self.invoice_date.strftime(WS_DATE_FORMAT['wsbfe']),
'CbtesAsoc': ArrayOfCbteAsoc([related_invoices]) if related_invoices else None,
'CondicionIVAReceptorId': int(self.partner_id.l10n_ar_afip_responsibility_type_id.code),
'Items': ArrayOfItem(self._get_line_details(base_lines=base_lines)),
}
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)})
if res.get("Imp_moneda_Id") != "PES":
res['CanMisMonExt'] = {"Yes": "S", "No": "N"}.get(self.l10n_ar_payment_foreign_currency)
return res
def _is_argentina_electronic_invoice(self):
return bool(self.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 ARCA
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 ARCA
@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