From e2c7572c306d8934990f1d723f46ce76c9f3aa6e Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Fri, 6 Oct 2017 00:10:27 -0500 Subject: [PATCH] Uso de CFDI en Socios --- source/app/controllers/main.py | 26 +- source/app/main.py | 4 +- source/app/models/db.py | 20 +- source/app/models/main.py | 184 +++++++++++++- source/static/img/file-xml.png | Bin 0 -> 1143 bytes source/static/js/controller/invoices.js | 314 ++++++++++++++++++++++-- source/static/js/controller/main.js | 42 +++- source/static/js/controller/partners.js | 20 +- source/static/js/controller/util.js | 11 + source/static/js/ui/invoices.js | 34 ++- source/static/js/ui/partners.js | 5 + 11 files changed, 613 insertions(+), 47 deletions(-) create mode 100644 source/static/img/file-xml.png diff --git a/source/app/controllers/main.py b/source/app/controllers/main.py index f739874..82d7ebc 100644 --- a/source/app/controllers/main.py +++ b/source/app/controllers/main.py @@ -78,13 +78,11 @@ class AppPartners(object): def on_get(self, req, resp): values = req.params - print ('GET VALUES', values) req.context['result'] = self._db.get_partners(values) resp.status = falcon.HTTP_200 def on_post(self, req, resp): values = req.params - print ('POST VALUES', values) req.context['result'] = self._db.partner(values) resp.status = falcon.HTTP_200 @@ -108,7 +106,6 @@ class AppProducts(object): def on_post(self, req, resp): values = req.params - #~ print ('VALUES', values) req.context['result'] = self._db.product(values) resp.status = falcon.HTTP_200 @@ -118,3 +115,26 @@ class AppProducts(object): resp.status = falcon.HTTP_200 else: resp.status = falcon.HTTP_204 + + +class AppInvoices(object): + + def __init__(self, db): + self._db = db + + def on_get(self, req, resp): + values = req.params + req.context['result'] = self._db.get_invoices(values) + resp.status = falcon.HTTP_200 + + def on_post(self, req, resp): + values = req.params + req.context['result'] = self._db.invoice(values) + resp.status = falcon.HTTP_200 + + def on_delete(self, req, resp): + values = req.params + if self._db.delete('invoice', values['id']): + resp.status = falcon.HTTP_200 + else: + resp.status = falcon.HTTP_204 diff --git a/source/app/main.py b/source/app/main.py index 3d795e2..921faf6 100644 --- a/source/app/main.py +++ b/source/app/main.py @@ -12,7 +12,8 @@ from middleware import ( ) from models.db import StorageEngine from controllers.main import ( - AppLogin, AppLogout, AppAdmin, AppMain, AppValues, AppPartners, AppProducts + AppLogin, AppLogout, AppAdmin, AppMain, AppValues, AppPartners, AppProducts, + AppInvoices ) from settings import DEBUG @@ -31,6 +32,7 @@ api.add_route('/main', AppMain(db)) api.add_route('/values/{table}', AppValues(db)) api.add_route('/partners', AppPartners(db)) api.add_route('/products', AppProducts(db)) +api.add_route('/invoices', AppInvoices(db)) if DEBUG: diff --git a/source/app/models/db.py b/source/app/models/db.py index 59a2a45..80fac0d 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -41,6 +41,9 @@ class StorageEngine(object): def _get_monedas(self, values): return main.SATMonedas.get_activos() + def _get_regimenes(self, values): + return main.Emisor.get_regimenes() + def _get_usocfdi(self, values): return main.SATUsoCfdi.get_activos() @@ -49,6 +52,8 @@ class StorageEngine(object): return main.Socios.remove(id) if table == 'product': return main.Productos.remove(id) + if table == 'invoice': + return main.Facturas.remove(id) return False def _get_client(self, values): @@ -61,8 +66,7 @@ class StorageEngine(object): return main.Socios.get_(values) def partner(self, values): - id = int(values['id']) - del values['id'] + id = int(values.pop('id', '0')) if id: return main.Socios.actualizar(values, id) return main.Socios.add(values) @@ -71,12 +75,20 @@ class StorageEngine(object): return main.Productos.get_(values) def product(self, values): - id = int(values['id']) - del values['id'] + id = int(values.pop('id', '0')) if id: return main.Productos.actualizar(values, id) return main.Productos.add(values) + def invoice(self, values): + id = int(values.pop('id', '0')) + if id: + return main.Facturas.actualizar(values, id) + return main.Facturas.add(values) + + def get_invoices(self, values): + return main.Facturas.get_(values) + diff --git a/source/app/models/main.py b/source/app/models/main.py index 0dfa407..aaf4ac3 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -102,11 +102,22 @@ class SATRegimenes(BaseModel): moral = BooleanField(default=False) class Meta: - order_by = ('name',) + order_by = ('-default', 'name',) indexes = ( (('key', 'name'), True), ) + @classmethod + def get_activos(cls): + rows = (SATRegimenes + .select( + SATRegimenes.key.alias('id'), + SATRegimenes.name.alias('value')) + .where(SATRegimenes.activo==True) + .dicts() + ) + return tuple(rows) + class Emisor(BaseModel): rfc = TextField(index=True) @@ -139,6 +150,12 @@ class Emisor(BaseModel): class Meta: order_by = ('nombre',) + @classmethod + def get_regimenes(cls): + obj = Emisor.select()[0] + rows = [{'id': row.key, 'value': row.name} for row in obj.regimenes] + return tuple(rows) + class Certificado(BaseModel): key = BlobField() @@ -248,7 +265,7 @@ class SATFormaPago(BaseModel): def get_activos(cls): rows = (SATFormaPago .select( - SATFormaPago.id, + SATFormaPago.key.alias('id'), SATFormaPago.name.alias('value')) .where(SATFormaPago.activo==True) .dicts() @@ -671,7 +688,7 @@ class Facturas(BaseModel): cliente = ForeignKeyField(Socios) serie = TextField(default='') folio = IntegerField(default=0) - fecha = DateTimeField(default=util.now) + fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S']) fecha_timbrado = DateTimeField(null=True) forma_pago = TextField(default='') condiciones_pago = TextField(default='') @@ -701,6 +718,167 @@ class Facturas(BaseModel): class Meta: order_by = ('fecha',) + @classmethod + def get_(cls, values): + rows = tuple(Facturas + .select(Facturas.id, Facturas.serie, Facturas.folio, Facturas.uuid, + Facturas.fecha, Facturas.tipo_comprobante, Facturas.estatus, + Facturas.total_mn, Socios.nombre.alias('cliente')) + .join(Socios) + .switch(Facturas).dicts() + ) + return {'ok': True, 'rows': rows} + + @classmethod + def remove(cls, id): + obj = Facturas.get(Facturas.id==id) + if obj.uuid: + return False + q = FacturasDetalle.delete().where(FacturasDetalle.factura==obj) + q.execute() + q = FacturasImpuestos.delete().where(FacturasImpuestos.factura==obj) + q.execute() + return bool(obj.delete_instance()) + + def _get_folio(self, serie): + inicio_serie = Folios.select( + Folios.inicio).where(Folios.serie==serie).scalar() + inicio = (Facturas + .select(fn.Max(Facturas.folio)) + .where(Facturas.serie==serie) + .scalar()) + if inicio is None: + inicio = inicio_serie + else: + inicio += 1 + return inicio + + def _calculate_totals(self, invoice, products): + subtotal = 0 + totals_tax = {} + total_trasladados = None + total_retenciones = None + total_iva = 0 + + for product in products: + id_product = product.pop('id') + p = Productos.get(Productos.id==id_product) + product['descripcion'] = p.descripcion + product['unidad'] = p.unidad.key + product['clave'] = p.clave + product['clave_sat'] = p.clave_sat + + product['factura'] = invoice.id + product['producto'] = id_product + product['importe'] = round( + float(product['cantidad']) * float(product['valor_unitario']), 2) + subtotal += product['importe'] + + FacturasDetalle.create(**product) + for tax in p.impuestos: + if tax.id in totals_tax: + totals_tax[tax.id]['importe'] += product['importe'] + else: + tax.importe = product['importe'] + totals_tax[tax.id] = tax + + for tax in totals_tax.values(): + if tax.tipo == 'E' or tax.tipo == 'R': + continue + import_tax = round(float(tax.tasa) * tax.importe, 2) + total_trasladados = (total_trasladados or 0) + import_tax + if tax.name == 'IVA': + total_iva += import_tax + + invoice_tax = { + 'factura': invoice.id, + 'impuesto': tax.id, + 'base': tax.importe, + 'importe': import_tax, + } + FacturasImpuestos.create(**invoice_tax) + + for tax in totals_tax.values(): + if tax.tipo == 'E' or tax.tipo == 'T': + continue + if tax.tasa == round(Decimal(2/3), 6): + import_tax = round(float(tax.tasa) * total_iva, 2) + else: + import_tax = round(float(tax.tasa) * tax.importe, 2) + total_retenciones = (total_retenciones or 0) + import_tax + + invoice_tax = { + 'factura': invoice.id, + 'impuesto': tax['id'], + 'base': tax['importe'], + 'importe': import_tax, + } + FacturasImpuestos.create(**invoice_tax) + + total = subtotal + (total_trasladados or 0) - (total_retenciones or 0) + total_mn = round(total * invoice.tipo_cambio, 2) + data = { + 'subtotal': subtotal, + 'total': total, + 'total_mn': total_mn, + 'total_trasladados': total_trasladados, + 'total_retenciones': total_retenciones, + } + return data + + @classmethod + def add(cls, values): + #~ print ('VALUES', values) + productos = util.loads(values.pop('productos')) + + emisor = Emisor.select()[0] + values['folio'] = cls._get_folio(cls, values['serie']) + values['tipo_cambio'] = float(values['tipo_cambio']) + values['lugar_expedicion'] = emisor.codigo_postal + + with database_proxy.atomic() as txn: + obj = Facturas.create(**values) + totals = cls._calculate_totals(cls, obj, productos) + obj.subtotal = totals['subtotal'] + obj.total_trasladados = totals['total_trasladados'] + obj.total_retenciones = totals['total_retenciones'] + obj.total = totals['total'] + obj.total_mn = totals['total_mn'] + obj.save() + + #~ obj.xml = self._make_xml(obj, emisor) + #~ obj.save() + + msg = 'Factura guardada correctamente. Enviando a timbrar' + #~ error = False + #~ result = util.timbra_xml(obj.xml) + #~ if result['ok']: + #~ obj.xml = result['xml'] + #~ obj.uuid = result['uuid'] + #~ obj.fecha_timbrado = result['fecha'] + #~ obj.estatus = 'Timbrada' + #~ obj.save() + #~ else: + #~ error = True + #~ msg = result['error'] + #~ obj.estatus = 'Error' + #~ obj.error = msg + #~ obj.save() + + row = { + 'id': obj.id, + 'serie': obj.serie, + 'folio': obj.folio, + 'uuid': obj.uuid, + 'fecha': obj.fecha, + 'tipo_comprobante': obj.tipo_comprobante, + 'estatus': obj.estatus, + 'total_mn': obj.total_mn, + 'cliente': obj.cliente.nombre, + } + data = {'ok': True, 'row': row, 'new': True, 'error': False, 'msg': msg} + return data + class FacturasDetalle(BaseModel): factura = ForeignKeyField(Facturas) diff --git a/source/static/img/file-xml.png b/source/static/img/file-xml.png new file mode 100644 index 0000000000000000000000000000000000000000..74dae07d49f129526e23ce0e9d08e96031b333cd GIT binary patch literal 1143 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+0817mi8Plzi}!7T>W8w~0| zbe%y1jf98;Nr)PVf@=(#K#@xfxFJOwvrKZ65A7DxiaxwiZ=8;~k53GxeOuzT4uWuL>H^cgEI z{PF%`{9OK;-~qP7bz|H z($E3C4;>y=~IA zIqT1#-^93}KlQBLw*5MLm>w|L8yi@-IPwVhGAwpUR{Xe8Ae>QRd1pa|P_T(@bL6bW z9WrM=+vqX`MyZ^?^oeWd6Nc4C)-%4|;<)ig+61AZ2~W0XMBm9wH{U9LdpWbE#gfI3 zCkM~DSgAF4U&ka*Hl~tJ?OkqdJ9jSSdH2H3#jNkKoZZ32cHI|(|NML^dNM{V^6%`g zkMp_|`sGh`r_T+Tr+!L)b%N%!qEIbS-B$Zu{QsFs*pjF6?9%`6_PhS_s~wxmo2Jj# zdcAA+EnrZqmbgZgq$HN4S|t~y0x1R~14A=iLjzqS(+~qQD