Commit 3cd0ed8a by Thibault Grandjean

Merge branch 'meal-vouchers' into docker

parents b0627233 7cd0bdea
......@@ -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
......
......@@ -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
......
......@@ -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
......@@ -12,7 +12,7 @@
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",
......
......@@ -63,15 +63,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')
......
......@@ -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">
......
......@@ -24,7 +24,10 @@
'data': [
'security/ir.model.access.csv',
'data/ir_cron.xml',
'data/ir_config_parameter_data.xml'
'data/ir_config_parameter_data.xml',
'static/src/xml/templates.xml',
'views/res_partner.xml',
],
'installable': True,
}
......@@ -26,7 +26,7 @@
</record>
<record id="committees_shift_id" model="ir.config_parameter">
<field name="key">lacagette_membership.committees_shift_id</field>
<field name="value"> </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>
......
......@@ -21,8 +21,8 @@
<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">1</field>
<field name="interval_type">hours</field>
<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'"/>
......
# 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
......@@ -5,4 +5,5 @@ from . import shift_registration
from . import shift_counter_event
from . import shift_shift
from . import member_state_change
from . import shift_template
\ No newline at end of file
from . import shift_template
from . import shift_template_registration_line
......@@ -11,6 +11,21 @@ TARGET_STATUS_SELECTION = [
('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'
......@@ -22,12 +37,14 @@ class ResPartner(models.Model):
target_status = fields.Selection(
selection=TARGET_STATUS_SELECTION, default='')
cooperative_state = fields.Selection(
selection=EXTRA_COOPERATIVE_STATE_SELECTION, default='not_concerned')
@api.model
def run_process_target_status(self):
"""Method called by cron task"""
# final_ftop_point, target_status
conf = self.env['ir.config_parameter']
absence_status = conf.get_param("lacagette_membership.absence_status")
makeups_todo_after_unsubscribed = conf.get_param("lacagette_membership.makeups_to_do_after_unsubscribed")
result = 'done'
for p in self.env['res.partner']\
......@@ -39,25 +56,55 @@ class ResPartner(models.Model):
new_values['cooperative_state'] = p.target_status
if new_values['cooperative_state'] == "unsubscribed":
new_values['makeups_to_do'] = makeups_todo_after_unsubscribed
new_values['final_' + p.shift_type + '_point'] = -2
if p.shift_type == 'standard':
new_values['display_std_points'] = -2
else:
new_values['display_ftop_points'] = -2
# 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', '>', datetime.datetime.now().isoformat())]):
('date_begin', '>', now)]):
sreg.unlink()
mail_template = self.env.ref('coop_membership.unsubscribe_email')
if mail_template:
mail_template.send_mail(p.id)
# 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))
......@@ -111,13 +158,19 @@ class ResPartner(models.Model):
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 reçues pour update partner = %s", str(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']
......@@ -133,7 +186,7 @@ class ResPartner(models.Model):
@api.multi
def write(self, vals):
_logger.info("valeurs reçues pour write partner = %s", str(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)
......
......@@ -10,11 +10,11 @@ class ShiftCounterEvent(models.Model):
def _update_partner_target_status(self, vals):
"""actions if point_qty is negative"""
"""actions when points are removed or added"""
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:
if vals['point_qty'] != 0:
res_partner = self.env['res.partner'].search([('id', '=', vals['partner_id'])])
if res_partner:
p = res_partner[0]
......@@ -36,13 +36,13 @@ class ShiftCounterEvent(models.Model):
@api.model
def write(self, vals):
res = super(ShiftCounterEvent, self).write(vals)
_logger.info("Vals reçues = %s", str(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 reçues creation = %s", str(vals))
_logger.info("Vals recues creation = %s", str(vals))
return super(ShiftCounterEvent, self).create(vals)
# -*- 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
#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
============================
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
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)
Note
~~~~
A new barcode rule is available for Paper Meal Voucher of 24 chars:
``...........{NNNDD}........``
If you scan the following barcode ``052566641320080017000000``, a new payment line will be added, with an amount of 8,00€ (``00800``)
Known issues / Roadmap
======================
* Introduce the Meal Voucher Issuer model
* When scaning Meal Voucher, deduce the Issuer
* Add a reporting to make easily the deposit of Meal Vouchers, per issuers.
* Add an option to add subtotal of products that can be paid with meal vouchers,
on the bill.
* Prevent to scan twice the same Meal Voucher barcode.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/legalsylvain/pos/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/legalsylvain/pos/issues/new?body=module:%20pos_meal_voucher%0Aversion:%2012.0-ADD-pos_meal_voucher%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* GRAP
Contributors
~~~~~~~~~~~~
* Sylvain LE GAL <https://twitter.com/legalsylvain>
Other credits
~~~~~~~~~~~~~
The development of this module has been financially supported by:
* Vracoop (https://portail.vracoop.fr/)
* Demain Supermarché (http://www.demainsupermarche.org/)
Maintainers
~~~~~~~~~~~
This module is part of the `legalsylvain/pos <https://github.com/legalsylvain/pos/tree/12.0-ADD-pos_meal_voucher/pos_meal_voucher>`_ project on GitHub.
You are welcome to contribute.
# Copyright (C) 2020 - Today: GRAP (http://www.grap.coop)
# @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
"name": "Point Of Sale - Meal Voucher",
"summary": "Handle meal vouchers in Point of Sale"
" with eligible amount and max amount",
"version": "9.0.1.0.0",
"category": "Point of Sale",
"author": "GRAP, Odoo Community Association (OCA)",
"website": "http://www.github.com/OCA/pos",
"license": "AGPL-3",
"depends": [
"point_of_sale",
],
"data": [
"data/barcode_rule.xml",
"views/view_account_journal.xml",
"views/view_pos_config.xml",
"views/view_product_category.xml",
"views/view_product_template.xml",
"views/templates.xml",
],
"qweb": [
"static/src/xml/pos_meal_voucher.xml",
],
"demo": [
"demo/product_category.xml",
"demo/product_product.xml",
],
"installable": True,
}
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2020 - Today: GRAP (http://www.grap.coop)
@author: Sylvain LE GAL (https://twitter.com/legalsylvain)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<openerp>
<data>
<record id="rule_meal_voucher" model="barcode.rule">
<field name="name">Meal Voucher Payment</field>
<field name="barcode_nomenclature_id" ref="barcodes.default_barcode_nomenclature"/>
<field name="type">meal_voucher_payment</field>
<field name="encoding">any</field>
<field name="sequence">1</field>
<field name="pattern">...........{NNNDD}........</field>
</record>
</data>
</openerp>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2020 - Today: GRAP (http://www.grap.coop)
@author: Sylvain LE GAL (https://twitter.com/legalsylvain)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<openerp>
<record id="food_category" model="product.category">
<field name="name">Food</field>
<field name="parent_id" ref="point_of_sale.product_category_pos"/>
<field name="meal_voucher_ok" eval="True"/>
</record>
</openerp>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2020 - Today: GRAP (http://www.grap.coop)
@author: Sylvain LE GAL (https://twitter.com/legalsylvain)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<openerp>
<record id="bread" model="product.product">
<field name="name">Organic Wholemeal Bread</field>
<field name="categ_id" ref="food_category"/>
<field name="meal_voucher_ok" eval="True"/>
<field name="available_in_pos" eval="True"/>
<field name="available_in_pos" eval="True"/>
<field name="list_price">5.30</field>
</record>
</openerp>
from . import account_journal
from . import barcode_rule
from . import pos_config
from . import pos_order
from . import product_category
from . import product_template
# 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)
# 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
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
# 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({
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);
},
});
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
);
},
});
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 _t = core._t;
screens.ScreenWidget.include({
barcode_meal_voucher_payment_action: function (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));
},
// 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({
update_summary: function () {
this._super.apply(this, arguments);
var order = this.pos.get_order();
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({
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;
},
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");
}
},
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, make the user confirm.
if (!force_validation && (total_received > current_max)) {
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;
}
return this._super.apply(this, arguments);
},
});
});
<?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>
</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"/>
</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