Commit e7a2eeed by François C.

Merge branch 'dev_cooperatic' into 'dev_principale'

Intégration développement Coopératic pour La Cagette

See merge request !38
parents c735219c b9bdac40
......@@ -660,7 +660,7 @@ msgstr "Remplacé"
#: code:addons/coop_membership/models/shift_shift.py:81
#, python-format
msgid "Shift Cloture"
msgstr "Clôlturer le service"
msgstr "Clôturer le service"
#. module: coop_membership
#: model:ir.ui.view,arch_db:coop_membership.view_shift_form_inherit
......
......@@ -409,6 +409,13 @@ msgid "Cancelled"
msgstr "Annulé"
#. module: coop_shift
#: code:addons/coop_shift/model/shift_template.py:527
#: code:addons/coop_shift/model/shift_template.py:534
#, python-format
msgid "Cannot change the date of an existing shift template. Delete and create a new template instead."
msgstr "Il n'est pas possible de modifier la date d'un créneau existant. Vous pouvez modifier l'heure uniquement. Veuillez supprimer et créer un nouveau créneau pour modifier les dates."
#. module: coop_shift
#: model:ir.model.fields,field_description:coop_shift.field_shift_shift_event_type_id
#: model:ir.model.fields,field_description:coop_shift.field_shift_shift_shift_type_id
#: model:ir.model.fields,field_description:coop_shift.field_shift_template_shift_type_id
......@@ -1825,7 +1832,7 @@ msgstr "Participant(e)"
#: model:ir.ui.view,arch_db:coop_shift.view_shift_template_registration_line_search
#: model:ir.ui.view,arch_db:coop_shift.view_template_registration_search
msgid "Partner"
msgstr "Coopérateur
msgstr "Coopérateur"
#. module: coop_shift
#: model:ir.model.fields,field_description:coop_shift.field_shift_template_registration_line_is_past
......@@ -2297,7 +2304,7 @@ msgstr "Service"
#: code:addons/coop_shift/model/shift_shift.py:434
#, python-format
msgid "Shift Cloture"
msgstr "Clôlturer le service"
msgstr "Clôturer le service"
#. module: coop_shift
#: model:ir.actions.act_window,name:coop_shift.action_res_partner_point_counter_view_ftop
......
......@@ -520,6 +520,20 @@ class ShiftTemplate(models.Model):
if 'updated_fields' not in vals.keys() and len(self.shift_ids):
vals['updated_fields'] = str(vals)
if 'start_datetime' in vals and self.start_datetime is not None:
new_date = datetime.date(datetime.strptime(vals['start_datetime'], "%Y-%m-%d %H:%M:%S"))
current_date = datetime.date(datetime.strptime(self.start_datetime, "%Y-%m-%d %H:%M:%S"))
if new_date != current_date:
raise UserError(_(
"Cannot change the date of an existing shift template. Delete and create a new template instead."))
if 'end_datetime' in vals and self.end_datetime is not None:
new_date = datetime.date(datetime.strptime(vals['end_datetime'], "%Y-%m-%d %H:%M:%S"))
current_date = datetime.date(datetime.strptime(self.end_datetime, "%Y-%m-%d %H:%M:%S"))
if new_date != current_date:
raise UserError(_(
"Cannot change the date of an existing shift template. Delete and create a new template instead."))
if 'user_ids' in vals and 'updated_fields' in vals \
and len(vals.keys()) <= 2:
self.update_shift(vals)
......
......@@ -20,6 +20,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from datetime import datetime
from openerp import models, fields, api
......@@ -99,8 +100,39 @@ class UpdateShiftsWizard(models.TransientModel):
if 'shift_ticket_ids' in vals.keys():
special = ['shift_ticket_ids']
del vals['shift_ticket_ids']
shift_obj.browse(shift_ids).with_context(
tracking_disable=True, special=special).write(vals)
# If the update contains 'start_datetime' or 'end_datetime',
# we need some custom logic to fix the shift start and end times.
# note: we do not touch the shift *dates* as this requires bigger changes in the module.
# See https://redmine.cooperatic.fr/issues/989
if 'start_datetime' in vals or 'end_datetime' in vals:
template_start_tz = datetime.strptime(wizard.template_id.start_datetime_tz, "%Y-%m-%d %H:%M:%S")
template_end_tz = datetime.strptime(wizard.template_id.end_datetime_tz, "%Y-%m-%d %H:%M:%S")
for shift in shift_obj.browse(shift_ids):
shift_vals = vals.copy()
shift_begin_tz = datetime.strptime(shift.date_begin_tz, "%Y-%m-%d %H:%M:%S")
shift_end_tz = datetime.strptime(shift.date_end_tz, "%Y-%m-%d %H:%M:%S")
new_shift_begin_tz = shift_begin_tz.replace(hour=template_start_tz.hour,
minute=template_start_tz.minute,
second=template_start_tz.second)
new_shift_end_tz = shift_end_tz.replace(hour=template_end_tz.hour,
minute=template_end_tz.minute,
second=template_end_tz.second)
shift_vals['date_begin_tz'] = datetime.strftime(new_shift_begin_tz, "%Y-%m-%d %H:%M:%S")
shift_vals['date_end_tz'] = datetime.strftime(new_shift_end_tz, "%Y-%m-%d %H:%M:%S")
shift_vals.pop('start_datetime', None)
shift_vals.pop('end_datetime', None)
# Also update the name because it contains the start time
shift_vals['name'] = wizard.template_id.name
shift.with_context(tracking_disable=True, special=special).write(shift_vals)
else:
# If no time change, simply do the bulk update as before
shift_obj.browse(shift_ids).with_context(
tracking_disable=True, special=special).write(vals)
wizard.template_id.updated_fields = ""
return True
......
......@@ -22,6 +22,9 @@
##############################################################################
from openerp import models, api, fields
import logging
_logger = logging.getLogger(__name__)
class ProductProduct(models.Model):
......@@ -56,5 +59,9 @@ class ProductProduct(models.Model):
if not self.product_tmpl_id.seller_ids:
return False
valid_si = self._all_valid_psi()[0]
seq = min([si.sequence for si in valid_si])
return valid_si.filtered(lambda si, seq=seq: si.sequence == seq)
if len(valid_si) > 0:
seq = min([si.sequence for si in valid_si])
return valid_si.filtered(lambda si, seq=seq: si.sequence == seq)
else:
_logger.warning("No fisrt_valid_psi %s", str(self.product_tmpl_id))
return False
......@@ -33,7 +33,8 @@
],
'data': [
'static/src/xml/templates.xml',
'data/ir_config_parameter_data.xml'
'data/ir_config_parameter_data.xml',
'views/product_template.xml',
],
'qweb': ['static/src/xml/label_print_button.xml'],
'installable': True,
......
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * cagette_product
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo 9.0\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Language: fr\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#. module: cagette_product
#: model:ir.model.fields,field_description:cagette_product.field_product_template_minimal_stock
msgid "Minimal stock"
msgstr "Stock minimum"
......@@ -27,6 +27,8 @@ import math
class ProductTemplate(models.Model):
_inherit = "product.template"
minimal_stock = fields.Integer(string="Minimal stock")
@api.constrains('barcode')
def _check_barcode(self):
""" Vérification du code barre avant de sauvegarder une fiche article"""
......
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_lacagette_product_product" model="ir.ui.view">
<field name="name">template.minimal.stock.form</field>
<field name="model">product.template</field>
<field eval="7" name="priority"/>
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<group name="group_lots_and_weight" position="inside">
<field name="minimal_stock" />
</group>
</field>
</record>
</odoo>
\ No newline at end of file
......@@ -7,12 +7,14 @@
'description': """
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
Utilisation du pavé numérique du clavier sur le panier\n
Personnalisation de la popup d'erreur "codebarre non reconnu" :\n
- Faire clignoter la popup d'erreur quand le codebarre n'est pas reconnu
- Jouer le son d'erreur plusieurs fois
- Afficher dans la popup le dernier produit scanné
- [FONCTION COMMENTEE] Afficher dans la popup le dernier produit scanné
""",
'author': "fracolo",
......@@ -33,6 +35,7 @@
# be careful : called sequently (exterenal ID must be put before)
#'security/ir.model.access.csv',
#'templates.xml',
"views/view_pos_config.xml",
'static/src/xml/templates.xml',
],
'installable': True,
......
<?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>
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * lacagette_custom_pos
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 9.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-15 08:00+0000\n"
"PO-Revision-Date: 2022-04-15 08:00+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: lacagette_custom_pos
#: model:ir.model.fields,field_description:lacagette_custom_pos.field_pos_config__reload_on_prices_change
msgid "Reload on prices change"
msgstr "Rechargement si des prix changent"
#. module: lacagette_custom_pos
#. openerp-web
#: code:lacagette_addons/lacagette_custom_pos/static/src/js/screens.js:73
#, python-format
msgid "POS data must been refreshed"
msgstr "Des données doivent être rafraichies."
#. module: lacagette_custom_pos
#. openerp-web
#: code:lacagette_addons/lacagette_custom_pos/static/src/js/screens.js:73
#, python-format
msgid "Some product prices have been changed, page will be refreshed"
msgstr "Des prix d'articles ont changé, la page va être rechargée."
from . import pos_config
\ No newline at end of file
from openerp import fields, models
class PosConfig(models.Model):
_inherit = 'pos.config'
reload_on_prices_change = fields.Boolean(
string="Reload on prices change",
default=False)
\ No newline at end of file
......@@ -2,8 +2,21 @@ odoo.define('lacagette_custom_pos.DB', function(require) {
"use strict";
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({
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){
var str = partner.name;
//Diacritric search
......@@ -12,9 +25,13 @@ odoo.define('lacagette_custom_pos.DB', function(require) {
} catch(e) {
console.log(e)
}
if(partner.barcode){
if (reduce_pos_members_search === 'False') {
if(partner.barcode){
str += '|' + partner.barcode;
}
}
}
if(partner.barcode_base){
str += '|' + partner.barcode_base;
}
......@@ -63,15 +80,17 @@ odoo.define('lacagette_custom_pos.DB', function(require) {
// Only on products screen
if (this.gui.get_current_screen() == 'products') {
// Get last product scanned
/*
var orderlines = this.pos.get_order().orderlines.models
var last_product_name = orderlines.length > 0
? orderlines[orderlines.length-1].product.display_name
: 'Panier vide'
*/
// Display custom popum with options
this.gui.show_popup('custom-error-barcode', {
barcode: show_code,
last_product_name: last_product_name
// last_product_name: last_product_name
});
var popup_barcode = $('.popup-barcode')
......
odoo.define("lacagette_custom_pos.screens", function (require) {
"use strict";
var screens = require("point_of_sale.screens");
var models = require('point_of_sale.models');
var core = require('web.core');
var dataModel = require('web.DataModel');
var lacagette_products = new dataModel('lacagette.products');
var _t = core._t;
const interval = 60 * 60 * 1000; // used for last_price_change call
var reload_on_prices_change = false;
models.load_fields("pos.config", ['reload_on_prices_change']);
const update_last_price_change_data = async function () {
/*
Ask odoo server data about last prices change
According to response, pos_session variables will be set
in order to make decision about reloading or not pos data
*/
lacagette_products.call('get_last_price_change')
.then(
function(received) {
if (typeof received.data !== "undefined" && received.data.length > 0) {
if (typeof posmodel.pos_session.last_price_update != "undefined") {
posmodel.pos_session.previous_price_update = posmodel.pos_session.last_price_update
}
posmodel.pos_session.last_price_update = received.data[0].write_date
posmodel.pos_session.last_price_update_load = Date.now();
if (posmodel.pos_session.last_price_update != posmodel.pos_session.previous_price_update)
posmodel.pos_session.needs_reload = true;
}
}
)
.fail(function(result, ev){
// Store event date. Used to make the "reloading" decision
posmodel.pos_session.last_network_failure = Date.now();
ev.preventDefault();
})
}
screens.ClientListScreenWidget.include({
init: function(parent, options) {
this._super(parent, options);
reload_on_prices_change = this.pos.config.reload_on_prices_change;
if (reload_on_prices_change) {
posmodel.pos_session.load_at = Date.now();
posmodel.pos_session.needs_reload = false;
update_last_price_change_data(); // don't wait for timer for first time
const confirmRefresh = event => {
// Called when unload is unsafe
event.preventDefault();
const message = _t("Are you sure you want to refresh the page?")
(event || window.event).returnValue = confirmationMessage; //Gecko + IE
return message; //Gecko + Webkit, Safari, Chrome etc.
};
const checkUnload = event => {
try {
if (window.navigator.onLine == true) {
return undefined;
} else {
return confirmRefresh(event);
}
} catch(err) {
return confirmRefresh(event);
}
}
window.addEventListener("beforeunload", checkUnload, { capture: true });
// Let's init a perpetual regular call to prices update data
setInterval(() => {
// catch all the errors.
update_last_price_change_data().catch(console.log);
}, interval);
}
},
show: function(){
if (typeof posmodel.pos_session.previous_price_update != "undefined"
&& posmodel.pos_session.needs_reload == true) {
/*
At least one product price has been changed since pos session has been started
We can't reload page without having a look to network state
If page reload is called while network is down, the browser will show a blank page
and there will be no way to show POS screens again.
A smarter way to refresh products data would be to process products data retrieve,
in ajax mode, without having to reload whole page code.
It could be the next improve step for this module.
*/
const now = Date.now();
let can_reload_page = false;
if (typeof posmodel.pos_session.last_network_failure == "undefined") {
/*
no network failure while retreiving price change data.
It doesn't mean that network is now available,
since it could be shutdown after last call
Let reload available
only if wifi icon is green is better
but not safe at all !
beforeunload event capture will ask user for confirmation if network has gone
*/
if ($('.js_connected.oe_icon.oe_green').length > 0)
can_reload_page = true;
} else {
// one network failure occured
if (posmodel.pos_session.last_price_update_load > posmodel.pos_session.last_network_failure) {
can_reload_page = true;
}
}
if (can_reload_page === true) {
this.gui.show_popup("alert", {
'title': _t("POS data must been refreshed"),
'body': _t("Some product prices have been changed, page will be refreshed")
});
setTimeout(function(){
window.location.reload();
}, 5000);
} else {
this._super();
}
} else {
this._super();
}
}
});
});
......@@ -11,11 +11,11 @@
Le Point de Vente n'a pas pu trouver de produit associé
au code-barre scanné.
</p>
<p class="body last-product-scanned">
<!--<p class="body last-product-scanned">
Dernier produit scanné :
<br />
<b><t t-esc="widget.options.last_product_name" /></b>
</p>
</p>-->
<div class="footer">
<div class="button cancel">
......
......@@ -5,6 +5,7 @@
<template id="assets" name="lacagette_pos_sales_assets" inherit_id="point_of_sale.assets">
<xpath expr="." position="inside">
<script type="text/javascript" src="/lacagette_custom_pos/static/src/js/backend.js"></script>
<script type="text/javascript" src="/lacagette_custom_pos/static/src/js/screens.js"></script>
</xpath>
<xpath expr="//link[@id='pos-stylesheet']" position="after">
<link rel="stylesheet" href="/lacagette_custom_pos/static/src/css/lacagette_custom_pos.css" />
......
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="view_custom_pos_config_form" model="ir.ui.view">
<field name="name">lacagette.custom.pos.config.form</field>
<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="cash_control" position="after">
<field name="reload_on_prices_change"/>
</field>
</field>
</record>
</data>
</openerp>
\ No newline at end of file
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:alt: License: AGPL-3
=========================
La Cagette specific rules
=========================
All shift template are FTOP type.
Every cooperative members is subscribing a FTOP shift template.
Every 28 days (4 weeks), every shift template subscriber losts 1 point.
If a cooperative member is not present to a due shift, she or he losts 1 point and has 2 makeups to do.
Every cooperative member move its regular shift to an other day.
When the coop. member counter reaches -2 points, the cooperative status is set to 'Suspended'.
When the coop. member counter reaches -3 points, the cooperatice status is set to 'Unsubscribed'
# -*- coding: utf-8 -*-
from . import models
\ No newline at end of file
# -*- coding: utf-8 -*-
{
'name': "La Cagette - Membership",
'summary': """
Tuning membership rules""",
'description': """
Specific rules are beeing implemented
lacagette_membership.absence_status can be excused or absent
""",
'author': "fracolo/cooperatic",
'website': "https://lacagette-coop.fr",
#
'category': 'Uncategorized',
'version': '0.0.6',
# any module necessary for this one to work correctly
'depends': ['base', 'coop_membership', 'coop_shift'],
# always loaded
'data': [
'security/ir.model.access.csv',
'data/ir_cron.xml',
'data/ir_config_parameter_data.xml',
'static/src/xml/templates.xml',
'views/res_partner.xml',
],
'installable': True,
}
<?xml version="1.0"?>
<odoo noupdate="1">
<record id="points_limit_to_get_suspended" model="ir.config_parameter">
<field name="key">lacagette_membership.points_limit_to_get_suspended</field>
<field name="value">-2</field>
</record>
<record id="points_limit_to_get_unsubscribed" model="ir.config_parameter">
<field name="key">lacagette_membership.points_limit_to_get_unsubscribed</field>
<field name="value">-3</field>
</record>
<record id="makeups_to_do_after_unsubscribed" model="ir.config_parameter">
<field name="key">lacagette_membership.makeups_to_do_after_unsubscribed</field>
<field name="value">2</field>
</record>
<record id="absence_status" model="ir.config_parameter">
<field name="key">lacagette_membership.absence_status</field>
<field name="value">excused</field>
</record>
<record id="extension_duration" model="ir.config_parameter">
<field name="key">lacagette_membership.extension_duration</field>
<field name="value">6 months</field>
</record>
<record id="max_successive_extensions" model="ir.config_parameter">
<field name="key">lacagette_membership.max_successive_extensions</field>
<field name="value">1</field>
</record>
<record id="committees_shift_id" model="ir.config_parameter">
<field name="key">lacagette_membership.committees_shift_id</field>
<field name="value">0</field>
</record>
<record id="shift_creation_days" model="ir.config_parameter">
<field name="key">lacagette_membership.shift_creation_days</field>
<field name="value">180</field>
</record>
</odoo>
<?xml version="1.0"?>
<!--
Copyright (C) 2021 - Today Cooperatic
Author : fracolo
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<odoo noupdate="1">
<record forcecreate="True" id="cron_close_unnecessary_extensions" model="ir.cron">
<field name="name">Close unnecessary Extensions</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="doall" eval="True"/>
<field name="model" eval="'res.partner'"/>
<field name="function" eval="'run_close_unnecessary_opened_extensions'"/>
<field name="args" eval="'()'"/>
<field name="active" eval="False"/>
<field name="priority">2</field>
</record>
<record forcecreate="True" id="cron_process_target_status" model="ir.cron">
<field name="name">Process coop target status</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_number">5</field>
<field name="interval_type">minutes</field>
<field name="numbercall">-1</field>
<field name="doall" eval="True"/>
<field name="model" eval="'res.partner'"/>
<field name="function" eval="'run_process_target_status'"/>
<field name="args" eval="'()'"/>
<field name="active" eval="False"/>
<field name="priority">1</field>
</record>
<record forcecreate="True" id="cron_close_ftop_shifts" model="ir.cron">
<field name="name">Daily close FTOP shifts</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="doall" eval="True"/>
<field name="model" eval="'shift.shift'"/>
<field name="function" eval="'run_close_today_ftop_shifts'"/>
<field name="args" eval="'()'"/>
<field name="active" eval="False"/>
<field name="priority">1</field>
</record>
<record forcecreate="True" id="cron_shifts_generation" model="ir.cron">
<field name="name">Generate shifts (La Cagette version)</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False"/>
<field name="model" eval="'lacagette.shift.template'"/>
<field name="function" eval="'run_shift_creation'"/>
<field name="args" eval="'()'"/>
<field name="active" eval="False"/>
<field name="priority">1</field>
</record>
</odoo>
\ No newline at end of file
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * lacagette_membership
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo 9.0\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Language: fr\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#. module: lacagette_membership
#: selection:res.partner,cooperative_state:0
msgid "Gone"
msgstr "Parti.e"
#. module: lacagette_membership
#: selection:res.partner,cooperative_state:0
msgid "Associated"
msgstr "En binôme"
\ No newline at end of file
# -*- coding: utf-8 -*-
from . import res_partner
from . import shift_registration
from . import shift_counter_event
from . import shift_shift
from . import member_state_change
from . import shift_template
from . import shift_template_registration_line
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html
from openerp import fields, models
class MembersStateChange(models.Model):
_name = "member.state.change"
member_id = fields.Many2one(
comodel_name='res.partner',
ondelete='cascade',
index=True
)
state = fields.Char(
'state',
size=15
)
# -*- coding: utf-8 -*-
from openerp import _, api, models, fields
import datetime
import logging
_logger = logging.getLogger(__name__)
TARGET_STATUS_SELECTION = [
('unsubscribed', 'Unsubscribed'),
('suspended', 'Suspended')
]
EXTRA_COOPERATIVE_STATE_SELECTION = [
('not_concerned', 'Not Concerned'),
('unsubscribed', 'Unsubscribed'),
('exempted', 'Exempted'),
('vacation', 'On Vacation'),
('up_to_date', 'Up to date'),
('alert', 'Alert'),
('suspended', 'Suspended'),
('delay', 'Delay'),
('blocked', 'Blocked'),
('unpayed', 'Unpayed'),
('gone', 'Gone'),
('associated', 'Associated')
]
class ResPartner(models.Model):
_inherit = 'res.partner'
# Columns Section
makeups_to_do = fields.Integer(
"Number of make-ups to done",
default= 0)
target_status = fields.Selection(
selection=TARGET_STATUS_SELECTION, default='')
cooperative_state = fields.Selection(
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
def run_process_target_status(self):
"""Method called by cron task"""
# final_ftop_point, target_status
conf = self.env['ir.config_parameter']
makeups_todo_after_unsubscribed = conf.get_param("lacagette_membership.makeups_to_do_after_unsubscribed")
result = 'done'
for p in self.env['res.partner']\
.search([('target_status', '!=', "")]):
try:
new_values = {'target_status': "", "date_alert_stop": False}
final_points = p.final_ftop_point if p.shift_type == "ftop" else p.final_standard_point
if final_points < 0:
new_values['cooperative_state'] = p.target_status
if new_values['cooperative_state'] == "unsubscribed":
new_values['makeups_to_do'] = makeups_todo_after_unsubscribed
# Get points difference to set points to -2
current_points = p['final_' + p.shift_type + '_point']
target_points = -2
points_diff = abs(current_points - target_points)
if points_diff != 0:
if current_points > target_points:
points_update = - points_diff
else:
points_update = points_diff
data = {
'name': "Désinscription : passage à -2 pts",
'shift_id': False,
'type': p.shift_type,
'partner_id': p.id,
'point_qty': points_update
}
self.env['shift.counter.event'].create(data)
"""
unlink model: "shift.template.registration"
to delete all future shifts linked to this coop.
"""
now = datetime.datetime.now().isoformat()
for streg in self.env['shift.template.registration']\
.search([('partner_id', '=', p.id)]):
streg.unlink()
for sreg in self.env['shift.registration']\
.search([('partner_id', '=', p.id),
('date_begin', '>', now)]):
sreg.unlink()
# Close extensions
for ext in self.env['shift.extension']\
.search([('partner_id', '=', p.id),
('date_start', '<=', now),
('date_stop', '>=', now)]):
ext.update({'date_stop': now})
try:
mail_template = self.env.ref('coop_membership.unsubscribe_email')
if mail_template:
mail_template.send_mail(p.id)
except Exception as e:
_logger.error("run_process_target_status - send mail : %s - Process not interrupted", str(e))
p.update(new_values)
except Exception as e:
_logger.error("run_process_target_status : %s", str(e))
result = 'error'
return result
#@api.onchange('cooperativestate') : could be used only if it is called from client
@api.model
def run_close_unnecessary_opened_extensions(self):
"""Method called by cron task"""
sql = """
SELECT s.id
FROM shift_extension as s
WHERE s.date_stop > now()
AND partner_id IN (SELECT id FROM res_partner WHERE cooperative_state in ('up_to_date', 'unsubscribed'))
"""
self.env.cr.execute(sql)
extension_ids = self.env.cr.fetchall()
if len(extension_ids) > 0:
extension_model = self.env['shift.extension']
today = datetime.datetime.now().strftime("%Y-%m-%d")
for ext in extension_model.search([('id', 'in', extension_ids)]):
ext.update({'date_stop': today})
@api.multi
def can_have_extension(self):
"""Return if the member can ask for an extension"""
answer = True
conf = self.env['ir.config_parameter']
max_nb = int(conf.get_param("lacagette_membership.max_successive_extensions"))
if max_nb == 1:
args = [('member_id', '=', self.id)]
states = self.env['member.state.change']\
.search(args, order='write_date DESC', limit=2)
"""
member.state.change may have no record for the current member
"""
if len(states) > 0:
if (len(states) == 1 and states[0]['state'] == 'delay'
or
len(states) == 2 and states[1]['state'] == 'delay'):
answer = False
else:
# need to load data
member = self.env['res.partner'].search([('id', '=', self.id)])[0]
# should exist an other way to do it , but haven't found it
if member.cooperative_state != "up_to_date":
answer = False
else:
# TODO : Must have a look to previous extensions
pass
return answer
def set_special_state(self, cr, uid, partner, context=None):
if partner['state'] == 'cancel_special':
partner['state'] = 'unsubscribed'
return self.write(cr, uid, [partner['id']], {'cooperative_state': partner['state']} , context=context)
def _write_state_change(self, state):
data = {'member_id': self.id, 'state': state}
self.env['member.state.change'].create(data)
@api.multi
def update(self, vals):
_logger.info("valeurs recues pour update partner = %s", str(vals))
state_to_record = ""
if 'cooperative_state' in vals:
state_to_record = vals['cooperative_state']
elif 'current_cooperative_state' in vals:
state_to_record = vals['current_cooperative_state']
del vals['current_cooperative_state']
if len(state_to_record) > 0:
self._write_state_change(state_to_record)
return super(ResPartner, self).update(vals)
@api.multi
def write(self, vals):
_logger.info("valeurs recues pour write partner = %s", str(vals))
if 'cooperative_state' in vals:
self._write_state_change(vals['cooperative_state'])
return super(ResPartner, self).write(vals)
# -*- coding: utf-8 -*-
from openerp import _, api, models, fields
import logging
_logger = logging.getLogger(__name__)
class ShiftCounterEvent(models.Model):
_inherit = 'shift.counter.event'
def _update_partner_target_status(self, vals):
"""actions when points are removed or added"""
try:
conf = self.env['ir.config_parameter']
suspension_limit = int(conf.get_param("lacagette_membership.points_limit_to_get_suspended"))
unsubscribe_limit = int(conf.get_param("lacagette_membership.points_limit_to_get_unsubscribed"))
if vals['point_qty'] != 0:
res_partner = self.env['res.partner'].search([('id', '=', vals['partner_id'])])
if res_partner:
p = res_partner[0]
points_before_removing_points = p.final_ftop_point if p.shift_type == "ftop" else p.final_standard_point
points_after_removal = points_before_removing_points + vals['point_qty']
_logger.info("points_after_removal = %s, current_state = %s", str(points_after_removal), p.cooperative_state)
if points_after_removal <= suspension_limit or points_after_removal <= unsubscribe_limit:
target_status = None
if (points_after_removal <= suspension_limit
and
p.cooperative_state != 'delay'):
target_status = 'suspended'
if points_after_removal <= unsubscribe_limit:
target_status = 'unsubscribed'
if target_status is not None:
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
def write(self, vals):
res = super(ShiftCounterEvent, self).write(vals)
_logger.info("Vals recues = %s", str(vals))
self._update_partner_target_status(vals)
return res
@api.model
def create(self, vals):
self._update_partner_target_status(vals)
_logger.info("Vals recues creation = %s", str(vals))
return super(ShiftCounterEvent, self).create(vals)
# -*- coding: utf-8 -*-
from openerp import _, api, models, fields
import logging
_logger = logging.getLogger(__name__)
class ShiftRegistration(models.Model):
_inherit = 'shift.registration'
is_late = fields.Boolean(
"Was the registration validated within grace period ?",
default= False)
is_makeup = fields.Boolean(
"Is this registration a consequence of a makeup to do",
default= False)
associate_registered = fields.Char(
"Who will do the shift, partner, associate or both",
default= False)
@api.multi
def write(self, vals):
conf = self.env['ir.config_parameter']
absence_status = conf.get_param("lacagette_membership.absence_status")
if 'state' in vals and vals['state'] == absence_status:
if self.ids:
for s in self.env['shift.registration']\
.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
s.partner_id.update({'makeups_to_do': new_makeups_to_do})
return super(ShiftRegistration, self).write(vals)
\ No newline at end of file
# -*- coding: utf-8 -*-
from openerp import _, api, models, fields
import logging
_logger = logging.getLogger(__name__)
class ShiftShift(models.Model):
_inherit = 'shift.shift'
@api.multi
def button_makeupok(self):
"""
@Function trigger to change the state from Confirm to Entry
"""
for shift in self:
shift.state = 'entry'
# Automatically mark attendance as "Attended" for
# makeup (ABCD Member)
for reg in shift.registration_ids:
if not reg.partner_id.in_ftop_team and \
not reg.tmpl_reg_line_id and \
reg.origin != 'memberspace' and \
reg.origin != 'bdm' and \
reg.state != 'replacing':
reg.button_reg_close()
@api.model
def run_close_today_ftop_shifts(self):
"""Method called by cron task"""
sql = """
SELECT id
FROM shift_shift
WHERE TO_CHAR(date_begin, 'YYYY-MM-DD') = TO_CHAR(now(), 'YYYY-MM-DD')
AND active = true AND state = 'confirm'
"""
self.env.cr.execute(sql)
shift_ids = self.env.cr.fetchall()
if len(shift_ids) > 0:
for s in self.env['shift.shift'].search([('id', 'in', shift_ids)]):
s.button_makeupok()
s.button_done()
# _logger.info('Traité avec action makeupok et done sur shift ' + str(s.id))
\ No newline at end of file
# -*- coding: utf-8 -*-
from openerp import _, api, models, fields
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
import locale
import logging
_logger = logging.getLogger(__name__)
class ShiftTemplate(models.Model):
_name = "lacagette.shift.template"
_inherit = 'shift.template'
@api.model
def ng_create_shifts_from_template(self, instance=None, after=False, before=False):
conf = self.env['ir.config_parameter']
shift_creation_days = int(conf.get_param("lacagette_membership.shift_creation_days"))
if not before:
before = fields.Datetime.to_string(
datetime.today() + timedelta(days=shift_creation_days))
for template in instance:
if len(str(template.rrule).strip()) == 0:
continue # It's not a recurring template
after = template.last_shift_date
rec_dates = template.get_recurrent_dates(
after=after, before=before)
for rec_date in rec_dates:
start_date_object_tz = datetime.strptime(
template.start_datetime_tz, '%Y-%m-%d %H:%M:%S')
date_begin = datetime.strftime(
rec_date + timedelta(hours=(start_date_object_tz.hour)) +
timedelta(minutes=(start_date_object_tz.minute)),
"%Y-%m-%d %H:%M:%S")
if date_begin.split(" ")[0] <= template.last_shift_date:
continue
end_date_object_tz = datetime.strptime(
template.end_datetime_tz, '%Y-%m-%d %H:%M:%S')
diff_day = end_date_object_tz.day - start_date_object_tz.day
diff_month = end_date_object_tz.month -\
start_date_object_tz.month
diff_year = end_date_object_tz.year - start_date_object_tz.year
date_end = datetime.strftime(
rec_date + timedelta(hours=(end_date_object_tz.hour)) +
timedelta(minutes=(end_date_object_tz.minute)) +
relativedelta(days=diff_day) +
relativedelta(months=diff_month) +
relativedelta(years=diff_year),
"%Y-%m-%d %H:%M:%S")
rec_date = datetime.strftime(rec_date, "%Y-%m-%d")
vals = {
'shift_template_id': template.id,
'name': template.name,
'user_ids': [(6, 0, template.user_ids.ids)],
'company_id': template.company_id.id,
'seats_max': template.seats_max,
'seats_availability': template.seats_availability,
'seats_min': template.seats_min,
'date_begin_tz': date_begin,
'date_end_tz': date_end,
'state': 'draft',
'reply_to': template.reply_to,
'address_id': template.address_id.id,
'description': template.description,
'shift_type_id': template.shift_type_id.id,
'week_number': template.week_number,
'week_list': template.week_list,
'shift_ticket_ids': None,
}
shift_id = self.env['shift.shift'].create(vals)
for ticket in template.shift_ticket_ids:
vals = {
'name': ticket.name,
'shift_id': shift_id.id,
'product_id': ticket.product_id.id,
'price': ticket.price,
'deadline': ticket.deadline,
'seats_availability': ticket.seats_availability,
'seats_max': ticket.seats_max,
}
if ticket.product_id.shift_type_id.is_ftop:
vals['seats_availability'] = 'limited'
vals['seats_max'] = 0
ticket_id = self.env['shift.ticket'].create(vals)
for attendee in ticket.registration_ids:
state, strl_id = attendee._get_state(rec_date)
if state:
vals = {
'partner_id': attendee.partner_id.id,
'user_ids': [(6, 0, template.user_ids.ids)],
'state': state,
'email': attendee.email,
'phone': attendee.phone,
'name': attendee.name,
'shift_id': shift_id.id,
'shift_ticket_id': ticket_id.id,
'tmpl_reg_line_id': strl_id,
'template_created': True,
}
self.env['shift.registration'].with_context(
from_shift_template=True).create(vals)
@api.model
def run_shift_creation(self):
# This method is called by the cron task
locale.setlocale(locale.LC_TIME, 'en_US.UTF-8')
conf = self.env['ir.config_parameter']
shift_creation_days = int(conf.get_param("lacagette_membership.shift_creation_days"))
today = datetime.today()
futur = today + timedelta(days=shift_creation_days)
filters = [('recurrency', '=', True),
('week_list', '=', today.strftime('%a').upper()[0:2]),
('last_shift_date', '<=', datetime.strftime(futur, "%Y-%m-%d"))]
templates = self.env['shift.template'].search(filters)
# templates = self.env['shift.template'].search([('id', '=', 60)])
for template in templates:
# _logger.info("Shift template = %s", str(template))
self.ng_create_shifts_from_template( instance=template,
before=fields.Datetime.to_string(
datetime.today() + timedelta(days=shift_creation_days)))
\ No newline at end of file
# -*- coding: utf-8 -*-
from openerp import api, models
import logging
_logger = logging.getLogger(__name__)
class ShiftTemplateRegistrationLine(models.Model):
_inherit = 'shift.template.registration.line'
@api.model
def create(self, vals):
super_res = super(ShiftTemplateRegistrationLine, self.with_context(
dict(self.env.context, **{'creation_in_progress': True}))).create(vals)
#_logger.info("valeurs recues pour create shift.template.registration.line = %s", str(vals))
if 'partner_id' in vals and 'shift_ticket_id' in vals:
conf = self.env['ir.config_parameter']
suspension_limit = int(conf.get_param("lacagette_membership.points_limit_to_get_suspended"))
unsubscribe_limit = int(conf.get_param("lacagette_membership.points_limit_to_get_unsubscribed"))
member = self.env['res.partner'].search([('id', '=', vals['partner_id'])])
shift_ticket = self.env['shift.ticket'].search([('id', '=', vals['shift_ticket_id'])])
if member and shift_ticket:
member = member[0]
shift_ticket = shift_ticket[0]
if shift_ticket.name == "ABCD":
points = member.final_standard_point
else:
points = member.final_ftop_point
if points <= suspension_limit or points <= unsubscribe_limit:
coop_state = 'suspended'
if points <= unsubscribe_limit:
coop_state = 'unsubscribed'
member.update({'cooperative_state': coop_state})
_logger.info("new cooperative_state = %s for %s", coop_state, str(member.id))
return super_res
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
,,,,,,,
,,,,,,,
access_model_member_state_change,access_model_member_state_change,model_member_state_change,coop_shift.group_shift_user,1,1,1,1
#coop_state_selection {
font-size: medium;
height: 26px;
}
\ No newline at end of file
odoo.define('lacagette_membership.backend', function(require){
"use strict";
var core = require('web.core');
var FormView = require('web.FormView');
var Model = require('web.DataModel');
var framework = require('web.framework');
var _t = core._t;
var partner_id = null;
var last_call = 0;
FormView.include({
load_record: function() {
var self = this;
return this._super.apply(this, arguments)
.then(function() {
partner_id = self.get_fields_values().id;
});
}
});
$(document).on('change', '#coop_state_selection', function(){
var d = new Date()
var elapsed_since_last_call = d.getTime() - last_call
let new_state = $(this).val()
if (elapsed_since_last_call > 5000) {
last_call = d.getTime()
let fields = {id: partner_id, state: new_state}
framework.blockUI();
new Model('res.partner').call('set_special_state',[fields]).then(function(rData){
window.location.reload()
},function(err,event){
event.preventDefault();
var error_body = _t('Your Internet connection is probably down.');
if (err.data) {
var except = err.data;
error_body = except.arguments && except.arguments[0] || except.message || error_body;
}
alert(error_body)
});
}
});
});
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<template id="assets" name="lacagette_membership assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" href="/lacagette_membership/static/src/css/backend.css" />
<script type="text/javascript" src="/lacagette_membership/static/src/js/backend.js"></script>
</xpath>
</template>
</data>
</openerp>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_partner_form" model="ir.ui.view">
<field name="model">res.partner</field>
<field name="inherit_id" ref="coop_membership.view_partner_form" />
<field name="arch" type="xml">
<field name="name" position="after">
<select id="coop_state_selection">
<option value="">--Statuts spéciaux--</option>
<option value="gone">Parti.e</option>
<option value="associated">En binôme</option>
<option value="cancel_special">Annulation statut spécial</option>
</select>
</field>
</field>
</record>
</odoo>
\ No newline at end of file
<?xml version="1.0"?>
<odoo>
<odoo noupdate="1">
<record id="nb_past_days_to_compute_sales_average" model="ir.config_parameter">
<field name="key">lacagette_products.nb_past_days_to_compute_sales_average</field>
<field name="value">100</field>
......
......@@ -7,6 +7,22 @@ import numpy
class LaCagetteProducts(models.Model):
_name = "lacagette.products"
@api.model
def get_last_price_change(self):
res = {}
try:
sql = """
SELECT
write_date, product_id
FROM product_price_history
ORDER BY write_date DESC LIMIT 1
"""
self.env.cr.execute(sql)
res['data'] = self.env.cr.dictfetchall()
except Exception as e:
res['error'] = str(e)
return res
def get_uoms(self):
res = {}
try:
......
============================
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>
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * pos_meal_voucher
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 12.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-09-01 17:04+0000\n"
"PO-Revision-Date: 2020-09-01 17:04+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_meal_voucher
#. openerp-web
#: code:addons/pos_meal_voucher/static/src/xml/pos_meal_voucher.xml:29
#, python-format
msgid "(Dematerialized)"
msgstr "(Dématérialisé)"
#. module: pos_meal_voucher
#. openerp-web
#: code:addons/pos_meal_voucher/static/src/js/screens.js:148
#, python-format
msgid ", when the maximum amount is "
msgstr ", alors que le montant maximum est de "
#. module: pos_meal_voucher
#. openerp-web
#: code:addons/pos_meal_voucher/static/src/js/screens.js:150
#, python-format
msgid ".\n"
"\n"
" Clicking 'Confirm' will validate the payment."
msgstr ".\n"
"\n"
" Cliquer sur 'confirmer' validera le paiement."
#. module: pos_meal_voucher
#: model_terms:ir.ui.view,arch_db:pos_meal_voucher.view_pos_config_form
msgid "<span class=\"o_form_label\">Meal Voucher Amount</span>"
msgstr "<span class=\"o_form_label\">Montant de titre restaurant</span>"
#. module: pos_meal_voucher
#: selection:barcode.rule,type:0
msgid "Alias"
msgstr "Alias"
#. module: pos_meal_voucher
#: model_terms:ir.ui.view,arch_db:pos_meal_voucher.view_product_category
msgid "Apply to all Products"
msgstr "Appliquer à tous les produits"
#. module: pos_meal_voucher
#: model:ir.model,name:pos_meal_voucher.model_barcode_rule
msgid "Barcode Rule"
msgstr "Règle de code barre"
#. module: pos_meal_voucher
#: selection:barcode.rule,type:0
msgid "Cashier"
msgstr "Caissier"
#. module: pos_meal_voucher
#: model:ir.model.fields,help:pos_meal_voucher.field_product_product__meal_voucher_ok
#: model:ir.model.fields,help:pos_meal_voucher.field_product_template__meal_voucher_ok
msgid "Check this box if the product can be paid with meal vouchers."
msgstr "Cocher cette case si le produit peut être payé avec des titres restaurants."
#. module: pos_meal_voucher
#: selection:barcode.rule,type:0
msgid "Client"
msgstr "Client"
#. module: pos_meal_voucher
#: selection:account.journal,meal_voucher_type:0
msgid "Credit Card / Dematerialized"
msgstr "Carte bleue / Dématérialisé"
#. module: pos_meal_voucher
#: selection:account.journal,meal_voucher_type:0
msgid "Dematerialized"
msgstr "Dématérialisé"
#. module: pos_meal_voucher
#: selection:barcode.rule,type:0
msgid "Discounted Product"
msgstr "Article en promotion"
#. module: pos_meal_voucher
#: model:ir.model.fields,field_description:pos_meal_voucher.field_pos_config__meal_voucher_display_product_screen
#: model_terms:ir.ui.view,arch_db:pos_meal_voucher.view_pos_config_form
msgid "Display icon before products on screen"
msgstr "Afficher une icone devant les produits"
#. module: pos_meal_voucher
#: model:product.category,name:pos_meal_voucher.food_category
msgid "Food"
msgstr "Alimentaire"
#. module: pos_meal_voucher
#: model_terms:ir.ui.view,arch_db:pos_meal_voucher.view_pos_config_form
msgid "If checked, an icon will be displayed on the screen, before each product that can be paid with meal vouchers."
msgstr "Si la case est cochée, une icône sera affiché sur l'écran du point de vente, avant chaque produit éligible au paiement par titre restaurant."
#. module: pos_meal_voucher
#: model:ir.model.fields,help:pos_meal_voucher.field_product_category__meal_voucher_ok
msgid "If checked, the products that belong to the category will be marked as 'can be paid with meal vouchers', by default."
msgstr "Si la case est cochée, les produits qui appartiennent à cette catégorie seront marqués comme 'Peuvent être payé en titre restaurant', par défaut."
#. module: pos_meal_voucher
#: model:ir.model,name:pos_meal_voucher.model_account_journal
msgid "Journal"
msgstr "Journal"
#. module: pos_meal_voucher
#: selection:barcode.rule,type:0
msgid "Location"
msgstr "Lieu"
#. module: pos_meal_voucher
#: selection:barcode.rule,type:0
msgid "Lot"
msgstr "Lot"
#. module: pos_meal_voucher
#. openerp-web
#: code:addons/pos_meal_voucher/static/src/xml/pos_meal_voucher.xml:52
#, python-format
msgid "Max Amount"
msgstr "Montant maximum"
#. module: pos_meal_voucher
#: model_terms:ir.ui.view,arch_db:pos_meal_voucher.view_pos_config_form
msgid "Maximum amount of Meal Vouchers that can be received for a PoS Order.\n"
" Let 0, if you don't want to enable this check."
msgstr "Montant total de titres restaurant qui peuvent être encaissé pour une vente.\n"
" Laisser vide si vous ne souhaitez pas activer cette vérification."
#. module: pos_meal_voucher
#. openerp-web
#: code:addons/pos_meal_voucher/static/src/xml/pos_meal_voucher.xml:39
#: model:ir.model.fields,field_description:pos_meal_voucher.field_product_category__meal_voucher_ok
#: model:ir.model.fields,field_description:pos_meal_voucher.field_product_product__meal_voucher_ok
#: model:ir.model.fields,field_description:pos_meal_voucher.field_product_template__meal_voucher_ok
#, python-format
msgid "Meal Voucher"
msgstr "Titre restaurant"
#. module: pos_meal_voucher
#: model:ir.model.fields,field_description:pos_meal_voucher.field_pos_config__max_meal_voucher_amount
msgid "Meal Voucher Amount"
msgstr "Montant de titre restaurant"
#. module: pos_meal_voucher
#: selection:barcode.rule,type:0
msgid "Meal Voucher Payment"
msgstr "Paiement en titre restaurant"
#. module: pos_meal_voucher
#: model:ir.model.fields,field_description:pos_meal_voucher.field_account_bank_statement_import_journal_creation__meal_voucher_type
#: model:ir.model.fields,field_description:pos_meal_voucher.field_account_journal__meal_voucher_type
msgid "Meal Voucher Type"
msgstr "Type de titre restaurant"
#. module: pos_meal_voucher
#. openerp-web
#: code:addons/pos_meal_voucher/static/src/xml/pos_meal_voucher.xml:16
#, python-format
msgid "Meal Voucher:"
msgstr "Titre restaurant :"
#. module: pos_meal_voucher
#: model_terms:ir.ui.view,arch_db:pos_meal_voucher.view_pos_config_form
msgid "Meal Vouchers"
msgstr "Titres Restaurant"
#. module: pos_meal_voucher
#: model:product.product,name:pos_meal_voucher.bread
#: model:product.template,name:pos_meal_voucher.bread_product_template
msgid "Organic Wholemeal Bread"
msgstr "Pain complet biologique"
#. module: pos_meal_voucher
#: selection:barcode.rule,type:0
msgid "Package"
msgstr "Colis"
#. module: pos_meal_voucher
#: selection:account.journal,meal_voucher_type:0
msgid "Paper"
msgstr "Papier"
#. module: pos_meal_voucher
#. openerp-web
#: code:addons/pos_meal_voucher/static/src/js/screens.js:145
#, python-format
msgid "Please Confirm Meal Voucher Amount"
msgstr "Veuillez confirmer le montant en titre restaurant"
#. module: pos_meal_voucher
#: model:ir.model,name:pos_meal_voucher.model_pos_config
msgid "Point of Sale Configuration"
msgstr "Paramétrage du point de vente"
#. module: pos_meal_voucher
#: model:ir.model,name:pos_meal_voucher.model_pos_order
msgid "Point of Sale Orders"
msgstr "Commandes du point de vente"
#. module: pos_meal_voucher
#: selection:barcode.rule,type:0
msgid "Priced Product"
msgstr "Article à prix fixe"
#. module: pos_meal_voucher
#: model:ir.model,name:pos_meal_voucher.model_product_category
msgid "Product Category"
msgstr "Catégorie d'article"
#. module: pos_meal_voucher
#: model:ir.model,name:pos_meal_voucher.model_product_template
msgid "Product Template"
msgstr "Modèle d'article"
#. module: pos_meal_voucher
#: model:ir.model.fields,field_description:pos_meal_voucher.field_account_bank_statement_import_journal_creation__meal_voucher_mixed_text
#: model:ir.model.fields,field_description:pos_meal_voucher.field_account_journal__meal_voucher_mixed_text
msgid "Text for Mixed Journal"
msgstr "Texte pour le journal mixte"
#. module: pos_meal_voucher
#: model:ir.model.fields,help:pos_meal_voucher.field_account_bank_statement_import_journal_creation__meal_voucher_mixed_text
#: model:ir.model.fields,help:pos_meal_voucher.field_account_journal__meal_voucher_mixed_text
msgid "Text that will be displayed in the point of sale if the journal is a mixed journal (Credit Card / Dematerialized) for the dematerialized button."
msgstr "Ce texte sera affiché dans le point de vente, pour le journal de type 'mixte' (Carte bleue / Dématérialisé) pour le bouton correspondant au paiement dématérialisé"
#. module: pos_meal_voucher
#. openerp-web
#: code:addons/pos_meal_voucher/static/src/xml/pos_meal_voucher.xml:44
#, python-format
msgid "Total Eligible"
msgstr "Total éligible"
#. module: pos_meal_voucher
#. openerp-web
#: code:addons/pos_meal_voucher/static/src/xml/pos_meal_voucher.xml:60
#, python-format
msgid "Total Received"
msgstr "Total encaissé"
#. module: pos_meal_voucher
#: model:ir.model.fields,field_description:pos_meal_voucher.field_barcode_rule__type
msgid "Type"
msgstr "Type"
#. module: pos_meal_voucher
#: selection:barcode.rule,type:0
msgid "Unit Product"
msgstr "Unité de produit"
#. module: pos_meal_voucher
#: model:product.product,uom_name:pos_meal_voucher.bread
#: model:product.template,uom_name:pos_meal_voucher.bread_product_template
msgid "Unit(s)"
msgstr "Unité(s)"
#. module: pos_meal_voucher
#: selection:barcode.rule,type:0
msgid "Weighted Product"
msgstr "Article pesé"
#. module: pos_meal_voucher
#. openerp-web
#: code:addons/pos_meal_voucher/static/src/js/screens.js:146
#, python-format
msgid "You are about to validate a meal voucher payment of "
msgstr "Vous êtes sur le point de valider un paiement en titre restaurant d'un montant de "
#. module: pos_meal_voucher
#: model_terms:ir.ui.view,arch_db:pos_meal_voucher.view_product_category
msgid "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'"
msgstr "En cliquant sur ce bouton, tous les produits de cette catégories auront le même paramétrage que cette catégorie, pour la valeur 'Titre Restaurant'"
#. module: pos_meal_voucher
#: model:product.product,weight_uom_name:pos_meal_voucher.bread
#: model:product.template,weight_uom_name:pos_meal_voucher.bread_product_template
msgid "kg"
msgstr "kg"
#. module: pos_meal_voucher
#. openerp-web
#: code:addons/pos_meal_voucher/static/src/js/screens.js:159
#, python-format
msgid "Meal Voucher Amount incorrect"
msgstr "Titre restaurant incorrect"
#. module: pos_meal_voucher
#. openerp-web
#: code:addons/pos_meal_voucher/static/src/js/screens.js:160
#, python-format
msgid "Warning, the maximum amount of meal voucher accepted ( "
msgstr "Attention, le montant éligible au paiement par titre restaurant( "
#. module: pos_meal_voucher
#. openerp-web
#: code:addons/pos_meal_voucher/static/src/js/screens.js:160
#, python-format
msgid " ) is under the amount input ( "
msgstr " ) est inférieur à la valeur du/des ticket(s)( "
#. module: pos_meal_voucher
#. openerp-web
#: code:addons/pos_meal_voucher/static/src/js/screens.js:110
#, python-format
msgid "Warning, the input amount of meal voucher is above the maximum amount of "
msgstr "Le montant saisi est supérieur au montant maximum/au maximum éligible de "
#. module: pos_meal_voucher
#. openerp-web
#: code:addons/pos_meal_voucher/static/src/js/screens.js:
#, python-format
msgid "Meal Voucher already used"
msgstr "Ticket restaurant déjà scanné"
#. module: pos_meal_voucher
#. openerp-web
#: code:addons/pos_meal_voucher/static/src/js/screens.js:
#, python-format
msgid "The paper meal voucher "
msgstr "Le ticket restaurant "
#. module: pos_meal_voucher
#. openerp-web
#: code:addons/pos_meal_voucher/static/src/js/screens.js:
#, python-format
msgid " was already used"
msgstr " a déjà été scanné"
#: code:addons/pos_meal_voucher/static/src/js/screens.js:196
#, python-format
msgid "Meal Voucher ticket"
msgstr "Chèque restaurant"
#. module: pos_meal_voucher
#. openerp-web
#: code:addons/pos_meal_voucher/static/src/js/screens.js:197
#, python-format
msgid "To add a meal voucher ticket close this window and scan the ticket. If the ticket can't be read please enter the code"
msgstr "Pour ajouter un chèque restaurant merci de fermer cette fenêtre et scanner le chèque. Si le chèque est illisible veuillez rentrer le code à la main."
#. module: pos_meal_voucher
#. openerp-web
#: code:addons/pos_meal_voucher/static/src/xml/pos_meal_voucher.xml:101
#, python-format
msgid "Eligible subtotal for meal vouchers:"
msgstr "Sous-total éligible en tickets restaurant :"
#. module: pos_meal_voucher
#. openerp-web
#: code:addons/pos_meal_voucher/static/src/xml/pos_meal_voucher.xml:120
#, python-format
msgid "Eligible subtotal for"
msgstr "Sous-total éligible en"
#. module: pos_meal_voucher
#. openerp-web
#: code:addons/pos_meal_voucher/static/src/xml/pos_meal_voucher.xml:122
#, python-format
msgid "meal vouchers:"
msgstr "tickets restaurant :"
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);
},
is_meal_voucher_used: function() {
var meal_voucher_used = false;
this.paymentlines.each( function(paymentline) {
if (paymentline.is_meal_voucher()) {
meal_voucher_used = true;
return false;
}
});
return meal_voucher_used;
},
/* point_of_sale/statis/src/js/models.js */
export_for_printing: function(){
var orderlines = [];
var self = this;
this.orderlines.each(function(orderline){
var orderline_for_printing = orderline.export_for_printing();
orderline_for_printing.product_meal_voucher_ok = orderline.product.meal_voucher_ok;
orderlines.push(orderline_for_printing);
});
var paymentlines = [];
this.paymentlines.each(function(paymentline){
paymentlines.push(paymentline.export_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,
meal_voucher_used: this.is_meal_voucher_used(),
total_meal_voucher_eligible: this.get_total_meal_voucher_eligible(),
curtlery_img_src: " data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABsAAAAgCAQAAABJYMuwAAABI2lDQ1BJQ0MgcHJvZmlsZQAAKJGdkLFKw1AUhr+0oiLqYnEQhQyOFlzM5FIVgqAQYwWrU5qkWExiSFKKb+Cb6MN0EAQfwQdQcPa/0cHBLF44/B+Hc/7/3gstOwnTcm4X0qwqXL83uBxc2QtvtNlilQ47QVjmPc87ofF8vmIZfekar+a5P898FJehdKbKwryowNoXO9MqN6yic9v3D8UPYjtKs0j8JN6O0siw2fXTZBL+eJrbLMfZxbnpqzZxOeYUD5shE8YkVHSlmTpHOOxJXQoC7ikJpQmxelPNVNyISjm5HIj6It2mIW+jzvOUMpTHWF4m4Y5UniYP87/fax9n9aa1PsuDIqhbbVVrNIL3R1gZwNozLF03ZC3+flvDjFPP/PONXwa+UHRuacLOAAAACXBIWXMAAAsTAAALEwEAmpwYAAABWUlEQVQ4y93UTUvUURTH8c+9IyEygxoxi3KRVApBL6CHhSv3afsWSvgKqpcQbSpw0dYWBe7ciAtLwVWL1r6BVpI2yBRqDcdNTY7+/c8wi4guXLicc7/3d564KfS1ghGvPTYgm/PO1WDCstkQgmzKK6s2bNq04a3LIeOKW+4YVHHPdZO4adztlEgXPPHcXXU1VVU119xgoEM6deyUeOi+fDbGXJpB3YPiG+XYhNFiRzk2IvWDpfMcub+2/a5kzUVNoxhL2dj5Op3YsDdahvHIjEu9qlFth13vHmQun7syrOmowPO1m9pnH8/YD61FlOYWrfTMD9MqbeuOFz51zS0aFu2fsK5bj+i3JH8bi+7tLlpbnqqgbsFgz1g0vIc0bv7fLcn/iEWv2DfNE9CXU4O873v73LLXxqLppZ1f5g9WTonvWtQAB5Zsk/48m5Ih2VEcFn6Z2RAO4iccAzBJUmYVC7MHAAAAAElFTkSuQmCC"
};
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;
});
// 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.screens", function (require) {
"use strict";
var screens = require("point_of_sale.screens");
var core = require('web.core');
var formats = require('web.formats');
var _t = core._t;
var QWeb = core.qweb;
screens.ScreenWidget.include({
barcode_meal_voucher_payment_action: function (code) {
var order = this.pos.get_order();
if(!order.paper_meal_vouche_number_already_used(code.code)){
// Display the payment screen, if it is not the current one.
if (this.pos.gui.current_screen.template !== "PaymentScreenWidget"){
this.gui.show_screen("payment");
}
var paymentScreen = this.pos.gui.current_screen;
var order = this.pos.get_order();
var amount = code.value;
var cashregister = null;
// find a meal voucher cash register, if exist
for ( var i = 0; i < this.pos.cashregisters.length; i++ ) {
if ( this.pos.cashregisters[i].journal.meal_voucher_type === "paper" ){
cashregister = this.pos.cashregisters[i];
break;
}
}
if (!cashregister){
return;
}
// Add new payment line with the amount found in the barcode
this.pos.get_order().add_paymentline(cashregister);
paymentScreen.reset_input()
order.selected_paymentline.set_amount(amount);
order.selected_paymentline.statement_note = code.code;
paymentScreen.order_changes();
paymentScreen.render_paymentlines();
paymentScreen.$(".paymentline.selected .edit").text(paymentScreen.format_currency_no_symbol(amount));
}else{
this.gui.show_popup("alert", {
'title': _t("Meal Voucher Amount already used"),
'body': _t("The paper meal voucher ") +
code.code + _t(" was already used"),
});
}
},
// Setup the callback action for the "meal_voucher_payment" barcodes.
show: function () {
this._super();
this.pos.barcode_reader.set_action_callback(
"meal_voucher_payment",
_.bind(this.barcode_meal_voucher_payment_action, this));
},
});
screens.OrderWidget.include({
//check if POS if configured to accept payment in meal voucher
meal_voucher_activated: function() {
var is_meal_voucher_method = this.pos.cashregisters.map(function(cash_register) {
if (cash_register.journal.meal_voucher_type === "paper" || cash_register.journal.meal_voucher_type === "dematerialized") {
return true
}
})
return is_meal_voucher_method.includes(true);
},
update_summary: function () {
this._super.apply(this, arguments);
var order = this.pos.get_order();
if (!this.meal_voucher_activated()) {
const meal_voucher_summary = this.el.querySelector(".summary .meal-voucher");
if (meal_voucher_summary != null) {
meal_voucher_summary.style.display = 'none';
}
// this.el.querySelector(".summary .meal-voucher").style.display = 'none';
return;
}
if (!order.get_orderlines().length) {
return;
}
this.el.querySelector(".summary .meal-voucher .value").textContent = this.format_currency(order.get_total_meal_voucher_eligible());
},
});
screens.PaymentScreenWidget.include({
//check if POS if configured to accept payment in meal voucher
meal_voucher_activated: function() {
var is_meal_voucher_method = this.pos.cashregisters.map(function(cash_register) {
if (cash_register.journal.meal_voucher_type === "paper" || cash_register.journal.meal_voucher_type === "dematerialized") {
return true
}
})
return is_meal_voucher_method.includes(true);
},
// odoo/addons/point_of_sale/static/src/js/screens.js
// We need to change the default behaviour of locking input with
// popup bcause of the papar meal voucher
init: function(parent, options) {
var self = this;
this._super(parent, options);
// This is a keydown handler that prevents backspace from
// doing a back navigation. It also makes sure that keys that
// do not generate a keypress in Chrom{e,ium} (eg. delete,
// backspace, ...) get passed to the keypress handler.
this.keyboard_keydown_handler = function(event){
// We overight the expected behaviour if a popup is open
if (self.gui.has_popup()) {
}
else if (event.keyCode === 8 || event.keyCode === 46) { // Backspace and Delete
event.preventDefault();
console.log('keyboard_keydown_handler')
// These do not generate keypress events in
// Chrom{e,ium}. Even if they did, we just called
// preventDefault which will cancel any keypress that
// would normally follow. So we call keyboard_handler
// explicitly with this keydown event.
self.keyboard_handler(event);
}
};
// This keyboard handler listens for keypress events. It is
// also called explicitly to handle some keydown events that
// do not generate keypress events.
this.keyboard_handler = function(event){
var key = '';
// We overight the expected behaviour if a popup is open
if (self.gui.has_popup()) {
}
else{
if (event.type === "keypress") {
if (event.keyCode === 13) { // Enter
self.validate_order();
} else if ( event.keyCode === 190 || // Dot
event.keyCode === 110 || // Decimal point (numpad)
event.keyCode === 188 || // Comma
event.keyCode === 46 ) { // Numpad dot
key = self.decimal_point;
} else if (event.keyCode >= 48 && event.keyCode <= 57) { // Numbers
key = '' + (event.keyCode - 48);
} else if (event.keyCode === 45) { // Minus
key = '-';
} else if (event.keyCode === 43) { // Plus
key = '+';
}
} else { // keyup/keydown
if (event.keyCode === 46) { // Delete
key = 'CLEAR';
} else if (event.keyCode === 8) { // Backspace
key = 'BACKSPACE';
}
}
self.payment_input(key);
event.preventDefault();
}
};
},
payment_input: function(input) {
var newbuf = this.gui.numpad_input(this.inputbuffer, input, {'firstinput': this.firstinput});
this.firstinput = (newbuf.length === 0);
if (newbuf !== this.inputbuffer) {
this.inputbuffer = newbuf;
var order = this.pos.get_order();
// We choose to unactivated the editing of the amount for the paper meal voucher
if (order.selected_paymentline && !order.selected_paymentline.is_paper_meal_voucher()) {
var amount = this.inputbuffer;
if (this.inputbuffer !== "-") {
amount = formats.parse_value(this.inputbuffer, {type: "float"}, 0.0);
}
order.selected_paymentline.set_amount(amount);
this.order_changes();
this.render_paymentlines();
this.$('.paymentline.selected .edit').text(this.format_currency_no_symbol(amount));
}
}
var order = this.pos.get_order();
if(order.selected_paymentline && order.selected_paymentline.is_dematerialized_meal_voucher()){
var total_eligible = order.get_total_meal_voucher_eligible();
var total_received = order.get_total_meal_voucher_received();
var max_amount = this.pos.config.max_meal_voucher_amount;
var current_max = total_eligible;
if (max_amount) {
current_max = Math.min(total_eligible, max_amount);
}
if (total_received > current_max){
this.gui.show_popup("alert", {
'title': _t("Meal Voucher Amount incorrect"),
'body': _t("Warning, the input amount of meal voucher is above the maximum amount of ") +
this.format_currency(current_max),
});
const max_curent_amount = current_max-total_received+order.selected_paymentline.get_amount() ;
order.selected_paymentline.set_amount(max_curent_amount);
this.order_changes();
this.render_paymentlines();
this.$('.paymentline.selected .edit').text(this.format_currency_no_symbol(max_curent_amount));
this.inputbuffer = "";
}
}
},
click_paymentmethods_meal_voucher_mixed: function(id) {
var cashregister = null;
for ( var i = 0; i < this.pos.cashregisters.length; i++ ) {
if ( this.pos.cashregisters[i].journal_id[0] === id ){
cashregister = this.pos.cashregisters[i];
break;
}
}
this.pos.get_order().add_paymentline( cashregister );
// manually set meal voucher
this.pos.get_order().selected_paymentline.manual_meal_voucher = true;
this.reset_input();
this.render_paymentlines();
},
render_paymentmethods: function() {
var self = this;
var methods = this._super.apply(this, arguments);
methods.on('click','.paymentmethod-meal-voucher-mixed',function(){
self.click_paymentmethods_meal_voucher_mixed($(this).data('id'));
});
return methods;
},
click_paymentmethods: function(id) {
var self = this;
var methods = this._super.apply(this, arguments);
var paymentScreen = this.pos.gui.current_screen;
var order = this.pos.get_order();
if(order.selected_paymentline.is_meal_voucher() && order.selected_paymentline.is_dematerialized_meal_voucher()){
var total_eligible = order.get_total_meal_voucher_eligible();
var total_received = order.get_total_meal_voucher_received();
var max_amount = this.pos.config.max_meal_voucher_amount;
var current_max = total_eligible;
if (max_amount) {
current_max = Math.min(total_eligible, max_amount);
}
// Check how much is still possible to pay with meal voucher
// The selected line is "by default" set to the rest to pay of the order
const max_curent_amount = current_max-total_received +order.selected_paymentline.get_amount();
order.selected_paymentline.set_amount(max_curent_amount);
paymentScreen.order_changes();
paymentScreen.render_paymentlines();
paymentScreen.$(".paymentline.selected .edit").text(paymentScreen.format_currency_no_symbol(max_curent_amount));
}
// If user choose paper meal voucher
// Open a popup asking for the number of the meal voucher
if(order.selected_paymentline.is_meal_voucher() && order.selected_paymentline.is_paper_meal_voucher()){
this.gui.show_popup("textinput", {
'title': _t("Meal Voucher ticket"),
'body': _t("To add a meal voucher ticket close this window and scan the ticket. If the ticket can't be read please enter the code"),
confirm: function(value) {
this.pos.get_order().remove_paymentline(order.selected_paymentline);
var paymentScreen = this.pos.gui.current_screen;
paymentScreen.reset_input();
paymentScreen.render_paymentlines();
var core = '';
odoo.define('coreservice', function(require){core=require('web.core');})
core.bus.trigger('barcode_scanned', value)
},
cancel: function(vaue) {
this.pos.get_order().remove_paymentline(order.selected_paymentline);
var paymentScreen = this.pos.gui.current_screen;
paymentScreen.reset_input();
paymentScreen.render_paymentlines();
},
});
}
},
render_paymentlines: function() {
var self = this;
this._super.apply(this, arguments);
var order = this.pos.get_order();
if (!order) {
return;
}
// Update meal voucher summary
var total_eligible = order.get_total_meal_voucher_eligible();
this.el.querySelector("#meal-voucher-eligible-amount").textContent = this.format_currency(total_eligible);
var max_amount = this.pos.config.max_meal_voucher_amount;
if (max_amount !== 0) {
this.el.querySelector("#meal-voucher-max-amount").textContent = this.format_currency(max_amount);
}
var total_received = order.get_total_meal_voucher_received();
this.el.querySelector("#meal-voucher-received-amount").textContent = this.format_currency(total_received);
// Display warnings
if (total_received > total_eligible) {
this.$("#meal-voucher-eligible-warning").removeClass("oe_hidden");
} else {
this.$("#meal-voucher-eligible-warning").addClass("oe_hidden");
}
if (total_received > max_amount) {
this.$("#meal-voucher-max-warning").removeClass("oe_hidden");
} else {
this.$("#meal-voucher-max-warning").addClass("oe_hidden");
}
if (!this.meal_voucher_activated()) {
this.$(".meal-voucher-summary").addClass("oe_hidden");
}
},
order_is_valid: function(force_validation) {
var self = this;
var order = this.pos.get_order();
var total_eligible = order.get_total_meal_voucher_eligible();
var total_received = order.get_total_meal_voucher_received();
var max_amount = this.pos.config.max_meal_voucher_amount;
var current_max = total_eligible;
if (max_amount) {
current_max = Math.min(total_eligible, max_amount);
}
// if the change is too large, it's probably an input error, if giving change is accepted make the user confirm.
if (!force_validation && (total_received > current_max) && this.pos.config.meal_voucher_change_accepted) {
this.gui.show_popup("confirm", {
'title': _t("Please Confirm Meal Voucher Amount"),
'body': _t("You are about to validate a meal voucher payment of ") +
this.format_currency(total_received) +
_t(", when the maximum amount is ") +
this.format_currency(current_max) +
_t(".\n\n Clicking 'Confirm' will validate the payment."),
confirm: function() {
// Note: Due to the bad design of the original function
// the check "Large Amount" will be skipped in that case.
self.validate_order("confirm");
},
});
return false;
}
//else force user to correct
else if (!force_validation && (total_received > current_max) && !this.pos.config.meal_voucher_change_accepted) {
this.gui.show_popup("alert", {
'title': _t("Meal Voucher Amount incorrect"),
'body': _t("Warning, the maximum amount of meal voucher accepted ( ") +
this.format_currency(current_max) +
_t(" ) is under the amount input ( ") +
this.format_currency(total_received) +
_t(")"),
});
return false;
}
return this._super.apply(this, arguments);
},
});
/* point_of_sale/statis/src/js/screens.js */
screens.ReceiptScreenWidget.include({
render_receipt: function() {
var order = this.pos.get_order();
order.meal_voucher_used = order.is_meal_voucher_used();
this.$('.pos-receipt-container').html(QWeb.render('PosTicket',{
widget:this,
order: order,
receipt: order.export_for_printing(),
orderlines: order.get_orderlines(),
paymentlines: order.get_paymentlines()
}));
}
});
});
<?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>
<t t-extend="PosTicket"> <!-- Ticket displayed on screen -->
<t t-jquery="table.receipt-orderlines tr td:first" t-operation="prepend"> <!-- Add cutlery on orderline if needed -->
<t t-if="order.meal_voucher_used === true and orderline.get_product().meal_voucher_ok === true">
<i class="fa fa-cutlery" />
</t>
</t>
<t t-jquery="table.receipt-total" t-operation="before"> <!-- meal voucher subtotal-->
<t t-if="order.meal_voucher_used === true">
<table class='receipt-eligible-meal-voucher'>
<colgroup>
<col width='75%' />
<col width='25%' />
</colgroup>
<tr>
<td><i class="fa fa-cutlery"/> Eligible subtotal for meal vouchers:</td>
<td class="pos-right-align">
<t t-esc="widget.format_currency(order.get_total_meal_voucher_eligible())"/>
</td>
</tr>
</table>
<br/>
</t>
</t>
</t>
<t t-extend="XmlReceipt"> <!-- Ticket sent to the printer -->
<t t-jquery="div.orderlines line:not([indent]) left" t-operation="prepend"> <!-- hack to get product name line (lines with qty have 'indent' attr) -->
<t t-if='receipt.meal_voucher_used === true and line.product_meal_voucher_ok === true'>
<span>#</span>
</t>
</t>
<t t-jquery="div.orderlines" t-operation="after">
<t t-if="receipt.meal_voucher_used === true">
<line><right>--------</right></line>
<line><left><span>#</span> Eligible subtotal for</left></line>
<line>
<left>meal vouchers:</left>
<right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='receipt.total_meal_voucher_eligible' /></value></right>
</line>
</t>
</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>
# -*- coding: utf-8 -*-
from . import models
# -*- coding: utf-8 -*-
# Copyright (C) 2016-Today: BEES coop (<http://www.bees-coop.be/>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
'name': 'Beesdoo - Remove pos order line with quantity set to 0',
'version': '9.0.1.0.0',
'category': 'Custom',
'summary': 'Remove pos order line with quantity set to 0',
'description': """
This module fix the issue on picking when there is two lines on
the pos order for the same product, with one of lines with a 0 quantity.
The lines at 0 are removed before the pos order is processed to avoid
such issue.
""",
'author': 'BEES coop - Houssine BAKKALI',
'website': 'http://www.bees-coop.be',
'depends': [
'point_of_sale',
],
'data': [],
'installable': True,
}
# -*- coding: utf-8 -*-
from . import pos_order
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import api, models
class PosOrder(models.Model):
_inherit = 'pos.order'
@api.model
def _process_order(self, order):
lines = order['lines']
order['lines'] = [l for l in lines if l[2]['qty'] !=0]
return super(PosOrder, self)._process_order(order)
\ No newline at end of file
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