En pocos días (si no se posterga) entra en vigencia el régimen A122R de retenciones de ingresos brutos de ARBA. La información sobre el mismo la pueden encontrar en este link. Por fortuna, la implementación del mismo se puede hacer mediante web-services y la descripción de como funcionan los mismos la pueden encontrar aca.
La idea de este post es explicar lo básico que se necesita para interactuar con los webservices de ARBA con este régimen. Este post no busca darles un link con el módulo funcionando. La idea es explicar como funcionan algunas cosas en Odoo.
Bien, hay módulos dentro de las diferentes localizaciones argentinas que ya tienen el régimen A122R en cuenta (ya empecé a aprender del tema usando un PR de AdHoc). Ahora en nuestro caso necesitamos trabajar con los webservices de ARBA de forma personalizada ya que tenemos un cliente que trabaja su contabilidad con un sistema en Fox que no tiene soporte de webservices. Entonces necesitabamos de alguna manera de complementar este sistema Fox, permitiendo que por cada retención que se registraba, se obtenga la aprobación de la misma por parte de ARBA. Para ello decidimos que cada retención que se grababa en Fox, la misma debía ser persistida en la base de datos PostgreSQL de Odoo. Dicha tabla tiene la siguiente estructura:
CREATE TABLE public.arba_retenciones_fox (
id bigint NOT NULL,
numero_orden_pago character varying(20) NOT NULL,
cuit_contribuyente character varying(11) NOT NULL,
cuit_agente character varying(11) NOT NULL,
sucursal integer DEFAULT 1 NOT NULL,
alicuota numeric(10,2) NOT NULL,
base_imponible numeric(18,2) NOT NULL,
importe_retencion numeric(18,2) NOT NULL,
razon_social_contribuyente character varying(255),
fecha_operacion timestamp with time zone NOT NULL,
calle character varying(128),
numero character varying(16),
piso character varying(16),
departamento character varying(16),
codigo_postal character varying(16),
localidad character varying(64),
provincia character varying(64),
n_transaccion_agente character varying(40),
procesado boolean DEFAULT false NOT NULL,
fecha_proceso timestamp with time zone,
jurada_id integer,
error_proceso text,
);
En esa table Fox va a persistir cada retención que necesite la aprobación de ARBA. Por nuestra parte, en Odoo vamos a tomar dicha información mediante SQL.
El primer paso es crear un modelo donde se persista la declaración jurada. La misma se debe crear al iniciar la quincena, y agregarse las retenciones a la misma al tiempo que se van creando.
class ArbaDj(models.Model):
_name = 'arba.dj'
_inherit = ['mail.thread','mail.activity.mixin']
_description = "ARBA DDJJ"
name = fields.Char('Nombre',tracking=True)
state = fields.Selection(
[
("draft", "Borrador"),
("open", "Abierta"),
("done", "Procesado"),
],
default="draft",
tracking=True
)
actividad_id = fields.Integer('Actvidad',default=6)
date = fields.Date('Fecha',default=fields.Date.today())
quincena = fields.Selection(selection=[('01','01'),('02','02')],string='Quincena',default='01',tracking=True)
mes = fields.Selection(selection=[('01','01'),('02','02'),('03','03'),('04','04'),('05','05'),('06','06'),('07','07'),('08','08'),('09','09>
anio = fields.Selection(selection=[('2025','2025'),('2026','2026'),('2027','2027'),('2028','2028'),('2029','2029'),('2030','2030')],string=>
jurada_id = fields.Integer('Jurada ID',copy=False)
line_ids = fields.One2many('arba.dj.line','dj_id','Comprobantes')
Esta es la definición de la cabecera de la declaración jurada. Y la misma contiene líneas, siendo cada una de las líneas una retención a ser aprobada por ARBA
class ArbaDjLine(models.Model):
_name = 'arba.dj.line'
_description = 'arba.dj.line'
dj_id = fields.Many2one('arba.dj',string='Declaración jurada')
cuitContribuyente = fields.Char('CUIT Contribuyente')
cuitAgente = fields.Char('CUIT Agente')
sucursal = fields.Char('Sucursal')
alicuota = fields.Float('Alicuota')
baseImponible = fields.Float('Base Imponible')
importeRetencion = fields.Float('Importe Retención')
razonSocialContribuyente = fields.Char('Razón Social Contribuyente')
fechaOperacion = fields.Datetime('fechaOperacion',default=fields.Datetime.now())
calle = fields.Char('calle')
numero = fields.Char('numero')
piso = fields.Char('piso')
departamento = fields.Char('departamento')
codigoPostal = fields.Char('codigoPostal')
localidad = fields.Char('localidad')
provincia = fields.Char('provincia')
nTransaccionAgente = fields.Char('nTransaccionAgente')
arba_id = fields.Integer('arba_id')
pdf_file = fields.Binary('PDF')
Y vamos a describir tres operaciones en este post, que son las relacionadas con ARBA. Estas son crear la declaración jurada, crear un comprobante de retención en ARBA y obtener el PDF de la retención.
Para crear la declaración jurada de ARBA a principios de mes se debe hacer un POST del método declaracionJurada. Para ello primero debo obtener el token que me va a brindar ARBA, y lo hacemos por medio de un método llamado get_token
def get_token(self):
self.ensure_one()
environment_type = self.env['ir.config_parameter'].sudo().get_param('ws_arba_env_type','testing')
ws_url = {
"production": "https://idp.arba.gov.ar/realms/ARBA/protocol/openid-connect/token",
"testing": "https://idp.test.arba.gov.ar/realms/ARBA/protocol/openid-connect/token",
}
url = ws_url.get(environment_type)
company_id = self.env['res.company'].search([])
user = company_id.partner_id.ensure_vat()
password = self.company_id.arba_cit
client_secret = self.company_id.l10n_ar_arba_client_secret
client_id = self.company_id.l10n_ar_arba_client_id
payload = (
f"client_id={client_id}&username={user}&password={password}&client_secret={client_secret}&"
"grant_type=password&scope=arba-profile%20arba-roles%20openid"
)
headers = {"Content-Type": "application/x-www-form-urlencoded"}
response = requests.request("POST", url, headers=headers, data=payload)
token = response.text
return token
Una vez obtenido el token, se puede crear la declaración jurada
def _request_params(self):
"""Computamos los parametros a ser enviados para abrir la declaración en ARBA"""
self.ensure_one()
company_id = self.env['res.company'].search([],limit=1)
return {
"cuitAgente": int(company_id.partner_id.ensure_vat()),
"quincena": int(self.quincena),
"actividadId": int(self.actividad_id),
"anio": int(self.anio),
"mes": int(self.mes),
}
def action_open(self):
self.ensure_one()
if self.state != 'draft':
raise ValidationError('Ya se encuentra abierto')
if self.jurada_id:
raise ValidationError('Declaración jurada iniciada')
token = json.loads(self.get_token()).get('access_token')
payload = self._request_params()
environment_type = self.env['ir.config_parameter'].sudo().get_param('ws_arba_env_type','testing')
ws_url = {
"testing": "https://app2.test.arba.gov.ar/a122rSrv/api/external/declaracionJurada",
"production":"https://app.arba.gov.ar/a122rSrv/api/external/declaracionJurada",
}
url = ws_url[environment_type]
payload = self._request_params()
headers = {
"Content-Type": "application/json",
"Authorization": 'Bearer %s'%(token),
"accept": "*/*",
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code < 400:
rjson = response.json()
self.jurada_id = rjson.get('id')
self.state = 'open'
else:
raise ValidationError('%s %s %s %s %s'%(url,response.status_code,payload,headers,response.text))
Como pueden ver, se crea un método que arma el payload a enviarse a ARBA. Y luego utilizando la librería requests se hace el POST para obtener el ID de la declaración jurada.
Ahora vamos a crear el comprobante en ARBA. Para ello en el modelo de las líneas de retenciones se crea un método _get_params que brinda el payload a utilizar en el POST:
def _request_params(self):
"""Computamos los parametros a ser enviados para abrir la declaración en ARBA"""
self.ensure_one()
company_id = self.env['res.company'].search([],limit=1)
if not self.cuitContribuyente:
raise ValidationError('Debe ingresar el CUIT del contribuyente')
return {
"idDj": self.dj_id.jurada_id,
"cuitAgente": int(company_id.partner_id.ensure_vat()),
"cuitContribuyente": int(self.cuitContribuyente),
"sucursal": "1",
"alicuota": self.alicuota,
"baseImponible": self.baseImponible,
"importeRetencion": self.importeRetencion,
"fechaOperacion": str(self.fechaOperacion).replace(' ','T')[:22],
"calle": self.calle,
"numero": self.numero,
"piso": self.piso,
"departamento": self.departamento,
"codigoPostal": self.codigoPostal,
"localidad": self.localidad,
"provincia": self.provincia,
"nTransaccionAgente": self.nTransaccionAgente,
}
def btnAddComprobante(self):
self.ensure_one()
if self.arba_id:
raise ValidationError('Comprobante ya agregado con ID %s'%(self.arba_id))
if not self.dj_id.jurada_id:
raise ValidationError('No se cuenta con declaracion jurada en ARBA')
if self.dj_id.state != 'open':
raise ValidationError('Declaración jurada en estado incorrecto')
token = json.loads(self.dj_id.get_token()).get('access_token')
payload = self._request_params()
environment_type = self.env['ir.config_parameter'].sudo().get_param('ws_arba_env_type','testing')
ws_url = {
"testing": "https://app2.test.arba.gov.ar/a122rSrv/api/external/comprobante",
"production":"https://app.arba.gov.ar/a122rSrv/api/external/comprobante",
}
#headers = {"Content-Type": "application/x-www-form-urlencoded", "Authorization": token}
response = requests.post(url, headers=headers, json=payload)
if response.status_code < 400:
rjson = response.json()
self.arba_id = rjson.get('id')
Como pueden ver, es otro método POST que crea el comprobante y devuelve su ID.
Por último, vamos a obtener el PDF de la retención y almacenarlo en un campo binario:
def action_download_pdf(self):
self.ensure_one()
if not self.arba_id:
raise ValidationError('Comprobante ya agregado con ID %s'%(self.arba_id))
token = json.loads(self.dj_id.get_token()).get('access_token')
if not self.pdf_file:
environment_type = self.env['ir.config_parameter'].sudo().get_param('ws_arba_env_type','testing')
ws_url = {
"testing": "https://app2.test.arba.gov.ar/a122rSrv/api/external/comprobantePdf",
"production":"https://app.arba.gov.ar/a122rSrv/api/external/comprobantePdf",
}
url = ws_url[environment_type]
payload = {
'id': self.arba_id,
}
headers = {
"Content-Type": "application/json",
"Authorization": 'Bearer %s'%(token),
"accept": "*/*",
}
response = requests.get(url, headers=headers, params=payload)
if response.status_code < 400:
pdf_base64 = base64.b64encode(response.content)
# Store in Odoo binary field
self.write({
'pdf_file': pdf_base64,
})
else:
raise ValidationError('%s %s'%(response.status_code,response.text))
return {
"type": "ir.actions.act_url",
"url": f"/web/content/{self._name}/{self.id}/pdf_file?download=true",
"target": "self",
}
Este método permite descargar el PDF desde una línea de comprobantes de ARBA
Bueno, finalizando quiero agradecer al personal de AdHoc que hicieron su módulo que sirvió de base e inspiración para conocer como resolver este problema y escribir este post.