diff --git a/l10n_ar_pos/__init__.py b/l10n_ar_pos/__init__.py new file mode 100644 index 00000000..d6210b12 --- /dev/null +++ b/l10n_ar_pos/__init__.py @@ -0,0 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/l10n_ar_pos/__manifest__.py b/l10n_ar_pos/__manifest__.py new file mode 100644 index 00000000..ffb7cada --- /dev/null +++ b/l10n_ar_pos/__manifest__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +{ + 'name': 'Argentinian Point of Sale', + 'version': "1.0", + 'description': "Adapt the Point of Sale to Argentinean localization.", + 'author': 'ADHOC SA, Ing. Gabriela Rivero', + 'category': 'Localization', + 'depends': [ + 'l10n_ar_edi', + 'point_of_sale', + ], + 'data': [ + 'views/assets_backend.xml', + 'views/pos_config.xml', + ], + 'demo': [ + ], + 'qweb': [ + 'static/src/xml/pos.xml', + 'static/src/xml/xml_receipt.xml', + ], + 'installable': True, + 'auto_install': False, + 'application': False, +} diff --git a/l10n_ar_pos/models/__init__.py b/l10n_ar_pos/models/__init__.py new file mode 100644 index 00000000..011438e7 --- /dev/null +++ b/l10n_ar_pos/models/__init__.py @@ -0,0 +1,2 @@ +from . import res_partner +from . import pos_config diff --git a/l10n_ar_pos/models/pos_config.py b/l10n_ar_pos/models/pos_config.py new file mode 100755 index 00000000..e2206c3c --- /dev/null +++ b/l10n_ar_pos/models/pos_config.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from odoo import fields, models, _ + + +class pos_config(models.Model): + + _inherit = "pos.config" + + pos_auto_invoice = fields.Boolean( + 'POS auto invoice', + help='POS auto to checked to invoice button', + default=True, + ) + receipt_invoice_number = fields.Boolean( + 'Receipt show invoice number', + default=True, + ) + receipt_customer_vat = fields.Boolean( + 'Receipt show customer VAT', + default=True, + ) diff --git a/l10n_ar_pos/models/res_partner.py b/l10n_ar_pos/models/res_partner.py new file mode 100755 index 00000000..b9c048df --- /dev/null +++ b/l10n_ar_pos/models/res_partner.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +from odoo import api, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + # TODO: sin esto sale un error al guardar un nuevo cliente. Revisar. + @api.constrains('vat', 'l10n_latam_identification_type_id') + def check_vat(self): + type_id = int(self.l10n_latam_identification_type_id.id) + l10n_ar_partners = self.filtered(lambda self: self.env['l10n_latam.identification.type'].browse(type_id).l10n_ar_afip_code) + l10n_ar_partners.l10n_ar_identification_validation() + return super(ResPartner, self - l10n_ar_partners).check_vat() + diff --git a/l10n_ar_pos/static/description/icon.png b/l10n_ar_pos/static/description/icon.png new file mode 100644 index 00000000..bc3bad4f Binary files /dev/null and b/l10n_ar_pos/static/description/icon.png differ diff --git a/l10n_ar_pos/static/src/css/pos_receipts.css b/l10n_ar_pos/static/src/css/pos_receipts.css new file mode 100644 index 00000000..fc6ddcb5 --- /dev/null +++ b/l10n_ar_pos/static/src/css/pos_receipts.css @@ -0,0 +1,5 @@ + +.pos-receipt .pos-receipt-contact { + text-align: left; + font-size: 95%; +} diff --git a/l10n_ar_pos/static/src/js/models.js b/l10n_ar_pos/static/src/js/models.js new file mode 100644 index 00000000..265da0af --- /dev/null +++ b/l10n_ar_pos/static/src/js/models.js @@ -0,0 +1,130 @@ +odoo.define('l10n_ar_pos.models', function (require) { +"use strict"; + + var models = require('point_of_sale.models'); + + models.load_fields('res.partner', ['l10n_latam_identification_type_id', 'l10n_ar_afip_responsibility_type_id']); + + models.load_models([ + { + model: 'l10n_latam.identification.type', + label: "AFIP Identification Types", + fields: ['id', 'name'], + loaded: function (self, category_id) { + self.category_id = []; + for (var i in category_id){ + self.category_id.push(category_id[i]); + } + }, + }, + { + model: 'l10n_ar.afip.responsibility.type', + label: "AFIP Responsibility Types", + fields: ['id', 'name'], + loaded: function (self, responsibility_type) { + self.responsibility_type = []; + for (var i in responsibility_type){ + self.responsibility_type.push(responsibility_type[i]); + } + }, + }, + ]); + + var _super_order = models.Order.prototype; + models.Order = models.Order.extend({ + initialize: function (attributes, options) { + _super_order.initialize.apply(this, arguments); + if (this.pos.config.pos_auto_invoice) { + this.to_invoice = true; + } + }, + init_from_JSON: function (json) { + var res = _super_order.init_from_JSON.apply(this, arguments); + if (json.to_invoice) { + this.to_invoice = json.to_invoice; + } + } + }); + // Este metodo es el mas importante para habilitar los campos que se necesitan usar + // por ejemplo "vat" fue sustituido por main_id_number + // En v13 se saca main_id_number y se empieza a usar vat + + // esto quiere decir que agregamos funcionalidad (EXTEND) + // a traves de la var models + + var _super_posmodel = models.PosModel.prototype; + models.PosModel = models.PosModel.extend({ + initialize: function (session, attributes) { + var partner_model = _.find(this.models, function (model) { + return model.model === 'res.partner'; + }); + + partner_model.fields.push('vat'); + partner_model.fields.push('website'); + partner_model.fields.push('l10n_ar_afip_responsibility_type_id'); + + _super_posmodel.initialize.apply(this, arguments); + + }, + + push_and_invoice_order: function (order) { + var self = this; + var invoiced = new Promise(function (resolveInvoiced, rejectInvoiced) { + if(!order.get_client()){ + rejectInvoiced({code:400, message:'Missing Customer', data:{}}); + } + else { + var order_id = self.db.add_order(order.export_as_JSON()); + self.flush_mutex.exec(function () { + var done = new Promise(function (resolveDone, rejectDone) { + // send the order to the server + // we have a 30 seconds timeout on this push. + // FIXME: if the server takes more than 30 seconds to accept the order, + // the client will believe it wasn't successfully sent, and very bad + // things will happen as a duplicate will be sent next time + // so we must make sure the server detects and ignores duplicated orders + + var transfer = self._flush_orders([self.db.get_order(order_id)], {timeout:30000, to_invoice:true}); + + transfer.catch(function (error) { + rejectInvoiced(error); + rejectDone(); + }); + + // on success, get the order id generated by the server + transfer.then(function(order_server_id){ + // generate the pdf and download it + if (order_server_id.length && !order.is_to_email()) { + debugger; + self.chrome.do_action('point_of_sale.pos_invoice_report',{additional_context:{ + active_ids:order_server_id, + }}).then(function () { + resolveInvoiced(order_server_id); + resolveDone(); + }).guardedCatch(function (error) { + rejectInvoiced({code:401, message:'Backend Invoice', data:{order: order}}); + rejectDone(); + }); + } else if (order_server_id.length) { + resolveInvoiced(order_server_id); + resolveDone(); + } else { + // The order has been pushed separately in batch when + // the connection came back. + // The user has to go to the backend to print the invoice + rejectInvoiced({code:401, message:'Backend Invoice', data:{order: order}}); + rejectDone(); + } + }); + return done; + }); + }); + } + }); + + return invoiced; + }, + + }); + +}); diff --git a/l10n_ar_pos/static/src/js/screens.js b/l10n_ar_pos/static/src/js/screens.js new file mode 100644 index 00000000..4413deee --- /dev/null +++ b/l10n_ar_pos/static/src/js/screens.js @@ -0,0 +1,209 @@ +odoo.define('l10n_ar_pos.screens', function (require) { + var screens = require('point_of_sale.screens'); + var rpc = require('web.rpc'); + var core = require('web.core'); + var qweb = core.qweb; + var _t = core._t; + + screens.ReceiptScreenWidget.include({ + // TODO: este método estaba en v11, en v13 parecería que paso a llamarse print_html, revisar + print_xml: function () { + var self = this; + if (this.pos.config.receipt_invoice_number) { + self.receipt_data = this.get_receipt_render_env(); + var order = this.pos.get_order(); + return rpc.query({ + // IMPORTANTISIMO **************************** + // DESDE ESTE MODELO SE ESTA IMPORTANDO account_move para relacionar + // + model: 'pos.order', + method: 'search_read', + domain: [['pos_reference', '=', order['name']]], + fields: ['account_move'], + }).then(function (orders) { + if (orders.length > 0) { + if (orders[0]['account_move']) { + // el array 1 de Split es el que tiene el nro! + var invoice_number = orders[0]['account_move'][1].split(" ")[1]; + var invoice_letter = orders[0]['account_move'][1].split(" ")[0].substring(3, 4); + var account_move = orders[0]['account_move'][0] + self.receipt_data['order']['invoice_number'] = invoice_number; + self.receipt_data['order']['invoice_letter'] = invoice_letter; + //---- + var company_id = self['pos']['company']['id']; + var partner_id = self['pos']['company']['partner_id'][0]; + rpc.query({ + model: 'res.company', + method: 'search_read', + args: [[['id', '=', company_id]], ['l10n_ar_afip_start_date']], + }).then(function (company_dict) { + self.pos.get_order()['l10n_ar_afip_start_date'] = company_dict[0]['l10n_ar_afip_start_date']; + + rpc.query({ + model: 'res.partner', + method: 'search_read', + args: [[['id', '=', partner_id]], ['vat', + 'l10n_ar_gross_income_number', + 'l10n_ar_afip_responsibility_type_id', + 'street', 'city', 'state_id', 'country_id', 'company_registry']], + } + + ).then(function (company_partner) { + self.receipt_data['order']['vat'] = company_partner[0]['vat']; + self.receipt_data['order']['l10n_ar_gross_income_number'] = company_partner[0]['l10n_ar_gross_income_number']; + self.receipt_data['order']['l10n_ar_afip_start_date'] = company_partner[0]['l10n_ar_afip_start_date']; + self.receipt_data['order']['l10n_ar_afip_responsibility_type_id'] = company_partner[0]['l10n_ar_afip_responsibility_type_id'][1]; + self.receipt_data['order']['street'] = company_partner[0]['street'] + ', ' + + company_partner[0]['city'] + ', ' + + company_partner[0]['state_id'][1] + ', ' + + company_partner[0]['country_id'][1]; + self.receipt_data['order']['company_registry'] = company_partner[0]['company_registry']; + //--------- + rpc.query({ + model: 'account.move', + method: 'search_read', + args: [[['id', '=', account_move]], ['l10n_ar_afip_auth_code', + 'l10n_ar_afip_auth_code_due', + 'l10n_ar_afip_qr_code', + 'l10n_latam_document_type_id', + ]], + } + + ).then(function (invoices) { + self.receipt_data['order']['l10n_ar_afip_qr_code'] = invoices[0]['l10n_ar_afip_qr_code']; + self.receipt_data['order']['l10n_ar_afip_auth_code'] = invoices[0]['l10n_ar_afip_auth_code']; + self.receipt_data['order']['l10n_ar_afip_auth_code_due'] = invoices[0]['l10n_ar_afip_auth_code_due']; + self.receipt_data['order']['l10n_latam_document_type_id'] = invoices[0]['l10n_latam_document_type_id'][1].split(" ")[0]; + var receipt = qweb.render('XmlReceipt', self.receipt_data); + self.pos.proxy.print_receipt(receipt); + }); + }); + }); + } + } + }); + } else { + this._super(); + } + }, + // CON LA FUNCION RENDER_RECEIPT deben traerse los modelos para poder usar en el recibo + render_receipt: function () { + this._super(); + var self = this; + var order = this.pos.get_order(); // Ok!! + + if (!this.pos.config.iface_print_via_proxy && this.pos.config.receipt_invoice_number && order.is_to_invoice()) { + var invoiced = new $.Deferred(); + rpc.query({ + // IMPORTANTISIMO **************************** + // DESDE ESTE MODELO SE ESTA IMPORTANDO account_move para relacionar + // + model: 'pos.order', + method: 'search_read', + domain: [['pos_reference', '=', order['name']]], + // account_move es un campo nativo de pos.order + fields: ['account_move'] + }).then(function (orders) { + if (orders.length > 0 && orders[0]['account_move'] && orders[0]['account_move'][1]) { + var invoice_number = orders[0]['account_move'][1].split(" ")[1]; + var invoice_letter = orders[0]['account_move'][1].split(" ")[0].substring(3, 4); + var account_move = orders[0]['account_move'][0] + self.pos.get_order()['invoice_number'] = invoice_number; + self.pos.get_order()['invoice_letter'] = invoice_letter; + //---- + var company_id = self['pos']['company']['id']; + var partner_id = self['pos']['company']['partner_id'][0]; + + rpc.query({ + model: 'res.company', + method: 'search_read', + args: [[['id', '=', company_id]], ['l10n_ar_afip_start_date', 'company_registry']], + }).then(function (company_dict) { + self.pos.get_order()['l10n_ar_afip_start_date'] = company_dict[0]['l10n_ar_afip_start_date']; + self.pos.get_order()['company_registry'] = company_dict[0]['company_registry']; + rpc.query({ + model: 'res.partner', + method: 'search_read', + args: [[['id', '=', partner_id]], ['vat', + 'l10n_ar_gross_income_number', + 'l10n_ar_afip_responsibility_type_id', + 'street', 'city', 'state_id', 'country_id']], + } + + ).then(function (company_partner) { + self.pos.get_order()['vat'] = company_partner[0]['vat']; + self.pos.get_order()['l10n_ar_gross_income_number'] = company_partner[0]['l10n_ar_gross_income_number']; + self.pos.get_order()['l10n_ar_afip_responsibility_type_id'] = company_partner[0]['l10n_ar_afip_responsibility_type_id'][1]; + self.pos.get_order()['street'] = company_partner[0]['street'] + ', ' + + company_partner[0]['city'] + ', ' + + company_partner[0]['state_id'][1] + ', ' + + company_partner[0]['country_id'][1]; + //--------- + rpc.query({ + model: 'account.move', + method: 'search_read', + args: [[['id', '=', account_move]], ['l10n_ar_afip_auth_code', + 'l10n_ar_afip_auth_code_due', + 'l10n_ar_afip_qr_code', + 'l10n_latam_document_type_id', + 'invoice_date', + ]], + } + + ).then(function (invoices) { + console.log('invoices', invoices); + self.pos.get_order()['l10n_ar_afip_qr_code'] = invoices[0]['l10n_ar_afip_qr_code']; + self.pos.get_order()['l10n_ar_afip_auth_code'] = invoices[0]['l10n_ar_afip_auth_code']; + self.pos.get_order()['l10n_ar_afip_auth_code_due'] = invoices[0]['l10n_ar_afip_auth_code_due']; + self.pos.get_order()['l10n_latam_document_type_id'] = invoices[0]['l10n_latam_document_type_id'][1].split(" ")[0]; + self.pos.get_order()['invoice_date'] = invoices[0]['invoice_date']; + self.$('.pos-receipt-container').html(qweb.render('OrderReceipt', self.get_receipt_render_env())); + + }); + }); + }); + } + invoiced.resolve(); + }).catch(function (type, error) { + invoiced.reject(error); + }); + return invoiced; + } else { + this._super(); + } + } + }) + + screens.ClientListScreenWidget.include({ + save_client_details: function (partner) { + var self = this; + var fields = {}; + this.$('.client-details-contents .detail').each(function(idx,el){ + if (self.integer_client_details.includes(el.name)){ + var parsed_value = parseInt(el.value, 10); + if (isNaN(parsed_value)){ + fields[el.name] = false; + } + else{ + fields[el.name] = parsed_value + } + } + else{ + fields[el.name] = el.value || false; + } + }); + // If the responsibility type is "responsable inscripto", then the document number is required. + if (!fields.vat && fields.l10n_ar_afip_responsibility_type_id == '1' ) { + this.gui.show_popup('error',_t('For the selected AFIP Responsibility el campo "Document number" es requerido.')); + return; + } + // If the responsibility type is "responsable inscripto", then the document type should be CUIT. + if (fields.l10n_latam_identification_type_id != '4' && fields.l10n_ar_afip_responsibility_type_id == '1' ) { + this.gui.show_popup('error',_t('For the selected AFIP Responsibility you will need to set CUIT Identification Type')); + return; + } + this._super(partner); + }, + }); + +}); diff --git a/l10n_ar_pos/static/src/xml/pos.xml b/l10n_ar_pos/static/src/xml/pos.xml new file mode 100644 index 00000000..b07d3cba --- /dev/null +++ b/l10n_ar_pos/static/src/xml/pos.xml @@ -0,0 +1,262 @@ + + + + + + +
+ AFIP Resp. + +
+
+ Document type + +
+
+
+ + + +
+ AFIP Resp. + + + + + N/A + +
+
+ Document type + + + + + N/A + +
+
+
+ + + + AFIP Responsibility + Document Type + Document Number + + + + + + + + + + + + + +
+ + + +
+
+ + +

+ +

+
+
+ +
+ +
CUIT:
+
Ing. Brutos:
+
Inicio de actividades:
+
+
Dirección:
+
+
+
+ + +
+
+ + FACTURA - ORIGINAL + + - COD + +
+ N° DE FACTURA:
+ FECHA:
+
+
+
+
+ + +
+ + CLIENTE:
+ + : + + +
+
+ +
+
+ + CALLE: + + , + + + , + + + , + +
+
+ + Email:
+
+ + Tel:
+
+ + Website:
+
+ +
+
+ +
+
+
Servido por
+
+
+
+ +
DESCRIPCIÓN
+
CANT. x PRECIO UNIT. +         +         +         + IMPORTE
+
+
+
+ + +
+ +
+ +
+ CAE: +
+ Vto. CAE: +
+
+
+
+ +
+ + + +
+ +
->
+
+ +
+ + + + + + +
+
+ +
+ Discount: % +
+
+
+ + + x + + + + + + + + + + + + + + + + + + +
+
+ + + +
--------
+
+
Subtotal
+ +
+ + +
+
+
+ +
--------
+
+
Subtotal
+ +
+ + +
+
+
+
+
+ + +
+ + diff --git a/l10n_ar_pos/static/src/xml/xml_receipt.xml b/l10n_ar_pos/static/src/xml/xml_receipt.xml new file mode 100644 index 00000000..b5f19fe9 --- /dev/null +++ b/l10n_ar_pos/static/src/xml/xml_receipt.xml @@ -0,0 +1,258 @@ + + + + + + + + + + + +
+
+ + + + +


+
+
+
+ CUIT: + +
+
+ IIBB: +
+
+ Inicio de actividades: +
+
+ Dirección: +
+
+ +
+ Teléfono: +
+
+
+
+ + +
+
+ FACTURA - ORIGINAL + + - COD + +
+ N° DE FACTURA:
+ FECHA: +
+
+
+ + + + CLIENTE: + + : + + +
+
+ +
+
+ + DIRECCION: + + , + + + , + + + , + +
+
+ + Email:
+
+ + Tel:
+
+ + Website:
+
+
+
+
+ + + + +
+ + + + + + + + + + + + + + Discount: % + + + + + + + + + + + + + + + + + + + + + + x + + + + + + + + + + + +
+ + + + + + + + -------- + Subtotal + + + + + + + + + + + -------- + +
        TOTAL
+ +
+

+ + + + + + + + + +
+ + + +
        CHANGE
+ +
+
+
+ + + + + + Discounts + + + + + + + + + + + + + + + Total Taxes + + + + + +