276 lines
9.8 KiB
Python
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
|
|
)
|
|
)
|