diff --git a/CHANGELOG.md b/CHANGELOG.md index 14b9eb1..db951e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +v 1.38.0 [08-mar-2020] +---------------------- + - Mejora: Factura global por ticket o nota + - Error: Al generar algunos PDFs + + v 1.37.0 [02-mar-2020] ---------------------- - Mejora: Soporte para complemento Leyendas Fiscales diff --git a/VERSION b/VERSION index bf50e91..ebeef2f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.37.0 +1.38.0 diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 7aa5973..aade77a 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -1336,7 +1336,7 @@ class LIBO(object): self._ine(data['ine']) self._divisas(data.get('divisas', {})) - self._leyendas(data['leyendas']) + self._leyendas(data.get('leyendas', '')) self._cancelado(data['cancelada']) self._clean() diff --git a/source/app/models/main.py b/source/app/models/main.py index b8f062c..25f8353 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -41,6 +41,7 @@ from settings import log, COMPANIES, VERSION, PATH_CP, PRE, CURRENT_CFDI, \ from controllers import utils from settings import ( DEBUG, + DEFAULT_GLOBAL, DB_COMPANIES, EXT, IS_MV, @@ -48,6 +49,7 @@ from settings import ( PATHS, URL, VALUES_PDF, + RFCS, ) FORMAT = '{0:.2f}' @@ -56,7 +58,7 @@ FORMAT4 = '{0:.4f}' FORMAT6 = '{0:.6f}' FORMAT_TAX = FORMAT4 FORMAT_PRECIO = FORMAT4 -RFC_PUBLICO = 'XAXX010101000' +# ~ RFC_PUBLICO = 'XAXX010101000' RFC_EXTRANJERO = 'XEXX010101000' @@ -559,6 +561,7 @@ class Configuracion(BaseModel): 'chk_ticket_edit_cant', 'chk_ticket_total_up', 'chk_ticket_user_show_doc', + 'chk_config_invoice_by_ticket', ) data = (Configuracion .select() @@ -3823,11 +3826,21 @@ class Facturas(BaseModel): obj.acuse = result['Acuse'] or '' self._actualizar_saldo_cliente(self, obj, True) self._update_inventory(self, obj, True) + self._uncancel_tickets(self, obj) else: obj.error = data['msg'] obj.save() return data + @utils.run_in_thread + def _uncancel_tickets(self, invoice): + query = (Tickets + .update(estatus='Generado', cancelado=False, factura=None) + .where(Tickets.factura==invoice) + ) + query.execute() + return + def _cancel_signature(self, id): msg = 'Factura cancelada correctamente' auth = Emisor.get_auth() @@ -4230,7 +4243,7 @@ class Facturas(BaseModel): if invoice.donativo and invoice.forma_pago == '12': return - if invoice.cliente.rfc == RFC_PUBLICO: + if invoice.cliente.rfc == RFCS['PUBLIC']: return importe = invoice.total_mn @@ -4792,6 +4805,9 @@ class Facturas(BaseModel): def _make_xml(self, invoice, auth): tax_decimals = Configuracion.get_bool('chk_config_tax_decimals') decimales_precios = Configuracion.get_bool('chk_config_decimales_precios') + invoice_by_ticket = Configuracion.get_bool('chk_config_invoice_by_ticket') + is_global = (invoice.cliente.rfc == RFCS['PUBLIC']) and invoice_by_ticket + frm_vu = FORMAT if decimales_precios: frm_vu = FORMAT_PRECIO @@ -4857,7 +4873,6 @@ class Facturas(BaseModel): 'Nombre': emisor.nombre, 'RegimenFiscal': invoice.regimen_fiscal, } - receptor = { 'Rfc': invoice.cliente.rfc, 'Nombre': invoice.cliente.nombre, @@ -4872,9 +4887,15 @@ class Facturas(BaseModel): conceptos = [] rows = FacturasDetalle.select().where(FacturasDetalle.factura==invoice) for row in rows: + if is_global: + key_sat = row.clave_sat + key = row.clave + else: + key_sat = row.producto.clave_sat + key = row.producto.clave concepto = { - 'ClaveProdServ': row.producto.clave_sat, - 'NoIdentificacion': row.producto.clave, + 'ClaveProdServ': key_sat, + 'NoIdentificacion': key, 'Cantidad': FORMAT.format(row.cantidad), 'ClaveUnidad': row.unidad, 'Unidad': SATUnidades.get(SATUnidades.key==row.unidad).name[:20], @@ -4906,7 +4927,21 @@ class Facturas(BaseModel): retenciones = [] if invoice.tipo_comprobante != 'T': - for impuesto in row.producto.impuestos: + + if is_global: + ticket = (Tickets + .get(fn.Concat(Tickets.serie, Tickets.folio)==row.clave) + ) + product_taxes = (TicketsImpuestos + .select() + .where(TicketsImpuestos.ticket==ticket) + ) + else: + product_taxes = row.producto.impuestos + + for impuesto in product_taxes: + if is_global: + impuesto = impuesto.impuesto base = float(row.importe - row.descuento) if impuesto.tipo == 'E': tax = { @@ -5186,7 +5221,7 @@ class Facturas(BaseModel): tipo_persona = 1 if receptor['rfc'] == 'XEXX010101000': tipo_persona = 4 - elif receptor['rfc'] == RFC_PUBLICO: + elif receptor['rfc'] == RFCS['PUBLIC']: tipo_persona = 3 elif len(receptor['rfc']) == 12: tipo_persona = 2 @@ -5290,7 +5325,7 @@ class Facturas(BaseModel): tipo_persona = 1 if receptor['rfc'] == 'XEXX010101000': tipo_persona = 4 - elif receptor['rfc'] == RFC_PUBLICO: + elif receptor['rfc'] == RFCS['PUBLIC']: tipo_persona = 3 elif len(receptor['rfc']) == 12: tipo_persona = 2 @@ -5443,7 +5478,7 @@ class Facturas(BaseModel): if invoice.donativo and invoice.forma_pago == '12': return - if invoice.cliente.rfc == RFC_PUBLICO: + if invoice.cliente.rfc == RFCS['PUBLIC']: return importe = invoice.total_mn @@ -7220,15 +7255,85 @@ class Tickets(BaseModel): } return data + def _get_totals_invoice_by_ticket(self, invoice, ids): + subtotal = 0 + descuento_cfdi = 0 + totals_tax = {} + total_trasladados = None + total_retenciones = None + notes = Tickets.get_notes(ids) + + rows = Tickets.select().where(Tickets.id.in_(ids)) + + for row in rows: + details = DEFAULT_GLOBAL.copy() + details['clave'] = row.serie + str(row.folio) + details['factura'] = invoice.id + + unit_value = row.subtotal + discount = row.descuento + final_price = unit_value - discount + importe = final_price + + details['valor_unitario'] = unit_value + details['descuento'] = discount + details['precio_final'] = final_price + details['importe'] = importe + + descuento_cfdi += details['descuento'] + subtotal += details['importe'] + + FacturasDetalle.create(**details) + + rows = (TicketsImpuestos + .select( + TicketsImpuestos.impuesto, + fn.Sum(TicketsImpuestos.base), + fn.Sum(TicketsImpuestos.importe)) + .where(TicketsImpuestos.ticket.in_(ids)) + .group_by(TicketsImpuestos.impuesto) + .order_by(TicketsImpuestos.impuesto) + ) + for tax in rows: + invoice_tax = { + 'factura': invoice.id, + 'impuesto': tax.impuesto.id, + 'base': tax.base, + 'importe': tax.importe, + } + FacturasImpuestos.create(**invoice_tax) + + if tax.impuesto.tipo == 'T' and tax.impuesto.key != '000': + total_trasladados = (total_trasladados or 0) + tax.importe + elif tax.impuesto.tipo == 'R' and tax.impuesto.key != '000': + total_retenciones = (total_retenciones or 0) + tax.importe + + total = subtotal - descuento_cfdi + \ + (total_trasladados or 0) - (total_retenciones or 0) + + type_change = Decimal(invoice.tipo_cambio) + total_mn = round(total * type_change, DECIMALES) + data = { + 'subtotal': subtotal, + 'descuento': descuento_cfdi, + 'total': total, + 'total_mn': total_mn, + 'total_trasladados': total_trasladados, + 'total_retenciones': total_retenciones, + 'notas': notes, + } + return data + @classmethod def invoice(cls, values, user): is_invoice_day = util.get_bool(values['is_invoice_day']) id_client = int(values['client']) tickets = util.loads(values['tickets']) + invoice_by_ticket = Configuracion.get_bool('chk_config_invoice_by_ticket') if is_invoice_day: filters = ( - Socios.rfc == RFC_PUBLICO and + Socios.rfc == RFCS['PUBLIC'] and Socios.slug == 'publico_en_general') try: client = Socios.get(filters) @@ -7244,12 +7349,15 @@ class Tickets(BaseModel): data = {'ok': False, 'msg': msg} return data + payment_type = cls._get_payment_type(cls, tickets) + emisor = Emisor.select()[0] data = {} data['cliente'] = client data['serie'] = cls._get_serie(cls, user, True) data['folio'] = cls._get_folio_invoice(cls, data['serie']) - data['forma_pago'] = client.forma_pago.key + # ~ data['forma_pago'] = client.forma_pago.key + data['forma_pago'] = payment_type data['tipo_cambio'] = 1.00 data['lugar_expedicion'] = emisor.cp_expedicion or emisor.codigo_postal if client.uso_cfdi is None: @@ -7260,7 +7368,10 @@ class Tickets(BaseModel): with database_proxy.atomic() as txn: obj = Facturas.create(**data) - totals = cls._calculate_totals_invoice(cls, obj, tickets) + if is_invoice_day and invoice_by_ticket: + totals = cls._get_totals_invoice_by_ticket(cls, obj, tickets) + else: + totals = cls._calculate_totals_invoice(cls, obj, tickets) obj.subtotal = totals['subtotal'] obj.descuento = totals['descuento'] obj.total_trasladados = totals['total_trasladados'] @@ -7276,6 +7387,20 @@ class Tickets(BaseModel): data = {'ok': True, 'msg': msg, 'id': obj.id} return data + def _get_payment_type(self, ids): + """Get max of payment type""" + query = (Tickets + .select( + Tickets.forma_pago, + fn.Sum(Tickets.subtotal).alias('total')) + .where(Tickets.id.in_(ids)) + .group_by(Tickets.forma_pago) + .order_by(SQL('total').desc()) + .limit(1) + .scalar() + ) + return query + def _update_inventory_if_cancel(self, id): products = TicketsDetalle.select().where(TicketsDetalle.ticket==id) for p in products: @@ -7418,6 +7543,7 @@ class Tickets(BaseModel): data['timbre'] = {} data['donataria'] = {} data['ine'] = {} + data['leyendas'] = () return data @@ -9154,7 +9280,7 @@ def _init_values(rfc): data = ( {'clave': 'version', 'valor': VERSION}, {'clave': 'migracion', 'valor': '0'}, - {'clave': 'rfc_publico', 'valor': RFC_PUBLICO}, + {'clave': 'rfc_publico', 'valor': RFCS['PUBLIC']}, {'clave': 'rfc_extranjero', 'valor': 'XEXX010101000'}, {'clave': 'decimales', 'valor': '2'}, {'clave': 'path_key', 'valor': ''}, diff --git a/source/app/settings.py b/source/app/settings.py index b1f5bc7..1b5bfdc 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -47,7 +47,7 @@ except ImportError: DEBUG = DEBUG -VERSION = '1.37.0' +VERSION = '1.38.0' EMAIL_SUPPORT = ('soporte@empresalibre.mx',) TITLE_APP = '{} v{}'.format(TITLE_APP, VERSION) @@ -234,6 +234,18 @@ VALUES_PDF = { }, } +RFCS = { + 'PUBLIC': 'XAXX010101000', + 'FOREIGN': 'XEXX010101000', +} + URL = { 'SEAFILE': 'https://seafile.elmau.net', } + +DEFAULT_GLOBAL = { + 'cantidad': 1.00, + 'unidad': 'ACT', + 'descripcion': 'Venta', + 'clave_sat': '01010101', +} diff --git a/source/static/js/controller/admin.js b/source/static/js/controller/admin.js index 310da10..6b41287 100644 --- a/source/static/js/controller/admin.js +++ b/source/static/js/controller/admin.js @@ -122,6 +122,8 @@ var controllers = { $$('chk_config_tax_locales_truncate').attachEvent('onItemClick', chk_config_item_click) $$('chk_config_decimales_precios').attachEvent('onItemClick', chk_config_item_click) $$('chk_config_user_show_doc').attachEvent('onItemClick', chk_config_item_click) + $$('chk_config_invoice_by_ticket').attachEvent('onItemClick', chk_config_item_click) + $$('chk_config_anticipo').attachEvent('onItemClick', chk_config_item_click) $$('chk_usar_punto_de_venta').attachEvent('onItemClick', chk_config_item_click) diff --git a/source/static/js/ui/admin.js b/source/static/js/ui/admin.js index 43d6f5b..0bc4e92 100644 --- a/source/static/js/ui/admin.js +++ b/source/static/js/ui/admin.js @@ -682,6 +682,11 @@ var options_admin_otros = [ {view: 'checkbox', id: 'chk_config_user_show_doc', labelWidth: 0, labelRight: 'Usuarios pueden ver todos los documentos'}, ]}, + {cols: [{maxWidth: 15}, + {view: 'checkbox', id: 'chk_config_invoice_by_ticket', labelWidth: 0, + labelRight: 'Factura global por ticket'}, + {}, + ]}, {maxHeight: 15}, {cols: [{maxWidth: 15}, {view: 'richselect', id: 'lst_pac', name: 'lst_pac', width: 300, diff --git a/source/templates/plantilla_factura.ods b/source/templates/plantilla_factura.ods index a35d18a..0505175 100644 Binary files a/source/templates/plantilla_factura.ods and b/source/templates/plantilla_factura.ods differ