Commit dcf8a236 by François C.

Merge branch 'dev_cooperatic' of…

Merge branch 'dev_cooperatic' of https://gl.cooperatic.fr/cooperatic-foodcoops/Odoo into dev_cooperatic
parents c2afa838 a4cf6b15
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
'description': """ 'description': """
Recherche par num. de coop\n Recherche par num. de coop\n
(la recherche inclut le code barre ou non,\n
le réglage se fait avec la clé lacagette_custom_pos.reduce_pos_members_search)\n
Après une recherche, le champ perd le focus\n Après une recherche, le champ perd le focus\n
Utilisation du pavé numérique du clavier sur le panier\n Utilisation du pavé numérique du clavier sur le panier\n
Personnalisation de la popup d'erreur "codebarre non reconnu" :\n Personnalisation de la popup d'erreur "codebarre non reconnu" :\n
......
<?xml version="1.0"?>
<odoo noupdate="0">
<record id="reduce_pos_members_search" model="ir.config_parameter">
<field name="key">lacagette_custom_pos.reduce_pos_members_search</field>
<field name="value">False</field>
</record>
</odoo>
...@@ -2,8 +2,21 @@ odoo.define('lacagette_custom_pos.DB', function(require) { ...@@ -2,8 +2,21 @@ odoo.define('lacagette_custom_pos.DB', function(require) {
"use strict"; "use strict";
var PosDB = require('point_of_sale.DB'); var PosDB = require('point_of_sale.DB');
var Model = require('web.DataModel');
var config_parameter = new Model('ir.config_parameter');
var reduce_pos_members_search = null;
PosDB.include({ PosDB.include({
init: function(parent, options){
this._super(parent,options);
config_parameter.call('get_param', ['lacagette_custom_pos.reduce_pos_members_search'])
.then(function(param){
if (param != undefined) {
reduce_pos_members_search = param;
}
})
},
_partner_search_string: function(partner){ _partner_search_string: function(partner){
var str = partner.name; var str = partner.name;
//Diacritric search //Diacritric search
...@@ -12,9 +25,13 @@ odoo.define('lacagette_custom_pos.DB', function(require) { ...@@ -12,9 +25,13 @@ odoo.define('lacagette_custom_pos.DB', function(require) {
} catch(e) { } catch(e) {
console.log(e) console.log(e)
} }
if(partner.barcode){
if (reduce_pos_members_search === 'False') {
if(partner.barcode){
str += '|' + partner.barcode; str += '|' + partner.barcode;
} }
}
if(partner.barcode_base){ if(partner.barcode_base){
str += '|' + partner.barcode_base; str += '|' + partner.barcode_base;
} }
......
...@@ -40,6 +40,10 @@ class ResPartner(models.Model): ...@@ -40,6 +40,10 @@ class ResPartner(models.Model):
cooperative_state = fields.Selection( cooperative_state = fields.Selection(
selection=EXTRA_COOPERATIVE_STATE_SELECTION, default='not_concerned') selection=EXTRA_COOPERATIVE_STATE_SELECTION, default='not_concerned')
extra_shift_done = fields.Integer(
"Number of shift done with both of the associate",
default= 0)
@api.model @api.model
def run_process_target_status(self): def run_process_target_status(self):
"""Method called by cron task""" """Method called by cron task"""
......
...@@ -11,27 +11,31 @@ class ShiftCounterEvent(models.Model): ...@@ -11,27 +11,31 @@ class ShiftCounterEvent(models.Model):
def _update_partner_target_status(self, vals): def _update_partner_target_status(self, vals):
"""actions when points are removed or added""" """actions when points are removed or added"""
conf = self.env['ir.config_parameter'] try:
suspension_limit = int(conf.get_param("lacagette_membership.points_limit_to_get_suspended")) conf = self.env['ir.config_parameter']
unsubscribe_limit = int(conf.get_param("lacagette_membership.points_limit_to_get_unsubscribed")) suspension_limit = int(conf.get_param("lacagette_membership.points_limit_to_get_suspended"))
if vals['point_qty'] != 0: unsubscribe_limit = int(conf.get_param("lacagette_membership.points_limit_to_get_unsubscribed"))
res_partner = self.env['res.partner'].search([('id', '=', vals['partner_id'])]) if vals['point_qty'] != 0:
if res_partner: res_partner = self.env['res.partner'].search([('id', '=', vals['partner_id'])])
p = res_partner[0] if res_partner:
points_before_removing_points = p.final_ftop_point if p.shift_type == "ftop" else p.final_standard_point p = res_partner[0]
points_after_removal = points_before_removing_points + vals['point_qty'] points_before_removing_points = p.final_ftop_point if p.shift_type == "ftop" else p.final_standard_point
_logger.info("points_after_removal = %s, current_state = %s", str(points_after_removal), p.cooperative_state) points_after_removal = points_before_removing_points + vals['point_qty']
if points_after_removal <= suspension_limit or points_after_removal <= unsubscribe_limit: _logger.info("points_after_removal = %s, current_state = %s", str(points_after_removal), p.cooperative_state)
target_status = None if points_after_removal <= suspension_limit or points_after_removal <= unsubscribe_limit:
if (points_after_removal <= suspension_limit target_status = None
and if (points_after_removal <= suspension_limit
p.cooperative_state != 'delay'): and
target_status = 'suspended' p.cooperative_state != 'delay'):
if points_after_removal <= unsubscribe_limit: target_status = 'suspended'
target_status = 'unsubscribed' if points_after_removal <= unsubscribe_limit:
if target_status is not None: target_status = 'unsubscribed'
p.update({'target_status': target_status, if target_status is not None:
'current_cooperative_state': p.cooperative_state}) p.update({'target_status': target_status,
'current_cooperative_state': p.cooperative_state})
except Exception as e:
# don't block process if an error occurs here
_logger.error("Error during _update_partner_target_status : %s", str(e))
@api.model @api.model
def write(self, vals): def write(self, vals):
......
...@@ -14,6 +14,10 @@ class ShiftRegistration(models.Model): ...@@ -14,6 +14,10 @@ class ShiftRegistration(models.Model):
is_makeup = fields.Boolean( is_makeup = fields.Boolean(
"Is this registration a consequence of a makeup to do", "Is this registration a consequence of a makeup to do",
default= False) default= False)
associate_registered = fields.Char(
"Who will do the shift, partner, associate or both",
default= False)
...@@ -23,10 +27,14 @@ class ShiftRegistration(models.Model): ...@@ -23,10 +27,14 @@ class ShiftRegistration(models.Model):
absence_status = conf.get_param("lacagette_membership.absence_status") absence_status = conf.get_param("lacagette_membership.absence_status")
if 'state' in vals and vals['state'] == absence_status: if 'state' in vals and vals['state'] == absence_status:
if self.ids: if self.ids:
# it is the case when called from "absence cron job" run in external third-party
to_add = 2 if absence_status == 'absent' else 1
for s in self.env['shift.registration']\ for s in self.env['shift.registration']\
.search([('id', 'in', self.ids)]): .search([('id', 'in', self.ids)]):
# it is the case when called from "absence cron job" run in external third-party
to_add = 2 if absence_status == 'absent' else 1
# Missing a makeup leads to have an additional makeup (the shift you initialy missed + the makeup you missed)
if s.is_makeup is True:
to_add += 1
new_makeups_to_do = s.partner_id.makeups_to_do + to_add new_makeups_to_do = s.partner_id.makeups_to_do + to_add
s.partner_id.update({'makeups_to_do': new_makeups_to_do}) s.partner_id.update({'makeups_to_do': new_makeups_to_do})
return super(ShiftRegistration, self).write(vals) return super(ShiftRegistration, self).write(vals)
\ No newline at end of file
============================
Point Of Sale - Meal Voucher
============================
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-legalsylvain%2Fpos-lightgray.png?logo=github
:target: https://github.com/legalsylvain/pos/tree/12.0-ADD-pos_meal_voucher/pos_meal_voucher
:alt: legalsylvain/pos
|badge1| |badge2| |badge3|
This module extend the Point of Sale Odoo module, regarding Meal Vouchers.
Meal voucher is a payment method, available in some countries (France, Belgium, Romania, ...)
that allows customer to buy food products in grocery stores or pay in restaurants.
**Table of contents**
.. contents::
:local:
Configuration
=============
Products
~~~~~~~~
* go to your products, and click on the 'Meal Voucher' checkbox, if your product
can be paid with meal vouchers.
.. figure:: https://raw.githubusercontent.com/legalsylvain/pos/12.0-ADD-pos_meal_voucher/pos_meal_voucher/static/description/product_product_form.png
* You can configure your product categories to have a default value for the products
that belong to this category.
.. figure:: https://raw.githubusercontent.com/legalsylvain/pos/12.0-ADD-pos_meal_voucher/pos_meal_voucher/static/description/product_category_form.png
* configures your Account journals, mentioning the type of Meal Voucher:
- Paper : the journal will be used when scanning meal voucher barcodes
- Dematerialized: the journal will be used for dematerialized meal vouchers
- Mixed: Specific configuration if your accountant want to use a single journal for Credit card AND dematerialized meal vouchers. In that case, the button of this journal will be duplicated, and an extra text can be set to display an alternative label.
.. figure:: https://raw.githubusercontent.com/legalsylvain/pos/12.0-ADD-pos_meal_voucher/pos_meal_voucher/static/description/account_journal_form.png
* go your point of sale configuration form, and set the maximum amount allowed by ticket. (optional)
.. figure:: https://raw.githubusercontent.com/legalsylvain/pos/12.0-ADD-pos_meal_voucher/pos_meal_voucher/static/description/pos_config_form.png
* configure if you want to allow or forbid to exceed the maximum allowed by ticket and for order (Give change on meal voucher)
Usage
=====
* Open your Point of Sale
* Cashier can see the food products, eligible for meal voucher payment, and see the total for
Meal Voucher amount
.. figure:: https://raw.githubusercontent.com/legalsylvain/pos/12.0-ADD-pos_meal_voucher/pos_meal_voucher/static/description/front_ui_pos_order_screen.png
* go to the payment screen
A Meal Voucher Summary is available:
.. figure:: https://raw.githubusercontent.com/legalsylvain/pos/12.0-ADD-pos_meal_voucher/pos_meal_voucher/static/description/front_ui_pos_payment_screen.png
If the amount received is too important, a warning icon is displayed
.. figure:: https://raw.githubusercontent.com/legalsylvain/pos/12.0-ADD-pos_meal_voucher/pos_meal_voucher/static/description/front_ui_pos_payment_screen_summary.png
If the cashier try to validate the order, a warning is also display, asking confirmation
.. figure:: https://raw.githubusercontent.com/legalsylvain/pos/12.0-ADD-pos_meal_voucher/pos_meal_voucher/static/description/front_ui_pos_payment_screen_warning.png
It is a non blocking warning, because we don't want to prevent an order to be done,
if products are not correctly set, or if a recent law changed the maximum amount that can
be used each day. (A recent case occured in France, during the Covid-19 pandemy)
If you want to make it impossible to finish an order if the amount of meal voucher is too big then uncheck the option "meal_voucher_change_accepted"
In this case if the cashier tries to validate the order the warning will redirect him to the paiement page.
Note
~~~~
A new barcode rule is available for Paper Meal Voucher of 24 chars:
``...........{NNNDD}........``
If you scan the following barcode ``052566641320080017000000``, a new payment line will be added, with an amount of 8,00€ (``00800``)
Known issues / Roadmap
======================
* Introduce the Meal Voucher Issuer model
* When scaning Meal Voucher, deduce the Issuer
* Add a reporting to make easily the deposit of Meal Vouchers, per issuers.
* Add an option to add subtotal of products that can be paid with meal vouchers,
on the bill.
* Prevent to scan twice the same Meal Voucher barcode.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/legalsylvain/pos/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 <https://github.com/legalsylvain/pos/issues/new?body=module:%20pos_meal_voucher%0Aversion:%2012.0-ADD-pos_meal_voucher%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* GRAP
Contributors
~~~~~~~~~~~~
* Sylvain LE GAL <https://twitter.com/legalsylvain>
Other credits
~~~~~~~~~~~~~
The development of this module has been financially supported by:
* Vracoop (https://portail.vracoop.fr/)
* Demain Supermarché (http://www.demainsupermarche.org/)
Maintainers
~~~~~~~~~~~
This module is part of the `legalsylvain/pos <https://github.com/legalsylvain/pos/tree/12.0-ADD-pos_meal_voucher/pos_meal_voucher>`_ project on GitHub.
You are welcome to contribute.
# Copyright (C) 2020 - Today: GRAP (http://www.grap.coop)
# @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 - Meal Voucher",
"summary": "Handle meal vouchers in Point of Sale"
" with eligible amount and max amount",
"version": "9.0.1.0.0",
"category": "Point of Sale",
"author": "GRAP, Odoo Community Association (OCA)",
"website": "http://www.github.com/OCA/pos",
"license": "AGPL-3",
"depends": [
"point_of_sale",
],
"data": [
"data/barcode_rule.xml",
"views/view_account_journal.xml",
"views/view_pos_config.xml",
"views/view_product_category.xml",
"views/view_product_template.xml",
"views/templates.xml",
],
"qweb": [
"static/src/xml/pos_meal_voucher.xml",
],
"demo": [
"demo/product_category.xml",
"demo/product_product.xml",
],
"installable": True,
}
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2020 - Today: GRAP (http://www.grap.coop)
@author: Sylvain LE GAL (https://twitter.com/legalsylvain)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<openerp>
<data>
<record id="rule_meal_voucher" model="barcode.rule">
<field name="name">Meal Voucher Payment</field>
<field name="barcode_nomenclature_id" ref="barcodes.default_barcode_nomenclature"/>
<field name="type">meal_voucher_payment</field>
<field name="encoding">any</field>
<field name="sequence">1</field>
<field name="pattern">...........{NNNDD}........</field>
</record>
</data>
</openerp>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2020 - Today: GRAP (http://www.grap.coop)
@author: Sylvain LE GAL (https://twitter.com/legalsylvain)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<openerp>
<record id="food_category" model="product.category">
<field name="name">Food</field>
<field name="parent_id" ref="point_of_sale.product_category_pos"/>
<field name="meal_voucher_ok" eval="True"/>
</record>
</openerp>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2020 - Today: GRAP (http://www.grap.coop)
@author: Sylvain LE GAL (https://twitter.com/legalsylvain)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<openerp>
<record id="bread" model="product.product">
<field name="name">Organic Wholemeal Bread</field>
<field name="categ_id" ref="food_category"/>
<field name="meal_voucher_ok" eval="True"/>
<field name="available_in_pos" eval="True"/>
<field name="available_in_pos" eval="True"/>
<field name="list_price">5.30</field>
</record>
</openerp>
from . import account_journal
from . import barcode_rule
from . import pos_config
from . import pos_order
from . import product_category
from . import product_template
from . import account_bank_statement_line
# Copyright (C) 2020 - Today: GRAP (http://www.grap.coop)
# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import api, fields, models
class AccountBankStatementLine(models.Model):
_inherit = 'account.bank.statement.line'
statement_note = fields.Char(
string="Number ID of the meal voucher")
# Copyright (C) 2020 - Today: GRAP (http://www.grap.coop)
# @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
class AccountJournal(models.Model):
_inherit = 'account.journal'
meal_voucher_type = fields.Selection(
string="Meal Voucher Type",
selection=[
("paper", "Paper"),
("dematerialized", "Dematerialized"),
("mixed", "Credit Card / Dematerialized"),
],
)
meal_voucher_mixed_text = fields.Char(
string="Text for Mixed journal",
help="Text that will be displayed in the point of sale"
" if the journal is a mixed journal (Credit Card / "
" Dematerialized) for the dematerialized button.")
# Copyright (C) 2020 - Today: GRAP (http://www.grap.coop)
# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import sets
from openerp import fields, models, api, _
class BarcodeRule(models.Model):
_inherit = "barcode.rule"
@api.model
def _get_type_selection(self):
types = sets.Set(super(BarcodeRule,self)._get_type_selection())
types.update([
('meal_voucher_payment', _('Meal Voucher Payment'))
])
return list(types)
# Copyright (C) 2020 - Today: GRAP (http://www.grap.coop)
# @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
class PosConfig(models.Model):
_inherit = 'pos.config'
max_meal_voucher_amount = fields.Monetary(
string="Meal Voucher Amount",
currency_field="currency_id",
)
meal_voucher_display_product_screen = fields.Boolean(
string="Display icon before products on screen",
default=True)
meal_voucher_change_accepted = fields.Boolean(
string="Give change on meal voucher",
default=True)
# Copyright (C) 2020 - Today: GRAP (http://www.grap.coop)
# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import models
import time
from datetime import datetime
import uuid
from openerp import tools
from openerp.osv import fields
class PosOrder(models.Model):
_inherit = 'pos.order'
def _payment_fields(self, cr, uid, ui_paymentline, context=None):
res = super(PosOrder, self)._payment_fields(cr, uid, ui_paymentline)
res["statement_note"] = ui_paymentline.get("statement_note", False)
return res
def add_payment(self, cr, uid, order_id, data, context=None):
# From odoo/addons/point_of_sale/point_of_sale.py line 1107
"""Create a new payment for the order"""
context = dict(context or {})
statement_line_obj = self.pool.get('account.bank.statement.line')
property_obj = self.pool.get('ir.property')
order = self.browse(cr, uid, order_id, context=context)
date = data.get('payment_date', time.strftime('%Y-%m-%d'))
if len(date) > 10:
timestamp = datetime.strptime(date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
ts = fields.datetime.context_timestamp(cr, uid, timestamp, context)
date = ts.strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
args = {
'amount': data['amount'],
'date': date,
'name': order.name + ': ' + (data.get('payment_name', '') or ''),
'partner_id': order.partner_id and self.pool.get("res.partner")._find_accounting_partner(order.partner_id).id or False,
}
journal_id = data.get('journal', False)
statement_id = data.get('statement_id', False)
assert journal_id or statement_id, "No statement_id or journal_id passed to the method!"
journal = self.pool['account.journal'].browse(cr, uid, journal_id, context=context)
# use the company of the journal and not of the current user
company_cxt = dict(context, force_company=journal.company_id.id)
account_def = property_obj.get(cr, uid, 'property_account_receivable_id', 'res.partner', context=company_cxt)
args['account_id'] = (order.partner_id and order.partner_id.property_account_receivable_id \
and order.partner_id.property_account_receivable_id.id) or (account_def and account_def.id) or False
if not args['account_id']:
if not args['partner_id']:
msg = _('There is no receivable account defined to make payment.')
else:
msg = _('There is no receivable account defined to make payment for the partner: "%s" (id:%d).') % (order.partner_id.name, order.partner_id.id,)
raise UserError(msg)
context.pop('pos_session_id', False)
for statement in order.session_id.statement_ids:
if statement.id == statement_id:
journal_id = statement.journal_id.id
break
elif statement.journal_id.id == journal_id:
statement_id = statement.id
break
if not statement_id:
raise UserError(_('You have to open at least one cashbox.'))
statement_note = ''
if 'statement_note' in data:
statement_note = data['statement_note']
args.update({
'statement_id': statement_id,
'pos_statement_id': order_id,
'journal_id': journal_id,
'ref': order.session_id.name,
'statement_note': statement_note,
})
statement_line_obj.create(cr, uid, args, context=context)
return statement_id
# Copyright (C) 2020 - Today: GRAP (http://www.grap.coop)
# @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 ProductCategory(models.Model):
_inherit = 'product.category'
meal_voucher_ok = fields.Boolean(
string="Meal Voucher",
help="If checked, the products that belong to the category"
" will be marked as 'can be paid with meal vouchers', by default."
)
@api.multi
def button_apply_meal_voucher_settings(self):
ProductTemplate = self.env["product.template"]
for category in self:
templates = ProductTemplate.with_context(
active_test=False).search([('categ_id', '=', category.id)])
templates.write({"meal_voucher_ok": category.meal_voucher_ok})
category.child_id.write({"meal_voucher_ok": category.meal_voucher_ok})
category.child_id.button_apply_meal_voucher_settings()
# Copyright (C) 2020 - Today: GRAP (http://www.grap.coop)
# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import api, fields, models
class ProductTemplate(models.Model):
_inherit = 'product.template'
meal_voucher_ok = fields.Boolean(
string="Meal Voucher",
help="Check this box if the product can be paid with meal vouchers."
)
@api.onchange("categ_id")
def onchange_categ_id(self):
for template in self:
template.meal_voucher_ok = template.categ_id.meal_voucher_ok
Products
~~~~~~~~
* go to your products, and click on the 'Meal Voucher' checkbox, if your product
can be paid with meal vouchers.
.. figure:: ../static/description/product_product_form.png
* You can configure your product categories to have a default value for the products
that belong to this category.
.. figure:: ../static/description/product_category_form.png
* configures your Account journals, mentioning the type of Meal Voucher:
- Paper : the journal will be used when scanning meal voucher barcodes
- Dematerialized: the journal will be used for dematerialized meal vouchers
- Mixed: Specific configuration if your accountant want to use a single journal for Credit card AND dematerialized meal vouchers. In that case, the button of this journal will be duplicated, and an extra text can be set to display an alternative label.
.. figure:: ../static/description/account_journal_form.png
* go your point of sale configuration form, and set the maximum amount allowed by ticket. (optional)
.. figure:: ../static/description/pos_config_form.png
* Sylvain LE GAL <https://twitter.com/legalsylvain>
The development of this module has been financially supported by:
* Vracoop (https://portail.vracoop.fr/)
* Demain Supermarché (http://www.demainsupermarche.org/)
This module extend the Point of Sale Odoo module, regarding Meal Vouchers.
Meal voucher is a payment method, available in some countries (France, Belgium, Romania, ...)
that allows customer to buy food products in grocery stores or pay in restaurants.
* Introduce the Meal Voucher Issuer model
* When scaning Meal Voucher, deduce the Issuer
* Add a reporting to make easily the deposit of Meal Vouchers, per issuers.
* Add an option to add subtotal of products that can be paid with meal vouchers,
on the bill.
* Prevent to scan twice the same Meal Voucher barcode.
* Open your Point of Sale
* Cashier can see the food products, eligible for meal voucher payment, and see the total for
Meal Voucher amount
.. figure:: ../static/description/front_ui_pos_order_screen.png
* go to the payment screen
A Meal Voucher Summary is available:
.. figure:: ../static/description/front_ui_pos_payment_screen.png
If the amount received is too important, a warning icon is displayed
.. figure:: ../static/description/front_ui_pos_payment_screen_summary.png
If the cashier try to validate the order, a warning is also display, asking confirmation
.. figure:: ../static/description/front_ui_pos_payment_screen_warning.png
It is a non blocking warning, because we don't want to prevent an order to be done,
if products are not correctly set, or if a recent law changed the maximum amount that can
be used each day. (A recent case occured in France, during the Covid-19 pandemy)
Note
~~~~
A new barcode rule is available for Paper Meal Voucher of 24 chars:
``...........{NNNDD}........``
If you scan the following barcode ``052566641320080017000000``, a new payment line will be added, with an amount of 8,00€ (``00800``)
.pos .order .summary .line .meal-voucher{
font-size: 16px;
font-weight: normal;
text-align: center;
font-style:italic;
color: #999;
}
.pos .paymentline .is-meal-voucher{
cursor: pointer;
text-align: center;
padding-top: 0px;
padding-bottom: 0px;
background-color: #f0eeee;
}
.pos .paymentline .is-meal-voucher .fa-cutlery{
border-radius: 20px;
border: 1px solid rgba(0,0,0,0.2);
font-size: 20px;
width: 40px;
height: 30px;
line-height: 30px;
vertical-align: 0%;
color:#AAA;
background-color: #FFF;
}
.payment-screen .paymentmethods .fa-cutlery{
width: 48px;
height: 48px;
line-height: 48px;
margin-top: -25px;
font-size: 20px;
border-radius: 26px;
border: 1px solid rgba(0,0,0,0.2);
background: rgba(255,255,255,0.4);
}
.payment-screen div.meal-voucher-summary{
border-top: dashed 1px gainsboro;
}
.payment-screen div.meal-voucher-summary .fa-warning{
color: red;
}
.payment-screen div.meal-voucher-summary table{
width: 100%;
}
.payment-screen div.meal-voucher-summary tbody{
background: white;
}
.payment-screen div.meal-voucher-summary th {
font-size: 16px;
padding: 8px;
text-align: center;
}
.payment-screen div.meal-voucher-summary td {
font-size: 22px;
padding: 8px 12px;
}
// Copyright (C) 2020 - Today: GRAP (http://www.grap.coop)
// @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
odoo.define("pos_meal_voucher.models", function (require) {
"use strict";
var models = require("point_of_sale.models");
var utils = require("web.utils");
var round_pr = utils.round_precision;
models.load_fields("product.product", ["meal_voucher_ok"]);
models.load_fields("account.journal", ["meal_voucher_type", "meal_voucher_mixed_text"]);
var OrderSuper = models.Order.prototype;
var Order = models.Order.extend({
paper_meal_vouche_number_already_used: function(meal_voucher_number){
for(const paiementLine of this.get_paymentlines()){
if(paiementLine.statement_note == meal_voucher_number) return true
}
return false;
},
get_total_meal_voucher_eligible: function() {
return round_pr(this.orderlines.reduce((function(sum, orderLine) {
if (orderLine.product.meal_voucher_ok && orderLine.get_price_with_tax() > 0){
return sum + orderLine.get_price_with_tax();
} else {
return sum;
}
}), 0), this.pos.currency.rounding);
},
get_total_meal_voucher_received: function(){
return round_pr(this.paymentlines.reduce((function(sum, paymentLine) {
if (paymentLine.is_meal_voucher()) {
return sum + paymentLine.get_amount();
} else {
return sum;
}
}), 0), this.pos.currency.rounding);
},
/* point_of_sale/statis/src/js/models.js */
export_for_printing: function(){
var orderlines = [];
var self = this;
var paymentlines = [];
// Add fork&knife symbol on pos ticket for products with meal voucher allowed & when meal voucher is used
var meal_voucher_used = false;
this.paymentlines.each(function(paymentline){
var line = paymentline.export_for_printing();
paymentlines.push(line);
if (paymentline.is_meal_voucher()) {
meal_voucher_used = true;
}
});
this.orderlines.each(function(orderline){
var orderline_for_printing = orderline.export_for_printing()
if (
meal_voucher_used === true &&
orderline.product.meal_voucher_ok === true &&
orderline_for_printing.product_name.includes(String.fromCharCode(0xD83C, 0xDF74)) === false
) {
orderline_for_printing.product_name = String.fromCharCode(0xD83C, 0xDF74) + " " + orderline_for_printing.product_name;
} else if (meal_voucher_used === false && orderline_for_printing.product_name.includes(String.fromCharCode(0xD83C, 0xDF74)) === true) {
orderline_for_printing.product_name = orderline_for_printing.product_name.replace(String.fromCharCode(0xD83C, 0xDF74) + " ", "");
}
orderlines.push(orderline_for_printing);
});
var client = this.get('client');
var cashier = this.pos.cashier || this.pos.user;
var company = this.pos.company;
var shop = this.pos.shop;
var date = new Date();
function is_xml(subreceipt){
return subreceipt ? (subreceipt.split('\n')[0].indexOf('<!DOCTYPE QWEB') >= 0) : false;
}
function render_xml(subreceipt){
if (!is_xml(subreceipt)) {
return subreceipt;
} else {
subreceipt = subreceipt.split('\n').slice(1).join('\n');
var qweb = new QWeb2.Engine();
qweb.debug = core.debug;
qweb.default_dict = _.clone(QWeb.default_dict);
qweb.add_template('<templates><t t-name="subreceipt">'+subreceipt+'</t></templates>');
return qweb.render('subreceipt',{'pos':self.pos,'widget':self.pos.chrome,'order':self, 'receipt': receipt}) ;
}
}
var receipt = {
orderlines: orderlines,
paymentlines: paymentlines,
subtotal: this.get_subtotal(),
total_with_tax: this.get_total_with_tax(),
total_without_tax: this.get_total_without_tax(),
total_tax: this.get_total_tax(),
total_paid: this.get_total_paid(),
total_discount: this.get_total_discount(),
tax_details: this.get_tax_details(),
change: this.get_change(),
name : this.get_name(),
client: client ? client.name : null ,
invoice_id: null, //TODO
cashier: cashier ? cashier.name : null,
precision: {
price: 2,
money: 2,
quantity: 3,
},
date: {
year: date.getFullYear(),
month: date.getMonth(),
date: date.getDate(), // day of the month
day: date.getDay(), // day of the week
hour: date.getHours(),
minute: date.getMinutes() ,
isostring: date.toISOString(),
localestring: date.toLocaleString(),
},
company:{
email: company.email,
website: company.website,
company_registry: company.company_registry,
contact_address: company.partner_id[1],
vat: company.vat,
name: company.name,
phone: company.phone,
logo: this.pos.company_logo_base64,
},
shop:{
name: shop.name,
},
currency: this.pos.currency,
};
if (is_xml(this.pos.config.receipt_header)){
receipt.header = '';
receipt.header_xml = render_xml(this.pos.config.receipt_header);
} else {
receipt.header = this.pos.config.receipt_header || '';
}
if (is_xml(this.pos.config.receipt_footer)){
receipt.footer = '';
receipt.footer_xml = render_xml(this.pos.config.receipt_footer);
} else {
receipt.footer = this.pos.config.receipt_footer || '';
}
return receipt;
},
});
models.Order = Order;
var PaymentlineSuper = models.Paymentline.prototype;
var Paymentline = models.Paymentline.extend({
initialize: function(){
PaymentlineSuper.initialize.apply(this, arguments);
// We use 'payment_note', because 'note' field is still used
// to set in the payment_name value.
// See odoo/addons/point_of_sale/models/pos_order.py:59
// and then in the name of the statement line.
// See odoo/addons/point_of_sale/models/pos_order.py:950
this.statement_note = '';
this.manual_meal_voucher = false;
},
init_from_JSON: function (json) {
PaymentlineSuper.init_from_JSON.apply(this, arguments);
this.statement_note = json.statement_note;
},
export_as_JSON: function () {
var res = PaymentlineSuper.export_as_JSON.apply(this, arguments);
res.statement_note = this.statement_note;
return res;
},
is_meal_voucher: function() {
return (
this.manual_meal_voucher === true ||
["paper", "dematerialized"].indexOf(
this.cashregister.journal.meal_voucher_type) !== -1
);
},
is_dematerialized_meal_voucher: function() {
return (
this.cashregister.journal.meal_voucher_type == "dematerialized") ;
},
is_paper_meal_voucher: function() {
return (
this.cashregister.journal.meal_voucher_type == "paper") ;
},
});
models.Paymentline = Paymentline;
});
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-extend="Orderline">
<t t-jquery=".product-name" t-operation="prepend">
<t t-if="line.get_product().meal_voucher_ok === true &amp;&amp;
widget.pos.config.meal_voucher_display_product_screen">
<i class="fa fa-lg fa-cutlery"/>
</t>
</t>
</t>
<t t-extend="OrderWidget">
<t t-jquery="div.line" t-operation="append">
<div class="meal-voucher">
(<span>Meal Voucher: </span> <span class="value">0.00 €</span>)
</div>
</t>
</t>
<t t-extend="PaymentScreen-Paymentmethods">
<t t-jquery="div.paymentmethod" t-operation="after">
<t t-if="['mixed'].indexOf(cashregister.journal.meal_voucher_type) !== -1">
<div class="button paymentmethod-meal-voucher-mixed" t-att-data-id="cashregister.journal_id[0]">
<t t-if="cashregister.journal.meal_voucher_mixed_text">
<t t-esc="cashregister.journal.meal_voucher_mixed_text" />
</t>
<t t-else="">
<t t-esc="cashregister.journal_id[1]" /> (Dematerialized)
</t>
</div>
</t>
</t>
<t t-jquery="div.paymentmethods" t-operation="after">
<div class="meal-voucher-summary">
<table class="meal-voucher-summary">
<thead>
<tr>
<th colspan="2">Meal Voucher</th>
</tr>
</thead>
<tbody>
<tr>
<td>Total Eligible</td>
<td>
<span id="meal-voucher-eligible-amount">0.00 €</span>
<i id="meal-voucher-eligible-warning" class="fa fa-warning"/>
</td>
</tr>
<t t-if="widget.pos.config.max_meal_voucher_amount !== 0">
<tr>
<td>Max Amount</td>
<td>
<span id="meal-voucher-max-amount">0.00 €</span>
<i id="meal-voucher-max-warning" class="fa fa-warning"/>
</td>
</tr>
</t>
<tr>
<td>Total Received</td>
<td>
<span id="meal-voucher-received-amount">0.00 €</span>
</td>
</tr>
</tbody>
</table>
</div>
</t>
</t>
<t t-extend="PaymentScreen-Paymentlines">
<t t-jquery="td.delete-button" t-operation="after">
<t t-if="['paper', 'dematerialized'].indexOf(line.cashregister.journal.meal_voucher_type) !== -1 ||
line.manual_meal_voucher === true">
<td class="is-meal-voucher">
<i class="fa fa-lg fa-cutlery" />
</td>
</t>
</t>
</t>
<t t-extend="TextInputPopupWidget">
<t t-jquery="p.title" t-operation="after">
<p class="body"><t t-esc=" widget.options.body || '' "/></p>
</t>
</t>
</templates>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2020 - Today: GRAP (http://www.grap.coop)
@author: Sylvain LE GAL (https://twitter.com/legalsylvain)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<openerp>
<template id="assets" inherit_id="point_of_sale.assets">
<xpath expr="." position="inside">
<link rel="stylesheet" href="/pos_meal_voucher/static/src/css/pos_meal_voucher.css"/>
<script type="text/javascript" src="/pos_meal_voucher/static/src/js/models.js"></script>
<script type="text/javascript" src="/pos_meal_voucher/static/src/js/screens.js"></script>
</xpath>
</template>
</openerp>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2020 - Today: GRAP (http://www.grap.coop)
@author: Sylvain LE GAL (https://twitter.com/legalsylvain)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<openerp>
<record id="view_account_journal" model="ir.ui.view">
<field name="model">account.journal</field>
<field name="inherit_id" ref="point_of_sale.view_account_journal_pos_user_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='amount_authorized_diff']/.." position="after">
<group name="meal_voucher" string="Meal Voucher">
<field name="meal_voucher_type"/>
<field name="meal_voucher_mixed_text"
attrs="{'invisible': [('meal_voucher_type', '!=', 'mixed')]}"/>
</group>
</xpath>
</field>
</record>
</openerp>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2020 - Today: GRAP (http://www.grap.coop)
@author: Sylvain LE GAL (https://twitter.com/legalsylvain)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<openerp>
<record id="view_pos_config_form" model="ir.ui.view">
<field name="model">pos.config</field>
<field name="inherit_id" ref="point_of_sale.view_pos_config_form"/>
<field name="arch" type="xml">
<field name="journal_ids" position="after">
<group string="Meal Vouchers">
<field name="max_meal_voucher_amount"/>
<field name="meal_voucher_display_product_screen"/>
<field name="meal_voucher_change_accepted"/>
</group>
</field>
</field>
</record>
</openerp>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2020 - Today: GRAP (http://www.grap.coop)
@author: Sylvain LE GAL (https://twitter.com/legalsylvain)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<openerp>
<record id="view_product_category" model="ir.ui.view">
<field name="model">product.category</field>
<field name="inherit_id" ref="product.product_category_form_view"/>
<field name="arch" type="xml">
<field name="type" position="after">
<field name="meal_voucher_ok" colspan="4"/>
<button type="object"
name="button_apply_meal_voucher_settings"
string="Apply Meal Voucher to all Products"
help="by clicking on this button, all the products of this category will have the same settings than the current category, for the value 'Meal Voucher'"
icon="fa-cogs" colspan="2"/>
</field>
</field>
</record>
</openerp>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2020 - Today: GRAP (http://www.grap.coop)
@author: Sylvain LE GAL (https://twitter.com/legalsylvain)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<openerp>
<record id="view_product_template" model="ir.ui.view">
<field name="model">product.template</field>
<field name="inherit_id" ref="point_of_sale.product_template_form_view"/>
<field name="arch" type="xml">
<field name="to_weight" position="after">
<field name="meal_voucher_ok"/>
</field>
</field>
</record>
</openerp>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment