l10n_ar_afipws_fe/models/invoice.py
# -*- coding: utf-8 -*-
##############################################################################
# For copyright and license notices, see __openerp__.py file in module root
# directory
##############################################################################
from pyi25 import PyI25
from openerp import fields, models, api, _
from openerp.exceptions import Warning
from cStringIO import StringIO as StringIO
import logging
import sys
import traceback
_logger = logging.getLogger(__name__)
try:
from pysimplesoap.client import SoapFault
except ImportError:
_logger.debug('Can not `from pyafipws.soap import SoapFault`.')
class invoice(models.Model):
_inherit = "account.invoice"
afip_auth_verify_type = fields.Selection(
related='company_id.afip_auth_verify_type',
readonly=True,
)
afip_batch_number = fields.Integer(
copy=False,
string='Batch Number',
readonly=True
)
afip_auth_verify_result = fields.Selection([
('A', 'Aprobado'), ('O', 'Observado'), ('R', 'Rechazado')],
string='AFIP authorization verification result',
copy=False,
readonly=True,
)
afip_auth_verify_observation = fields.Char(
string='AFIP authorization verification observation',
copy=False,
readonly=True,
)
afip_auth_mode = fields.Selection([
('CAE', 'CAE'), ('CAI', 'CAI'), ('CAEA', 'CAEA')],
string='AFIP authorization mode',
copy=False,
readonly=True,
states={'draft': [('readonly', False)]},
)
afip_auth_code = fields.Char(
copy=False,
string='CAE/CAI/CAEA Code',
readonly=True,
oldname='afip_cae',
size=24,
states={'draft': [('readonly', False)]},
)
afip_auth_code_due = fields.Date(
copy=False,
readonly=True,
oldname='afip_cae_due',
string='CAE/CAI/CAEA due Date',
states={'draft': [('readonly', False)]},
)
# for compatibility
afip_cae = fields.Char(
related='afip_auth_code'
)
afip_cae_due = fields.Date(
related='afip_auth_code_due'
)
afip_barcode = fields.Char(
compute='_get_barcode',
string=_('AFIP Barcode')
)
afip_barcode_img = fields.Binary(
compute='_get_barcode',
string=_('AFIP Barcode Image')
)
afip_message = fields.Text(
string='AFIP Message',
copy=False,
)
afip_xml_request = fields.Text(
string='AFIP XML Request',
copy=False,
)
afip_xml_response = fields.Text(
string='AFIP XML Response',
copy=False,
)
afip_result = fields.Selection([
('', 'n/a'),
('A', 'Aceptado'),
('R', 'Rechazado'),
('O', 'Observado')],
'Resultado',
readonly=True,
states={'draft': [('readonly', False)]},
copy=False,
help="AFIP request result"
)
validation_type = fields.Char(
'Validation Type',
compute='get_validation_type',
)
@api.one
def get_validation_type(self):
# for compatibility with account_invoice_operation, if module installed
# and there are operations we return no_validation so no validate
# button is displayed
if self._fields.get('operation_ids') and self.operation_ids:
self.validation_type = 'no_validation'
# if invoice has cae then me dont validate it against afip
elif self.journal_id.point_of_sale_id.afip_ws and not self.afip_auth_code:
self.validation_type = self.env[
'res.company']._get_environment_type()
@api.one
@api.depends('afip_auth_code')
def _get_barcode(self):
barcode = False
if self.afip_auth_code:
cae_due = ''.join(
[c for c in str(self.afip_auth_code_due or '') if c.isdigit()])
barcode = ''.join(
[str(self.company_id.partner_id.vat[2:]),
"%02d" % int(self.afip_document_class_id.afip_code),
"%04d" % int(self.journal_id.point_of_sale_id.number),
str(self.afip_auth_code), cae_due])
barcode = barcode + self.verification_digit_modulo10(barcode)
self.afip_barcode = barcode
self.afip_barcode_img = self._make_image_I25(barcode)
@api.model
def _make_image_I25(self, barcode):
"Generate the required barcode Interleaved of 7 image using PIL"
image = False
if barcode:
# create the helper:
pyi25 = PyI25()
output = StringIO()
# call the helper:
bars = ''.join([c for c in barcode if c.isdigit()])
if not bars:
bars = "00"
pyi25.GenerarImagen(bars, output, extension="PNG")
# get the result and encode it for openerp binary field:
image = output.getvalue()
image = output.getvalue().encode("base64")
output.close()
return image
@api.model
def verification_digit_modulo10(self, code):
"Calculate the verification digit 'modulo 10'"
# Step 1: sum all digits in odd positions, left to right
code = code.strip()
if not code or not code.isdigit():
return ''
etapa1 = sum([int(c) for i, c in enumerate(code) if not i % 2])
# Step 2: multiply the step 1 sum by 3
etapa2 = etapa1 * 3
# Step 3: start from the left, sum all the digits in even positions
etapa3 = sum([int(c) for i, c in enumerate(code) if i % 2])
# Step 4: sum the results of step 2 and 3
etapa4 = etapa2 + etapa3
# Step 5: the minimun value that summed to step 4 is a multiple of 10
digito = 10 - (etapa4 - (int(etapa4 / 10) * 10))
if digito == 10:
digito = 0
return str(digito)
@api.multi
def get_related_invoices_data(self):
"""
List related invoice information to fill CbtesAsoc.
"""
self.ensure_one()
rel_invoices = self.search([
('number', '=', self.origin),
('state', 'not in',
['draft', 'proforma', 'proforma2', 'cancel'])])
return rel_invoices
@api.multi
# def invoice_validate(self):
def action_number(self):
"""
We would prefere use invoice_validate or call request cae after
action_number so that CAE is requested at the last part but raise error
can not fall back the sequence next number.
We want to add a inv._cr.commit() after sucesfuul cae request because
we dont want to loose cae data because of a raise error on next steps
but it doesn work as expected
"""
self.check_afip_auth_verify_required()
self.do_pyafipws_request_cae()
# self._cr.commit()
res = super(invoice, self).action_number()
return res
@api.multi
def check_afip_auth_verify_required(self):
for inv in self:
if (
inv.type in ['in_invoice', 'in_refund'] and
inv.afip_auth_verify_type == 'required' and
inv.document_type in [
'invoice', 'debit_note', 'credit_note',
'receipt_invoice'] and
not inv.afip_auth_verify_result):
raise Warning(_(
'You can not validate invoice as AFIP authorization '
'verification is required'))
@api.multi
def verify_on_afip(self):
"""
cbte_modo = "CAE" # modalidad de emision: CAI, CAE,
CAEA
cuit_emisor = "20267565393" # proveedor
pto_vta = 4002 # punto de venta habilitado en AFIP
cbte_tipo = 1 # 1: factura A (ver tabla de parametros)
cbte_nro = 109 # numero de factura
cbte_fch = "20131227" # fecha en formato aaaammdd
imp_total = "121.0" # importe total
cod_autorizacion = "63523178385550" # numero de CAI, CAE o CAEA
doc_tipo_receptor = 80 # CUIT (obligatorio Facturas A o M)
doc_nro_receptor = "30628789661" # numero de CUIT del cliente
ok = wscdc.ConstatarComprobante(
cbte_modo, cuit_emisor, pto_vta, cbte_tipo,
cbte_nro, cbte_fch, imp_total, cod_autorizacion,
doc_tipo_receptor, doc_nro_receptor)
print "Resultado:", wscdc.Resultado
print "Mensaje de Error:", wscdc.ErrMsg
print "Observaciones:", wscdc.Obs
"""
afip_ws = "wscdc"
ws = self.company_id.get_connection(afip_ws).connect()
for inv in self:
cbte_modo = inv.afip_auth_mode
cod_autorizacion = inv.afip_auth_code
if not cbte_modo or not cod_autorizacion:
raise Warning(_(
'AFIP authorization mode and Code are required!'))
# get issuer and receptor depending on supplier or customer invoice
if inv.type in ['in_invoice', 'in_refund']:
issuer = inv.commercial_partner_id
receptor = inv.company_id.partner_id
else:
issuer = inv.company_id.partner_id
receptor = inv.commercial_partner_id
issuer_doc_code = str(issuer.document_type_id.afip_code)
cuit_emisor = issuer.document_number
if issuer_doc_code != '80' or not cuit_emisor:
raise Warning(_('Issuer must have a CUIT configured'))
receptor_doc_code = str(receptor.document_type_id.afip_code)
doc_tipo_receptor = receptor_doc_code or '99'
doc_nro_receptor = (
receptor_doc_code and receptor.document_number or "0")
afip_doc_class = inv.afip_document_class_id
if (
afip_doc_class.document_letter_id.name in ['A', 'M'] and
doc_tipo_receptor != '80' or not doc_nro_receptor):
raise Warning(_(
'Para Comprobantes tipo A o tipo M:\n'
'* el documento del receptor debe ser CUIT\n'
'* el documento del Receptor es obligatorio\n'
))
cbte_nro = inv.invoice_number
pto_vta = inv.point_of_sale
cbte_tipo = afip_doc_class.afip_code
if not pto_vta or not cbte_nro or not cbte_tipo:
raise Warning(_(
'Point of sale and document number and document type '
'are required!'))
cbte_fch = inv.date_invoice
if not cbte_fch:
raise Warning(_('Invoice Date is required!'))
cbte_fch = cbte_fch.replace("-", "")
imp_total = str("%.2f" % abs(inv.amount_total))
_logger.info('Constatando Comprobante en afip')
# atrapado de errores en afip
msg = False
try:
ws.ConstatarComprobante(
cbte_modo, cuit_emisor, pto_vta, cbte_tipo, cbte_nro,
cbte_fch, imp_total, cod_autorizacion, doc_tipo_receptor,
doc_nro_receptor)
except SoapFault as fault:
msg = 'Falla SOAP %s: %s' % (
fault.faultcode, fault.faultstring)
except Exception, e:
msg = e
except Exception:
if ws.Excepcion:
# get the exception already parsed by the helper
msg = ws.Excepcion
else:
# avoid encoding problem when raising error
msg = traceback.format_exception_only(
sys.exc_type,
sys.exc_value)[0]
if msg:
raise Warning(_('AFIP Verification Error. %s' % msg))
inv.write({
'afip_auth_verify_result': ws.Resultado,
'afip_auth_verify_observation': '%s%s' % (ws.Obs, ws.ErrMsg)
})
@api.multi
def do_pyafipws_request_cae(self):
"Request to AFIP the invoices' Authorization Electronic Code (CAE)"
for inv in self:
# Ignore invoices with cae
if inv.afip_auth_code and inv.afip_auth_code_due:
continue
afip_ws = inv.journal_id.point_of_sale_id.afip_ws
# Ignore invoice if not ws on point of sale
if not afip_ws:
continue
# get the electronic invoice type, point of sale and afip_ws:
commercial_partner = inv.commercial_partner_id
country = commercial_partner.country_id
journal = inv.journal_id
point_of_sale = journal.point_of_sale_id
pos_number = point_of_sale.number
doc_afip_code = inv.afip_document_class_id.afip_code
# authenticate against AFIP:
ws = inv.company_id.get_connection(afip_ws).connect()
next_invoice_number = inv.next_invoice_number
# get the last invoice number registered in AFIP
if afip_ws == "wsfe" or afip_ws == "wsmtxca":
ws_invoice_number = ws.CompUltimoAutorizado(
doc_afip_code, pos_number)
elif afip_ws == 'wsfex':
ws_invoice_number = ws.GetLastCMP(
doc_afip_code, pos_number)
if not country:
raise Warning(_(
'For WS "%s" country is required on partner' % (
afip_ws)))
elif not country.code:
raise Warning(_(
'For WS "%s" country code is mandatory'
'Country: %s' % (
afip_ws, country.name)))
elif not country.afip_code:
raise Warning(_(
'For WS "%s" country afip code is mandatory'
'Country: %s' % (
afip_ws, country.name)))
ws_next_invoice_number = int(ws_invoice_number) + 1
# verify that the invoice is the next one to be registered in AFIP
if next_invoice_number != ws_next_invoice_number:
raise Warning(_(
'Error!'
'Invoice id: %i'
'Next invoice number should be %i and not %i' % (
inv.id,
ws_next_invoice_number,
next_invoice_number)))
partner_doc_code = commercial_partner.document_type_id.afip_code
tipo_doc = partner_doc_code or '99'
nro_doc = (
partner_doc_code and commercial_partner.document_number or "0")
cbt_desde = cbt_hasta = cbte_nro = next_invoice_number
concepto = tipo_expo = int(inv.afip_concept)
fecha_cbte = inv.date_invoice
if afip_ws != 'wsmtxca':
fecha_cbte = fecha_cbte.replace("-", "")
# due and billing dates only for concept "services"
if int(concepto) != 1:
fecha_venc_pago = inv.date_due
fecha_serv_desde = inv.afip_service_start
fecha_serv_hasta = inv.afip_service_end
if afip_ws != 'wsmtxca':
fecha_venc_pago = fecha_venc_pago.replace("-", "")
fecha_serv_desde = fecha_serv_desde.replace("-", "")
fecha_serv_hasta = fecha_serv_hasta.replace("-", "")
else:
fecha_venc_pago = fecha_serv_desde = fecha_serv_hasta = None
# # invoice amount totals:
imp_total = str("%.2f" % abs(inv.amount_total))
# ImpTotConc es el iva no gravado
imp_tot_conc = str("%.2f" % abs(inv.vat_untaxed))
# en la v9 lo hicimos diferente, aca restamos al vat amount
# lo que seria exento y no gravado
imp_neto = str("%.2f" % abs(
inv.vat_base_amount - inv.vat_untaxed - inv.vat_exempt_amount))
imp_iva = str("%.2f" % abs(inv.vat_amount))
imp_subtotal = str("%.2f" % abs(inv.amount_untaxed))
imp_trib = str("%.2f" % abs(inv.other_taxes_amount))
imp_op_ex = str("%.2f" % abs(inv.vat_exempt_amount))
moneda_id = inv.currency_id.afip_code
moneda_ctz = inv.currency_rate
# moneda_ctz = str(inv.company_id.currency_id.compute(
# 1., inv.currency_id))
# # foreign trade data: export permit, country code, etc.:
if inv.afip_incoterm_id:
incoterms = inv.afip_incoterm_id.afip_code
incoterms_ds = inv.afip_incoterm_id.name
else:
incoterms = incoterms_ds = None
# por lo que verificamos, se pide permiso existente solo
# si es tipo expo 1 y es factura (codigo 19), para todo el
# resto pasamos cadena vacia
if int(doc_afip_code) == 19 and tipo_expo == 1:
# TODO investigar si hay que pasar si ("S")
permiso_existente = "N"
else:
permiso_existente = ""
obs_generales = inv.comment
if inv.payment_term:
forma_pago = inv.payment_term.name
obs_comerciales = inv.payment_term.name
else:
forma_pago = obs_comerciales = None
idioma_cbte = 1 # invoice language: spanish / espaƱol
# customer data (foreign trade):
nombre_cliente = commercial_partner.name
# If argentinian and cuit, then use cuit
if country.code == 'AR' and tipo_doc == 80 and nro_doc:
id_impositivo = nro_doc
cuit_pais_cliente = None
# If not argentinian and vat, use vat
elif country.code != 'AR' and commercial_partner.vat:
id_impositivo = commercial_partner.vat[2:]
cuit_pais_cliente = None
# else use cuit pais cliente
else:
id_impositivo = None
if commercial_partner.is_company:
cuit_pais_cliente = country.cuit_juridica
else:
cuit_pais_cliente = country.cuit_fisica
domicilio_cliente = " - ".join([
commercial_partner.name or '',
commercial_partner.street or '',
commercial_partner.street2 or '',
commercial_partner.zip or '',
commercial_partner.city or '',
])
pais_dst_cmp = commercial_partner.country_id.afip_code
# create the invoice internally in the helper
if afip_ws == 'wsfe':
ws.CrearFactura(
concepto, tipo_doc, nro_doc, doc_afip_code, pos_number,
cbt_desde, cbt_hasta, imp_total, imp_tot_conc, imp_neto,
imp_iva,
imp_trib, imp_op_ex, fecha_cbte, fecha_venc_pago,
fecha_serv_desde, fecha_serv_hasta,
moneda_id, moneda_ctz
)
elif afip_ws == 'wsmtxca':
ws.CrearFactura(
concepto, tipo_doc, nro_doc, doc_afip_code, pos_number,
cbt_desde, cbt_hasta, imp_total, imp_tot_conc, imp_neto,
imp_subtotal, # difference with wsfe
imp_trib, imp_op_ex, fecha_cbte, fecha_venc_pago,
fecha_serv_desde, fecha_serv_hasta,
moneda_id, moneda_ctz,
obs_generales # difference with wsfe
)
elif afip_ws == 'wsfex':
ws.CrearFactura(
doc_afip_code, pos_number, cbte_nro, fecha_cbte,
imp_total, tipo_expo, permiso_existente, pais_dst_cmp,
nombre_cliente, cuit_pais_cliente, domicilio_cliente,
id_impositivo, moneda_id, moneda_ctz, obs_comerciales,
obs_generales, forma_pago, incoterms,
idioma_cbte, incoterms_ds
)
# TODO ver si en realidad tenemos que usar un vat pero no lo
# subimos
if afip_ws != 'wsfex':
for vat in inv.vat_tax_ids:
# we dont send no gravado y exento
if vat.tax_code_id.afip_code in [1, 2]:
continue
_logger.info('Adding VAT %s' % vat.tax_code_id.name)
# use instaed of "base_x" so it is not converted to
# company currency
ws.AgregarIva(
vat.tax_code_id.afip_code,
"%.2f" % abs(vat.base),
"%.2f" % abs(vat.amount),
)
for tax in inv.not_vat_tax_ids:
_logger.info('Adding TAX %s' % tax.tax_code_id.name)
ws.AgregarTributo(
tax.tax_code_id.application_code,
tax.tax_code_id.name,
"%.2f" % abs(tax.base),
# como no tenemos la alicuota pasamos cero, en v9
# podremos pasar la alicuota
0,
"%.2f" % abs(tax.amount),
)
CbteAsoc = inv.get_related_invoices_data()
if CbteAsoc:
ws.AgregarCmpAsoc(
CbteAsoc.afip_document_class_id.afip_code,
CbteAsoc.point_of_sale,
CbteAsoc.invoice_number,
)
# analize line items - invoice detail
# wsfe do not require detail
if afip_ws != 'wsfe':
for line in inv.invoice_line:
codigo = line.product_id.code
# unidad de referencia del producto si se comercializa
# en una unidad distinta a la de consumo
if not line.uos_id.afip_code:
raise Warning(_('Not afip code con producto UOM %s' % (
line.uos_id.name)))
cod_mtx = line.uos_id.afip_code
ds = line.name
qty = line.quantity
umed = line.uos_id.afip_code
precio = line.price_unit
importe = line.price_subtotal
# calculamos bonificacion haciendo teorico menos importe
bonif = line.discount and (precio * qty - importe) or None
if afip_ws == 'wsmtxca':
if not line.product_id.uom_id.afip_code:
raise Warning(_('Not afip code con producto UOM %s' % (
line.product_id.uom_id.name)))
u_mtx = line.product_id.uom_id.afip_code or line.uos_id.afip_code
if inv.invoice_id.type in ('out_invoice', 'in_invoice'):
iva_id = line.vat_tax_ids.tax_code_id.afip_code
else:
iva_id = line.vat_tax_ids.ref_tax_code_id.afip_code
vat_taxes_amounts = line.vat_tax_ids.compute_all(
line.price_unit, line.quantity,
product=line.product_id,
partner=inv.partner_id)
imp_iva = vat_taxes_amounts[
'total_included'] - vat_taxes_amounts['total']
ws.AgregarItem(
u_mtx, cod_mtx, codigo, ds, qty, umed,
precio, bonif, iva_id, imp_iva, importe + imp_iva)
elif afip_ws == 'wsfex':
ws.AgregarItem(
codigo, ds, qty, umed, precio, importe,
bonif)
# Request the authorization! (call the AFIP webservice method)
vto = None
msg = False
try:
if afip_ws == 'wsfe':
ws.CAESolicitar()
vto = ws.Vencimiento
elif afip_ws == 'wsmtxca':
ws.AutorizarComprobante()
vto = ws.Vencimiento
elif afip_ws == 'wsfex':
ws.Authorize(inv.id)
vto = ws.FchVencCAE
except SoapFault as fault:
msg = 'Falla SOAP %s: %s' % (
fault.faultcode, fault.faultstring)
except Exception, e:
msg = e
except Exception:
if ws.Excepcion:
# get the exception already parsed by the helper
msg = ws.Excepcion
else:
# avoid encoding problem when raising error
msg = traceback.format_exception_only(
sys.exc_type,
sys.exc_value)[0]
if msg:
raise Warning(_('AFIP Validation Error. %s' % msg))
msg = u"\n".join([ws.Obs or "", ws.ErrMsg or ""])
if not ws.CAE or ws.Resultado != 'A':
raise Warning(_('AFIP Validation Error. %s' % msg))
# TODO ver que algunso campos no tienen sentido porque solo se
# escribe aca si no hay errores
_logger.info('CAE solicitado con exito. CAE: %s. Resultado %s' % (
ws.CAE, ws.Resultado))
inv.write({
'afip_auth_mode': 'CAE',
'afip_auth_code': ws.CAE,
'afip_auth_code_due': vto,
'afip_result': ws.Resultado,
'afip_message': msg,
'afip_xml_request': ws.XmlRequest,
'afip_xml_response': ws.XmlResponse,
})
class AccountInvoiceRefund(models.TransientModel):
_inherit = "account.invoice.refund"
filter_refund = fields.Selection(
[('refund', 'Create a draft refund')],
)