From 5bdc5ad39bc6f1733dcaa3d0669736f9b926932e Mon Sep 17 00:00:00 2001 From: Sylvain LE GAL Date: Mon, 1 Feb 2016 00:22:05 +0100 Subject: [PATCH 01/46] [ADD] new module 'pos_access_right' --- pos_access_right/README.rst | 84 ++++++++++++++ pos_access_right/__init__.py | 2 + pos_access_right/__openerp__.py | 25 ++++ pos_access_right/demo/res_groups.yml | 18 +++ pos_access_right/models/__init__.py | 2 + pos_access_right/models/pos_config.py | 52 +++++++++ pos_access_right/security/res_groups.yml | 17 +++ pos_access_right/static/description/icon.png | Bin 0 -> 4374 bytes .../static/src/css/pos_access_right.css | 12 ++ .../static/src/js/pos_access_right.js | 108 ++++++++++++++++++ pos_access_right/static/src/xml/templates.xml | 17 +++ 11 files changed, 337 insertions(+) create mode 100644 pos_access_right/README.rst create mode 100644 pos_access_right/__init__.py create mode 100644 pos_access_right/__openerp__.py create mode 100644 pos_access_right/demo/res_groups.yml create mode 100644 pos_access_right/models/__init__.py create mode 100644 pos_access_right/models/pos_config.py create mode 100644 pos_access_right/security/res_groups.yml create mode 100644 pos_access_right/static/description/icon.png create mode 100644 pos_access_right/static/src/css/pos_access_right.css create mode 100644 pos_access_right/static/src/js/pos_access_right.js create mode 100644 pos_access_right/static/src/xml/templates.xml diff --git a/pos_access_right/README.rst b/pos_access_right/README.rst new file mode 100644 index 0000000000..021c681e53 --- /dev/null +++ b/pos_access_right/README.rst @@ -0,0 +1,84 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +====================================================== +Point of Sale - Extra Access Right for Certain Actions +====================================================== + +This module will add the following groups to Odoo: + +* PoS - Negative Quantity: The cashier can sell negative quantity in Point Of + Sale (ie, can return products); + +* PoS - Discount: The cashier can set Discount in Point Of Sale; + +* PoS - Change Unit Price: The cashier can change the unit price of a product + in Point Of Sale; + +Important Note +-------------- + +* On PoS Front End, the cashier access right are used. This feature allow + a manager to log into PoS to unblock a specific feature, + + +.. image:: /pos_access_right/static/description/pos_xxx.png + + +Installation +============ + +Normal installation. + +Configuration +============= + +Once installed, you have to give correct access right to your cashiers. + +Usage +===== + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/184/9.0 + +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 +`_. + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Sylvain LE GAL + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://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 https://odoo-community.org. diff --git a/pos_access_right/__init__.py b/pos_access_right/__init__.py new file mode 100644 index 0000000000..a0fdc10fe1 --- /dev/null +++ b/pos_access_right/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models diff --git a/pos_access_right/__openerp__.py b/pos_access_right/__openerp__.py new file mode 100644 index 0000000000..6211f8bd71 --- /dev/null +++ b/pos_access_right/__openerp__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016-Today: La Louve () +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + 'name': 'Point of Sale - Extra Access Right', + 'version': '9.0.1.0.0', + 'category': 'Point Of Sale', + 'summary': 'Point of Sale - Extra Access Right for certain actions', + 'author': 'La Louve, Odoo Community Association (OCA)', + 'website': 'http://www.lalouve.net/', + 'license': 'AGPL-3', + 'depends': [ + 'point_of_sale', + ], + 'data': [ + 'security/res_groups.yml', + 'static/src/xml/templates.xml', + ], + 'demo': [ + 'demo/res_groups.yml', + ], + 'installable': True, +} diff --git a/pos_access_right/demo/res_groups.yml b/pos_access_right/demo/res_groups.yml new file mode 100644 index 0000000000..d79411cbe9 --- /dev/null +++ b/pos_access_right/demo/res_groups.yml @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016-Today: La Louve () +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + + +- !record {model: res.groups, id: group_pos_negative_qty}: + users: + - base.user_root + +- !record {model: res.groups, id: group_pos_discount}: + users: + - base.user_root + - base.user_demo + +- !record {model: res.groups, id: group_pos_change_unit_price}: + users: + - base.user_root diff --git a/pos_access_right/models/__init__.py b/pos_access_right/models/__init__.py new file mode 100644 index 0000000000..e77b60150a --- /dev/null +++ b/pos_access_right/models/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import pos_config diff --git a/pos_access_right/models/pos_config.py b/pos_access_right/models/pos_config.py new file mode 100644 index 0000000000..19e5fb01a7 --- /dev/null +++ b/pos_access_right/models/pos_config.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016-Today: La Louve () +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openerp import fields, models, api + + +class PosConfig(models.Model): + _inherit = 'pos.config' + + group_pos_negative_qty = fields.Many2one( + comodel_name='res.groups', + compute='_compute_group_pos_negative_qty', + string='Point of Sale - Allow Negative Quantity', + help="This field is there to pass the id of the 'PoS - Allow Negative" + " Quantity' Group to the Point of Sale Frontend.") + + group_pos_discount = fields.Many2one( + comodel_name='res.groups', + compute='_compute_group_pos_discount', + string='Point of Sale - Allow Discount', + help="This field is there to pass the id of the 'PoS - Allow Discount'" + " Group to the Point of Sale Frontend.") + + group_pos_change_unit_price = fields.Many2one( + comodel_name='res.groups', + compute='_compute_group_pos_change_unit_price', + string='Point of Sale - Allow Unit Price Change', + help="This field is there to pass the id of the 'PoS - Allow Unit" + " Price Change' Group to the Point of Sale Frontend.") + + @api.multi + def _compute_group_pos_negative_qty(self): + print self.env.ref('pos_access_right.group_pos_negative_qty') + for config in self: + self.group_pos_negative_qty = \ + self.env.ref('pos_access_right.group_pos_negative_qty') + + @api.multi + def _compute_group_pos_discount(self): + print self.env.ref('pos_access_right.group_pos_discount') + for config in self: + self.group_pos_discount = \ + self.env.ref('pos_access_right.group_pos_discount') + + @api.multi + def _compute_group_pos_change_unit_price(self): + print self.env.ref('pos_access_right.group_pos_change_unit_price') + for config in self: + self.group_pos_change_unit_price = \ + self.env.ref('pos_access_right.group_pos_change_unit_price') diff --git a/pos_access_right/security/res_groups.yml b/pos_access_right/security/res_groups.yml new file mode 100644 index 0000000000..ff4389d1cf --- /dev/null +++ b/pos_access_right/security/res_groups.yml @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016-Today: La Louve () +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + + +- !record {model: res.groups, id: group_pos_negative_qty}: + name: Point of Sale - Allow Negative Quantity + category_id: base.module_category_usability + +- !record {model: res.groups, id: group_pos_discount}: + name: Point of Sale - Allow Discount + category_id: base.module_category_usability + +- !record {model: res.groups, id: group_pos_change_unit_price}: + name: Point of Sale - Allow Price Change + category_id: base.module_category_usability diff --git a/pos_access_right/static/description/icon.png b/pos_access_right/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2c83d7101b7d63990c85fa50905fc5904048354b GIT binary patch literal 4374 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 DMU_o_ literal 0 HcmV?d00001 diff --git a/pos_access_right/static/src/css/pos_access_right.css b/pos_access_right/static/src/css/pos_access_right.css new file mode 100644 index 0000000000..00bc938abf --- /dev/null +++ b/pos_access_right/static/src/css/pos_access_right.css @@ -0,0 +1,12 @@ +/* + Copyright (C) 2016-Today: La Louve () + @author: Sylvain LE GAL (https://twitter.com/legalsylvain) + License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +*/ + +.pos-disabled-mode { + color: #bbb !important; +} +.pos-disabled-mode:hover { + background: #e2e2e2 !important; +} diff --git a/pos_access_right/static/src/js/pos_access_right.js b/pos_access_right/static/src/js/pos_access_right.js new file mode 100644 index 0000000000..4013d20474 --- /dev/null +++ b/pos_access_right/static/src/js/pos_access_right.js @@ -0,0 +1,108 @@ +/* + Copyright (C) 2016-Today: La Louve () + @author: Sylvain LE GAL (https://twitter.com/legalsylvain) + License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +*/ + + +odoo.define('pos_access_right.pos_access_right', function (require) { + "use strict"; + + var screens = require('point_of_sale.screens'); + var models = require('point_of_sale.models'); + var gui = require('point_of_sale.gui'); + var core = require('web.core'); + var _t = core._t; + +/* ******************************************************** +point_of_sale.gui +******************************************************** */ + + // New function 'display_access_right' to display disabled functions + gui.Gui.prototype.display_access_right = function(user){ + if (user.groups_id.indexOf(this.pos.config.group_pos_negative_qty[0]) != -1){ + $('.numpad-minus').removeClass('pos-disabled-mode'); + } + else{ + $('.numpad-minus').addClass('pos-disabled-mode'); + } + if (user.groups_id.indexOf(this.pos.config.group_pos_discount[0]) != -1){ + $(".mode-button[data-mode='discount']").removeClass('pos-disabled-mode'); + } + else{ + $(".mode-button[data-mode='discount']").addClass('pos-disabled-mode'); + } + if (user.groups_id.indexOf(this.pos.config.group_pos_change_unit_price[0]) != -1){ + $(".mode-button[data-mode='price']").removeClass('pos-disabled-mode'); + } + else{ + $(".mode-button[data-mode='price']").addClass('pos-disabled-mode'); + } + }; + + +/* ******************************************************** +point_of_sale.models +******************************************************** */ + + // load extra data from 'pos_config' (ids of new groups) + models.load_fields("pos.config", "group_pos_negative_qty"); + models.load_fields("pos.config", "group_pos_discount"); + models.load_fields("pos.config", "group_pos_change_unit_price"); + + // Overload 'set_cashier' function to display correctly + // unauthorized function after cashier changed + var _set_cashier_ = models.PosModel.prototype.set_cashier; + models.PosModel.prototype.set_cashier = function(user){ + this.gui.display_access_right(user); + _set_cashier_.call(this, user); + }; + + +/* ******************************************************** +screens.NumpadWidget +******************************************************** */ + screens.NumpadWidget.include({ + + // Overload 'start' function to display correctly unauthorized function + // at the beginning of the session, based on current user + start: function() { + this._super(); + this.gui.display_access_right(this.pos.get_cashier()); + }, + + // block '+/-' button if user doesn't belong to the correct group + clickSwitchSign: function() { + if (this.pos.get_cashier().groups_id.indexOf(this.pos.config.group_pos_negative_qty[0]) == -1) { + this.gui.show_popup('error',{ + 'title': _t('Negative Quantity - Unauthorized function'), + 'body': _t('Please ask your manager to do it.'), + }); + } + else { + return this._super(); + } + }, + + // block 'discount' or 'price' button if user doesn't belong to the correct group + clickChangeMode: function(event) { + if (event.currentTarget.attributes['data-mode'].nodeValue == 'discount' && + this.pos.get_cashier().groups_id.indexOf(this.pos.config.group_pos_discount[0]) == -1) { + this.gui.show_popup('error',{ + 'title': _t('Discount - Unauthorized function'), + 'body': _t('Please ask your manager to do it.'), + }); + } + else if (event.currentTarget.attributes['data-mode'].nodeValue == 'price' && + this.pos.get_cashier().groups_id.indexOf(this.pos.config.group_pos_change_unit_price[0]) == -1) { + this.gui.show_popup('error',{ + 'title': _t('Change Unit Price - Unauthorized function'), + 'body': _t('Please ask your manager to do it.'), + }); + } + else { + return this._super(event); + } + }, + }); +}); diff --git a/pos_access_right/static/src/xml/templates.xml b/pos_access_right/static/src/xml/templates.xml new file mode 100644 index 0000000000..3742269873 --- /dev/null +++ b/pos_access_right/static/src/xml/templates.xml @@ -0,0 +1,17 @@ + + + + +