diff --git a/pos_order_load/README.rst b/pos_order_load/README.rst new file mode 100644 index 00000000..54d8bace --- /dev/null +++ b/pos_order_load/README.rst @@ -0,0 +1,38 @@ +POS Order Load and Save +======================= + +This module allows to load existing POS order. +In this version, when the loaded order +is validated in the POS, a new one is created. + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed feedback +`here `_. + +Credits +======= + +Contributors +------------ + +* Sylvain Calador +* Sylvain Le Gal (https://twitter.com/legalsylvain) + +Maintainer +---------- + +.. image:: http://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: http://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. + +To contribute to this module, please visit http://odoo-community.org. + diff --git a/pos_order_load/__init__.py b/pos_order_load/__init__.py new file mode 100644 index 00000000..89d26e2f --- /dev/null +++ b/pos_order_load/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +import models diff --git a/pos_order_load/__openerp__.py b/pos_order_load/__openerp__.py new file mode 100644 index 00000000..87a0a749 --- /dev/null +++ b/pos_order_load/__openerp__.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2014-TODAY Akretion (). +# Copyright (C) 2014 Sylvain Calador (sylvain.calador@akretion.com). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +{ + 'name': 'POS Order Load and Save', + 'version': '0.2', + 'author': 'Akretion,GRAP,Odoo Community Association (OCA)', + 'category': 'Point Of Sale', + 'depends': [ + 'point_of_sale', + ], + 'website': 'https://www.akretion.com', + 'data': [ + 'view/pos_order_load.xml', + ], + 'qweb': [ + 'static/src/xml/pos_order_load.xml', + ], +} diff --git a/pos_order_load/i18n/fr.po b/pos_order_load/i18n/fr.po new file mode 100644 index 00000000..4451beab --- /dev/null +++ b/pos_order_load/i18n/fr.po @@ -0,0 +1,145 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_order_load +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-07-27 17:43+0000\n" +"PO-Revision-Date: 2015-07-27 17:43+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: pos_order_load +#. openerp-web +#: code:addons/pos_order_load/static/src/js/pos_order_load.js:291 +#: code:addons/pos_order_load/static/src/js/pos_order_load.js:317 +#, python-format +msgid "Can not execute this action because the POS is currently offline" +msgstr "Impossible d'exécuter cette action car le Point de vente est actuellement hors ligne" + +#. module: pos_order_load +#. openerp-web +#: code:addons/pos_order_load/static/src/js/pos_order_load.js:182 +#, python-format +msgid "Can not load the Selected Order because the POS is currently offline" +msgstr "Impossible de charger la vente sélectionnée car le Point de vente est actuellement hors ligne" + +#. module: pos_order_load +#. openerp-web +#: code:addons/pos_order_load/static/src/xml/pos_order_load.xml:28 +#, python-format +msgid "Cancel" +msgstr "Annuler" + +#. module: pos_order_load +#. openerp-web +#: code:addons/pos_order_load/static/src/js/pos_order_load.js:181 +#: code:addons/pos_order_load/static/src/js/pos_order_load.js:290 +#: code:addons/pos_order_load/static/src/js/pos_order_load.js:316 +#, python-format +msgid "Connection error" +msgstr "Erreur de connection" + +#. module: pos_order_load +#. openerp-web +#: code:addons/pos_order_load/static/src/xml/pos_order_load.xml:60 +#, python-format +msgid "Customer" +msgstr "Client" + +#. module: pos_order_load +#. openerp-web +#: code:addons/pos_order_load/static/src/xml/pos_order_load.xml:5 +#, python-format +msgid "Load Draft Order" +msgstr "Charger la vente en brouillon" + +#. module: pos_order_load +#. openerp-web +#: code:addons/pos_order_load/static/src/xml/pos_order_load.xml:59 +#, python-format +msgid "Order" +msgstr "Vente" + +#. module: pos_order_load +#: model:ir.model,name:pos_order_load.model_pos_order +msgid "Point of Sale" +msgstr "Point de Vente" + +#. module: pos_order_load +#. openerp-web +#: code:addons/pos_order_load/static/src/xml/pos_order_load.xml:10 +#, python-format +msgid "Save Current Order" +msgstr "Sauvegarder la vente" + +#. module: pos_order_load +#. openerp-web +#: code:addons/pos_order_load/static/src/js/pos_order_load.js:71 +#, python-format +msgid "Save The current Order ?" +msgstr "Sauvegarder la vente en cours ?" + +#. module: pos_order_load +#. openerp-web +#: code:addons/pos_order_load/static/src/xml/pos_order_load.xml:36 +#, python-format +msgid "Search Orders" +msgstr "Chercher des ventes" + +#. module: pos_order_load +#. openerp-web +#: code:addons/pos_order_load/static/src/xml/pos_order_load.xml:40 +#, python-format +msgid "Select Order" +msgstr "Choisir une vente" + +#. module: pos_order_load +#. openerp-web +#: code:addons/pos_order_load/static/src/js/pos_order_load.js:72 +#, python-format +msgid "This operation will save the current order in a draft state. You'll have to mark it as paid after." +msgstr "Cette opération va sauvegarder la vente en cours à l'état de brouillon. Vous devrez la marquer comme payée ultérieurement" + +#. module: pos_order_load +#. openerp-web +#: code:addons/pos_order_load/static/src/xml/pos_order_load.xml:61 +#, python-format +msgid "Total Amount" +msgstr "Montant Total" + +#. module: pos_order_load +#. openerp-web +#: code:addons/pos_order_load/static/src/js/pos_order_load.js:269 +#, python-format +msgid "Unable to load some order lines because the products are not available in the POS cache.\n" +"\n" +"Please check that lines :\n" +"\n" +" * " +msgstr "Impossible de charger certaines lignes de ventes parce que les produits ne sont pas disponibles dans le cache du point de vente.\n" +"\n" +"Veuillez vérifier ces lignes :\n" +"\n" +" * " + +#. module: pos_order_load +#. openerp-web +#: code:addons/pos_order_load/static/src/js/pos_order_load.js:268 +#, python-format +msgid "Unknown Products" +msgstr "Produits Iconnu" + +#. module: pos_order_load +#. openerp-web +#: code:addons/pos_order_load/static/src/xml/pos_order_load.xml:32 +#, python-format +msgid "Validate" +msgstr "Valider" + diff --git a/pos_order_load/models/__init__.py b/pos_order_load/models/__init__.py new file mode 100644 index 00000000..4ab141fe --- /dev/null +++ b/pos_order_load/models/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +import pos_order diff --git a/pos_order_load/models/pos_order.py b/pos_order_load/models/pos_order.py new file mode 100644 index 00000000..c68ce4db --- /dev/null +++ b/pos_order_load/models/pos_order.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2014 Akretion (). +# @author Sylvain LE GAL (https://twitter.com/legalsylvain) +# @author Sylvain Calador (sylvain.calador@akretion.com). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp import models, api + + +class PosOrder(models.Model): + _inherit = 'pos.order' + + # Overload Section + @api.model + def _order_fields(self, ui_order): + res = super(PosOrder, self)._order_fields(ui_order) + if 'order_id' in ui_order: + res['order_id'] = ui_order['order_id'] + return res + + @api.model + def create_from_ui(self, orders): + """ + Remove from the 'orders' list all orders where amount_return is < 0 + (because that means they are not paid, but just in draft state). + * call a specific function for the draft orders list + * call the parent create_from_ui() for the remaining orders""" + draft_orders = [] + for tmp_order in orders: + if tmp_order['data']['amount_return'] < 0\ + and abs(tmp_order['data']['amount_return']) > 0.000001: + draft_orders.append(tmp_order) + orders.remove(tmp_order) + + # Save Draft Orders + self._create_draft_order_from_ui(draft_orders) + + # Save Paid Orders + return super(PosOrder, self).create_from_ui(orders) + + + # Custom Section + @api.model + def search_read_orders(self, query): + condition = [ + ('state', '=', 'draft'), + ('statement_ids', '=', False), + '|', + ('name', 'ilike', query), + ('partner_id', 'ilike', query) + ] + fields = ['name', 'partner_id', 'amount_total'] + return self.search_read(condition, fields, limit=10) + + @api.one + def load_order(self): + condition = [('order_id', '=', self.id)] + fields = ['product_id', 'price_unit', 'qty', 'discount'] + orderlines = self.lines.search_read(condition, fields) + return { + 'id': self.id, + 'name': self.pos_reference, + 'partner_id': self.partner_id and self.partner_id.id or False, + 'orderlines': orderlines + } + + @api.model + def _create_draft_order_from_ui(self, orders): + for order_tmp in orders: + order_data = order_tmp['data'] + statements_data = order_data['statement_ids'] + order_data.pop('statement_ids') + + # create Order + order = self.create(self._order_fields(order_data)) + + # Create payment + for statement_data in statements_data: + self.add_payment( + order.id, self._payment_fields(statement_data[2])) diff --git a/pos_order_load/static/src/css/pos_order_load.css b/pos_order_load/static/src/css/pos_order_load.css new file mode 100644 index 00000000..62bf9b5a --- /dev/null +++ b/pos_order_load/static/src/css/pos_order_load.css @@ -0,0 +1,54 @@ +.screen .top-content .button.validate { + left: 0px; + margin-left: 150px; +} + +.pos .orderlist-screen .order-list{ + font-size: 16px; + width: 100%; + line-height: 40px; +} +.pos .orderlist-screen .order-list th, +.pos .orderlist-screen .order-list td { + padding: 0px 8px; +} +.pos .orderlist-screen .order-list tr{ + transition: all 150ms linear; + background: rgb(230,230,230); + cursor: pointer; +} +.pos .orderlist-screen .order-list thead > tr, +.pos .orderlist-screen .order-list tr:nth-child(even) { + background: rgb(247,247,247); +} +.pos .orderlist-screen .order-list tr.highlight{ + transition: all 150ms linear; + background: rgb(110,200,155) !important; + color: white; +} +.pos .orderlist-screen .order-list tr.lowlight{ + transition: all 150ms linear; + background: rgb(216, 238, 227); +} +.pos .orderlist-screen .order-list tr.lowlight:nth-child(even){ + transition: all 150ms linear; + background: rgb(227, 246, 237); +} +.pos .orderlist-screen .order-details{ + padding: 16px; + border-bottom: solid 5px rgb(110,200,155); +} + +.pos .orderlist-screen .searchbox{ + right: auto; + margin-left: -90px; + margin-top:8px; + left: 50%; +} +.pos .orderlist-screen .searchbox input{ + width: 120px; +} + +.pos .order-line .total { + text-align: right; +} diff --git a/pos_order_load/static/src/js/pos_order_load.js b/pos_order_load/static/src/js/pos_order_load.js new file mode 100644 index 00000000..0a0fe604 --- /dev/null +++ b/pos_order_load/static/src/js/pos_order_load.js @@ -0,0 +1,363 @@ +/****************************************************************************** + * Point Of Sale - Product Template module for Odoo + * Copyright (C) 2014-Today Akretion (http://www.akretion.com) + * @author Sylvain Calador (sylvain.calador@akretion.com) + * @author Sylvain Le Gal (https://twitter.com/legalsylvain) + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + *****************************************************************************/ + +openerp.pos_order_load = function(instance, local) { + module = instance.point_of_sale; + var QWeb = instance.web.qweb; + var _t = instance.web._t; + var round_pr = instance.web.round_precision; + + /************************************************************************* + Extend Model Order: + * Add getter and setter function for field 'order_id'; + */ + module.Order = module.Order.extend({ + + set_order_id: function(id) { + this.set({ + order_id: id, + }); + }, + + get_order_id: function() { + return this.get('order_id'); + }, + + }); + + /************************************************************************* + New Widget LoadButtonWidget: + * On click, display a new screen to select draft orders; + */ + module.LoadButtonWidget = module.PosBaseWidget.extend({ + template: 'LoadButtonWidget', + + renderElement: function() { + var self = this; + this._super(); + this.$el.click(function(){ + var ss = self.pos.pos_widget.screen_selector; + ss.set_current_screen('orderlist'); + }); + }, + }); + + /************************************************************************* + New Widget SaveButtonWidget: + * On click, backup the current draft order; + */ + module.SaveButtonWidget = module.PosBaseWidget.extend({ + template: 'SaveButtonWidget', + + renderElement: function() { + var self = this; + this._super(); + this.$el.click(function(){ + self.pos.pos_widget.screen_selector.show_popup('confirm',{ + message: _t('Save The current Order ?'), + comment: _t('This operation will save the current order in a draft state. You\'ll have to mark it as paid after.'), + confirm: function(){ + var currentOrder = this.pos.get('selectedOrder'); + this.pos.push_order(currentOrder); + self.pos.get('selectedOrder').destroy(); + }, + }); + }); + }, + }); + + + /************************************************************************* + Extend PosWidget: + * Create new screen; + * Add load and save button; + */ + module.PosWidget = module.PosWidget.extend({ + build_widgets: function() { + this._super(); + + // New Screen to select Draft Orders + this.orderlist_screen = new module.OrderListScreenWidget(this, {}); + this.orderlist_screen.appendTo(this.$('.screens')); + this.orderlist_screen.hide(); + + this.screen_selector.screen_set['orderlist'] = + this.orderlist_screen; + + // Add buttons + this.load_button = new module.LoadButtonWidget(this,{}); + this.load_button.appendTo(this.pos_widget.$('li.orderline.empty')); + + this.save_button = new module.SaveButtonWidget(this,{}); + + }, + }); + + + /************************************************************************* + Extend OrderWidget: + */ + module.OrderWidget = module.OrderWidget.extend({ + renderElement: function(scrollbottom){ + this._super(scrollbottom); + if (this.pos_widget.load_button) { + this.pos_widget.load_button.appendTo( + this.pos_widget.$('li.orderline.empty') + ); + } + if (this.pos_widget.save_button && (this.pos.get('selectedOrder').get('orderLines').length > 0)) { + this.pos_widget.save_button.appendTo( + this.pos_widget.$('div.summary') + ); + } + } + }); + + /************************************************************************* + New ScreenWidget OrderListScreenWidget: + * On show, display all draft orders; + * on click on an order, display the content; + * on click on 'validate', allow to use this POS Order; + * on click on 'cancel', display the preview screen; + */ + module.OrderListScreenWidget = module.ScreenWidget.extend({ + template: 'OrderListScreenWidget', + show_leftpane: true, + model: 'pos.order', + current_order_id: 0, + + init: function(parent, options){ + this._super(parent, options); + }, + + reset_order: function(order) { + order.set_client(undefined); + order.set_order_id(undefined); + order.get('orderLines').reset(); + return order; + }, + + start: function() { + var self = this; + this._super(); + this.$el.find('span.button.back').click(function(){ + order = self.pos.get('selectedOrder'); + self.reset_order(order); + self.pos_widget.order_widget.change_selected_order(); + var ss = self.pos.pos_widget.screen_selector; + ss.set_current_screen('products'); + }); + this.$el.find('span.button.validate').click(function(){ + var orderModel = new instance.web.Model('pos.order'); + return orderModel.call('unlink', [[self.current_order_id]]) + .then(function (result) { + var ss = self.pos.pos_widget.screen_selector; + ss.set_current_screen('products'); + }).fail(function (error, event){ + if (parseInt(error.code) === 200) { + // Business Logic Error, not a connection problem + self.pos_widget.screen_selector.show_popup( + 'error-traceback', { + message: error.data.message, + comment: error.data.debug + }); + } + else{ + self.pos_widget.screen_selector.show_popup('error',{ + message: _t('Connection error'), + comment: _t('Can not load the Selected Order because the POS is currently offline'), + }); + } + event.preventDefault(); + }); + }); + + var search_timeout = null; + + this.$('.searchbox input').on('keyup',function(event){ + clearTimeout(search_timeout); + + var query = this.value; + + search_timeout = setTimeout(function(){ + self.perform_search(query); + },70); + + }); + + this.$('.searchbox .search-clear').click(function(){ + self.clear_search(); + }); + + }, + + // to override if necessary + add_product_attribute: function(product, key, orderline){ + return product; + }, + + load_order_fields: function(order, fields) { + order.set_order_id(fields.id); + var partner = this.pos.db.get_partner_by_id( + fields.partner_id); + order.set_client(partner || undefined); + return order; + }, + + prepare_orderline_options: function(orderline) { + return { + quantity: orderline.qty, + price: orderline.price_unit, + discount: orderline.discount, + }; + }, + + load_order: function(order_id) { + var self = this; + var orderModel = new instance.web.Model(this.model); + return orderModel.call('load_order', [order_id]) + .then(function (result) { + var order = self.pos.get('selectedOrder'); + var result = result[0]; + order = self.load_order_fields(order, result); + order.get('orderLines').reset(); + var orderlines = result.orderlines || []; + var unknown_products = []; + for (var i=0, len=orderlines.length; i 0){ + self.pos_widget.screen_selector.show_popup( + 'error-traceback', { + message: _t('Unknown Products'), + comment: _t('Unable to load some order lines because the ' + + 'products are not available in the POS cache.\n\n' + + 'Please check that lines :\n\n * ') + unknown_products.join("; \n *") + }); + self.$el.find('span.button.validate').hide(); + } + else{ + self.$el.find('span.button.validate').show(); + } + + }).fail(function (error, event){ + if (parseInt(error.code) === 200) { + // Business Logic Error, not a connection problem + self.pos_widget.screen_selector.show_popup( + 'error-traceback', { + message: error.data.message, + comment: error.data.debug + }); + } + else{ + self.pos_widget.screen_selector.show_popup('error',{ + message: _t('Connection error'), + comment: _t('Can not execute this action because the POS is currently offline'), + }); + } + event.preventDefault(); + }); + }, + + load_orders: function(query) { + var self = this; + var orderModel = new instance.web.Model(this.model); + return orderModel.call('search_read_orders', [query || '']) + .then(function (result) { + self.render_list(result); + }).fail(function (error, event){ + if (parseInt(error.code) === 200) { + // Business Logic Error, not a connection problem + self.pos_widget.screen_selector.show_popup( + 'error-traceback', { + message: error.data.message, + comment: error.data.debug + } + ); + } + else{ + self.pos_widget.screen_selector.show_popup('error',{ + message: _t('Connection error'), + comment: _t('Can not execute this action because the POS is currently offline'), + }); + } + event.preventDefault(); + }); + }, + + show: function() { + this._super(); + var ss = this.pos.pos_widget.screen_selector; + if (ss.get_current_screen() == 'orderlist') { + this.load_orders(); + } + }, + + render_list: function(orders){ + var self = this; + var contents = this.$el[0].querySelector('.order-list-contents'); + contents.innerHTML = ""; + for (var i = 0, len = orders.length; i < len; i++){ + var order = orders[i]; + var orderline_html = QWeb.render('LoadOrderLine', + {widget: this, order:orders[i]}); + var orderline = document.createElement('tbody'); + orderline.innerHTML = orderline_html; + orderline = orderline.childNodes[1]; + orderline.addEventListener('click', function() { + self.current_order_id = parseInt(this.dataset['orderId']); + self.load_order(self.current_order_id); + }); + contents.appendChild(orderline); + } + }, + + perform_search: function(query){ + this.load_orders(query) + }, + + clear_search: function(){ + this.load_orders(); + this.$('.searchbox input')[0].value = ''; + this.$('.searchbox input').focus(); + }, + + }); + +} diff --git a/pos_order_load/static/src/xml/pos_order_load.xml b/pos_order_load/static/src/xml/pos_order_load.xml new file mode 100644 index 00000000..220e221e --- /dev/null +++ b/pos_order_load/static/src/xml/pos_order_load.xml @@ -0,0 +1,76 @@ + + diff --git a/pos_order_load/view/pos_order_load.xml b/pos_order_load/view/pos_order_load.xml new file mode 100644 index 00000000..f4d0c506 --- /dev/null +++ b/pos_order_load/view/pos_order_load.xml @@ -0,0 +1,18 @@ + + + + + + + + +