From 421b5eb2f5399ba0aff5e44f9b7a9ae16cec50db Mon Sep 17 00:00:00 2001 From: Alejandro Capellan Date: Fri, 26 Apr 2024 09:05:10 -0400 Subject: [PATCH] [ADD] mejoras y controles en los tipos de comprobantes y reconociendo por el RNC el nombre del contacto --- l10n_do_pos/controller/main.py | 44 ++++- l10n_do_pos/models/__init__.py | 1 + l10n_do_pos/models/pos_order.py | 8 +- l10n_do_pos/models/res_partner.py | 66 +++++++ .../static/src/js/ClientDetailsEdit.js | 170 +++++++++++++++--- l10n_do_pos/static/src/js/Popup.js | 62 +++++-- .../static/src/xml/ClientDetailsEditPlus.xml | 28 ++- l10n_do_pos/static/src/xml/pos.xml | 64 ------- 8 files changed, 311 insertions(+), 132 deletions(-) create mode 100644 l10n_do_pos/models/res_partner.py diff --git a/l10n_do_pos/controller/main.py b/l10n_do_pos/controller/main.py index b28ee2fdb..99b1323a6 100644 --- a/l10n_do_pos/controller/main.py +++ b/l10n_do_pos/controller/main.py @@ -1,13 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import base64 -import datetime -import json -import os import logging - -import odoo - from odoo import http, models, fields, _ from odoo.http import request @@ -23,3 +16,40 @@ def get_ncf_order(self, order): if pos_order_id: if pos_order_id.account_move: pos_order_id.write({'l10n_latam_document_number': pos_order_id.account_move.l10n_latam_document_number}) + + class DGIIController(http.Controller): + @http.route('/dgii/get_contribuyentes', type='json', auth='user', methods=['POST']) + def get_contribuyentes(self, vat): + if not vat: + return {'error': 'VAT is missing'} + + result = request.env['res.partner'].sudo().get_contribuyentes(vat) + return result + + class PosOrderController(http.Controller): + @http.route('/pos/my_orders', type='json', auth='user') + def get_pos_orders(self): + # Busca todos los pedidos POS para la sesión POS activa y en estado 'draft' + pos_session = request.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', request.uid)], + limit=1) + if pos_session: + orders = request.env['pos.order'].search([ + ('session_id', '=', pos_session.id), + ('state', '=', 'draft') + ]) + orders_data = [{ + 'id': order.id, + 'name': order.name, + 'total': order.amount_total + } for order in orders] + return {'orders': orders_data} + else: + return {'error': 'No active POS session found'} + + @http.route('/check_invoices', type='json', auth='user') + def check_invoices(self, partner_id=None, **kwargs): + if partner_id: + invoices = request.env['account.move'].sudo().search( + [('partner_id', '=', partner_id), ('state', '=', 'posted')]) + return {'has_invoices': bool(invoices)} + return {'has_invoices': False} diff --git a/l10n_do_pos/models/__init__.py b/l10n_do_pos/models/__init__.py index 4d9fad81f..44b2444d2 100644 --- a/l10n_do_pos/models/__init__.py +++ b/l10n_do_pos/models/__init__.py @@ -2,3 +2,4 @@ from . import pos_order from . import ir_sequence from . import pos_session +from . import res_partner diff --git a/l10n_do_pos/models/pos_order.py b/l10n_do_pos/models/pos_order.py index 22346d4e0..700ecadc4 100644 --- a/l10n_do_pos/models/pos_order.py +++ b/l10n_do_pos/models/pos_order.py @@ -94,8 +94,6 @@ def _order_fields(self, ui_order): if ui_order["to_invoice"]: res.update( { - # "account_move": ui_order['account_move'], - # "l10n_latam_sequence_id": ui_order["l10n_latam_sequence_id"], "l10n_latam_document_number": ui_order[ "l10n_latam_document_number" ], @@ -105,7 +103,7 @@ def _order_fields(self, ui_order): "l10n_latam_use_documents": True, "l10n_do_origin_ncf": ui_order["l10n_do_origin_ncf"], "l10n_do_return_status": ui_order["l10n_do_return_status"], - # "l10n_do_is_return_order": ui_order["l10n_do_is_return_order"], + "l10n_do_is_return_order": ui_order["l10n_do_is_return_order"], "l10n_do_return_order_id": ui_order["l10n_do_return_order_id"], # "l10n_do_ncf_expiration_date": ui_order[ # "l10n_do_ncf_expiration_date" @@ -194,7 +192,7 @@ def _prepare_invoice_vals(self): # invoice_vals["is_l10n_do_internal_sequence"] = True if self.l10n_do_is_return_order: - invoice_vals["type"] = "out_refund" + invoice_vals["move_type"] = "out_refund" return invoice_vals @@ -225,7 +223,7 @@ def _process_order(self, order, draft, existing_order): @api.model def order_search_from_ui(self, day_limit=0, config_id=0, session_id=0): - invoice_domain = [("type", "=", "out_invoice")] + invoice_domain = [("move_type", "=", "out_invoice")] pos_order_domain = [] if day_limit: diff --git a/l10n_do_pos/models/res_partner.py b/l10n_do_pos/models/res_partner.py new file mode 100644 index 000000000..3474df30a --- /dev/null +++ b/l10n_do_pos/models/res_partner.py @@ -0,0 +1,66 @@ +from odoo import models, fields, api, _ +import requests +import xml.etree.ElementTree as ET +import json + + +class Partner(models.Model): + _inherit = 'res.partner' + + @api.model + def get_contribuyentes(self, vat): + rnc = vat # Use the passed parameter directly + if not rnc or len(rnc) not in (9, 11): + warning_message = _( + 'No es una secuencia valida de Cedula o RNC, puede continuar si no estas validando este dato de lo ' + 'contrario verificar %s') % rnc + return {'warning': {'title': _('Warning'), 'message': warning_message}} + + # SOAP Request Setup + soap_request = f""" + + + + {rnc} + 0 + 0 + 1 + + + + """ + url = "https://dgii.gov.do/wsMovilDGII/WSMovilDGII.asmx?WSDL" + headers = {"Content-Type": "application/soap+xml; charset=utf-8"} + + # Sending the SOAP request + response = requests.post(url, data=soap_request, headers=headers) + if response.status_code == 200: + try: + root = ET.fromstring(response.content) + result_element = root.find('.//{http://dgii.gov.do/}GetContribuyentesResult') + if result_element is None: + raise ValueError("The desired element was not found in the XML response") + + data_dict = json.loads(result_element.text) + estatus = data_dict.get('ESTATUS') + if estatus == "0": + return self._generate_warning_message(rnc, data_dict, 'SUSPENDIDO') + elif estatus == "2": + return {'name': data_dict['RGE_NOMBRE']} + elif estatus == "3": + return self._generate_warning_message(rnc, data_dict, 'DADO DE BAJA') + + except Exception as e: + error_message = _('Error processing response from DGII: %s') % str(e) + return {'warning': {'title': _('Error'), 'message': error_message}} + else: + error_message = _('Request failed with status code: %s') % response.status_code + return {'warning': {'title': _('Error'), 'message': error_message}} + + def _generate_warning_message(self, rnc, data_dict, status): + warning_message = _( + 'Este contribuyente se encuentra inactivo. \n\nCédula/RNC: %s\nNombre/Razón Social: %s\nEstado: %s') % ( + rnc, data_dict["RGE_NOMBRE"], status) + return {'warning': {'title': _('Warning'), 'message': warning_message}} diff --git a/l10n_do_pos/static/src/js/ClientDetailsEdit.js b/l10n_do_pos/static/src/js/ClientDetailsEdit.js index 6c706f9e1..27cdb889b 100644 --- a/l10n_do_pos/static/src/js/ClientDetailsEdit.js +++ b/l10n_do_pos/static/src/js/ClientDetailsEdit.js @@ -1,4 +1,4 @@ -odoo.define('l10n_do_pos.ClientDetailsEditL10n', function(require) { +odoo.define('point_of_sale.ClientDetailsEdit', function(require) { 'use strict'; const { _t } = require('web.core'); @@ -9,12 +9,15 @@ odoo.define('l10n_do_pos.ClientDetailsEditL10n', function(require) { class ClientDetailsEdit extends PosComponent { constructor() { super(...arguments); - this.intFields = ['country_id', 'state_id', 'property_product_pricelist', 'l10n_do_dgii_tax_payer_type']; - const partner = this.props.partner; + this.loading = false; + this.widget = {}; + this.intFields = ['country_id', 'state_id', 'property_product_pricelist']; + const partner = this.props.partner || {}; this.changes = { 'country_id': partner.country_id && partner.country_id[0], 'state_id': partner.state_id && partner.state_id[0], - 'l10n_do_dgii_tax_payer_type': this.props.partner.l10n_do_dgii_tax_payer_type || "non_payer" + 'l10n_do_dgii_tax_payer_type': partner.l10n_do_dgii_tax_payer_type || "non_payer", + 'vat': partner.vat }; if (!partner.property_product_pricelist) this.changes['property_product_pricelist'] = this.env.pos.default_pricelist.id; @@ -26,8 +29,6 @@ odoo.define('l10n_do_pos.ClientDetailsEditL10n', function(require) { this.env.bus.off('save-customer', this); } get partnerImageUrl() { - // We prioritize image_1920 in the `changes` field because we want - // to show the uploaded image without fetching new data from the server. const partner = this.props.partner; if (this.changes.image_1920) { return this.changes.image_1920; @@ -37,39 +38,162 @@ odoo.define('l10n_do_pos.ClientDetailsEditL10n', function(require) { return false; } } + + async hasInvoices(partnerId) { + try { + const response = await this.rpc({ + route: '/check_invoices', + params: { partner_id: partnerId }, + }); + return response.has_invoices; + } catch (error) { + console.error(error); + await this.showPopup('ErrorPopup', { + title: this.env._t('Error'), + body: this.env._t('An error occurred while checking invoices.'), + }); + return false; + } + } + /** - * Save to field `changes` all input changes from the form fields. - */ + * Save to field `changes` all input changes from the form fields. + */ captureChange(event) { - this.changes[event.target.name] = event.target.value; + const name = event.target.name; + const value = event.target.value; + + this.changes[name] = value; + + if (name === 'vat') { + this.widget.loading = true; + this.checkVat(value).finally(() => { + this.widget.loading = false; + }); + } + } + + async checkVat(vatValue) { + try { + this.loading = true; // Activar indicador de carga + + const response = await this.rpc({ + route: '/dgii/get_contribuyentes', + params: { vat: vatValue }, + }); + + console.log("DGII Response:", response); // Imprimir la respuesta para depurar + + // Verificar el formato de la respuesta antes de continuar + if (typeof response !== 'object' || response === null || response.error) { + await this.showPopup('ErrorPopup', { + title: this.env._t('Error'), + body: this.env._t(response && response.error ? response.error : 'Invalid response from DGII.'), + }); + return; + } + + if (response.warning) { + await this.showPopup('ErrorPopup', { + title: this.env._t('Warning'), + body: this.env._t(response.warning.message), + }); + } else if (response.name) { + this.changes['name'] = response.name; + console.log('Assigned name from DGII:', response.name); // Registro de consola + this.render(); + } + } catch (error) { + console.error(error); + await this.showPopup('ErrorPopup', { + title: this.env._t('Network Error'), + body: this.env._t('Unable to connect. Please check your network connection.'), + }); + } finally { + this.loading = false; // Desactivar indicador de carga al finalizar + } } - saveChanges() { + + async saveChanges() { let processedChanges = {}; + const partner = this.props.partner || {}; + for (let [key, value] of Object.entries(this.changes)) { if (this.intFields.includes(key)) { - console.log("VAT", this.props.partner.vat) - processedChanges[key] = value || false; + processedChanges[key] = parseInt(value, 10) || false; } else { processedChanges[key] = value; } } - if ((!this.props.partner.name && !processedChanges.name) || - processedChanges.name === '' ){ + + // Comprobar si el contacto tiene facturas registradas y publicadas + const hasInvoices = await this.hasInvoices(partner.id); + + if (hasInvoices) { + return this.showPopup('ErrorPopup', { + title: this.env._t('Error'), + body: this.env._t('Cannot change the RNC for a contact with registered and published invoices.'), + }); + } + + if ((!partner.name && !processedChanges.name) || processedChanges.name === '') { return this.showPopup('ErrorPopup', { - title: _t('A Customer Name Is Required'), + title: _t('A Customer Name Is Required'), }); } - processedChanges.id = this.props.partner.id || false; + + // Antes de la validación + console.log("Partner:", partner); + console.log("Processed Changes:", processedChanges); + const newTaxPayerType = processedChanges.l10n_do_dgii_tax_payer_type; + const newVat = processedChanges.vat; + console.log("newTaxPayerType:", newTaxPayerType); + console.log("newVat:", newVat); + + // 1. Si l10n_do_dgii_tax_payer_type es diferente de 'non_payer', el vat es obligatorio + if (newTaxPayerType !== 'non_payer' && newVat === '') { + return this.showPopup('ErrorPopup', { + title: _t('VAT Required for Tax Payers'), + body: _t('A VAT number is required for tax payers.') + }); + } + + // 2. Si l10n_do_dgii_tax_payer_type es igual a 'non_payer', el vat no es obligatorio +// if (newTaxPayerType === 'non_payer' && newVat !== '') { +// return this.showPopup('ErrorPopup', { +// title: _t('VAT Not Required for Non-Payers'), +// body: _t('VAT should not be present for non-payers.') +// }); +// } + + // 3. Si ambos valores cambian, se valida primero l10n_do_dgii_tax_payer_type + if (processedChanges.l10n_do_dgii_tax_payer_type && processedChanges.vat) { + if (newTaxPayerType !== 'non_payer' && newVat === '') { + return this.showPopup('ErrorPopup', { + title: _t('VAT Required for Tax Payers'), + body: _t('A VAT number is required for tax payers.') + }); + } + } + + // 4. Si l10n_do_dgii_tax_payer_type es igual a 'taxpayer', el vat es obligatorio + if (newTaxPayerType === 'taxpayer' && newVat === '') { + return this.showPopup('ErrorPopup', { + title: _t('VAT Required for Tax Payers'), + body: _t('A VAT number is required for tax payers.') + }); + } + + processedChanges.id = partner.id || false; this.trigger('save-changes', { processedChanges }); } + async uploadImage(event) { const file = event.target.files[0]; if (!file.type.match(/image.*/)) { await this.showPopup('ErrorPopup', { title: this.env._t('Unsupported File Format'), - body: this.env._t( - 'Only web-compatible Image formats such as .png or .jpeg are supported.' - ), + body: this.env._t('Only web-compatible Image formats such as .png or .jpeg are supported.'), }); } else { const imageUrl = await getDataURLFromFile(file); @@ -77,11 +201,11 @@ odoo.define('l10n_do_pos.ClientDetailsEditL10n', function(require) { if (loadedImage) { const resizedImage = await this._resizeImage(loadedImage, 800, 600); this.changes.image_1920 = resizedImage.toDataURL(); - // Rerender to reflect the changes in the screen this.render(); } } } + _resizeImage(img, maxwidth, maxheight) { var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); @@ -115,9 +239,7 @@ odoo.define('l10n_do_pos.ClientDetailsEditL10n', function(require) { img.addEventListener('error', () => { this.showPopup('ErrorPopup', { title: this.env._t('Loading Image Error'), - body: this.env._t( - 'Encountered error when loading image. Please try again.' - ), + body: this.env._t('Encountered error when loading image. Please try again.') }); resolve(false); }); @@ -130,4 +252,4 @@ odoo.define('l10n_do_pos.ClientDetailsEditL10n', function(require) { Registries.Component.add(ClientDetailsEdit); return ClientDetailsEdit; -}); +}); \ No newline at end of file diff --git a/l10n_do_pos/static/src/js/Popup.js b/l10n_do_pos/static/src/js/Popup.js index 7a9f145a2..c74df3afd 100644 --- a/l10n_do_pos/static/src/js/Popup.js +++ b/l10n_do_pos/static/src/js/Popup.js @@ -1,35 +1,63 @@ +//odoo.define('l10n_do_pos.DocumentTypeSelectInfoPopup', function(require) { +// 'use strict'; +// +// const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); +// const Registries = require('point_of_sale.Registries'); +// const { posbus } = require('point_of_sale.utils') +// +// +// +// class DocumentTypeSelectInfoPopup extends AbstractAwaitablePopup { +// constructor() { +// super(...arguments); +// console.log(arguments) +// } +// async willStart() { +// const order = this.env.pos.get_order(); +// +// } +// change_doc_type_order(ev){ +// +// let doc_id = ev.target.value; +// const current_order = this.env.pos.get_order() +// +// let latam_doc_type = this.env.pos.l10n_latam_document_types.filter(d => { +// if(d.id == doc_id) +// current_order.set_latam_document_type(d); +// }) +// +// } +// +// } +// +// DocumentTypeSelectInfoPopup.template = 'DocumentTypeSelectInfoPopup'; +// Registries.Component.add(DocumentTypeSelectInfoPopup); +//}); odoo.define('l10n_do_pos.DocumentTypeSelectInfoPopup', function(require) { 'use strict'; const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); const Registries = require('point_of_sale.Registries'); - const { posbus } = require('point_of_sale.utils') - - class DocumentTypeSelectInfoPopup extends AbstractAwaitablePopup { constructor() { super(...arguments); - console.log(arguments) - } - async willStart() { - const order = this.env.pos.get_order(); - } - change_doc_type_order(ev){ - let doc_id = ev.target.value; - const current_order = this.env.pos.get_order() - - let latam_doc_type = this.env.pos.l10n_latam_document_types.filter(d => { - if(d.id == doc_id) - current_order.set_latam_document_type(d); - }) + change_doc_type_order(ev) { + let doc_id = parseInt(ev.target.value); + const current_order = this.env.pos.get_order(); + const latam_doc_type = this.env.pos.l10n_latam_document_types.find(d => d.id === doc_id); + if (latam_doc_type) { + current_order.set_latam_document_type(latam_doc_type); + } else { + console.error('Document type not found for ID:', doc_id); + } } - } DocumentTypeSelectInfoPopup.template = 'DocumentTypeSelectInfoPopup'; Registries.Component.add(DocumentTypeSelectInfoPopup); }); + diff --git a/l10n_do_pos/static/src/xml/ClientDetailsEditPlus.xml b/l10n_do_pos/static/src/xml/ClientDetailsEditPlus.xml index e64293005..e3bc26efb 100644 --- a/l10n_do_pos/static/src/xml/ClientDetailsEditPlus.xml +++ b/l10n_do_pos/static/src/xml/ClientDetailsEditPlus.xml @@ -3,6 +3,11 @@
+ +
+
+
+
Language -
Pricelist diff --git a/l10n_do_pos/static/src/xml/pos.xml b/l10n_do_pos/static/src/xml/pos.xml index d932f7f7b..24169df1b 100644 --- a/l10n_do_pos/static/src/xml/pos.xml +++ b/l10n_do_pos/static/src/xml/pos.xml @@ -1,28 +1,5 @@ - - - - - - - - - - - - - - - - - - - - - - -