forked from Mapan/odoo17e
842 lines
49 KiB
Python
842 lines
49 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 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('<p><b>%s%s</b></p>') % (_('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
|