Commit abbf0618 by François C.

Merge branch 'easy_shift_presence_validation_with_conditions' into 'dev_cooperatic'

Easy shift presence validation with conditions (and eslint auto fix clean)

See merge request !41
parents 6b87d971 d0157c9c
Pipeline #1154 passed with stage
in 1 minute 21 seconds
......@@ -97,7 +97,8 @@ SHIFT_INFO = """A la cagette, un service est une plage de trois heures un jour
PB_INSTRUCTIONS = """Si j'ai un problème, que je suis désinscrit, que je veux changer de créneaux ou quoi que ce soit, merci de vous rendre dans la section \"J'ai un problème\" sur le site web de <a href=\"https://lacagette-coop.fr/?MonEspaceMembre\">La Cagette</a>"""
ENTRANCE_COME_FOR_SHOPING_MSG = "Hey coucou toi ! Cet été nous sommes plus de <strong>1000 acheteur·euses</strong> pour seulement <strong>300 coopérateur·rice·s</strong> en service. <br />Tu fais tes courses à La Cagette cet été ?<br/> Inscris-toi sur ton espace membre !"
ENTRANCE_EXTRA_BUTTONS_DISPLAY = False
ENTRANCE_EASY_SHIFT_VALIDATE = True
# Members space / shifts
UNSUBSCRIBED_MSG = 'Vous êtes désincrit·e, merci de remplir <a href="https://docs.google.com/forms/d/e/1FAIpQLSfPiC2PkSem9x_B5M7LKpoFNLDIz0k0V5I2W3Mra9AnqnQunw/viewform">ce formulaire</a> pour vous réinscrire sur un créneau.<br />Vous pouvez également contacter le Bureau des Membres en remplissant <a href="https://docs.google.com/forms/d/e/1FAIpQLSeZP0m5-EXPVJxEKJk6EjwSyZJtnbiGdYDuAeFI3ENsHAOikg/viewform">ce formulaire</a>'
CONFIRME_PRESENT_BTN = 'Clique ici pour valider ta présence'
......
......@@ -22,7 +22,7 @@ class CagetteMember(models.Model):
"""Class to handle cagette Odoo member."""
m_default_fields = ['name', 'sex', 'image_medium', 'active',
'barcode_base', 'barcode', 'in_ftop_team',
'is_associated_people', 'is_member',
'is_associated_people', 'is_member', 'shift_type',
'display_ftop_points', 'display_std_points',
'is_exempted', 'cooperative_state', 'date_alert_stop']
......@@ -1176,6 +1176,44 @@ class CagetteServices(models.Model):
result['service_found'] = False
return result
@staticmethod
def easy_validate_shift_presence(coop_id):
"""Add a presence point if the request is valid."""
res = {}
try:
api = OdooAPI()
# let verify coop_id is corresponding to a ftop subscriber
cond = [['id', '=', coop_id]]
fields = ['shift_type']
coop = api.search_read('res.partner', cond, fields)
if coop:
if coop[0]['shift_type'] == 'ftop':
evt_name = getattr(settings, 'ENTRANCE_ADD_PT_EVENT_NAME', 'Validation service comité')
c = [['partner_id', '=', coop_id], ['name', '=', evt_name]]
f = ['create_date']
last_point_mvts = api.search_read('shift.counter.event', c, f,
order ="create_date DESC", limit=1)
ok_for_adding_pt = False
if len(last_point_mvts):
now = datetime.datetime.now()
past = datetime.datetime. strptime(last_point_mvts[0]['create_date'],
'%Y-%m-%d %H:%M:%S')
if (now - past).total_seconds() >= 3600 * 24:
ok_for_adding_pt = True
else:
ok_for_adding_pt = True
if ok_for_adding_pt is True:
res['evt_id'] = CagetteMember(coop_id).add_pts('ftop', 1, evt_name)
else:
res['error'] = "One point has been added less then 24 hours ago"
else:
res['error'] = "Unallowed coop"
else:
res['error'] = "Invalid coop id"
except Exception as e:
coop_logger.error("easy_validate_shift_presence : %s %s", str(coop_id), str(e))
return res
class CagetteUser(models.Model):
@staticmethod
......
......@@ -67,3 +67,5 @@ h1 .member_name {font-weight: bold;}
.msg-big {font-size: xx-large; background: #fff; padding:25px; text-align: center;}
#member_advice {background: #FFF; color: red;}
.easy_shift_validate {text-align: center; margin-top: 3em;}
......@@ -16,6 +16,7 @@
})(jQuery);
var current_displayed_member = null,
operator = null,
results = null,
loaded_services = null,
selected_service = null,
......@@ -23,6 +24,8 @@ var current_displayed_member = null,
rattrapage_ou_volant = null,
timeout_counter = null;
var search_button = $('.btn--primary.search');
var sm_search_member_button = $('#sm_search_member_button'),
sm_search_member_input = $('#sm_search_member_input');
var loading2 = $('.loading2');
var search_field = $('input[name="search_string"]');
var shift_title = $('#current_shift_title');
......@@ -162,6 +165,7 @@ function canSearch() {
function search_member(force_search = false) {
chars = []; // to prevent false "as barcode-reader" input
operator = null;
if (canSearch() || force_search) {
html_elts.member_slide.hide();
......@@ -279,7 +283,15 @@ function fill_service_entry(s) {
rattrapage_wanted.show();
}
function clean_search_for_easy_validate_zone() {
$('.search_member_results_area').hide();
$('.search_member_results').empty();
sm_search_member_input.val('');
operator = null;
}
function clean_service_entry() {
clean_search_for_easy_validate_zone();
rattrapage_wanted.hide();
shift_title.text('');
shift_members.html('');
......@@ -667,6 +679,73 @@ html_elts.image_medium.on('click', function() {
}
});
function ask_for_easy_shift_validation() {
//alert("operator = " + JSON.stringify(operator))
msg = "<p>Je suis bien " + operator.name + "<br/> et <br/>je valide mon service 'Comité' </p>";
openModal(msg, function() {
try {
post_form(
'/members/easy_validate_shift_presence',
{
coop_id: operator.id
},
function(err, result) {
if (!err) {
alert("1 point volant vient d'être ajouté.");
clean_search_for_easy_validate_zone();
closeModal();
} else {
if (typeof (err.responseJSON) != "undefined"
&& typeof (err.responseJSON.error) != "undefined") {
alert(err.responseJSON.error);
} else {
console.log(err);
}
}
}
);
} catch (e) {
console.log(e);
}
}, 'Confirmer');
}
// Display the members from the search result (copied from stock_movements)
function display_possible_members() {
$('.search_member_results_area').show();
$('.search_member_results').empty();
if (members_search_results.length > 0) {
for (member of members_search_results) {
let btn_classes = "btn";
if (operator != null && operator.id == member.id) {
btn_classes = "btn--success";
}
// Display results (possible members) as buttons
var member_button = '<button class="' + btn_classes + ' btn_member" member_id="'
+ member.id + '">'
+ member.barcode_base + ' - ' + member.name
+ '</button>';
$('.search_member_results').append(member_button);
// Set action on click on a member button
$('.btn_member').on('click', function() {
for (member of members_search_results) {
if (member.id == $(this).attr('member_id')) {
operator = member;
// Enable validation button when operator is selected
ask_for_easy_shift_validation();
break;
}
}
display_possible_members();
});
}
} else {
$('.search_member_results').html('<p><i>Aucun résultat ! Faites-vous partie d\'un comité ? <br/> Si oui, vérifiez la recherche..</i></p>');
}
}
$(document).ready(function() {
var shopping_entry_btn = $('a[data-next="shopping_entry"]');
......@@ -708,6 +787,44 @@ $(document).ready(function() {
init_webcam();
});
$('#sm_search_member_form').submit(function() {
if (is_time_to('search_member', 1000)) {
sm_search_member_button.empty().append(`<i class="fas fa-spinner fa-spin"></i>`);
let search_str = sm_search_member_input.val();
$.ajax({
url: '/members/search/' + search_str,
dataType : 'json',
success: function(data) {
members_search_results = [];
for (member of data.res) {
if (member.shift_type == 'ftop') {
members_search_results.push(member);
}
}
display_possible_members();
},
error: function(data) {
err = {
msg: "erreur serveur lors de la recherche de membres",
ctx: 'easy_validate.search_members'
};
report_JS_error(err, 'members');
$.notify("Erreur lors de la recherche de membre, il faut ré-essayer plus tard...", {
globalPosition:"top right",
className: "error"
});
},
complete: function() {
sm_search_member_button.empty().append(`Recherche`);
}
});
}
});
});
Webcam.on('live', function() {
......
......@@ -46,6 +46,7 @@ urlpatterns = [
url(r'^remove_data_from_couchdb$', views.remove_data_from_CouchDB),
url(r'^image/([0-9]+)', views.getmemberimage),
url(r'^add_pts_to_everybody/([0-9]+)/([a-zA-Z0-9_ ]+)$', admin.add_pts_to_everybody),
url(r'^easy_validate_shift_presence$', views.easy_validate_shift_presence),
# conso / groupe recherche / socio
url(r'^panel_get_purchases$', views.panel_get_purchases),
]
......@@ -33,6 +33,8 @@ def index(request):
for_shoping_msg = msettings['msg_accueil']['value']
context['ENTRANCE_COME_FOR_SHOPING_MSG'] = for_shoping_msg
context['ftop_btn_display'] = getattr(settings, 'ENTRANCE_FTOP_BUTTON_DISPLAY', True)
context['extra_btns_display'] = getattr(settings, 'ENTRANCE_EXTRA_BUTTONS_DISPLAY', True)
context['easy_shift_validate'] = getattr(settings, 'ENTRANCE_EASY_SHIFT_VALIDATE', False)
if 'no_picture_member_advice' in msettings:
if len(msettings['no_picture_member_advice']['value']) > 0:
context['no_picture_member_advice'] = msettings['no_picture_member_advice']['value']
......@@ -283,6 +285,22 @@ def record_service_presence(request):
res['error'] = str(e)
return JsonResponse({'res': res})
def easy_validate_shift_presence(request):
"""Add a presence point if the request is valid."""
res = {}
try:
coop_id = int(request.POST.get("coop_id", "nan"))
res = CagetteServices.easy_validate_shift_presence(coop_id)
except Exception as e:
res['error'] = str(e)
if 'error' in res:
if res['error'] == "One point has been added less then 24 hours ago":
# TODO : use translation (all project wide)
res['error'] = "Vous ne pouvez pas valider plus d'un service par 24h"
return JsonResponse(res, status=500)
else:
return JsonResponse(res, safe=False)
def record_absences(request):
return JsonResponse({'res': CagetteServices.record_absences()})
......
......@@ -119,6 +119,7 @@ function add_product() {
contentType: "application/json; charset=utf-8",
success: function(data) {
let res = data.products[0];
if (typeof res.id != "undefined") {
res.suppliersinfo = [];
res.default_code = ' ';
......@@ -154,21 +155,21 @@ function compute_products_coverage_qties() {
] of Object.entries(products)) {
if ('suppliersinfo' in product && product.suppliersinfo.length > 0) {
let purchase_qty_for_coverage = null;
// Durée couverture produit = (stock + qté entrante + qté commandée ) / conso quotidienne
const stock = product.qty_available;
const incoming_qty = product.incoming_qty;
const daily_conso = product.daily_conso;
purchase_qty_for_coverage = order_doc.coverage_days * daily_conso - stock - incoming_qty;
purchase_qty_for_coverage = (purchase_qty_for_coverage < 0) ? 0 : purchase_qty_for_coverage;
// Reduce to nb of packages to purchase
purchase_package_qty_for_coverage = purchase_qty_for_coverage / product.suppliersinfo[0].package_qty;
// Round up to unit for all products
purchase_package_qty_for_coverage = Math.ceil(purchase_package_qty_for_coverage);
// Set qty to purchase for first supplier only
products[key].suppliersinfo[0].qty = purchase_package_qty_for_coverage;
}
......@@ -191,7 +192,8 @@ function check_products_data() {
}
);
clicked_order_pill.find('.pill_order_name').empty().append(`<i class="fas fa-spinner fa-spin"></i>`);
clicked_order_pill.find('.pill_order_name').empty()
.append(`<i class="fas fa-spinner fa-spin"></i>`);
$.ajax({
type: 'GET',
......@@ -208,12 +210,14 @@ function check_products_data() {
// Override products data with new data (without suppliersinfo so we don't override qty)
const updated_suppliersinfo = product.suppliersinfo;
delete product.suppliersinfo;
products[p_index] = { ...products[p_index], ...product };
// Update suppliers info
// Update suppliers info
for (let psi_index in products[p_index].suppliersinfo) {
const updated_psi = updated_suppliersinfo.find(psi => psi.supplier_id == products[p_index].suppliersinfo[psi_index].supplier_id);
if (updated_psi !== undefined) {
products[p_index].suppliersinfo[psi_index].package_qty = updated_psi.package_qty;
products[p_index].suppliersinfo[psi_index].price = updated_psi.price;
......@@ -279,6 +283,7 @@ function add_supplier() {
selected_suppliers.push(supplier);
let url = "/orders/get_supplier_products";
url += "?sids=" + encodeURIComponent(supplier.id);
// Fetch supplier products
......@@ -486,6 +491,7 @@ function _compute_total_values_by_supplier() {
let supplier_index = selected_suppliers.findIndex(s => s.id == supinfo.supplier_id);
let product_supplier_value = ('qty' in supinfo) ? supinfo.qty * supinfo.package_qty * supinfo.price : 0;
selected_suppliers[supplier_index].total_value += product_supplier_value;
}
}
......@@ -590,7 +596,8 @@ function generate_inventory() {
modal_create_inventory.html(),
() => {
if (is_time_to('validate_generate_inventory')) {
$('#do_inventory').empty().append(`<i class="fas fa-spinner fa-spin"></i>`);
$('#do_inventory').empty()
.append(`<i class="fas fa-spinner fa-spin"></i>`);
$.ajax({
type: "POST",
url: "/inventory/generate_inventory_list",
......@@ -600,10 +607,11 @@ function generate_inventory() {
data: JSON.stringify(data),
success: () => {
unselect_all_rows();
// Give time for modal to fade
setTimeout(function() {
$('#do_inventory').empty().append(`Faire un inventaire`);
$('#do_inventory').empty()
.append(`Faire un inventaire`);
$('#do_inventory').notify(
"Inventaire créé !",
{
......@@ -614,15 +622,16 @@ function generate_inventory() {
}, 200);
},
error: function(data) {
$('#do_inventory').empty().append(`Faire un inventaire`);
$('#do_inventory').empty()
.append(`Faire un inventaire`);
let msg = "erreur serveur lors de la création de l'inventaire".
err = {msg: msg, ctx: 'generate_inventory'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
alert("Erreur lors de la création de l'inventaire. Réessayez plus tard.");
}
});
......@@ -644,12 +653,12 @@ function order_pill_on_click() {
clicked_order_pill = $(this);
let order_name_container = clicked_order_pill.find('.pill_order_name');
let doc_id = $(order_name_container).text();
dbc.get(doc_id).then((doc) => {
if (doc.last_update.fingerprint !== fingerprint) {
time_diff = dates_diff(new Date(doc.last_update.timestamp), new Date());
diff_str = ``;
if (time_diff.days !== 0) {
diff_str += `${time_diff.days} jour(s), `;
}
......@@ -660,11 +669,11 @@ function order_pill_on_click() {
diff_str += `${time_diff.min} min, `;
}
diff_str += `${time_diff.sec}s`;
let modal_order_access = $('#templates #modal_order_access');
modal_order_access.find(".order_last_update").text(diff_str);
openModal(
modal_order_access.html(),
() => {
......@@ -778,17 +787,19 @@ function create_orders() {
} else {
// Default date : tomorrow
let date_object = new Date();
date_object.setDate(date_object.getDate() + 1);
// Get ISO format bare string
formatted_date = date_object.toISOString().replace('T', ' ').split('.')[0];
formatted_date = date_object.toISOString().replace('T', ' ')
.split('.')[0];
}
// Create an entry for this supplier
orders_data.suppliers_data[supplier.id] = {
date_planned: formatted_date,
lines: []
}
};
}
openModal();
......@@ -858,7 +869,7 @@ function create_orders() {
order_doc._deleted = true;
update_cdb_order().then(() => {
update_order_selection_screen();
})
});
reset_data();
switch_screen('orders_created');
closeModal();
......@@ -934,7 +945,7 @@ function goto_main_screen(doc) {
update_cdb_order();
update_main_screen();
switch_screen();
})
});
}
function back() {
......@@ -980,6 +991,7 @@ function display_suppliers() {
const clicked_supplier = selected_suppliers.find(s => s.id == supplier_id);
let modal_remove_supplier = $('#templates #modal_remove_supplier');
modal_remove_supplier.find(".supplier_name").text(clicked_supplier.display_name);
openModal(
......@@ -1037,11 +1049,12 @@ function _compute_product_data(product) {
if (order_doc.coverage_days !== null) {
let qty_not_covered = 0;
let days_covered = 0;
if (product.daily_conso !== 0) {
qty_not_covered = product.daily_conso * order_doc.coverage_days - product.qty_available - product.incoming_qty - purchase_qty;
qty_not_covered = -Math.ceil(qty_not_covered); // round up, so if a value is not fully covered display it
qty_not_covered = -Math.ceil(qty_not_covered); // round up, so if a value is not fully covered display it
qty_not_covered = (qty_not_covered > 0) ? 0 : qty_not_covered; // only display qty not covered (neg value)
days_covered = (product.qty_available + product.incoming_qty + purchase_qty) / product.daily_conso;
days_covered = Math.floor(days_covered);
}
......@@ -1225,7 +1238,7 @@ function prepare_datatable_columns() {
},
width: "4%"
});
return columns;
}
......@@ -1266,8 +1279,20 @@ function display_products(params) {
stripeClasses: [], // Remove datatable cells coloring
orderClasses: false,
aLengthMenu: [
[25, 50, 100, 200, -1],
[25, 50, 100, 200, "Tout"]
[
25,
50,
100,
200,
-1
],
[
25,
50,
100,
200,
"Tout"
]
],
iDisplayLength: -1,
scrollX: true,
......@@ -1469,8 +1494,10 @@ function display_total_values() {
_compute_total_values_by_supplier();
let order_total_value = 0;
for (let supplier of selected_suppliers) {
$(`#pill_supplier_${supplier.id}`).find('.supplier_total_value').text(parseFloat(supplier.total_value).toFixed(2));
$(`#pill_supplier_${supplier.id}`).find('.supplier_total_value')
.text(parseFloat(supplier.total_value).toFixed(2));
order_total_value += supplier.total_value;
}
......@@ -1526,8 +1553,9 @@ function update_order_selection_screen() {
}).then(function (result) {
// Remove listener before recreating them
$(".order_pill").off();
let existing_orders_container = $("#existing_orders");
existing_orders_container.empty();
$('#new_order_name').val('');
......@@ -1536,6 +1564,7 @@ function update_order_selection_screen() {
} else {
for (let row of result.rows) {
let template = $("#templates #order_pill_template");
template.find(".pill_order_name").text(row.id);
existing_orders_container.append(template.html());
......@@ -1654,9 +1683,9 @@ $(document).ready(function() {
e.preventDefault();
if (is_time_to('submit_coverage_form', 1000)) {
let val = $("#coverage_days_input").val();
val = parseInt(val);
if (!isNaN(val)) {
order_doc.coverage_days = val;
compute_products_coverage_qties();
......@@ -1699,18 +1728,19 @@ $(document).ready(function() {
$('#create_orders').on('click', function() {
if (is_time_to('create_orders', 1000)) {
let modal_create_order = $('#templates #modal_create_order');
modal_create_order.find('.suppliers_date_planned_area').empty();
for (let supplier of selected_suppliers) {
let supplier_date_planned_template = $('#templates #modal_create_order__supplier_date_planned');
supplier_date_planned_template.find(".supplier_name").text(supplier.display_name);
supplier_date_planned_template.find(".modal_input_container").attr('id', `container_date_planned_supplier_${supplier.id}`);
modal_create_order.find('.suppliers_date_planned_area').append(supplier_date_planned_template.html());
}
openModal(
modal_create_order.html(),
() => {
......@@ -1721,12 +1751,13 @@ $(document).ready(function() {
'Valider',
false
);
// Add id to input once modal is displayed
for (let supplier of selected_suppliers) {
$(`#modal #container_date_planned_supplier_${supplier.id}`).find(".supplier_date_planned").attr('id', `date_planned_supplier_${supplier.id}`);
$(`#modal #container_date_planned_supplier_${supplier.id}`).find(".supplier_date_planned")
.attr('id', `date_planned_supplier_${supplier.id}`);
}
$("#modal .supplier_date_planned")
.datepicker({
defaultDate: "+1d",
......@@ -1742,7 +1773,7 @@ $(document).ready(function() {
}
});
}
return 0;
});
......
......@@ -228,6 +228,13 @@
- ENTRANCE_SERVICE_BTN = "…faire <b>mon service 🤝"
- ENTRANCE_EXTRA_BUTTONS_DISPLAY = False (no button is shown above shift coop. list) (True if not set)
- ENTRANCE_EASY_SHIFT_VALIDATE = False (default value)
When set to True allow coop to identify and have 1 point (only if FTOP)
- ENTRANCE_ADD_PT_EVENT_NAME = 'Add 1 point name throught easy validate' (default : 'Validation service comité'')
### Member space
- EM_URL = ''
......
......@@ -59,10 +59,9 @@ function post_form(url, data, callback) {
$.post(ajax_params)
.done(function(rData) {
callback(null, rData);
})
.fail(function(err) {
console.log(err);
callback(err, {});
});
}
......
......@@ -137,13 +137,16 @@
<div class="col-1"></div>
<div class="col-4">
<h1 class="col-4 txtcenter">Qui es-tu ?</h1>
<section class="outside_list">
<a class="btn b_orange" data-next="rattrapage_1" data-type="rattrapage">Je viens faire un rattrapage</a>
{% if ftop_btn_display %}
<a class="btn b_yellow" data-next="rattrapage_1" data-type="volant">Je suis volant</a>
{% if extra_btns_display %}
<section class="outside_list">
<a class="btn b_orange" data-next="rattrapage_1" data-type="rattrapage">Je viens faire un rattrapage</a>
{% if ftop_btn_display %}
<a class="btn b_yellow" data-next="rattrapage_1" data-type="volant">Je suis volant</a>
{% endif %}
</section>
{% endif %}
</section>
</div>
<div class="col-1"></div>
<div class="col-1"></div>
<section id="service_en_cours" class="col-4">
......@@ -153,6 +156,12 @@
<div id="current_shift_members">
</div>
{% if easy_shift_validate %}
<div class="easy_shift_validate">
<p>Je valide mon service "Comité"</p>
{% include "members/member_selection.html" %}
</div>
{% endif %}
</section>
</section>
......
<form id="sm_search_member_form" action="javascript:;" method="post">
<input type="text" id="sm_search_member_input" value="" placeholder="Votre nom ou numéro..." required>
<button type="submit" class="btn--primary" id="sm_search_member_button">Recherche</button>
</form>
<div class="search_member_results_area" style="display:none;">
<div>
<p>Vous êtes (cliquez sur votre nom) :</p>
</div>
<div class="search_member_results"></div>
</div>
\ No newline at end of file
......@@ -83,16 +83,7 @@
<hr />
<section class="set_member_to_movement">
<p>Avant de valider l'opération, merci de nous dire qui vous êtes :</p>
<form id="sm_search_member_form" action="javascript:;" method="post">
<input type="text" id="sm_search_member_input" value="" placeholder="Votre nom ou numéro..." required>
<button type="submit" class="btn--primary" id="sm_search_member_button">Recherche</button>
</form>
<div class="search_member_results_area" style="display:none;">
<div>
<p>Vous êtes (cliquez sur votre nom) :</p>
</div>
<div class="search_member_results"></div>
</div>
{% include "members/member_selection.html" %}
</section>
<hr />
</div>
......
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