trytondo-account_invoice_facho/invoice.py

276 lines
9.8 KiB
Python

from trytond.model import fields, ModelView, ModelSQL
from trytond.pool import PoolMeta, Pool
from trytond.pyson import Eval
from trytond.transaction import Transaction
from trytond.rpc import RPC
from trytond.exceptions import UserError
from facho.fe import form
from facho import fe
from facho.fe.client import dian
import io
from datetime import datetime
class Cron(metaclass=PoolMeta):
__name__ = 'ir.cron'
@classmethod
def __setup__(cls):
super().__setup__()
cls.method.selection.extend([
('account.invoice|fe_delivery', 'FE Delivery')
])
class Party(metaclass=PoolMeta):
__name__ = 'party.party'
fe_identifiers = fields.One2Many('account_invoice_facho.party.fe_identifier',
'party', 'Fe Identifiers')
fe_organizations = fields.One2Many('account_invoice_facho.party.fe_organization',
'party', 'Fe Organizations')
fe_identifier = fields.Function(fields.Many2One('account_invoice_facho.party.fe_identifier',
'Fe Identifier'),
'get_fe_identifier')
fe_organization = fields.Function(fields.Many2One('account_invoice_facho.party.fe_organization',
'Fe Organization'),
'get_fe_organization')
def get_fe_identifier(self, name):
try:
return self.fe_identifiers[0]
except IndexError:
return None
def get_fe_organization(self, name):
try:
return self.fe_organizations[0]
except IndexError:
return None
def tofacho(self):
tax_identifier = self.tax_identifier
if tax_identifier is None:
tax_identifier = ''
return form.Party(
name = self.name,
ident = tax_identifier,
responsability_code = self.fe_identifier.fe_code,
organization_code = self.fe_organization.fe_code,
)
class PartyFeOrganization(ModelSQL, ModelView):
'Party Facturacion Electronica Organizacion'
__name__ = 'account_invoice_facho.party.fe_organization'
name = fields.Char('Name', required=True)
party = fields.Many2One('party.party', 'Party', ondelete='CASCADE',
required=True, select=True)
fe_tipo = fields.Many2One('account_invoice_facho.fe_generic_code',
'Tipo Organizacion',
domain=[('resource', '=', 'tipo_organizacion')],
required=True)
@property
def fe_code(self):
return self.fe_tipo.code
class PartyFeIdentifier(ModelSQL, ModelView):
'Party Facturaction Electron Identificador'
__name__ = 'account_invoice_facho.party.fe_identifier'
_rec_name = 'code'
code = fields.Char('Code', required=True)
party = fields.Many2One('party.party', 'Party', ondelete='CASCADE',
required=True, select=True)
fe_tipo = fields.Many2One('account_invoice_facho.fe_generic_code',
'Tipo Responsabilidad',
domain=[('resource', '=', 'tipo_responsabilidad')],
required=True)
@property
def fe_code(self):
return self.fe_tipo.code
class Invoice(metaclass=PoolMeta):
__name__ = 'account.invoice'
_states = {'invisible': ~Eval('is_fe_colombia', False),
'readonly': Eval('state') != 'draft'}
_states_readonly = {'readonly': Eval('state') != 'draft'}
_depends = ['is_fe_colombia', 'state']
is_fe_colombia = fields.Boolean('FE Colombia?',
states={'invisible': False})
fe_habilitacion = fields.Boolean('FE Habilitacion?',
states=_states,
depends=_depends)
# TODO adicionar atributo fe_identifier y permitir seleccion desde form
fe_delivery_state = fields.Selection([
('draft', 'draft'),
('queued', 'Queued'), # local encola
('delivered', 'Delivered'), # remoto encola
('exception', 'Exception'), # local exception
('failure', 'Failure'), # remoto fallo
('done', 'Done') # remoto ok
], 'Delivery State', states=_states_readonly, depends=_depends)
fe_delivery_trackid = fields.Char('Delivery TrackID',
states=_states_readonly,
depends=_depends)
fe_delivery_status = fields.Text('Delivery Status',
states=_states_readonly,
depends=_depends)
fe_delivery_checked_at = fields.DateTime('Delivery Checked At',
states=_states_readonly,
depends=_depends)
del _states, _states_readonly
del _depends
@staticmethod
def default_fe_delivery_state():
return 'draft'
@staticmethod
def default_is_fe_colombia():
return Transaction().context.get('is_fe_colombia', False)
@classmethod
def validate(cls, invoices):
super().validate(invoices)
for invoice in invoices:
for line in invoice.lines:
for taxes, unit_price, quantity in line.taxable_lines:
for tax in taxes:
if tax.type != 'percentage':
raise UserError('Solo se soporta impuesto tipo porcentaje para producto')
facho_invoice = invoice.tofacho()
validator = form.DianResolucion0001Validator()
validator.validate(facho_invoice)
for (model, field, error) in validator.errors:
raise UserError('model %s in field %s has %s' % (model, field, error))
def tofacho(self):
inv = form.Invoice()
inv.set_period(self.invoice_date, self.invoice_date)
inv.set_issue(self.invoice_date)
inv.set_ident(self.number)
inv.set_customer(self.party.tofacho())
inv.set_supplier(self.company.party.tofacho())
for line in self.lines:
inv.add_invoice_line(line.tofacho())
inv.calculate()
return inv
def do_dian_request(self, request, config=None):
if config is None:
config = Pool().get('account_invoice_facho.configuration')(1)
client = dian.DianSignatureClient(config.dian_fe_llave_privada,
config.dian_fe_llave_publica,
password=config.dian_fe_llave_frasepaso)
return client.request(request)
def fe_update_status(self):
config = Pool().get('account_invoice_facho.configuration')(1)
req = dian.GetStatusZip
if config.dian_fe_habilitacion:
req = dian.Habilitacion.GetStatusZip
resp = self.do_dian_request(req(trackId = self.fe_delivery_trackid))
params = {}
params['fe_delivery_status'] = resp.StatusDescription
if resp.IsValid:
params['fe_delivery_state'] = 'donde'
else:
params['fe_delivery_state'] = 'failure'
self._force_write(params)
def _force_write(self, params):
self.state = 'draft'
params['fe_delivery_checked_at'] = datetime.now()
self.write([self], params)
def _dian_zip_io(self, facho_invoice):
xml_invoice = form.DIANInvoiceXML(facho_invoice)
zipdata = io.BytesIO()
with fe.DianZIP(zipdata) as dianzip:
dianzip.add_invoice_xml(facho_invoice.invoice_ident, str(xml_invoice))
zipdata.seek(0)
return zipdata
def do_fe_delivery(self):
config = Pool().get('account_invoice_facho.configuration')(1)
if self.fe_delivery_state not in ['queued', 'draft']:
return
facho_invoice = self.tofacho()
req = dian.SendBillAsync
filename = 'invoice_%s' % (facho_invoice.invoice_ident)
args = [filename, self._dian_zip_io(facho_invoice).read()]
if config.dian_fe_habilitacion:
req = dian.Habilitacion.SendTestSetAsync
args.append(config.dian_fe_test_setid)
res = self.do_dian_request(req(*args))
if not res.ZipKey:
raise UserError(str(res))
self._force_write({'fe_delivery_state': 'delivered',
'fe_delivery_trackid': res.ZipKey})
def fe_process(self):
self.do_fe_delivery()
@classmethod
def fe_delivery(cls):
for inv in cls.search([('is_fe_colombia', '=', True),
('state', 'in', ['posted', 'paid'])]):
inv.fe_process()
if inv.fe_delivery_state in ['delivered', 'failure']:
inv.fe_update_status()
class Product(metaclass=PoolMeta):
__name__ = 'product.product'
def tofacho(self):
return form.StandardItem(
description = self.name,
id = self.code
)
class InvoiceLine(metaclass=PoolMeta):
__name__ = 'account.invoice.line'
def tofacho(self):
tax_subtotals = []
for taxes, unit_price, quantity in self.taxable_lines:
for tax in taxes:
tax_subtotals.append(form.TaxSubTotal(
percent = tax.rate
))
return form.InvoiceLine(
quantity = self.quantity,
description = self.description,
# TODO debe ser decimal
price_amount = float(self.unit_price),
item = self.product.tofacho(),
tax = form.TaxTotal(
subtotals = tax_subtotals
)
)