Commit 04aedf66 by François C.

Merge branch 'dev_cooperatic' into 'dev_principale'

Intégration des dév. Cooperatic pour la Cagette

See merge request !164
parents 99590052 b1c17470
Pipeline #2157 passed with stage
in 1 minute 28 seconds
......@@ -115,10 +115,13 @@ ENTRANCE_EASY_SHIFT_VALIDATE_MSG = """Si vous faites un service dans un comité,
valider votre présence en cherchant<br/>
votre nom ou numéro ci-dessous
"""
PREPA_ODOO_URL = '[...]'
# Members space / shifts
UNSUBSCRIBED_FORM_LINK = 'https://docs.google.com/forms/d/e/1FAIpQLScWcpls-ruYIp7HdrjRF1B1TyuzdqhvlUIcUWynbEujfj3dTg/viewform'
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'
BLOCK_ACTIONS_FOR_ATTACHED_PEOPLE = False
RECEPTION_PB = "Ici, vous pouvez signaler toute anomalie lors d'une réception, les produits non commandés, cassés ou pourris. \
......@@ -136,3 +139,6 @@ ORDERS_HELPER_METABASE_URL = "url_meta_base"
USE_NEW_MEMBERS_SPACE = True
START_DATE_FOR_SHIFTS_HISTORY = "2018-01-01"
AMNISTIE_DATE= "2021-11-24 00:00:00"
# BDM Admin
BDM_SHOW_FTOP_BUTTON = True
......@@ -43,13 +43,32 @@ class CagetteEnvelops(models.Model):
try:
# Get invoice
cond = [['partner_id', '=', data['partner_id']]]
# Get specific invoice if id is given
if 'invoice_id' in data:
cond = [['id', '=', data['invoice_id']], ["number", "!=", False]]
else:
cond = [['partner_id', '=', data['partner_id']], ["number", "!=", False]]
fields = ['id', 'name', 'number', 'partner_id', 'residual_signed']
invoice_res = self.o_api.search_read('account.invoice', cond, fields)
# Check if invoice exists
if len(invoice_res) > 0:
invoice = invoice_res[0]
invoice = None
# Get first invoice for which amount being paid <= amount left to pay in invoice
for invoice_item in invoice_res:
if int(float(data['amount']) * 100) <= int(float(invoice_item['residual_signed']) * 100):
invoice = invoice_item
if invoice is None:
res['error'] = 'The amount is too high for the invoices found for this partner.'
try:
# Got an error while logging...
coop_logger.error(res['error'] + ' : %s', str(data))
except Exception as e:
print(str(e))
return res
else:
res['error'] = 'No invoice found for this partner, can\'t create payment.'
coop_logger.error(res['error'] + ' : %s', str(data))
......@@ -79,6 +98,7 @@ class CagetteEnvelops(models.Model):
except Exception as e:
res['error'] = repr(e)
coop_logger.error(res['error'] + ' : %s', str(args))
# Exception rises when odoo method returns nothing
marshal_none_error = 'cannot marshal None unless allow_none is enabled'
try:
......@@ -102,7 +122,6 @@ class CagetteEnvelops(models.Model):
coop_logger.error(res['error'] + ' : %s', str(data))
if not ('error' in res):
res['done'] = True
res['payment_id'] = payment_id
......@@ -111,6 +130,11 @@ class CagetteEnvelops(models.Model):
def delete_envelop(self, envelop):
return self.c_db.delete(envelop)
def archive_envelop(self, envelop):
envelop['archive'] = True
envelop['cashing_date'] = datetime.date.today().strftime("%d/%m/%Y")
return self.c_db.dbc.update([envelop])
def generate_envelop_display_id(self):
"""Generate a unique incremental id to display"""
c_db = CouchDB(arg_db='envelops')
......@@ -172,7 +196,7 @@ class CagetteEnvelops(models.Model):
else:
# Get the oldest check envelops, limited by the number of checks
docs = []
for item in c_db.dbc.view('index/by_type', key='ch', include_docs=True, limit=payment_data['checks_nb']):
for item in c_db.dbc.view('index/by_type_not_archive', key='ch', include_docs=True, limit=payment_data['checks_nb']):
docs.append(item.doc)
# If only 1 check to save
......
#cash, #ch {
margin-top: 15px;
}
#admin_connexion_button {
position: absolute;
top: 5px;
right: 5px;
}
.envelop_section {
margin-bottom: 10px;
}
......@@ -6,14 +16,132 @@
display: none;
cursor: pointer;
margin-bottom: 15px;
width: 25%;
}
#cash_envelops {
#cash_envelops, #ch_envelops, #archive_cash_envelops, #archive_ch_envelops {
margin-top: 30px;
}
#ch_envelops {
margin-top: 30px;
.delete_envelop_button, .envelop_comment {
margin: 0 0 15px 15px;
}
.update_envelop_button, .add_to_envelop_button {
margin: 0 0 15px 10px;
}
.envelop_content_list {
margin: 20px 0 15px 0;
}
/* Update envelop modal */
.envelop_lines {
margin: 20px 0;
display: flex;
flex-direction: column;
align-items: center;
}
.update_envelop_line {
margin: 5px 0;
width: 60%;
display: flex;
position: relative;
}
.deleted_line_through {
border-bottom: 1px solid #d9534f;
position: absolute;
content: "";
width: 100%;
height: 50%;
display: none;
}
.update_envelop_line div {
flex: 50% 1 0;
}
.update_envelop_line .line_partner_name_container {
display: flex;
justify-content: flex-start;
align-items: center;
}
.update_envelop_line .line_partner_name {
text-align: left;
padding: 0 5px;
}
.update_envelop_line .line_partner_input_container {
display: flex;
align-items: center;
}
.delete_envelop_line_icon {
margin-left: 10px;
color: #d9534f;
cursor: pointer;
}
.envelop_comments {
width: 60%;
margin-bottom: 20px;
}
/* Add payments to envelop modal */
.search_member_area {
margin: 20px 0;
}
.search_member_results_area {
margin: 20px 0;
}
.add_to_envelop_lines_area {
margin: 20px 0;
}
.add_to_envelop_lines {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin: 10px 0;
}
.add_to_envelop_line {
display: flex;
justify-content: center;
margin: 10px 0;
}
.add_to_envelop_line .partner_name_container {
display: flex;
justify-content: flex-start;
align-items: center;
}
.add_to_envelop_line .partner_input_container {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-left: 10px;
width: 300px;
}
.line_partner_amount_error {
display: none;
color: #d9534f;
font-style: italic;
}
.confirm_add_payment_details {
font-weight: bold;
margin: 0 3px;
}
/* Accordion style */
......@@ -23,7 +151,6 @@
color: #212529;
cursor: pointer;
padding: 18px;
/* width: 80%; */
text-align: left;
border: none;
outline: none;
......@@ -32,7 +159,6 @@
.archive_button {
padding: 18px;
/* width: 20%; */
}
hr {
......
var cash_envelops = [];
var archive_cash_envelops = [];
var ch_envelops = [];
var archive_ch_envelops = [];
var envelop_to_update = null;
var members_search_results = [];
var selected_member = null;
function reset() {
$('#cash_envelops').empty();
$('#ch_envelops').empty();
$('#archive_cash_envelops').empty();
$('#archive_ch_envelops').empty();
archive_cash_envelops = [];
archive_ch_envelops = [];
cash_envelops = [];
ch_envelops = [];
}
......@@ -12,7 +21,9 @@ function toggle_error_alert() {
$('#envelop_cashing_error').toggle(250);
}
function toggle_success_alert() {
function toggle_success_alert(message) {
$('#envelop_cashing_success').find(".success_alert_content")
.text(message);
$('#envelop_cashing_success').toggle(250);
}
......@@ -20,9 +31,56 @@ function toggle_deleted_alert() {
$('#envelop_deletion_success').toggle(250);
}
// Set an envelop content on the document
/**
* Get an envelop from the cash or cheque lists dependings on the params
* @param {String} type
* @param {String} index
* @returns
*/
function get_envelop_from_type_index(type, index) {
if (type === "cash") {
return cash_envelops[index];
} else {
return ch_envelops[index];
}
}
/**
* Define a name for an envelop depending on its type, with or with its type
* @param {Object} envelop
* @param {String} name_type short | long
* @returns
*/
function get_envelop_name(envelop, name_type = 'short') {
let envelop_name = "";
if (envelop.type === "cash") {
let split_id = envelop._id.split('_');
let envelop_date = split_id[3].padStart(2, '0') + "/" + split_id[2].padStart(2, '0') + "/" + split_id[1];
envelop_name = `Enveloppe${(name_type === "short") ? "" : " de liquide"} du ${envelop_date}`;
} else if (envelop.type == "ch") {
envelop_name = `Enveloppe${(name_type === "short") ? "" : " de chèques"} #${envelop.display_id}`;
}
return envelop_name;
}
/**
* Set the envelops contents on the document (could use a little cleanup someday: don't generate html in js, etc...)
* @param {Object} envelop
* @param {String} envelop_name
* @param {Int} envelop_content_id
* @param {Int} envelop_index
*/
function set_envelop_dom(envelop, envelop_name, envelop_content_id, envelop_index) {
var envelops_section = $('#' + envelop.type + '_envelops');
var envelops_section ="";
if (!envelop.archive)
envelops_section = $('#' + envelop.type + '_envelops');
else
envelops_section = $('#archive_' + envelop.type + '_envelops');
// Calculate envelop total amount
var total_amount = 0;
......@@ -35,16 +93,28 @@ function set_envelop_dom(envelop, envelop_name, envelop_content_id, envelop_inde
+ '<div class="flex-container">';
// Allow checking for all cash and first check envelops
if (envelop.type == 'cash' || envelop.type == 'ch' && envelop_index == 0) {
if ((envelop.type == 'cash' || envelop.type == 'ch' && envelop_index == 0) && !envelop.archive) {
new_html += '<button class="accordion w80">' + envelop_name + ' - <i>' + total_amount + '€</i></button>'
+ '<button class="btn--success archive_button item-fluid" onClick="openModal(\'<h3>Êtes-vous sûr ?</h3>\', function() {archive_envelop(\'' + envelop.type + '\', ' + envelop_index + ');}, \'Encaisser\')">Encaisser</button>';
+ '<button class="btn--success archive_button item-fluid" onClick="openModal(\'<h3>Êtes-vous sûr ?</h3>\', function() {archive_envelop(\'' + envelop.type + '\', ' + envelop_index + ');}, \'Encaisser\', false)">Encaisser</button>';
} else if (envelop.archive ===true) {
new_html += '<button class="accordion w100">' + envelop_name + ' - <i>' + total_amount + '€';
if (envelop.cashing_date !== undefined) {
new_html += ' - Encaissée le ' + envelop.cashing_date;
}
if (envelop.canceled) {
new_html += ' - Enveloppe supprimée';
}
new_html += '</i></button>';
} else {
new_html += '<button class="accordion w100">' + envelop_name + ' - <i>' + total_amount + '€</i></button>';
}
new_html += '</div>'
+ '<div class="panel panel_' + envelop_content_id + '"><ol id="' + envelop_content_id + '"></ol></div>'
+ '</div>';
+ '<div class="panel panel_' + envelop_content_id + '"><ol class="envelop_content_list" id="' + envelop_content_id + '"></ol></div>'
+ '</div>';
$(new_html).appendTo(envelops_section);
......@@ -54,7 +124,7 @@ function set_envelop_dom(envelop, envelop_name, envelop_content_id, envelop_inde
var content = envelop.envelop_content[node].partner_name + ' : ' + envelop.envelop_content[node].amount + '€';
if ('payment_id' in envelop.envelop_content[node]) {
content += " - déjà comptabilisé.";
content += " -- paiement comptabilisé.";
}
var textnode = document.createTextNode(content); // Create a text node
......@@ -64,38 +134,173 @@ function set_envelop_dom(envelop, envelop_name, envelop_content_id, envelop_inde
}
let envelop_panel = $(`.panel_${envelop_content_id}`);
envelop_panel.append('<button class="btn--danger delete_envelop_button item-fluid" onClick="openModal(\'<h3>Supprimer enveloppe ?</h3>\', function() {delete_envelop(\'' + envelop.type + '\', ' + envelop_index + ');}, \'Supprimer\')">Supprimer l\'enveloppe</button>');
if (envelop.comments) envelop_panel.append(`<p class="envelop_comment"> <b>Commentaire :</b> ${envelop.comments}</p>`);
if (!envelop.archive) {
let envelop_panel = $(`.panel_${envelop_content_id}`);
envelop_panel.append(`<button class="btn--danger delete_envelop_button item-fluid" id="update_envelop_${envelop.type}_${envelop_index}">Supprimer l'enveloppe</button>`);
envelop_panel.append(`
<button
class="btn--primary update_envelop_button item-fluid"
id="update_envelop_${envelop.type}_${envelop_index}"
>
Modifier
</button>`);
envelop_panel.append(`
<button
class="btn--primary add_to_envelop_button item-fluid"
id="add_to_envelop_${envelop.type}_${envelop_index}"
>
Ajouter un paiement ou des parts sociales
</button>`);
$(".update_envelop_button").off("click");
$(".update_envelop_button").on("click", function() {
let el_id = $(this).attr("id")
.split("_");
envelop_to_update = {
type: el_id[2],
index: el_id[3],
lines_to_delete: []
};
set_update_envelop_modal();
});
$(".delete_envelop_button").off("click");
$(".delete_envelop_button").on("click", function() {
let el_id = $(this).attr("id")
.split("_");
let type = el_id[2];
let index = el_id[3];
let envelop = get_envelop_from_type_index(type, index);
openModal(
"<h3>Supprimer l'enveloppe ?</h3>",
function() {
archive_canceled_envelop(envelop);
},
'Supprimer'
);
});
$(".add_to_envelop_button").off("click");
$(".add_to_envelop_button").on("click", function() {
let el_id = $(this).attr("id")
.split("_");
envelop_to_update = {
type: el_id[el_id.length-2],
index: el_id[el_id.length-1]
};
let envelop = get_envelop_from_type_index(envelop_to_update.type, envelop_to_update.index);
let envelop_name = get_envelop_name(envelop, 'long');
let modal_add_to_envelop = $('#templates #modal_add_to_envelop');
modal_add_to_envelop.find(".envelop_name").text(envelop_name);
openModal(
modal_add_to_envelop.html(),
() => {},
'',
false,
true,
() => {
envelop_to_update = null;
selected_member = null;
modal.find(".btn-modal-ok").show();
}
);
// No validation button
modal.find(".btn-modal-ok").hide();
modal.find(".add_to_envelop_lines").empty();
// Set action to search for the member
modal.find('.search_member_form').submit(function() {
let search_str = modal.find('.search_member_input').val();
$.ajax({
url: '/members/search/' + search_str + "?search_type=short",
dataType : 'json',
success: function(data) {
members_search_results = data.res;
display_possible_members();
},
error: function() {
err = {
msg: "erreur serveur lors de la recherche de membres",
ctx: 'add_payment_to_envelop.search_members'
};
report_JS_error(err, 'envelops');
alert("Erreur lors de la recherche de membre, il faut ré-essayer plus tard...");
}
});
});
});
}
}
// Set the envelops data according to their type
/**
* Given the raw list of envelop documents, generate the cash and cheque lists
* @param {Array} envelops
*/
function set_envelops(envelops) {
var cash_index = 0;
var ch_index = 0;
var archive_cash_index = 0;
var archive_ch_index = 0;
reset();
for (var i= 0; i < envelops.length; i++) {
var envelop = envelops[i].doc;
if (envelop.type == "cash") {
//If the envelop is archived and more than 1 year old we delete it
if (envelop.archive && (new Date()-new Date(envelop.creation_date))/ (1000 * 3600 * 24 * 365)>1) {
delete_envelop(envelop);
} else if (envelop.type == "cash" && envelop.archive != true) {
cash_envelops.push(envelop);
let split_id = envelop._id.split('_');
let envelop_date = split_id[3] + "/" + split_id[2] + "/" + split_id[1];
let envelop_name = 'Enveloppe du ' + envelop_date;
let envelop_name = get_envelop_name(envelop);
let envelop_content_id = 'content_cash_list_' + cash_index;
set_envelop_dom(envelop, envelop_name, envelop_content_id, cash_index);
cash_index += 1;
} else if (envelop.type == "ch") {
} else if (envelop.type == "cash" && envelop.archive == true) {
archive_cash_envelops.push(envelop);
let envelop_name = get_envelop_name(envelop);
let envelop_content_id = 'content_archive_cash_list_' + archive_cash_index;
set_envelop_dom(envelop, envelop_name, envelop_content_id, archive_cash_index);
archive_cash_index += 1;
} else if (envelop.type == "ch" && envelop.archive != true) {
ch_envelops.push(envelop);
let envelop_name = 'Enveloppe #' + envelop.display_id;
let envelop_name = get_envelop_name(envelop);
let envelop_content_id = 'content_ch_list_' + ch_index;
set_envelop_dom(envelop, envelop_name, envelop_content_id, ch_index);
ch_index += 1;
} else if (envelop.type == "ch" && envelop.archive == true) {
archive_ch_envelops.push(envelop);
let envelop_name = get_envelop_name(envelop);
let envelop_content_id = 'content_archive_ch_list_' + archive_ch_index;
set_envelop_dom(envelop, envelop_name, envelop_content_id, archive_ch_index);
archive_ch_index += 1;
}
}
......@@ -125,18 +330,133 @@ function set_envelops(envelops) {
}
}
function delete_envelop(type, index) {
if (is_time_to('delete_envelop', 1000)) {
openModal();
/**
* Generate content & set listeners for the modal to update an envelop
*/
function set_update_envelop_modal() {
let envelop = get_envelop_from_type_index(envelop_to_update.type, envelop_to_update.index);
let envelop_name = get_envelop_name(envelop, 'long');
var envelop = null;
if (type == "cash") {
envelop = cash_envelops[index];
} else {
envelop = ch_envelops[index];
let modal_update_envelop = $('#templates #modal_update_envelop');
modal_update_envelop.find(".envelop_name").text(envelop_name);
modal_update_envelop.find(".envelop_lines").empty();
let update_line_template = $('#templates #update_envelop_line_template');
let cpt = 1;
for (let partner_id in envelop.envelop_content) {
let line = envelop.envelop_content[partner_id];
update_line_template.find(".update_envelop_line").attr('id', `update_line_${partner_id}`);
update_line_template.find(".line_number").html(`${cpt}.&nbsp;`);
update_line_template.find(".line_partner_name").text(line.partner_name);
modal_update_envelop.find(".envelop_lines").append(update_line_template.html());
cpt += 1;
}
openModal(
modal_update_envelop.html(),
() => {
update_envelop_action();
},
'Mettre à jour',
true,
true,
() => {
envelop_to_update = null;
}
);
// Elements needs to be on the document so value & listeners can be set
for (let partner_id in envelop.envelop_content) {
let line = envelop.envelop_content[partner_id];
$(`#update_line_${partner_id}`).find('.line_partner_amount')
.val(line.amount);
}
modal.find('.envelop_comments').val((envelop.comments !== undefined) ? envelop.comments : '');
$(".delete_envelop_line_icon").off("click");
$(".delete_envelop_line_icon").on("click", function() {
let line_id = $(this).closest(".update_envelop_line")
.attr("id")
.split("_");
let partner_id = line_id[line_id.length-1];
envelop_to_update.lines_to_delete.push(partner_id);
$(this).hide();
$(this).closest(".update_envelop_line")
.find(".deleted_line_through")
.show();
});
}
/**
* Update an envelop data with modal data
*/
function update_envelop_action() {
if (is_time_to('update_envelop_action', 1000)) {
let envelop = get_envelop_from_type_index(envelop_to_update.type, envelop_to_update.index);
// Update lines amounts
let amount_inputs = modal.find('.line_partner_amount');
amount_inputs.each(function (i, e) {
let line_id = $(e).closest(".update_envelop_line")
.attr("id")
.split("_");
let partner_id = line_id[line_id.length-1];
envelop.envelop_content[partner_id].amount = parseInt($(e).val());
});
// Delete lines
for (let partner_id of envelop_to_update.lines_to_delete) {
delete(envelop.envelop_content[partner_id]);
}
// Envelop comments
envelop.comments = modal.find('.envelop_comments').val();
update_envelop(envelop);
toggle_success_alert("Enveloppe modifiée !");
}
}
/**
* Update an envelop in couchdb
* @param {Object} envelop
*/
function update_envelop(envelop) {
if (is_time_to('update_envelop', 1000)) {
dbc.put(envelop, function callback(err, result) {
envelop_to_update = null;
if (!err && result !== undefined) {
get_envelops();
} else {
alert("Erreur lors de la mise à jour de l'enveloppe. Si l'erreur persiste contactez un administrateur svp.");
console.log(err);
}
});
}
}
/**
* archive and canceled an envelop from couchdb.
* @param {Object} envelop
*/
function archive_canceled_envelop(envelop) {
if (is_time_to('archive_canceled_envelop', 1000)) {
envelop.archive = true;
envelop.canceled = true;
envelop._deleted = true;
dbc.put(envelop, function callback(err, result) {
if (!err && result !== undefined) {
toggle_deleted_alert();
......@@ -150,18 +470,39 @@ function delete_envelop(type, index) {
}
}
/**
* Delete an envelop from couchdb.
* @param {Object} envelop
*/
function delete_envelop(envelop) {
if (is_time_to('delete_envelop', 1000)) {
envelop._deleted = true;
dbc.put(envelop, function callback(err, result) {
if (!err && result !== undefined) {
get_envelops();
} else {
alert("Erreur lors de la suppression de l'enveloppe... Essaye de recharger la page et réessaye.");
console.log(err);
}
});
}
}
/**
* Send the request to save an envelop payments in Odoo. The envelop will be deleted from couchdb.
* @param {String} type
* @param {String} index
*/
function archive_envelop(type, index) {
if (is_time_to('archive_envelop', 1000)) {
if (is_time_to('archive_envelop', 5000)) {
$('#envelop_cashing_error').hide();
$('#envelop_cashing_success').hide();
// Loading on
openModal();
if (type == "cash") {
envelop = cash_envelops[index];
} else {
envelop = ch_envelops[index];
}
let envelop = get_envelop_from_type_index(type, index);
// Proceed to envelop cashing
$.ajax({
......@@ -184,7 +525,13 @@ function archive_envelop(type, index) {
if (error_payments[i].done == false) {
error_message += "<p>Erreur lors de l'enregistrement du paiement de <b>" + error_payments[i]['partner_name']
+ "</b> (id odoo : " + error_payments[i]['partner_id'] + ", valeur à encaisser : " + error_payments[i]['amount'] + "€).";
error_message += "<br/><b>L'opération est à reprendre manuellement dans Odoo pour ce paiement.</b></p>";
error_message += "<br/><b>L'opération est à reprendre manuellement dans Odoo pour ce paiement.</b>";
if ('error' in error_payments[i]) {
error_message += `<br/>(error: ${error_payments[i]['error']})`;
}
error_message += "</p>";
}
}
......@@ -204,7 +551,7 @@ function archive_envelop(type, index) {
}
if (display_success_alert) {
toggle_success_alert();
toggle_success_alert("Enveloppe encaissée !");
}
},
error: function() {
......@@ -212,10 +559,14 @@ function archive_envelop(type, index) {
alert('Erreur serveur. Merci de ne pas ré-encaisser l\'enveloppe qui a causé l\'erreur.');
}
});
} else {
alert("Par sécurité, il faut attendre 5s entre l'encaissement de deux enveloppes.");
}
}
// Get all the envelops from couch db
/**
* Get all the envelops from couchdb
*/
function get_envelops() {
dbc.allDocs({
include_docs: true,
......@@ -229,20 +580,208 @@ function get_envelops() {
});
}
// Hande change in couc db
sync.on('change', function (info) {
// handle change
if (info.direction == 'pull') {
/**
* Display the members from the search result in the "add payments to envelop" modal
*/
function display_possible_members() {
modal.find('.search_member_results_area').show();
modal.find('.search_member_results').empty();
if (members_search_results.length > 0) {
$(".search_results_text").show();
for (member of members_search_results) {
// Display results (possible members) as buttons
var member_button = '<button class="btn--success btn_possible_member" member_id="'
+ member.id + '">'
+ member.barcode_base + ' - ' + member.name
+ '</button>';
$('.search_member_results').append(member_button);
}
// Set action on member button click
$('.btn_possible_member').on('click', function() {
const mid = $(this).attr('member_id');
selected_member = members_search_results.find(m => m.id == mid);
members_search_results = [];
modal.find('.search_member_input').val('');
modal.find('.search_member_results').empty();
modal.find('.search_member_results_area').hide();
// Adding line for this member in modal...
display_line_add_payment();
});
} else {
$(".search_results_text").hide();
$('.search_member_results').html(`<p>
<i>Aucun résultat ! Vérifiez votre recherche...</i>
</p>`);
}
}
/**
* Display a line for adding a member's payment in the "add payments to envelop" modal
*/
function display_line_add_payment() {
let envelop = get_envelop_from_type_index(envelop_to_update.type, envelop_to_update.index);
// Block adding payment if member is already in the envelop
for (let env_partner_id in envelop.envelop_content) {
if (env_partner_id == selected_member.id) {
alert("Ce membre est déjà dans l'enveloppe, impossible de lui rajouter un paiement.\nVous pouvez modifier le montant de son paiement dans la fenêtre de modification de l'enveloppe.");
return -1;
}
}
modal.find('.search_member_area').hide();
let modal_line = $('#templates #add_to_envelop_line_template');
modal_line.find(".line_partner_name").text(selected_member.name);
modal.find(".add_to_envelop_lines").append(modal_line.html());
modal.find(".add_to_envelop_lines_area").show();
// Add payment button
$('.add_payment_button').off('click');
$('.add_payment_button').on('click', function() {
let amount = parseInt(modal.find(".line_partner_amount").val(), 10);
if (isNaN(amount)) {
modal.find(".line_partner_amount_error").show();
} else {
modal.find(".line_partner_amount_error").hide();
let modal_confirm_add_payment = $('#templates #modal_confirm_add_payment');
modal_confirm_add_payment.find(".amount").text(amount);
modal_confirm_add_payment.find(".member").text(selected_member.name);
modal_confirm_add_payment.find(".envelop").text(get_envelop_name(envelop, 'long'));
openModal(
modal_confirm_add_payment.html(),
() => {
add_payment_to_envelop(amount, envelop);
},
"Confirmer"
);
modal.find(".btn-modal-ok").show();
}
});
// Add shares button
$('.add_shares_button').off('click');
$('.add_shares_button').on('click', function() {
let amount = parseInt(modal.find(".line_partner_amount").val(), 10);
if (isNaN(amount)) {
modal.find(".line_partner_amount_error").show();
} else {
modal.find(".line_partner_amount_error").hide();
let modal_confirm_add_shares = $('#templates #modal_confirm_add_shares');
modal_confirm_add_shares.find(".amount").text(amount);
modal_confirm_add_shares.find(".member").text(selected_member.name);
modal_confirm_add_shares.find(".envelop").text(get_envelop_name(envelop, 'long'));
openModal(
modal_confirm_add_shares.html(),
() => {
add_shares_to_member(amount, envelop);
},
"Confirmer",
false
);
modal.find(".btn-modal-ok").show();
}
});
return null;
}
/**
* Add a payment in an envelop & save in couchdb
* @param {Int} amount
* @param {Object} envelop
* @param {Int} invoice_id
* @param {String} message
*/
function add_payment_to_envelop(amount, envelop, invoice_id=null, message="Paiement ajouté !") {
if (is_time_to('add_payment_to_envelop', 1000)) {
envelop.envelop_content[selected_member.id] = {
partner_name: selected_member.name,
amount: amount
};
if (invoice_id != null) {
envelop.envelop_content[selected_member.id].invoice_id = invoice_id;
}
update_envelop(envelop);
toggle_success_alert(message);
envelop_to_update = null;
selected_member = null;
get_envelops();
}
}).on('error', function (err) {
// handle error
console.log('erreur sync');
console.log(err);
});
}
/**
* Send request to add shares & then add payment
* @param {Int} amount
* @param {Object} envelop
*/
function add_shares_to_member(amount, envelop) {
if (is_time_to('add_shares_to_member', 1000)) {
openModal();
data = {
partner_id: selected_member.id,
amount: amount
};
$.ajax({
type: "POST",
url: "/members/add_shares_to_member",
headers: { "X-CSRFToken": getCookie("csrftoken") },
dataType: "json",
traditional: true,
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
success: function(response) {
closeModal();
invoice_id = response[0];
add_payment_to_envelop(amount, envelop, invoice_id, "Parts sociales ajoutées !");
},
error: function() {
closeModal();
alert('Un erreur est survenue lors de l\'ajout de parts sociales.');
}
});
}
}
$(document).ready(function() {
if (typeof must_identify == "undefined" || coop_is_connected()) {
get_envelops();
// Hande change in couc db
sync.on('change', function (info) {
// handle change
if (info.direction == 'pull') {
get_envelops();
}
}).on('error', function (err) {
// handle error
console.log('erreur sync');
console.log(err);
});
}
});
......@@ -21,13 +21,13 @@ def index(request):
return HttpResponse(template.render(context, request))
def archive_envelop(request):
"""Save members payment and destroy the envelop"""
"""Save members payment and archive the envelop"""
m = CagetteEnvelops()
res_payments = []
res_envelop = ""
envelop = json.loads(request.body.decode())
# save each partner payment
for partner_id in envelop['envelop_content']:
# If payment_id in payment details: payment already saved. Skip saving.
......@@ -40,6 +40,9 @@ def archive_envelop(request):
'type' : envelop['type']
}
if 'invoice_id' in envelop['envelop_content'][partner_id]:
data['invoice_id'] = int(envelop['envelop_content'][partner_id]['invoice_id'])
res = m.save_payment(data)
except Exception as e:
res = {
......@@ -52,7 +55,7 @@ def archive_envelop(request):
# Immediately save a token than this payment has been saved
# If an error occurs, this payment won't be saved again
envelop['envelop_content'][partner_id]['payment_id'] = res['payment_id']
updated_envelop = m.c_db.updateDoc(envelop);
updated_envelop = m.c_db.updateDoc(envelop)
envelop['_rev'] = updated_envelop['_rev']
else:
# Handling error when saving payment, return data to display error message
......@@ -75,8 +78,8 @@ def archive_envelop(request):
coop_logger.error("Cannot attach payment error message to member : %s", str(e))
try:
# Delete envelop from couchdb
res_envelop = m.delete_envelop(envelop)
# archive envelop in couchdb
res_envelop = m.archive_envelop(envelop)
except Exception as e:
res_envelop = "error"
......
......@@ -368,10 +368,10 @@ class CagetteInventory(models.Model):
return {'missed': missed, 'unchanged': unchanged, 'done': done}
@staticmethod
def update_stock_with_shelf_inventory_data(inventory_data):
"""Updates Odoo stock after a shelf inventory"""
def update_products_stock(inventory_data, precision=2):
""" Updates Odoo stock after a shelf inventory or another action"""
TWOPLACES = Decimal(10) ** -2
TWOPLACES = Decimal(10) ** -precision
api = OdooAPI()
missed = []
unchanged = []
......
......@@ -94,7 +94,7 @@ def do_custom_list_inventory(request):
full_inventory_data = CagetteInventory.get_full_inventory_data(inventory_data)
# Proceed with inventory
res['inventory'] = CagetteInventory.update_stock_with_shelf_inventory_data(full_inventory_data)
res['inventory'] = CagetteInventory.update_products_stock(full_inventory_data)
# remove file
CagetteInventory.remove_custom_inv_file(inventory_data['id'])
......
from django.contrib import admin
from outils.common_imports import *
from outils.for_view_imports import *
from outils.common import OdooAPI
from members.models import CagetteUser
from members.models import CagetteMembers
from members.models import CagetteMember
from members.models import CagetteServices
from shifts.models import CagetteShift
from outils.common import MConfig
from datetime import datetime
default_msettings = {'msg_accueil': {'title': 'Message borne accueil',
'type': 'textarea',
......@@ -311,11 +314,55 @@ def admin(request):
'module': 'Membres'}
return HttpResponse(template.render(context, request))
def manage_makeups(request):
""" Administration des membres """
template = loader.get_template('members/admin/manage_makeups.html')
context = {'title': 'BDM - Rattrapages',
'module': 'Membres'}
return HttpResponse(template.render(context, request))
def manage_shift_registrations(request):
""" Administration des services des membres """
template = loader.get_template('members/admin/manage_shift_registrations.html')
context = {'title': 'BDM - Services',
'module': 'Membres'}
return HttpResponse(template.render(context, request))
def manage_attached(request):
""" Administration des binômes membres """
template = loader.get_template('members/admin/manage_attached.html')
context = {'title': 'BDM - Binômes',
'module': 'Membres'}
return HttpResponse(template.render(context, request))
def manage_regular_shifts(request):
""" Administration des créneaux des membres """
template = loader.get_template('members/admin/manage_regular_shifts.html')
committees_shift_id = CagetteServices.get_committees_shift_id()
context = {
'title': 'BDM - Créneaux',
'module': 'Membres',
'couchdb_server': settings.COUCHDB['url'],
'db': settings.COUCHDB['dbs']['member'],
'max_begin_hour': settings.MAX_BEGIN_HOUR,
'mag_place_string': settings.MAG_NAME,
'open_on_sunday': getattr(settings, 'OPEN_ON_SUNDAY', False),
'show_ftop_button': getattr(settings, 'BDM_SHOW_FTOP_BUTTON', True),
'has_committe_shift': committees_shift_id is not None,
'ASSOCIATE_MEMBER_SHIFT' : getattr(settings, 'ASSOCIATE_MEMBER_SHIFT', '')
}
return HttpResponse(template.render(context, request))
def get_makeups_members(request):
""" Récupération des membres qui doivent faire des rattrapages """
res = CagetteMembers.get_makeups_members()
return JsonResponse({ 'res' : res })
def get_attached_members(request):
""" Récupération des membres en binôme """
res = CagetteMembers.get_attached_members()
return JsonResponse({ 'res' : res })
def update_members_makeups(request):
""" Met à jour les rattrapages des membres passés dans la requête """
res = {}
......@@ -328,7 +375,7 @@ def update_members_makeups(request):
cm = CagetteMember(int(member_data["member_id"]))
res["res"].append(cm.update_member_makeups(member_data))
# Update member standard points, for standard members only
if member_data["member_shift_type"] == "standard":
# Set points to minus the number of makeups to do (limited to -2)
......@@ -363,3 +410,421 @@ def update_members_makeups(request):
res["message"] = "Unauthorized"
response = JsonResponse(res, status=403)
return response
# --- Gestion des créneaux
def delete_shift_registration(request):
""" From BDM admin, delete (cancel) a member shift registration """
res = {}
is_connected_user = CagetteUser.are_credentials_ok(request)
if is_connected_user is True:
data = json.loads(request.body.decode())
member_id = int(data["member_id"])
shift_registration_id = int(data["shift_registration_id"])
shift_is_makeup = data["shift_is_makeup"]
# Note: 'upcoming_registration_count' in res.partner won't change because the _compute method
# in odoo counts canceled shift registrations.
m = CagetteShift()
res["cancel_shift"] = m.cancel_shift([shift_registration_id], origin='bdm')
if shift_is_makeup is True:
fields = {
'name': "Admin BDM - Suppression d'un rattrapage",
'shift_id': False,
'type': data["member_shift_type"],
'partner_id': member_id,
'point_qty': 1
}
res["update_counter"] = m.update_counter_event(fields)
response = JsonResponse(res, safe=False)
else:
res["message"] = "Unauthorized"
response = JsonResponse(res, status=403)
return response
def delete_shift_template_registration(request):
""" From BDM admin, delete a member shift template registration """
res = {}
is_connected_user = CagetteUser.are_credentials_ok(request)
if is_connected_user is True:
try:
data = json.loads(request.body.decode())
partner_id = int(data["partner_id"])
shift_template_id = int(data["shift_template_id"])
makeups_to_do = int(data["makeups_to_do"])
permanent_unsuscribe = data["permanent_unsuscribe"]
cm = CagetteMember(partner_id)
# Get partner nb of future makeup shifts
partner_makeups = cm.get_member_selected_makeups()
target_makeup = makeups_to_do + len(partner_makeups)
if target_makeup > 2:
target_makeup = 2
# Update partner makeups to do
res["update_makeups"] = cm.update_member_makeups({'target_makeups_nb': target_makeup})
# Delete all shift registrations & shift template registration
res["unsubscribe_member"] = cm.unsubscribe_member()
if permanent_unsuscribe is True:
res["set_done"] = cm.set_cooperative_state("gone")
except Exception as e:
res["error"] = str(e)
response = JsonResponse(res, safe=False)
else:
res["message"] = "Unauthorized"
response = JsonResponse(res, status=403)
return response
def shift_subscription(request):
"""
Register a member to a shift template.
If the member was already subscribed to a shift template, unsubscribe him.her first
and delete all existing shifts EXCEPT makeups.
"""
res = {}
if CagetteUser.are_credentials_ok(request):
data = json.loads(request.body.decode())
partner_id = int(data["partner_id"])
shift_type = data["shift_type"]
if shift_type == 1:
# 1 = standard
shift_template_id = data["shift_template_id"]
else:
# 2 = ftop
# First try to get committees shift
shift_template_id = CagetteServices.get_committees_shift_id()
# If None, no committees shift, get the first ftop shift
if shift_template_id is None:
shift_template_id = CagetteServices.get_first_ftop_shift_id()
m = CagetteMember(partner_id)
unsubscribe_first = data["unsubscribe_first"]
if unsubscribe_first is True:
# If the member is registered to a shift on the shift template, registering to this shift template will fail.
has_makeups_in_new_shift = m.is_member_registered_to_makeup_shift_template(shift_template_id)
if has_makeups_in_new_shift is True:
return JsonResponse(
{
"message": "A makeup is registered on this shift template",
"code": "makeup_found"
},
status=409
)
res["unsubscribe_member"] = m.unsubscribe_member(changing_shift = True)
m.create_coop_shift_subscription(shift_template_id, shift_type)
# Return necessary data
api = OdooAPI()
c = [['id', '=', shift_template_id]]
f = ['id', 'name']
res["shift_template"] = api.search_read('shift.template', c, f)[0]
c = [['id', '=', partner_id]]
f = ['cooperative_state']
res["cooperative_state"] = api.search_read('res.partner', c, f)[0]['cooperative_state']
response = JsonResponse(res)
else:
response = JsonResponse({"message": "Unauthorized"}, status=403)
return response
# --- Gestion des binômes
def get_member_info(request, id):
"""Retrieve information about a member."""
res = {}
is_connected_user = CagetteUser.are_credentials_ok(request)
if is_connected_user:
api = OdooAPI()
fields = [
'id',
'name',
'sex',
'cooperative_state',
'email',
'street',
'street2',
'zip',
'city',
'current_template_name',
'shift_type',
'parent_id',
'is_associated_people',
'parent_name',
"makeups_to_do",
"barcode_base"
]
cond = [['id', '=', id]]
member = api.search_read('res.partner', cond, fields)
if member:
member = member[0]
parent = None
if member['parent_id']:
res_parent = api.search_read('res.partner', [['id', '=', int(member['parent_id'][0])]], ['barcode_base', 'email'])
if res_parent:
parent = res_parent[0]
member['parent_barcode_base'] = parent['barcode_base']
member['parent_email'] = parent['email']
res['member'] = member
response = JsonResponse(res)
else:
response = JsonResponse({"message": "Not found"}, status=404)
else:
res['message'] = "Unauthorized"
response = JsonResponse(res, status=403)
return response
def create_pair(request):
"""Create pair
payload example:
{
"parent": {"id": 3075},
"child": {"id": 3067}
}
"""
if request.method == 'GET':
template = loader.get_template('members/admin/manage_attached_create_pair.html')
context = {'title': 'BDM - Binômes',
'module': 'Membres'}
return HttpResponse(template.render(context, request))
if request.method == 'POST':
if CagetteUser.are_credentials_ok(request):
api = OdooAPI()
data = json.loads(request.body.decode())
parent_id = data['parent']['id']
child_id = data['child']['id']
# create attached account for child
fields = [
"birthdate",
"city",
"commercial_partner_id",
"company_id",
"company_type",
"cooperative_state",
"barcode_rule_id",
"country_id",
"customer",
"department_id",
"email",
"employee",
"image",
"image_medium",
"image_small",
"mobile",
"name",
"phone",
"sex",
"street",
"street2",
"zip",
"nb_associated_people",
"current_template_name",
"parent_id",
"is_associated_people",
"makeups_to_do",
"final_standard_points",
"final_ftop_points",
"shift_type"
]
child = api.search_read('res.partner', [['id', '=', child_id]], fields)[0]
parent = api.search_read('res.partner', [['id', '=', parent_id]],
['commercial_partner_id',
'nb_associated_people',
'current_template_name',
'makeups_to_do',
"final_standard_points",
"final_ftop_points",
'shift_type'
'parent_id'])[0]
errors = []
if child['nb_associated_people'] > 0:
# le membre est déjà titulaire d'un binôme
errors.append("Le membre suppléant sélectionné est titulaire d'un bînome")
# le membre suppléant fait parti du commité?
if child['current_template_name'] == "Services des comités":
errors.append("Le membre suppléant séléctionné fait parti du comité")
# Verifier que le suppléant n'est pas déjà en binôme soit titulaire soit suppléant
for m in api.search_read('res.partner', [['email', '=', child['email']]]):
if m['is_associated_people']:
errors.append('Le membre suppléant est déjà en bînome')
if m['child_ids']:
errors.append("Le membre suppléant sélectionné est titulaire d'un binôme")
# le membre titulaire a déjà un/des suppléants?
if parent['nb_associated_people'] >= 1:
# On récupère le/s suppléant(s)
associated_members = api.search_read('res.partner', [['parent_id', '=', parent_id]], ['id', 'age'])
# le suppléant est un mineur?
for m in associated_members:
if m['age'] > 18:
errors.append("Le membre titulaire sélectionné a déjà un suppléant")
if errors:
return JsonResponse({"errors": errors}, status=409)
del child["id"]
for field in child.keys():
if field.endswith("_id"):
try:
child[field] = child[field][0]
except TypeError:
child[field] = False
child['is_associated_people'] = True
child['parent_id'] = parent['id']
# Following lines are useful if parent or child is unsubscribed
if not 'shift_type' in parent:
parent['shift_type'] = 'standard'
if not 'shift_type' in child:
child['shift_type'] = 'standard'
# fusion des rattrapages
child_makeups = child['makeups_to_do']
parent_makeups = parent['makeups_to_do']
child_scheduled_makeups = api.search_read('shift.registration', [['partner_id', '=', child_id],
['is_makeup', '=', True],
['state', '=', 'open'],
['date_begin', '>', datetime.now().isoformat()]])
parent_scheduled_makeups = api.search_read('shift.registration', [['partner_id', '=', parent_id],
['is_makeup', '=', True],
['state', '=', 'open'],
['date_begin', '>', datetime.now().isoformat()]])
child_makeups += len(child_scheduled_makeups)
parent_makeups += len(parent_scheduled_makeups)
if child_makeups:
# le suppléant a des rattrapages
if child_makeups + parent_makeups <=2:
# on transfert les rattrapages sur le parent
api.update("res.partner", [parent_id], {"makeups_to_do": parent['makeups_to_do'] + child['makeups_to_do']})
# On annule les rattrapages du child
api.update('res.partner', [child_id], {"makeups_to_do": 0})
for makeup in range(child_makeups):
# reset du compteur du suppléant
api.create('shift.counter.event', {"name": 'passage en binôme',
"shift_id": False,
"type": child['shift_type'],
"partner_id": child_id,
"point_qty": 1})
# on retire les points au titulaire
api.create('shift.counter.event', {"name": 'passage en binôme',
"shift_id": False,
"type": parent['shift_type'],
"partner_id": parent_id,
"point_qty": -1})
elif child_makeups + parent_makeups > 2:
# on annule les rattrapages du suppléant et on met 2 rattrapages sur le titulaire
api.update('res.partner', [parent_id], {"makeups_to_do": 2})
api.update('res.partner', [child_id], {"makeups_to_do": 0})
for makeup in range(child_makeups):
# reset du compteur du suppléant
api.create('shift.counter.event', {"name": 'passage en binôme',
"shift_id": False,
"type": child['shift_type'],
"partner_id": child_id,
"point_qty": 1})
for i in range((parent_makeups + child_makeups) - 2):
# màj du compteur du titulaire
api.create('shift.counter.event', {"name": "passage en binôme",
"shift_id": False,
"type": parent['shift_type'],
"partner_id": parent_id,
"point_qty": -1})
api.execute('res.partner', 'run_process_target_status', [])
m = CagetteMember(child_id).unsubscribe_member()
# update child base account state
api.update("res.partner", [child_id], {'cooperative_state': "associated"})
# get barcode rule id
bbcode_rule = api.search_read("barcode.rule", [['for_associated_people', "=", True]], ['id'])[0]
child['barcode_rule_id'] = bbcode_rule["id"]
child['cooperative_state'] = 'associated'
for field in ["nb_associated_people",
"current_template_name",
"makeups_to_do",
"final_standard_points",
"final_ftop_points",
"shift_type"]:
try:
del child[field]
except KeyError:
pass
attached_account = api.create('res.partner', child)
# generate_base
api.execute('res.partner', 'generate_base', [attached_account])
response = JsonResponse({"message": "Succesfuly paired members"}, status=200)
else:
response = JsonResponse({"message": "Unauthorized"}, status=403)
return response
else:
return JsonResponse({"message": "Method Not Allowed"}, status=405)
def delete_pair(request):
"""
Administration des binômes membres
Delete pair
GET:
Return template
POST:
payload example:
{
"child": {
"id": "1620"
},
"gone": [
"parent",
"child"
]
}
"""
if request.method == 'GET':
template = loader.get_template('members/admin/manage_attached_delete_pair.html')
context = {'title': 'BDM - Binômes',
'module': 'Membres'}
return HttpResponse(template.render(context, request))
elif request.method == 'POST':
if CagetteUser.are_credentials_ok(request):
api = OdooAPI()
data = json.loads(request.body.decode())
child_id = int(data['child']['id'])
child = api.search_read('res.partner', [['id', '=', child_id]], ['email', 'id', 'parent_id'])[0]
child_accounts = api.search_read('res.partner', [['email', '=', child['email']]], ['id', 'email'])
prev_child = [x['id'] for x in child_accounts if x['id'] != child_id]
parent = api.search_read('res.partner', [['id', '=', child['parent_id'][0]]], ['cooperative_state'])[0]
api.update('res.partner', [child_id], {"parent_id": False, "is_associated_people": False, "active": False, "is_former_associated_people": True})
child_update_fields = {'cooperative_state': "unsubscribed", "is_former_associated_people": True}
if 'gone' in data and 'child' in data['gone']:
child_update_fields['cooperative_state'] = "gone"
for id in prev_child:
api.update("res.partner", [id], child_update_fields)
if 'gone' in data and 'parent' in data['gone']:
api.update("res.partner", [parent['id']], {'cooperative_state': "gone", "is_former_associated_people": True})
response = JsonResponse({"message": "Succesfuly unpaired members"}, status=200)
else:
response = JsonResponse({"message": "Unauthorized"}, status=403)
return response
else:
return JsonResponse({"message": "Method Not Allowed"}, status=405)
......@@ -53,9 +53,17 @@ class Command(BaseCommand):
byTypeMapFunction = '''function(doc) {
emit(doc.type);
}'''
byTypeNotArchiveMapFunction = '''function(doc) {
if(doc.archive != true){
emit(doc.type);
}
}'''
views = {
"by_type": {
"map": byTypeMapFunction
},
"by_type_not_archive": {
"map": byTypeNotArchiveMapFunction
}
}
self.createView(dbConn, "index", views)
......
......@@ -13,6 +13,7 @@ import pytz
import locale
import re
import dateutil.parser
from datetime import date
......@@ -26,6 +27,8 @@ class CagetteMember(models.Model):
'display_ftop_points', 'display_std_points',
'is_exempted', 'cooperative_state', 'date_alert_stop']
m_short_default_fields = ['name', 'barcode_base']
def __init__(self, id):
"""Init with odoo id."""
self.id = int(id)
......@@ -84,7 +87,7 @@ class CagetteMember(models.Model):
'point_qty': pts
}
"""
try:
return self.o_api.create('shift.counter.event', data)
except Exception as e:
......@@ -93,7 +96,6 @@ class CagetteMember(models.Model):
# # # BDM
def save_partner_info(self, partner_id, fieldsDatas):
print(fieldsDatas)
return self.o_api.update('res.partner', partner_id, fieldsDatas)
......@@ -129,6 +131,7 @@ class CagetteMember(models.Model):
fp = request.POST.get('fp') # fingerprint (prevent using stolen cookies)
if login and password:
api = OdooAPI()
login = login.strip()
cond = [['email', '=', login]]
if getattr(settings, 'ALLOW_NON_MEMBER_TO_CONNECT', False) is False:
cond.append('|')
......@@ -152,7 +155,7 @@ class CagetteMember(models.Model):
if (password == d + m + y):
if coop_id is None:
coop_id = coop['id']
data['id'] = coop_id
data['id'] = coop_id
auth_token_seed = fp + coop['create_date']
data['auth_token'] = hashlib.sha256(auth_token_seed.encode('utf-8')).hexdigest()
data['token'] = hashlib.sha256(coop['create_date'].encode('utf-8')).hexdigest()
......@@ -347,6 +350,23 @@ class CagetteMember(models.Model):
return answer
@staticmethod
def is_associated(id_parent):
api = OdooAPI()
cond = [['parent_id', '=', int(id_parent)]]
fields = ['id','name','parent_id','birthdate']
res = api.search_read('res.partner', cond, fields, 10, 0, 'id DESC')
already_have_adult_associated = False
for partner in res:
birthdate = partner['birthdate']
if(birthdate):
today = date.today()
date1 = datetime.datetime.strptime(birthdate, "%Y-%m-%d")
age = today.year - date1.year - ((today.month, today.day) < (date1.month, date1.day))
if age > 17 :
already_have_adult_associated = True
return already_have_adult_associated
@staticmethod
def finalize_coop_creation(post_data):
""" Update coop data. """
res = {}
......@@ -502,13 +522,38 @@ class CagetteMember(models.Model):
m.create_capital_subscription_invoice(post_data['shares_euros'], today)
res['bc'] = m.generate_base_and_barcode(post_data)
# Create shift suscription
shift_template = json.loads(post_data['shift_template'])
shift_t_id = shift_template['data']['id']
stype = shift_template['data']['type']
res['shift'] = \
m.create_coop_shift_subscription(shift_t_id, stype)
m.add_first_point(stype)
# if the new member is associated with an already existing member
# then we put the state in "associated" and we create the "associated" member
if 'is_associated_people' in post_data and 'parent_id' in post_data :
fields = {}
fields['cooperative_state'] = 'associated'
api.update('res.partner', [partner_id], fields)
associated_member = {
'email': post_data['_id'],
'name': name,
'birthdate': birthdate,
'sex': sex,
'street': post_data['address'],
'zip': post_data['zip'],
'city': post_data['city'],
'phone': format_phone_number(post_data['mobile']), # Because list view default show Phone and people mainly gives mobile
'barcode_rule_id': settings.ASSOCIATE_BARCODE_RULE_ID,
'parent_id' : post_data['parent_id'],
'is_associated_people': True
}
associated_member_id = api.create('res.partner', associated_member)
am = CagetteMember(associated_member_id)
res['bca'] = am.generate_base_and_barcode(post_data)
# If it's an new associated member with a new partner. Link will be made by the user in BDM/admin
# We add the associated member to the "associate" shift template so we can find them in Odoo
elif 'is_associated_people' not in post_data or 'is_associated_people' in post_data and 'parent_id' not in post_data:
# Create shift suscription if is not associated
shift_template = json.loads(post_data['shift_template'])
shift_t_id = shift_template['data']['id']
stype = shift_template['data']['type']
res['shift'] = \
m.create_coop_shift_subscription(shift_t_id, stype)
# m.add_first_point(stype) # Not needed anymore
# Update couchdb do with new data
try:
......@@ -718,7 +763,7 @@ class CagetteMember(models.Model):
return m_list
@staticmethod
def search(k_type, key, shift_id=None):
def search(k_type, key, shift_id=None, search_type="full"):
"""Search member according 3 types of key."""
api = OdooAPI()
if k_type == 'id':
......@@ -731,45 +776,78 @@ class CagetteMember(models.Model):
cond = [['name', 'ilike', str(key)]]
cond.append('|')
cond.append(['is_member', '=', True])
cond.append(['is_associated_people', '=', True])
if search_type != 'members':
cond.append(['is_associated_people', '=', True])
else:
cond.append(['is_associated_people', '=', False])
# cond.append(['cooperative_state', '!=', 'unsubscribed'])
fields = CagetteMember.m_default_fields
if not shift_id is None:
CagetteMember.m_default_fields.append('tmpl_reg_line_ids')
res = api.search_read('res.partner', cond, fields)
members = []
if len(res) > 0:
for m in res:
keep_it = False
if not shift_id is None and len(shift_id) > 0:
# Only member registred to shift_id will be returned
cond = [['id', '=', m['tmpl_reg_line_ids'][0]]]
fields = ['shift_template_id']
shift_templ_res = api.search_read('shift.template.registration.line', cond, fields)
if (len(shift_templ_res) > 0
and
int(shift_templ_res[0]['shift_template_id'][0]) == int(shift_id)):
if search_type == "full" or search_type == 'members':
fields = CagetteMember.m_default_fields
if not shift_id is None:
CagetteMember.m_default_fields.append('tmpl_reg_line_ids')
res = api.search_read('res.partner', cond, fields)
members = []
if len(res) > 0:
for m in res:
keep_it = False
if not shift_id is None and len(shift_id) > 0:
# Only member registred to shift_id will be returned
if len(m['tmpl_reg_line_ids']) > 0:
cond = [['id', '=', m['tmpl_reg_line_ids'][0]]]
fields = ['shift_template_id']
shift_templ_res = api.search_read('shift.template.registration.line', cond, fields)
if (len(shift_templ_res) > 0
and
int(shift_templ_res[0]['shift_template_id'][0]) == int(shift_id)):
keep_it = True
else:
keep_it = True
else:
keep_it = True
if keep_it is True:
try:
img_code = base64.b64decode(m['image_medium'])
extension = imghdr.what('', img_code)
m['image_extension'] = extension
except Exception as e:
coop_logger.info("Img error : %s", e)
m['state'] = m['cooperative_state']
m['cooperative_state'] = \
CagetteMember.get_state_fr(m['cooperative_state'])
# member = CagetteMember(m['id'], m['email'])
# m['next_shifts'] = member.get_next_shift()
if not m['parent_name'] is False:
m['name'] += ' / ' + m['parent_name']
del m['parent_name']
members.append(m)
return CagetteMember.add_next_shifts_to_members(members)
if keep_it is True:
try:
img_code = base64.b64decode(m['image_medium'])
extension = imghdr.what('', img_code)
m['image_extension'] = extension
except Exception as e:
coop_logger.info("Img error : %s", e)
m['state'] = m['cooperative_state']
m['cooperative_state'] = \
CagetteMember.get_state_fr(m['cooperative_state'])
# member = CagetteMember(m['id'], m['email'])
# m['next_shifts'] = member.get_next_shift()
if not m['parent_name'] is False:
m['name'] += ' (en binôme avec ' + m['parent_name'] + ')'
del m['parent_name']
members.append(m)
return CagetteMember.add_next_shifts_to_members(members)
elif search_type == "makeups_data":
fields = CagetteMember.m_short_default_fields
fields = fields + ['shift_type', 'makeups_to_do', 'display_ftop_points', 'display_std_points', 'shift_type']
return api.search_read('res.partner', cond, fields)
elif search_type == "shift_template_data":
fields = CagetteMember.m_short_default_fields
fields = fields + ['id', 'makeups_to_do', 'cooperative_state']
res = api.search_read('res.partner', cond, fields)
if res:
for partner in res:
c = [['partner_id', '=', int(partner['id'])], ['state', 'in', ('draft', 'open')]]
f = ['shift_template_id']
shift_template_reg = api.search_read('shift.template.registration', c, f)
if shift_template_reg:
partner['shift_template_id'] = shift_template_reg[0]['shift_template_id']
else:
partner['shift_template_id'] = None
return res
else:
# TODO differentiate short & subscription_data searches
fields = CagetteMember.m_short_default_fields
fields = fields + ['total_partner_owned_share','amount_subscription']
res = api.search_read('res.partner', cond, fields)
return res
@staticmethod
def remove_data_from_CouchDB(request):
......@@ -845,7 +923,7 @@ class CagetteMember(models.Model):
def update_member_makeups(self, member_data):
api = OdooAPI()
res = {}
f = { 'makeups_to_do': int(member_data["target_makeups_nb"]) }
res_item = api.update('res.partner', [self.id], f)
res = {
......@@ -855,6 +933,92 @@ class CagetteMember(models.Model):
return res
def get_member_selected_makeups(self):
res = {}
c = [["partner_id", "=", self.id], ["is_makeup", "=", True], ["state", "=", "open"]]
f=['id']
res = self.o_api.search_read("shift.registration", c, f)
return res
def is_member_registered_to_makeup_shift_template(self, shift_template_id):
""" Given a shift template, check if the member is registered to a makeup on this shift template """
try:
c = [["partner_id", "=", self.id], ["is_makeup", "=", True], ["state", "=", "open"]]
f=['shift_id']
res_shift_ids = self.o_api.search_read("shift.registration", c, f)
if res_shift_ids:
shift_ids = [int(d['shift_id'][0]) for d in res_shift_ids]
c = [["id", "in", shift_ids]]
f=['shift_template_id']
stis = self.o_api.search_read("shift.shift", c, f)
for sti in stis:
if sti['shift_template_id'][0] == shift_template_id:
return True
except Exception as e:
print(str(e))
return False
def unsubscribe_member(self, changing_shift = False):
""" If changing_shift, don't delete makeups registrations & don't close extension """
res = {}
now = datetime.datetime.now().isoformat()
# Get and then delete shift template registration
c = [['partner_id', '=', self.id]]
f = ['id']
res_ids = self.o_api.search_read("shift.template.registration", c, f)
ids = [d['id'] for d in res_ids]
if ids:
res["delete_shift_template_reg"] = self.o_api.execute('shift.template.registration', 'unlink', ids)
# Get and then delete shift registrations
c = [['partner_id', '=', self.id], ['date_begin', '>', now]]
if changing_shift is True:
c.append(['is_makeup', '!=', True])
f = ['id']
res_ids = self.o_api.search_read("shift.registration", c, f)
ids = [d['id'] for d in res_ids]
if ids:
res["delete_shifts_reg"] = self.o_api.execute('shift.registration', 'unlink', ids)
if changing_shift is False:
# Close extensions if just unsubscribing, else keep it
c = [['partner_id', '=', self.id], ['date_start', '<=', now], ['date_stop', '>=', now]]
f = ['id']
res_ids = self.o_api.search_read("shift.extension", c, f)
ids = [d['id'] for d in res_ids]
if ids:
f = {'date_stop': now}
res["close_extensions"] = self.o_api.update('shift.extension', ids, f)
return res
def set_cooperative_state(self, state):
f = {'cooperative_state': state}
return self.o_api.update('res.partner', [self.id], f)
def update_extra_shift_done(self, value):
api = OdooAPI()
res = {}
f = { 'extra_shift_done': value }
res_item = api.update('res.partner', [self.id], f)
res = {
'mid': self.id,
'update': res_item
}
return res
class CagetteMembers(models.Model):
"""Class to manage operations on all members or part of them."""
......@@ -1080,7 +1244,15 @@ class CagetteMembers(models.Model):
def get_makeups_members():
api = OdooAPI()
cond = [['makeups_to_do','>', 0]]
fields = ['id', 'name', 'makeups_to_do','shift_type']
fields = ['id', 'name', 'display_std_points', 'display_ftop_points', 'shift_type', 'makeups_to_do']
res = api.search_read('res.partner', cond, fields)
return res
@staticmethod
def get_attached_members():
api = OdooAPI()
cond = [['is_associated_people','=', True]]
fields = ['id', 'name', 'parent_name']
res = api.search_read('res.partner', cond, fields)
return res
......@@ -1189,7 +1361,7 @@ class CagetteServices(models.Model):
).total_seconds() / 60 > default_acceptable_minutes_after_shift_begins
if with_members is True:
cond = [['id', 'in', s['registration_ids']], ['state', 'not in', ['cancel', 'waiting', 'draft']]]
fields = ['partner_id', 'shift_type', 'state', 'is_late']
fields = ['partner_id', 'shift_type', 'state', 'is_late', 'associate_registered']
members = api.search_read('shift.registration', cond, fields)
s['members'] = sorted(members, key=lambda x: x['partner_id'][0])
if len(s['members']) > 0:
......@@ -1198,22 +1370,27 @@ class CagetteServices(models.Model):
for m in s['members']:
mids.append(m['partner_id'][0])
cond = [['parent_id', 'in', mids]]
fields = ['parent_id', 'name']
fields = ['id', 'parent_id', 'name','barcode_base']
associated = api.search_read('res.partner', cond, fields)
if len(associated) > 0:
for m in s['members']:
for a in associated:
if int(a['parent_id'][0]) == int(m['partner_id'][0]):
m['partner_id'][1] += ' / ' + a['name']
m['partner_name'] = m['partner_id'][1]
m['partner_id'][1] += ' en binôme avec ' + a['name']
m['associate_name'] = str(a['barcode_base']) + ' - ' + a['name']
return services
@staticmethod
def registration_done(registration_id, overrided_date=""):
def registration_done(registration_id, overrided_date="", typeAction=""):
"""Equivalent to click present in presence form."""
api = OdooAPI()
f = {'state': 'done'}
if(typeAction != "normal" and typeAction != ""):
f['associate_registered'] = typeAction
late_mode = getattr(settings, 'ENTRANCE_WITH_LATE_MODE', False)
if late_mode is True:
# services = CagetteServices.get_services_at_time('14:28',0, with_members=False)
......@@ -1235,7 +1412,14 @@ class CagetteServices(models.Model):
return api.update('shift.registration', [int(registration_id)], f)
@staticmethod
def record_rattrapage(mid, sid, stid):
def reopen_registration(registration_id, overrided_date=""):
api = OdooAPI()
f = {'state': 'open'}
return api.update('shift.registration', [int(registration_id)], f)
@staticmethod
def record_rattrapage(mid, sid, stid, typeAction):
"""Add a shift registration for member mid.
(shift sid, shift ticket stid)
......@@ -1251,6 +1435,8 @@ class CagetteServices(models.Model):
"state": 'open'}
reg_id = api.create('shift.registration', fields)
f = {'state': 'done'}
if(typeAction != "normal" and typeAction != ""):
f['associate_registered'] = typeAction
return api.update('shift.registration', [int(reg_id)], f)
@staticmethod
......@@ -1266,6 +1452,20 @@ class CagetteServices(models.Model):
# let authorized people time to set presence for those who came in late
end_date = now - datetime.timedelta(hours=2)
api = OdooAPI()
# Let's start by adding an extra shift to associated member who came together
cond = [['date_begin', '>=', date_24h_before.isoformat()],
['date_begin', '<=', end_date.isoformat()],
['state', '=', 'done'], ['associate_registered', '=', 'both']]
fields = ['state', 'partner_id', 'date_begin']
res = api.search_read('shift.registration', cond, fields)
for r in res:
cond = [['id', '=', r['partner_id'][0]]]
fields = ['id','extra_shift_done']
res = api.search_read('res.partner', cond, fields)
f = {'extra_shift_done': res[0]['extra_shift_done'] + 1 }
api.update('res.partner', [r['partner_id'][0]], f)
absence_status = 'excused'
res_c = api.search_read('ir.config_parameter',
[['key', '=', 'lacagette_membership.absence_status']],
......@@ -1364,6 +1564,29 @@ class CagetteServices(models.Model):
return shift_id
@staticmethod
def get_first_ftop_shift_id():
shift_id = None
try:
api = OdooAPI()
res = api.search_read('shift.template',
[['shift_type_id','=', 2]],
['id', 'registration_qty'])
# Get the ftop shift template with the max registrations: most likely the one in use
ftop_shift = {'id': None, 'registration_qty': 0}
for shift_reg in res:
if shift_reg["registration_qty"] > ftop_shift["registration_qty"]:
ftop_shift = shift_reg
try:
shift_id = int(ftop_shift['id'])
except:
pass
except:
pass
return shift_id
@staticmethod
def easy_validate_shift_presence(coop_id):
"""Add a presence point if the request is valid."""
res = {}
......@@ -1462,4 +1685,3 @@ class CagetteUser(models.Model):
pass
return answer
.header {
margin-top: 15px;
}
.management_type_buttons {
margin-top: 60px;
}
.management_type_button {
height: 2.2em;
width: 30%;
border-radius: 3px;
margin: 10px;
font-size: 1.3em;
}
.management_type_button_icons {
float: right;
margin: 2px;
}
.login_area {
position: absolute;
top: 5px;
right: 5px;
}
.header {
margin: 1rem 0;
}
.login_area {
position: absolute;
display: block;
top: 5px;
right: 5px;
}
#back_to_admin_index {
position: absolute;
top: 5px;
left: 5px;
}
/* Buttons */
.management_type_buttons {
margin-top: 60px;
display: flex;
justify-content: center;
}
.management_type_button {
height: 2.2em;
width: 30%;
border-radius: 3px;
margin: 10px;
font-size: 1.3em;
}
.create_pair_button {
display: flex;
justify-content: center;
margin: 120px 0 120px 0;
}
@media screen and (max-width: 992px) {
.create_pair_button {
margin: auto;
}
}
#createPair {
border-radius: 30px;
}
/* Search membres area */
.search_member_form_area {
align-items: center;
padding-bottom: 20px;
}
.search_member_form {
margin-left: 10px;
}
/* Member infos */
.tile_icon {
margin-right: 15px;
color: #00a573;
}
.member_info {
font-weight: bold;
}
.member_status_text_container {
margin: 1rem;
}
/* Attached members table */
.table_area {
margin-top: 20px;
}
#table_top_area {
display: flex;
justify-content: space-between;
}
/* -- Tiles */
.tiles_container {
display: flex;
flex-wrap: wrap;
}
@media screen and (max-width: 992px) {
.tiles_container {
flex-direction: column;
}
}
.tile {
flex: 1 0 20%;
display: flex;
flex-direction: column;
text-align: center;
border-radius: 30px;
margin: 1rem 1rem;
padding: 1rem;
box-shadow: 2px 2px 3px rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.1);
}
.tile_content {
height: 100%;
flex-direction: column;
text-align: center;
font-size: 1.6rem;
}
.spinner {
height:40px;
}
/* Modale */
.attached-members .member div {width: 50px; display: inline-block; margin: 1rem 0;}
.attached-members .member div.name {width: 100%; position: relative;}
.attached-members .member div.select_after_unattached_state {
position: absolute;
right:5rem;
top:0;
bottom:0;
margin: 0;
display:flex;
justify-content: center ;
align-items: center;
}
.attached-members .member .after_unattached_state {
cursor: pointer;
}
\ No newline at end of file
.page_body{
position: relative;
}
.header {
margin: 1.5rem 0;
margin: 1rem 0;
}
.login_area {
position: absolute;
top: 0;
left: 0;
right: 0;
display: block;
top: 5px;
right: 5px;
}
.tabs {
margin-top: 1em;
margin-bottom: 1em;
overflow: hidden;
}
.tabs .tab {
background-color: #f1f1f1;
border: 1px solid #ccc;
outline: none;
cursor: pointer;
padding: 14px 16px;
transition: 0.3s;
}
.tabs .tab:hover {
background-color: #ccc;
}
.tabs .active {
background-color: transparent;
border: 1px solid #ccc;
border-width: 1px 0 0 0;
}
.tabs .active:hover {
background-color: white;
}
.tab_content {
animation: fadeEffect 1s; /* Fading effect takes 1 second */
}
/* Go from zero to full opacity */
@keyframes fadeEffect {
from {opacity: 0;}
to {opacity: 1;}
}
#tab_makeups_content {
padding: 2rem 0;
#back_to_admin_index {
position: absolute;
top: 5px;
left: 5px;
}
#table_top_area {
......
.header {
margin: 1rem 0;
}
.login_area {
position: absolute;
display: block;
top: 5px;
right: 5px;
}
#back_to_admin_index {
position: absolute;
top: 5px;
left: 5px;
}
/* Search members area */
#search_member_area {
margin-top: 30px;
display: flex;
flex-direction: column;
align-items: center;
}
#search_member_form_area {
display:flex;
align-items: center;
}
#search_member_form {
margin-left: 10px;
}
.search_member_results_area {
margin-top: 15px;
display: flex;
align-items: center;
}
.search_results_text {
min-width: 150px;
}
.search_member_results {
display: flex;
flex-wrap: wrap;
}
.btn_possible_member {
margin: 0.5rem 1rem;
}
/* Member info area */
#partner_data_area {
width: 80%;
display: none;
flex-direction: column;
align-items: center;
border-radius: 30px;
margin: 30px auto;
padding: 15px;
box-shadow: 2px 2px 3px rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.1);
}
.member_name_container {
margin: 1rem 0 2rem 0;
}
.member_name_icon {
color: #00a573;
margin-right: 5px;
}
.member_info {
font-weight: bold;
}
/* Actions */
#remove_shift_template_button,
#change_shift_template_button,
#subscribe_to_shift_template_button {
display: none;
margin: 15px;
}
.checkbox_area {
margin: 10px 0;
}
#permanent_unsuscribe {
margin-right: 5px;
}
.error_modal_title {
color:#d9534f;
font-weight: bold;
}
/* Calendar */
#shifts_calendar_area {
display: none;
margin-top: 15px;
}
.shift[data-place="Cleme"], [data-select="Cleme"] {background: #c8deff;}
#subs_cap {width: 200px;}
.lat_menu button {margin-bottom:5px;}
.oddeven_selector {margin-right:25px;}
.main_content .shift {float:left;}
.main_content .shift.full {display:none;}
.shift {margin:2px; padding:2px; cursor: cell;}
.shift.alert,span.alert {border-bottom: 3px #e52121 solid;}
.highlighted {box-shadow: 10px 10px 5px grey;}
.lat_menu.highlighted {border: 2px #fffc07 dotted;}
#coop_list_view td.coop:hover,
#coop_list_view td.c_shift:hover {background:#fffc07; cursor:pointer;}
.b_red {color:#ffffff;}
.shift[data-type="compact"] {border: 1px solid #000000;border-radius: 5px; padding:5px; cursor: pointer;}
.shift_template, .next_shift {font-weight: bold;}
\ No newline at end of file
.header {
margin: 1rem 0;
}
.login_area {
position: absolute;
display: block;
top: 5px;
right: 5px;
}
#back_to_admin_index {
position: absolute;
top: 5px;
left: 5px;
}
#table_top_area {
display: none;
width: 90%;
margin: 35px auto 0 auto;
text-align: center;
}
.table_area {
margin: 0 auto;
width: 90%;
display: flex;
justify-content: center;
}
.delete_shift_registration {
color: #d9534f;
cursor: pointer;
}
#member_shifts_table_filter {
padding-top: 0.755em;
}
/* Search members area */
.makeup_row {
background-color: #ffc854 !important;
}
#search_member_area {
margin-top: 30px;
display: flex;
flex-direction: column;
align-items: center;
}
#search_member_form_area {
display:flex;
align-items: center;
}
#search_member_form {
margin-left: 10px;
}
.search_member_results_area {
margin-top: 15px;
display: flex;
align-items: center;
}
.search_results_text {
min-width: 150px;
}
.search_member_results {
display: flex;
flex-wrap: wrap;
}
.btn_possible_member {
margin: 0.5rem 1rem;
}
\ No newline at end of file
.nav .fr {margin-left:10px;}
.nav .fl {margin-right:10px;}
.nav #process_state_container{
min-height: 36px;
}
.nav #process_state{
min-height: 36px;
display: flex;
align-items: center;
gap: 3px;
}
.nav #goto_prepa_odoo_button,
.nav #goto_prepa_odoo_button:hover {
text-decoration: none;
}
#shift_choice > div, #new_coop, #coop_list_view,#coop_registration_details {display:none;}
......@@ -23,6 +38,71 @@
[data-week="4"] {border: 2px #eed000 solid;}
#new_coop [name="email"] {width:25em;}
#new_coop{
max-width: 75%;
}
#mail_generation {position:absolute; bottom:30px;}
#sex {padding: 0;}
#existing_partner{
padding-right: 15px;
width: 45%;
}
#new_partner{
padding-left: 15px;
width: 45%;
}
#add_binome{
cursor: pointer;
text-decoration: underline;
font-weight: bold;
}
#existing_member_choice, #new_member_choice{
border: 1px solid black;
width: 40%;
cursor: pointer;
text-align: center;
max-width: 400px;
background-color: #e7e9ed;
color: black;
padding: 1rem 1.5rem;
}
#existing_member_choice:hover, #new_member_choice:hover{
background-color: #ccc;
}
.choice_active{
background-color: #0275d8 !important;
color: white !important;
}
#existing_member_choice{
margin-right: 15px;
}
.choice_button_area{
margin-bottom: 10px;
}
#associate_area{
margin-bottom: 15px;
}
.remove_binome_icon {
margin-left: 10px;
color: #e62720;
}
.remove_binome_icon:hover {
cursor: pointer;
}
.chosen_associate_group {
display: flex;
align-items: center;
}
\ No newline at end of file
......@@ -44,8 +44,10 @@ h1 .member_name {font-weight: bold;}
.members_list {list-style: none;}
.members_list li {display:block;margin-bottom:5px;}
.members_list li.btn--inverse {background: #449d44; cursor:not-allowed; color: #FFF; }
.members_list li.btn--inverse {background: #449d44; color: #FFF; }
.members_list li.btn--inverse.not_connected {cursor:not-allowed}
.members_list li.btn--inverse.late {background-color: #de9b00; color: white}
.members_list li.btn--inverse.both {background-color: #0275d8 ; color: white}
#service_entry_success {font-size: x-large;}
#service_entry_success .explanations {margin: 25px 0; font-size: 18px;}
......@@ -64,7 +66,7 @@ h1 .member_name {font-weight: bold;}
#service_en_cours .info a {line-height: 24px; font-size:14px; margin-top:15px;}
.outside_list a {margin-left:15px;}
#rattrapage_1 .advice {margin-top:15px;}
.btn.present {background:#50C878;}
.btn.present {background:#50C878; color: white !important; font-weight: bold;}
.btn.return {background: #ff3333; color:#FFF !important; margin-top:25px;}
.msg-big {font-size: xx-large; background: #fff; padding:25px; text-align: center;}
......@@ -72,3 +74,6 @@ h1 .member_name {font-weight: bold;}
#member_advice {background: #FFF; color: red;}
.easy_shift_validate {text-align: center; margin-top: 3em;}
.button_is_member {background-color: #439D44; color: #fff;}
.button_is_associated_people {background: #0275D8; color: #fff;}
$(document).ready(function() {
if (coop_is_connected()) {
$.ajaxSetup({ headers: { "X-CSRFToken": getCookie('csrftoken') } });
$(".page_content").show();
let location = window.location.href.replace(/\/$/, '');
$('.management_type_button').on('click', function() {
if (this.id == 'manage_makeups_button') {
window.location.assign(location + "/manage_makeups");
} else if (this.id == 'manage_shift_registrations_button') {
window.location.assign(location + "/manage_shift_registrations");
} else if (this.id == 'manage_attached_button') {
window.location.assign(location + "/manage_attached");
} else if (this.id == 'manage_attached_delete_pair_button') {
window.location.assign(location + "/delete_pair");
} else if (this.id == 'manage_attached_create_pair_button') {
window.location.assign(location + "/create_pair");
} else if (this.id == 'manage_leaves_button') {
console.log('coming soon...');
} else if (this.id == 'manage_regular_shifts_button') {
window.location.assign(location + "/manage_regular_shifts");
}
});
} else {
$(".page_content").hide();
}
});
\ No newline at end of file
var parentId = null;
var childId = null;
var parentName = null;
var childName = null;
var parentEmail = null;
var childEmail = null;
const possible_cooperative_state = {
suspended: "Rattrapage",
exempted: "Exempté.e",
alert: "En alerte",
up_to_date: "À jour",
unsubscribed: "Désinscrit.e des créneaux",
delay: "En délai",
gone: "Parti.e",
associated: "En binôme"
};
/**
* Load member infos
*/
function load_member_infos(divId, memberId) {
$.ajax({
type: 'GET',
url: "/members/get_member_info/" + memberId,
dataType:"json",
traditional: true,
contentType: "application/json; charset=utf-8",
success: function(data) {
if (divId === 'parentInfo') {
parentId = data.member.id;
parentName = data.member.barcode_base + ' ' + data.member.name;
} else if (divId === 'childInfo') {
childId = data.member.id;
childName = data.member.barcode_base + ' ' + data.member.name;
}
display_member_infos(divId, data.member);
},
error: function(data) {
err = {msg: "erreur serveur lors de la récupération des infos du membre", ctx: 'load_member_infos'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'members.admin');
closeModal();
alert('Erreur lors de la récupération des infos du membre.');
}
});
}
/**
* Display member info
*/
function display_member_infos(divId, memberData) {
$("#" + divId).show();
$("#" + divId).find(".member_name")
.text(memberData.name);
$("#" + divId).find(".member_email")
.text(memberData.email);
$("#" + divId).find(".member_status")
.text(possible_cooperative_state[memberData.cooperative_state]);
$("#" + divId).find(".member_makeups_to_do")
.text(memberData.makeups_to_do);
let member_shift_name = memberData.current_template_name === false ? 'X' : memberData.current_template_name;
$("#" + divId).find(".member_shift_name")
.text(member_shift_name);
if (memberData.is_associated_people === false) {
$("#" + divId).find(".member_associated_partner_area")
.hide();
}
if (parentId != null && childId != null) {
$("#createPair").prop("disabled", false);
$("#createPair").addClass("btn--primary");
}
}
/**
* Load attached members
*/
function load_attached_members() {
openModal();
$.ajax({
type: 'GET',
url: "/members/get_attached_members",
dataType:"json",
traditional: true,
contentType: "application/json; charset=utf-8",
success: function(data) {
attached_members = data.res;
display_attached_members();
closeModal();
},
error: function(data) {
err = {msg: "erreur serveur lors de la récupération des membres en binôme", ctx: 'load_makeups_members'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'members');
closeModal();
alert('Erreur serveur lors de la récupération des membres en binôme. Ré-essayez plus tard.');
}
});
}
/**
* Display table of attached members
*/
function display_attached_members() {
// if (attached_members_table) {
// $('#attached_members_table').off();
// attached_members_table.clear().destroy();
// $('#attached_members_table').empty();
// }
attached_members_table = $('#attached_members_table').DataTable({
data: attached_members,
columns: [
{
data: "id",
title: '',
className: "dt-body-center",
orderable: false,
render: function (data) {
return `<input type="checkbox" class="select_member_cb" id="select_member_${data}" value="${data}">`;
},
width: "3%"
},
{
data: "parent_name",
title: "Nom du titulaire"
},
{
data: "name",
title: "en binôme avec"
},
{
data: "action",
title: "Action",
width: "10%",
render: function (data, type, full) {
return `
<button class="delete_pair btn--danger" id="delete_pair_${full.id}">
Désolidariser
</button>`;
}
}
],
aLengthMenu: [
[
25,
50,
-1
],
[
25,
50,
"Tout"
]
],
iDisplayLength: 25,
oLanguage: {
"sProcessing": "Traitement en cours...",
"sSearch": "Rechercher dans le tableau",
"sLengthMenu": "Afficher _MENU_ &eacute;l&eacute;ments",
"sInfo": "Affichage de l'&eacute;l&eacute;ment _START_ &agrave; _END_ sur _TOTAL_ &eacute;l&eacute;ments",
"sInfoEmpty": "Affichage de l'&eacute;l&eacute;ment 0 &agrave; 0 sur 0 &eacute;l&eacute;ment",
"sInfoFiltered": "(filtr&eacute; de _MAX_ &eacute;l&eacute;ments au total)",
"sInfoPostFix": "",
"sLoadingRecords": "Chargement en cours...",
"sZeroRecords": "Aucun &eacute;l&eacute;ment &agrave; afficher",
"sEmptyTable": "Aucune donn&eacute;e disponible dans le tableau",
"oPaginate": {
"sFirst": "Premier",
"sPrevious": "Pr&eacute;c&eacute;dent",
"sNext": "Suivant",
"sLast": "Dernier"
},
"oAria": {
"sSortAscending": ": activer pour trier la colonne par ordre croissant",
"sSortDescending": ": activer pour trier la colonne par ordre d&eacute;croissant"
},
"select": {
"rows": {
"_": "%d lignes séléctionnées",
"0": "Aucune ligne séléctionnée",
"1": "1 ligne séléctionnée"
}
}
}
});
}
function delete_pair(childId, gone_checked) {
var payload = {"child": {"id": childId}, "gone": []};
if (gone_checked.length > 0) {
$.each(gone_checked, function(i, e) {
const elts = $(e).attr('name')
.split("_");
payload['gone'].push(elts[0]);
});
}
$.ajax({
type: "POST",
url: '/members/admin/manage_attached/delete_pair',
dataType: 'json',
contentType: "application/json; charset=utf-8",
data: JSON.stringify(payload),
success: function() {
enqueue_message_for_next_loading("Binôme désolidarisé.");
location.reload();
},
error: function(data) {
err = {msg: "Erreur serveur lors de la désolidarisation du binôme.", ctx: 'load_makeups_members'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
closeModal();
alert('Erreur serveur lors de la désolidarisation du binôme. Ré-essayez plus tard.');
}
});
}
function confirmDeletion(childId) {
var modalContent = $('#confirmModal');
modalContent.find("#parentName").text(parentName);
modalContent.find("#childName").text(childName);
if (parentEmail != false) {
modalContent.find("#parentEmail").text(parentEmail);
}
if (childEmail != false) {
modalContent.find("#childEmail").text(childEmail);
}
modalContent = modalContent.html();
openModal(modalContent, () => {
if (is_time_to('delete_pair')) {
const gone_checked = $('input.after_unattached_state:checked');
closeModal();
openModal();
delete_pair(childId, gone_checked);
}
}, 'Valider', false);
}
function create_pair(payload) {
$.ajax({
type: 'POST',
url: "/members/admin/manage_attached/create_pair",
dataType:"json",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(payload),
success: function() {
enqueue_message_for_next_loading("Binôme créé.");
location.reload();
},
error: function(data) {
err = {msg: "erreur serveur", ctx: 'create pair'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.errors != 'undefined') {
err.msg += ' : ' + data.responseJSON.errors;
}
report_JS_error(err, 'members.admin');
closeModal();
var message = 'Erreur lors de création du binôme.';
data.responseJSON.errors.map(function(error) {
message += ('\n' + error);
return null;
});
alert(message);
}
});
}
$(document).ready(function() {
if (coop_is_connected()) {
$.ajaxSetup({ headers: { "X-CSRFToken": getCookie('csrftoken') } });
$(".page_content").show();
} else {
$(".page_content").hide();
}
$('#back_to_admin_index').on('click', function() {
let base_location = window.location.href.split("manage_attached")[0].slice(0, -1);
window.location.assign(base_location);
});
$("#search_member_input").autocomplete({source: function(request, response) {
$.ajax({
url: '/members/search/' + request.term,
dataType : 'json',
success: function(data) {
members_search_results = [];
for (member of data.res) {
if (member.is_member || member.is_associated_people) {
members_search_results.push(member);
}
}
response($.map(data.res, function(item) {
return {
label: item.barcode_base + ' ' + item.name,
value: item.id
};
}));
},
error: function() {
err = {
msg: "erreur serveur lors de la recherche de membres",
ctx: 'search_member_form.search_members'
};
report_JS_error(err, 'members.admin');
$.notify("Erreur lors de la recherche de membre, il faut ré-essayer plus tard...", {
globalPosition:"top right",
className: "error"
});
}
});
},
minLength: 1,
search: function() {
$('#spinner1').show();
},
response: function() {
$('#spinner1').hide();
},
select: function(event, ui) {
event.preventDefault();
if (ui.item) {
load_member_infos("parentInfo", ui.item.value);
$('#search_member_input').val(ui.item.label);
return false;
}
return null;
}
});
$("#search_child_input").autocomplete({source: function(request, response) {
$.ajax({
url: '/members/search/' + request.term,
dataType : 'json',
success: function(data) {
members_search_results = [];
for (member of data.res) {
if (member.is_member || member.is_associated_people) {
members_search_results.push(member);
}
}
response($.map(data.res, function(item) {
return {
label: item.barcode_base + ' ' + item.name,
value: item.id
};
}));
},
error: function() {
err = {
msg: "erreur serveur lors de la recherche de membres",
ctx: 'search_member_form.search_members'
};
report_JS_error(err, 'members.admin');
$.notify("Erreur lors de la recherche de membre, il faut ré-essayer plus tard...", {
globalPosition:"top right",
className: "error"
});
}
});
},
minLength: 1,
search: function() {
$('#spinner2').show();
},
response: function() {
$('#spinner2').hide();
},
select: function(event, ui) {
if (ui.item) {
load_member_infos("childInfo", ui.item.value);
$('#search_child_input').val(ui.item.label);
return false;
}
return null;
}
});
$("#createPair").on('click', function() {
if (parentId && childId) { // Note : after reload, button "Créer le binôme" is clickable...It shouldn't
var payload = {
"parent": {"id": parentId},
"child": {"id": childId}
};
var modalContent = $('#confirmModal');
modalContent.find("#parentName").text(parentName);
modalContent.find("#childName").text(childName);
modalContent = modalContent.html();
openModal(modalContent, () => {
if (is_time_to('create_pair')) {
closeModal();
openModal(); // Show gears
create_pair(payload);
}
}, 'Valider', false);
}
});
if ($("#attached_members_table") != "undefined") {
load_attached_members();
}
$(document).on('click', '.delete_pair', function (event) {
var childId = event.target.id.split('_').slice(-1)[0];
$.ajax({
type: 'GET',
url: "/members/get_member_info/" + childId,
dataType:"json",
traditional: true,
contentType: "application/json; charset=utf-8",
success: function(data) {
if (data.member.parent_barcode_base !== undefined) {
parentName = data.member.parent_barcode_base + ' - ' + data.member.parent_name;
} else {
parentName = data.member.parent_name;
}
parentEmail = data.member.parent_email;
childName = data.member.barcode_base + ' - ' + data.member.name;
childEmail = data.member.email;
confirmDeletion(childId);
},
error: function(data) {
err = {msg: "erreur serveur lors de la récupération des infos du membre", ctx: 'load_member_infos'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'members.admin');
closeModal();
alert('Erreur lors de la récupération des infos du membre.');
}
});
});
});
......@@ -3,33 +3,6 @@ var makeups_members_table = null,
members_search_results = [],
selected_rows = []; // Contain members id
function switch_active_tab() {
// Set tabs
$('.tab').removeClass('active');
$(this).addClass('active');
// Tabs content
$('.tab_content').hide();
let tab = $(this).attr('id');
if (tab == 'tab_makeups') {
$('#tab_makeups_content').show();
}
load_tab_data();
}
/**
* Load data for the current tab
*/
function load_tab_data() {
let current_tab = $('.tab .active').attr('id');
if (current_tab === 'tab_makeups' && makeups_members === null) {
load_makeups_members();
}
}
/**
* Load partners who have makeups to do
......@@ -96,6 +69,21 @@ function display_makeups_members() {
title: "Nom"
},
{
data: "shift_type",
title: "Nb de points",
className: "dt-body-center",
width: "10%",
render: function (data, type, row) {
if (data == 'ftop') {
return row.display_ftop_points;
} else if (data == 'standard') {
return row.display_std_points;
}
return null;
}
},
{
data: "makeups_to_do",
title: "Nb rattrapages",
className: "dt-body-center",
......@@ -238,16 +226,36 @@ function update_members_makeups(member_ids, action) {
data = [];
for (mid of member_ids) {
member_index = makeups_members.findIndex(m => m.id == mid);
/* Becareful : makeups_members will be modified while nobody knows wether ajax process will succeed or not !
TODO : make the changes only when it is sure that odoo records have been changed
*/
if (action === "increment") {
makeups_members[member_index].makeups_to_do += 1;
} else {
makeups_members[member_index].makeups_to_do -= 1;
}
if (makeups_members[member_index].shift_type === 'standard') {
if (action === "increment") {
if (makeups_members[member_index].display_std_points >= -1)
makeups_members[member_index].display_std_points -= 1;
} else if (makeups_members[member_index].display_std_points < 0) {
makeups_members[member_index].display_std_points += 1;
}
} else {
if (action === "increment") {
if (makeups_members[member_index].display_ftop_points >= -1)
makeups_members[member_index].display_ftop_points -= 1;
} else {
makeups_members[member_index].display_ftop_points += 1;
}
}
data.push({
member_id: mid,
target_makeups_nb: makeups_members[member_index].makeups_to_do,
member_shift_type: makeups_members[member_index].shift_type
member_shift_type: makeups_members[member_index].shift_type,
display_ftop_points: makeups_members[member_index].display_ftop_points,
display_std_points: makeups_members[member_index].display_std_points
});
}
......@@ -282,6 +290,7 @@ function update_members_makeups(member_ids, action) {
function display_possible_members() {
$('.search_member_results_area').show();
$('.search_member_results').empty();
$('.btn_possible_member').off();
let no_result = true;
......@@ -303,39 +312,42 @@ function display_possible_members() {
$('.search_member_results').append(member_button);
// Set action on member button click
$('.btn_possible_member').on('click', function() {
for (member of members_search_results) {
if (member.id == $(this).attr('member_id')) {
if (makeups_members === null) {
makeups_members = [];
}
makeups_members.unshift({
id: member.id,
name: member.name,
makeups_to_do: 0,
shift_type: member.shift_type
});
openModal(
`Ajouter un rattrapage à ${member.name} ?`,
() => {
update_members_makeups([member.id], "increment");
members_search_results = [];
$('#search_member_input').val('');
$('.search_member_results_area').hide();
$('.search_member_results').empty();
},
"Confirmer",
false
);
break;
}
// Set action on member button click
$('.btn_possible_member').on('click', function() {
for (member of members_search_results) {
if (member.id == $(this).attr('member_id')) {
if (makeups_members === null) {
makeups_members = [];
}
makeups_members.unshift({
id: member.id,
name: member.name,
makeups_to_do: 0,
shift_type: member.shift_type,
display_std_points: member.display_std_points,
display_ftop_points: member.display_ftop_points
});
openModal(
`Ajouter un rattrapage à ${member.name} ?`,
() => {
update_members_makeups([member.id], "increment");
members_search_results = [];
$('#search_member_input').val('');
$('.search_member_results_area').hide();
$('.search_member_results').empty();
},
"Confirmer",
false
);
break;
}
});
}
}
});
}
if (no_result === true) {
......@@ -352,34 +364,33 @@ $(document).ready(function() {
$(".page_content").show();
load_makeups_members();
$(".tabs .tab").on('click', switch_active_tab);
} else {
$(".page_content").hide();
}
$('#back_to_admin_index').on('click', function() {
let base_location = window.location.href.split("manage_makeups")[0].slice(0, -1);
window.location.assign(base_location);
});
// Set action to search for the member
$('#search_member_form').submit(function() {
let search_str = $('#search_member_input').val();
$.ajax({
url: '/members/search/' + search_str,
url: '/members/search/' + search_str +'?search_type=makeups_data',
dataType : 'json',
success: function(data) {
members_search_results = [];
for (member of data.res) {
if (member.is_member || member.is_associated_people) {
members_search_results.push(member);
}
}
members_search_results = data.res;
display_possible_members();
},
error: function() {
err = {
msg: "erreur serveur lors de la recherche de membres",
ctx: 'confirm_movement.search_members'
ctx: 'members.admin.manage_makeups.search_members'
};
report_JS_error(err, 'stock');
......
var selected_member = null,
possible_cooperative_state = {
suspended: "Rattrapage",
exempted: "Exempté.e",
alert: "En alerte",
up_to_date: "À jour",
unsubscribed: "Désinscrit.e des créneaux",
delay: "En délai",
gone: "Parti.e"
};
/**
* Send request to remove partner from shift template
*/
function remove_from_shift_template() {
let permanent_unsuscribe = modal.find("#permanent_unsuscribe").prop('checked');
openModal();
let data = {
partner_id: selected_member.id,
shift_template_id: selected_member.shift_template_id[0],
permanent_unsuscribe: permanent_unsuscribe,
makeups_to_do: selected_member.makeups_to_do
};
$.ajax({
type: 'POST',
url: '/members/delete_shift_template_registration',
data: JSON.stringify(data),
dataType:"json",
traditional: true,
contentType: "application/json; charset=utf-8",
success: function() {
selected_member.shift_template_id = null;
selected_member.cooperative_state = (permanent_unsuscribe === true) ? "gone" : "unsubscribed";
display_member_info();
closeModal();
},
error: function() {
err = {
msg: "erreur serveur lors de la suppression du membre du créneau",
ctx: 'members.admin.manage_regular_shifts.remove_from_shift_template'
};
report_JS_error(err, 'members.admin');
closeModal();
$.notify("Une erreur est survenue lors du processus de suppression du membre du créneau.", {
globalPosition:"top right",
className: "error"
});
}
});
}
/**
* Send the request to register a member to a shift template.
* Ask to unsuscribe first if the member was subscribed.
*
* @param {int} shift_type 1 === standard ; 2 === ftop
* @param {int} shift_template_id null for ftop shift type
* @param {String} shift_template_name selected shift template name
*/
function shift_subscrition(shift_type, shift_template_id = null, shift_template_name = null) {
openModal();
let data = {
partner_id: selected_member.id,
shift_type: shift_type,
shift_template_id: shift_template_id,
unsubscribe_first: selected_member.shift_template_id !== undefined && selected_member.shift_template_id !== null
};
$.ajax({
type: 'POST',
url: '/members/shift_subscription',
data: JSON.stringify(data),
dataType:"json",
traditional: true,
contentType: "application/json; charset=utf-8",
success: function(data) {
stdata = data.shift_template;
selected_member.shift_template_id = [
stdata.id,
stdata.name
];
selected_member.cooperative_state = data.cooperative_state;
display_member_info();
$("#shifts_calendar_area").hide();
closeModal();
setTimeout(() => {
$.notify("Inscription au nouveau service réussie.", {
globalPosition:"top right",
className: "success"
});
}, 200);
},
error: function(err_data) {
if (
err_data.status == 409
&& typeof (err_data.responseJSON) != "undefined"
&& err_data.responseJSON.code === "makeup_found"
) {
let modal_template = $("#modal_error_change_shift_template");
modal_template.find(".shift_template_name").text(shift_template_name);
closeModal();
openModal(
modal_template.html(),
() => {},
"Compris !",
true,
false
);
} else {
err = {
msg: "erreur serveur lors de l'inscription du membre au créneau",
ctx: 'members.admin.manage_regular_shifts.shift_subscrition'
};
report_JS_error(err, 'members.admin');
closeModal();
$.notify("Une erreur est survenue lors de l'inscription du membre au créneau.", {
globalPosition:"top right",
className: "error"
});
}
}
});
}
/**
* When a member is selected, display the selected member relevant info
*/
function display_member_info() {
$('.member_name').text(`${selected_member.barcode_base} - ${selected_member.name}`);
$('.member_status').text(possible_cooperative_state[selected_member.cooperative_state]);
$('.member_makeups').text(selected_member.makeups_to_do);
$('.search_member_results_area').hide();
$("#remove_shift_template_button").hide();
$("#remove_shift_template_button").off();
$("#change_shift_template_button").hide();
$("#change_shift_template_button").off();
$("#subscribe_to_shift_template_button").hide();
$("#subscribe_to_shift_template_button").off();
$("#shifts_calendar_area").hide();
// Member is unsuscribed
if (selected_member.shift_template_id === undefined || selected_member.shift_template_id === null) {
$('.member_shift').text("X");
$("#subscribe_to_shift_template_button").show();
$("#subscribe_to_shift_template_button").on("click", set_subscription_area);
} else {
$('.member_shift').text(selected_member.shift_template_id[1]);
$("#remove_shift_template_button").show();
$("#remove_shift_template_button").on("click", () => {
let modal_template = $("#modal_remove_shift_template");
modal_template.find(".shift_template_name").text(selected_member.shift_template_id[1]);
openModal(
modal_template.html(),
remove_from_shift_template,
"Valider",
false
);
});
$("#change_shift_template_button").show();
$("#change_shift_template_button").on("click", set_subscription_area);
}
$('#search_member_input').val();
$('#partner_data_area').css('display', 'flex');
}
/**
* Set calendar and associated listeners.
*/
function set_subscription_area() {
retrieve_and_draw_shift_tempates();
$("#shifts_calendar_area").show();
// Wait for listeners to be set in common.js
// TODO use "signals" to avoid waiting an arbitrary time
setTimeout(() => {
// Cancel listeners from subscription page & set custom listeners
$("#shifts_calendar_area button[data-select='Volant']").off("click");
$("#shifts_calendar_area button[data-select='Volant']").on("click", function() {
// Subscribe to comitee/ftop shift
msg = (has_committe_shift === "True")
? `Inscrire ${selected_member.name} au service des Comités ?`
: `Inscrire ${selected_member.name} en Volant ?`;
openModal(
msg,
() => {
shift_subscrition(2);
},
"Confirmer",
false
);
});
$(".shift").off("click");
$(".shift").on("click", function() {
// Subscribe to shift template
let shift_template_id = select_shift_among_compact(null, this, false); // method from common.js
let shift_template_data = shift_templates[shift_template_id].data;// shift_templates: var from common.js
let shift_template_name = get_shift_name(shift_template_data);
openModal(
`Inscrire ${selected_member.name} au créneau ${shift_template_name} ?`,
() => {
shift_subscrition(1, parseInt(shift_template_id), shift_template_name);
},
"Confirmer",
false
);
});
}, 1000);
}
/**
* Display the members from the search result
*/
function display_possible_members() {
$('.search_member_results_area').show();
$('.search_member_results').empty();
$('.btn_possible_member').off();
let no_result = true;
if (members_search_results.length > 0) {
for (member of members_search_results) {
$(".search_results_text").show();
no_result = false;
// Display results (possible members) as buttons
var member_button = '<button class="btn--success btn_possible_member" member_id="'
+ member.id + '">'
+ member.barcode_base + ' - ' + member.name
+ '</button>';
$('.search_member_results').append(member_button);
}
// Set action on member button click
$('.btn_possible_member').on('click', function() {
for (member of members_search_results) {
if (member.id == $(this).attr('member_id')) {
selected_member = member;
display_member_info();
$('.search_member_results').empty();
$('.search_member_results_area').hide();
$('#search_member_input').val('');
break;
}
}
});
}
if (no_result === true) {
$(".search_results_text").hide();
$('.search_member_results').html(`<p>
<i>Aucun résultat ! Vérifiez votre recherche...</i>
</p>`);
}
}
$(document).ready(function() {
if (coop_is_connected()) {
$.ajaxSetup({ headers: { "X-CSRFToken": getCookie('csrftoken') } });
dbc = new PouchDB(couchdb_dbname);
$(".page_content").show();
if (has_committe_shift === "True") {
$("#shifts_calendar_area button[data-select='Volant']").text("Comités");
}
// Set action to search for the member
$('#search_member_form').submit(function() {
let search_str = $('#search_member_input').val();
$.ajax({
url: `/members/search/${search_str}?search_type=shift_template_data`,
dataType : 'json',
success: function(data) {
$('#partner_data_area').hide();
if (data.res.length === 1) {
selected_member = data.res[0];
display_member_info();
} else {
members_search_results = data.res;
display_possible_members();
}
},
error: function() {
err = {
msg: "erreur serveur lors de la recherche de membres",
ctx: 'members.admin.manage_regular_shifts.search_members'
};
report_JS_error(err, 'members.admin');
$.notify("Erreur lors de la recherche de membre, il faut ré-essayer plus tard...", {
globalPosition:"top right",
className: "error"
});
}
});
});
} else {
$(".page_content").hide();
}
$('#back_to_admin_index').on('click', function() {
let base_location = window.location.href.split("manage_regular_shifts")[0].slice(0, -1);
window.location.assign(base_location);
});
});
var member_shifts_table = null,
members_search_results = [],
selected_member = null,
incoming_shifts = null;
/**
* Load partners who have makeups to do
*/
function load_member_future_shifts() {
$.ajax({
type: 'GET',
url: "/shifts/get_list_shift_partner/" + selected_member.id,
dataType:"json",
traditional: true,
contentType: "application/json; charset=utf-8",
success: function(data) {
incoming_shifts = data;
display_member_shifts();
},
error: function(data) {
err = {msg: "erreur serveur lors de la récupération des services du membre", ctx: 'load_member_future_shifts'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'members.admin');
closeModal();
alert('Erreur lors de la récupération des services du membre.');
}
});
}
/**
* Display table of member future shifts
*/
function display_member_shifts() {
if (member_shifts_table) {
$('#member_shifts_table').off();
member_shifts_table.clear().destroy();
$('#member_shifts_table').empty();
}
$('#table_top_area #member_name').text(selected_member.name);
$('#table_top_area').show();
member_shifts_table = $('#member_shifts_table').DataTable({
data: incoming_shifts,
columns: [
{
data: "date_begin",
title: "",
visible: false
},
{
data: "shift_id",
title: "Service",
orderable: false,
render: function (data) {
return data[1];
}
},
{
data: null,
title: "",
className: "dt-body-center",
orderable: false,
width: "5%",
render: function () {
return `<i class="fa fa-lg fa-times delete_shift_registration"></i>`;
}
}
],
order: [
[
0,
"asc"
]
],
paging: false,
dom: 'tif',
oLanguage: {
"sProcessing": "Traitement en cours...",
"sSearch": "Rechercher dans le tableau",
"sInfo": "Total de _TOTAL_ &eacute;l&eacute;ments",
"sInfoEmpty": "",
"sInfoFiltered": "(filtr&eacute; de _MAX_ &eacute;l&eacute;ments au total)",
"sInfoPostFix": "",
"sLoadingRecords": "Chargement en cours...",
"sZeroRecords": "Aucun &eacute;l&eacute;ment &agrave; afficher",
"sEmptyTable": "Aucun futur service pour ce.tte membre"
},
createdRow: function(row, rdata) {
if (rdata.is_makeup === true) {
$(row).addClass("makeup_row");
$(row).prop('title', 'Ce service est un rattrapage');
}
}
});
$('#member_shifts_table').on('click', 'tbody td .delete_shift_registration', function () {
const row_data = member_shifts_table.row($(this).parents('tr')).data();
const shift_reg_id = row_data.id;
const shift_is_makeup = row_data.is_makeup;
let msg = `<p>Enlever la présence de <b>${member.name}</b> au service du <b>${row_data.shift_id[1]}</b> ?</p>`;
if (shift_is_makeup === true) {
msg += `<p><i class="fas fa-exclamation-triangle"></i> Ce service est un rattrapage. Le supprimer ajoutera un point au compteur de ce.tte membre.</p>`;
}
openModal(
msg,
() => {
delete_shift_registration(shift_reg_id, shift_is_makeup);
},
"Confirmer",
false
);
});
}
/**
* Send request to delete shift registration
* @param {Int} shift_reg_id Id of the shift_registration to delete
* @param {Boolean} shift_is_makeup Is the shift a makeup?
*/
function delete_shift_registration(shift_reg_id, shift_is_makeup) {
openModal();
data = {
member_id: selected_member.id,
member_shift_type: selected_member.shift_type,
shift_registration_id: shift_reg_id,
shift_is_makeup: shift_is_makeup
};
$.ajax({
type: 'POST',
url: "/members/delete_shift_registration",
data: JSON.stringify(data),
dataType:"json",
traditional: true,
contentType: "application/json; charset=utf-8",
success: function() {
closeModal();
alert("La présence a bien été annulée.");
const i = incoming_shifts.findIndex(is => is.id === shift_reg_id);
incoming_shifts.splice(i, 1);
display_member_shifts();
},
error: function(data) {
err = {msg: "erreur serveur pour supprimer la présence au service", ctx: 'delete_shift_registration'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'members.admin');
closeModal();
alert('Erreur serveur pour supprimer la présence au service. Ré-essayez plus tard.');
}
});
}
/**
* Display the members from the search result
*/
function display_possible_members() {
$('.search_member_results_area').show();
$('.search_member_results').empty();
$('.btn_possible_member').off();
let no_result = true;
if (members_search_results.length > 0) {
for (member of members_search_results) {
$(".search_results_text").show();
no_result = false;
// Display results (possible members) as buttons
var member_button = '<button class="btn--success btn_possible_member" member_id="'
+ member.id + '">'
+ member.barcode_base + ' - ' + member.name
+ '</button>';
$('.search_member_results').append(member_button);
}
// Set action on member button click
$('.btn_possible_member').on('click', function() {
for (member of members_search_results) {
if (member.id == $(this).attr('member_id')) {
selected_member = member;
load_member_future_shifts();
$('.search_member_results').empty();
$('.search_member_results_area').hide();
$('#search_member_input').val('');
break;
}
}
});
}
if (no_result === true) {
$(".search_results_text").hide();
$('.search_member_results').html(`<p>
<i>Aucun résultat ! Vérifiez votre recherche, ou si le.la membre n'est pas déjà dans le tableau...</i>
</p>`);
}
}
$(document).ready(function() {
if (coop_is_connected()) {
$.ajaxSetup({ headers: { "X-CSRFToken": getCookie('csrftoken') } });
$(".page_content").show();
} else {
$(".page_content").hide();
}
$('#back_to_admin_index').on('click', function() {
let base_location = window.location.href.split("manage_shift_registrations")[0].slice(0, -1);
window.location.assign(base_location);
});
// Set action to search for the member
$('#search_member_form').submit(function() {
let search_str = $('#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.is_member || member.is_associated_people) {
members_search_results.push(member);
}
}
display_possible_members();
},
error: function() {
err = {
msg: "erreur serveur lors de la recherche de membres",
ctx: 'search_member_form.search_members'
};
report_JS_error(err, 'members.admin');
$.notify("Erreur lors de la recherche de membre, il faut ré-essayer plus tard...", {
globalPosition:"top right",
className: "error"
});
}
});
});
});
......@@ -17,8 +17,9 @@ var latest_odoo_coop_bb = null,
subs_cap = $('#subs_cap'),
m_barcode = $('#m_barcode'),
sex = $('#sex'),
self_records = [],
selected_associate=null,
associated_old_choice= null,
choose_shift_msg = "Il est nécessaire de choisir un créneau (ABCD ou Volant) avant de pouvoir faire quoique ce soit d'autre.\nUne personne qui souhaite être rattachée au compte d'un autre membre dans le cadre d'un binôme doit choisir le créneau volant.";
......@@ -91,12 +92,26 @@ function new_coop_validation() {
coop_registration_details.find('.shift_template').text(st);
process_state.html(current_coop.firstname + ' ' +current_coop.lastname);
get_next_shift(current_coop.shift_template.data.id, function(data) {
if (data != null)
coop_registration_details.find('.next_shift').text(data.date_begin);
coop_registration_details.show();
});
coop_registration_details.find("#parentName").text("")
coop_registration_details.find("#parent").attr("hidden", true)
if (current_coop.parent_name !== undefined) {
coop_registration_details.find("#parentName").text(current_coop.parent_name)
coop_registration_details.find("#parent").removeAttr("hidden")
}
if (current_coop.shift_template.data && current_coop.shift_template.data.id != ASSOCIATE_MEMBER_SHIFT) {
get_next_shift(current_coop.shift_template.data.id, function(data) {
if (data != null) {
coop_registration_details.find('#next_shift_registration_detail').show();
coop_registration_details.find('.next_shift').text(data.date_begin);
}
coop_registration_details.show();
});
} else {
coop_registration_details.find('#next_shift_registration_detail').hide();
coop_registration_details.show();
}
}
function reset_sex_radios() {
......@@ -106,6 +121,12 @@ function reset_sex_radios() {
}
function create_new_coop() {
selected_associate= null;
$('#associate_area').hide();
$('.chosen_associate').html("");
$('.chosen_associate_area').hide();
$('.member_choice').removeClass('choice_active');
$(".remove_binome_icon").on("click", hide_chosen_associate)
local_in_process = getLocalInProcess();
if (getLocalInProcess().length > 0) {
empty_waiting_local_processes();
......@@ -129,7 +150,6 @@ function create_new_coop() {
alert(choose_shift_msg);
}
}
}
function swipe_to_shift_choice() {
ncoop_view.hide();
......@@ -167,6 +187,30 @@ function _really_save_new_coop(email, fname, lname, cap, pm, cn, bc, msex) {
coop.payment_meaning = pm;
coop.checks_nb = cn;
coop.fingerprint = fingerprint;
if (associated_old_choice == 'existing_member_choice') {
if (selected_associate!=null) {
coop.is_associated_people = true;
coop.parent_id=selected_associate.id;
coop.parent_name=selected_associate.barcode_base + ' - '+ selected_associate.name;
coop.shift_template = shift_templates[ASSOCIATE_MEMBER_SHIFT];
}
} else if (associated_old_choice == 'new_member_choice' && $('#new_member_input').val()!='') {
coop.is_associated_people = true;
coop.parent_name=$('#new_member_input').val();
delete coop.parent_id;
coop.shift_template = shift_templates[ASSOCIATE_MEMBER_SHIFT];
} else {
delete coop.is_associated_people;
delete coop.parent_id;
delete coop.parent_name;
}
selected_associate=null;
$('#new_member_input').val('');
$('#associate_area').hide();
$('.chosen_associate_area').hide();
$('.chosen_associate').html("");
associated_old_choice= null;
if (m_barcode.length > 0) coop.m_barcode = bc;
if (sex.length > 0) coop.sex = msex;
coop.validation_state = "to_fill";
......@@ -174,11 +218,14 @@ function _really_save_new_coop(email, fname, lname, cap, pm, cn, bc, msex) {
if (!err) {
coop._rev = result.rev;
current_coop = coop;
if (typeof coop.shift_template != "undefined") {
if (typeof coop.shift_template != "undefined" && coop.shift_template.data.id != ASSOCIATE_MEMBER_SHIFT) {
openModal(
'Voulez-vous modifier le créneau choisi ?', swipe_to_shift_choice, 'oui',
false, true, show_coop_list
);
} else if (coop.is_associated_people && typeof coop.shift_template != "undefined" && coop.shift_template.data.id == ASSOCIATE_MEMBER_SHIFT) {
ncoop_view.hide();
new_coop_validation();
} else {
swipe_to_shift_choice();
}
......@@ -192,9 +239,11 @@ function _really_save_new_coop(email, fname, lname, cap, pm, cn, bc, msex) {
function store_new_coop(event) {
event.preventDefault();
var errors = [],
bc = '', // barcode may not be present
msex = ''; // sex may not be present
msex = '', // sex may not be present
active_asso_area = $('#associate_area .choice_active'); // need to ckeck if associated data are available
// 1- Un coop avec le meme mail ne doit pas exister dans odoo (dans base intermediaire, le cas est géré par l'erreur à l'enregistrement)
let email = $('input[name="email"]').val()
.trim(),
......@@ -219,6 +268,19 @@ function store_new_coop(event) {
}
}
if (active_asso_area.length > 0) {
// If user click as if a "binôme" is beeing created, data about parent member must exist
let associated_data_ok = false;
if (
($(active_asso_area[0]).attr('id') === "new_member_choice" && $('#new_member_input').val().trim().length > 0)
||
($(active_asso_area[0]).attr('id') === "existing_member_choice" && $('#existing_member_choice_action .chosen_associate div.member').length > 0)
) {
associated_data_ok = true;
}
if (associated_data_ok === false) errors.push("Le membre 'titulaire' du binôme n'est pas défini");
}
$.ajax({url : '/members/exists/' + email,
dataType :'json'
})
......@@ -226,19 +288,37 @@ function store_new_coop(event) {
if (typeof(rData.answer) == 'boolean' && rData.answer == true) {
errors.push("Il y a déjà un enregistrement Odoo avec cette adresse mail !");
}
});
if (errors.length == 0) {
_really_save_new_coop(
email, fname, lname,
subs_cap.val(), payment_meaning.val(), ch_qty.val(), bc, msex
);
} else {
alert(errors.join("\n"));
}
if (selected_associate!=null) {
$.ajax({url : '/members/is_associated/' + selected_associate.id,
dataType :'json'
}).done(function(rData) {
if (typeof(rData.answer) == 'boolean' && rData.answer == true) {
errors.push("Ce membre a déjà un binôme majeur");
}
if (errors.length == 0) {
_really_save_new_coop(
email, fname, lname,
subs_cap.val(), payment_meaning.val(), ch_qty.val(), bc, msex
);
} else {
alert(errors.join("\n"));
}
});
}
} else {
if (errors.length == 0) {
_really_save_new_coop(
email, fname, lname,
subs_cap.val(), payment_meaning.val(), ch_qty.val(), bc, msex
);
} else {
alert(errors.join("\n"));
}
}
});
}
......@@ -273,6 +353,30 @@ function modify_current_coop() {
} else {
ch_qty.hide();
}
if (current_coop.is_associated_people) {
$('.member_choice').removeClass('choice_active');
$('#associate_area').show();
if (current_coop.parent_id) {
$('#existing_member_choice_action').show();
$('#new_member_choice_action').hide();
$('#existing_member_choice').addClass('choice_active');
var member_button = '<div>' + current_coop.parent_name + '</div>';
$('.chosen_associate').html(member_button);
$('.chosen_associate_area').show();
associated_old_choice = 'existing_member_choice';
} else {
$('#new_member_choice_action').show();
$('#existing_member_choice_action').hide();
$('#new_member_choice').addClass('choice_active');
$('#new_member_input').val(current_coop.parent_name);
$('.chosen_associate').html('');
$('.chosen_associate_area').hide();
associated_old_choice = 'new_member_choice';
}
}
subs_cap.val(current_coop.shares_euros);
if (m_barcode.length > 0) m_barcode.val(current_coop.m_barcode);
if (sex.length > 0) {
......@@ -281,6 +385,13 @@ function modify_current_coop() {
}
ncoop_view.show();
}
function hide_chosen_associate() {
selected_associate=null;
$(".chosen_associate_area").hide();
$('.chosen_associate').html("");
}
function modify_coop_by_btn_triggered() {
var clicked = $(this);
var coop_id = clicked.find('div').data('id');
......@@ -497,8 +608,49 @@ $('#coop_create').submit(store_new_coop);
$('#generate_email').click(generate_email);
$('#odoo_user_connect').click();
$('#add_binome').click(function() {
if ($('#associate_area').is(':visible')) {
$('#associate_area').hide();
$('#new_member_input').val('');
$('#associate_area .choice_active').removeClass("choice_active");
associated_old_choice = null;
if (current_coop !=null) {
delete current_coop.parent_name;
delete current_coop.parent_id;
delete current_coop.is_associated_people;
delete current_coop.shift_template;
}
} else {
$('#associate_area').show();
$('.member_choice').removeClass('choice_active');
$('#existing_member_choice_action').hide();
$('#new_member_choice_action').hide();
associated_old_choice = null;
}
});
$('.member_choice').on('click', function() {
if (associated_old_choice !=null && associated_old_choice!=$(this).attr('id')) {
$('#'+$(this).attr('id')+'_action').show();
$('#'+associated_old_choice+'_action').hide();
$('#'+associated_old_choice).removeClass('choice_active');
} else if (associated_old_choice ==null) {
$('#'+$(this).attr('id')+'_action').show();
}
associated_old_choice=$(this).attr('id');
$(this).addClass('choice_active');
});
$('#shift_calendar').click(show_shift_calendar);
$('#search_member_input').keypress((event) => {
if (event.keyCode==13) {
event.preventDefault();
searchMembersForAssociate();
}
});
//get_latest_odoo_coop_bb();
update_self_records();
......@@ -516,3 +668,112 @@ payment_meaning.change(function() {
window.addEventListener("beforeunload", keep_in_process_work);
empty_waiting_local_processes();
/**
* Display the members from the search result
*/
function display_possible_members() {
$('.search_member_results_area').show();
$('.search_member_results').empty();
$('.btn_possible_member').off();
$('.chosen_associate').html("");
$('.chosen_associate_area').hide();
let no_result = true;
if (members_search_results.length > 0) {
for (member of members_search_results) {
$(".search_results_text").show();
no_result = false;
// Display results (possible members) as buttons
var member_button = '<button class="btn--success btn_possible_member" member_id="'
+ member.id + '">'
+ member.barcode_base + ' - ' + member.name
+ '</button>';
$('.search_member_results').append(member_button);
}
// Set action on member button click
$('.btn_possible_member').on('click', function() {
for (member of members_search_results) {
if (member.id == $(this).attr('member_id')) {
selected_associate = member;
var member_button = '<div member_id="' + member.id + '" class="member">' + member.barcode_base + ' - ' + member.name + '</div>';
$('.chosen_associate').html(member_button);
$('.chosen_associate_area').show();
$('.search_member_results').empty();
$('.search_member_results_area').hide();
$('#search_member_input').val('');
break;
}
}
});
}
if (no_result === true) {
$(".search_results_text").hide();
$('.search_member_results').html(`<p>
<i>Aucun résultat ! Vérifiez votre recherche, ou si le.la membre n'est pas déjà dans le tableau...</i>
</p>`);
}
}
/**
* Search for members to associate a new member with an old one.
*/
function searchMembersForAssociate() {
let search_str = $('#search_member_input').val();
if (search_str) {
$.ajax({
url: '/members/search/' + search_str+ "?search_type=members",
dataType : 'json',
success: function(data) {
members_search_results = [];
for (member of data.res) {
if (member.is_member || member.is_associated_people) {
members_search_results.push(member);
}
}
display_possible_members();
},
error: function() {
err = {
msg: "erreur serveur lors de la recherche de membres",
ctx: 'search_member_form.search_members'
};
report_JS_error(err, 'members.admin');
$.notify("Erreur lors de la recherche de membre, il faut ré-essayer plus tard...", {
globalPosition:"top right",
className: "error"
});
}
});
} else {
members_search_results = [];
display_possible_members();
}
}
$(document).ready(function() {
retrieve_and_draw_shift_tempates();
// Set action to search for the member
$('#search_member_button').on('click', function() {
searchMembersForAssociate();
});
if (committees_shift_id !== "None") {
$("#shift_choice button[data-select='Volant']").text("Comités");
}
});
......@@ -31,12 +31,14 @@ var search_field = $('input[name="search_string"]');
var shift_title = $('#current_shift_title');
var shift_members = $('#current_shift_members');
var service_validation = $('#service_validation');
var associated_service_validation = $('#associated_service_validation');
var validation_last_call = 0;
var rattrapage_wanted = $('[data-next="rattrapage_1"]');
var webcam_is_attached = false;
var photo_advice = $('#photo_advice');
var photo_studio = $('#photo_studio');
var coop_info = $('.coop-info');
var service_data = null;
const missed_begin_msg = $('#missed_begin_msg').html();
......@@ -93,8 +95,9 @@ function fill_member_slide(member) {
html_elts.image_medium.html('<img src="'+img_src+'" width="128" />');
html_elts.cooperative_state.html(member.cooperative_state);
if (member.cooperative_state == 'Rattrapage') {
var explanation = "Tu as dû manquer un service! Pour pouvoir faire tes courses aujourd'hui, tu dois d'abord sélectionner un rattrapage sur ton espace membre."
html_elts.status_explanation.html(explanation)
var explanation = "Tu as dû manquer un service! Pour pouvoir faire tes courses aujourd'hui, tu dois d'abord sélectionner un rattrapage sur ton espace membre.";
html_elts.status_explanation.html(explanation);
}
if (member.cooperative_state == 'Désinscrit(e)') coop_info.addClass('b_red');
else if (member.cooperative_state == 'En alerte' || member.cooperative_state == 'Délai accordé' || member.cooperative_state == 'Rattrapage') coop_info.addClass('b_orange');
......@@ -146,9 +149,17 @@ function preview_results() {
for (i in results) {
if (results[i].is_member != false || results[i].is_associated_people != false) {
var m = $('<button>').attr('data-i', i)
.text(results[i].name);
if (results[i].is_member != false) {
var m = $('<button class="button_is_member">').attr('data-i', i)
.text(results[i].barcode_base + ' - ' + results[i].name);
html_elts.multi_results.append(m);
}
if (results[i].is_associated_people != false) {
m = $('<button class="button_is_associated_people"></button_is_member>').attr('data-i', i)
.text('B ' + results[i].barcode_base + ' - ' + results[i].name);
html_elts.multi_results.append(m);
}
......@@ -255,7 +266,7 @@ function get_simple_service_name(s) {
}
function move_service_validation_to(page) {
service_validation.find('.btn').data('stid', '0');
service_data.stid=0;
page.find('.validation_wrapper')
.append(service_validation.detach());
}
......@@ -275,11 +286,21 @@ function fill_service_entry(s) {
var li_class = "btn";
var li_data = "";
if (e.state == "done") {
if (e.state == "done" && coop_is_connected()) {
li_data = ' data-rid="'+e.id+'" data-mid="'+e.partner_id[0]+'"';
li_class += "--inverse";
if (e.is_late == true) {
li_class += " late";
}
if (e.associate_registered=='both') {
li_class += " both";
}
} else if (e.state == "done" && !coop_is_connected()) {
li_data = ' data-rid="'+e.id+'" data-mid="'+e.partner_id[0]+'"';
li_class += "--inverse not_connected";
if (e.is_late == true) {
li_class += " late";
}
} else {
li_data = ' data-rid="'+e.id+'" data-mid="'+e.partner_id[0]+'"';
}
......@@ -312,13 +333,29 @@ function clean_service_entry() {
function fill_service_validation(rid, coop_num_name, coop_id) {
var coop_name_elts = coop_num_name.split(' - ');
for (member of loaded_services[0].members) {
if (member.id ==rid) {
if (member.associate_name) {
pages.service_entry_validation.find('#service_validation').hide();
pages.service_entry_validation.find('#associated_service_validation').show();
pages.service_entry_validation.find('#associated_btn').text(member.associate_name);
pages.service_entry_validation.find('#partner_btn').text(member.partner_name);
} else {
pages.service_entry_validation.find('#associated_service_validation').hide();
pages.service_entry_validation.find('#service_validation').show();
}
}
}
service_data={
rid: rid,
sid: selected_service.id,
mid: coop_id};
pages.service_entry_validation.find('span.member_name').text(coop_name_elts[1]);
move_service_validation_to(pages.service_entry_validation);
service_validation.find('.btn')
.data('rid', rid)
.data('sid', selected_service.id)
.data('mid', coop_id);
}
function select_possible_service() {
......@@ -363,7 +400,6 @@ function get_service_entry_data() {
dataType : 'json'
})
.done(function(rData) {
//console.log(rData);
info_place.text('');
var page_title = pages.service_entry.find('h1');
......@@ -388,6 +424,7 @@ function get_service_entry_data() {
page_title.text('Quel est ton service ?');
} else {
loaded_services = rData.res;
fill_service_entry(rData.res[0]);
}
}
......@@ -434,22 +471,21 @@ function fill_service_entry_sucess(member) {
}
function record_service_presence() {
function record_service_presence(e) {
var d = new Date();
var elapsed_since_last_call = d.getTime() - validation_last_call;
if (elapsed_since_last_call > 10000) {
if (elapsed_since_last_call > 1000) {
loading2.show();
validation_last_call = d.getTime();
var clicked = service_validation.find('.btn');
var rid = clicked.data('rid');
var mid = clicked.data('mid');
var sid = clicked.data('sid');
var stid = clicked.data('stid');
var rid = service_data.rid;
var mid = service_data.mid;
var sid = service_data.sid;
var stid = service_data.stid;
post_form(
'/members/service_presence/',
{'mid': mid, 'rid': rid, 'sid': sid, 'stid' : stid},
{'mid': mid, 'rid': rid, 'sid': sid, 'stid' : stid, 'cancel': false, 'type': e.data.type},
function(err, rData) {
if (!err) {
var res = rData.res;
......@@ -471,6 +507,28 @@ function record_service_presence() {
}
}
function cancel_service_presence(mid, rid) {
var d = new Date();
var elapsed_since_last_call = d.getTime() - validation_last_call;
if (elapsed_since_last_call > 1000) {
loading2.show();
validation_last_call = d.getTime();
var sid = selected_service.id;
post_form(
'/members/service_presence/',
{'mid': mid, 'rid': rid, 'sid': sid, 'stid' : 0, 'cancel': true},
function(err) {
if (!err) {
get_service_entry_data();
}
loading2.hide();
}
);
}
}
function fill_rattrapage_2() {
pages.rattrapage_2.find('span.member_name').text(current_displayed_member.name);
var msg = "Bienvenue pour ton rattrapage !";
......@@ -485,13 +543,11 @@ function fill_rattrapage_2() {
msg = "Tu es en désincrit.e ... La situation doit être réglée avez le Bureau des Membres";
} else {
move_service_validation_to(pages.rattrapage_2);
service_validation.find('.btn')
.data('rid', 0)
.data('sid', selected_service.id)
.data('stid', shift_ticket_id)
.data('mid', current_displayed_member.id);
service_data = {
rid : 0,
sid : selected_service.id,
stid : shift_ticket_id,
mid : current_displayed_member.id};
}
pages.rattrapage_2.find('h2').text(msg);
......@@ -632,7 +688,10 @@ $('.btn[data-next]').click(function() {
});
service_validation.on("click", ".btn", record_service_presence);
service_validation.on("click", ".btn", {type:'normal'}, record_service_presence);
associated_service_validation.on("click", "#associated_btn", {type:'associate'}, record_service_presence);
associated_service_validation.on("click", "#partner_btn", {type:'partner'}, record_service_presence);
associated_service_validation.on("click", "#both_btn", {type:'both'}, record_service_presence);
shift_members.on("click", '.btn[data-rid]', function() {
var clicked = $(this);
......@@ -644,6 +703,16 @@ shift_members.on("click", '.btn[data-rid]', function() {
});
shift_members.on("click", '.btn--inverse', function() {
if (coop_is_connected()) {
var clicked = $(this);
var rid = clicked.data('rid');
var mid = clicked.data('mid');
cancel_service_presence(mid, rid);
}
});
pages.shopping_entry.on('css', function() {
photo_advice.hide();
photo_studio.hide();
......
......@@ -32,6 +32,15 @@ function display_current_coop_form() {
let street2_input = form.find('[name="street2"]'),
phone_input = form.find('[name="phone"]');
if (current_coop.parent_name) {
$('#associated_member').show();
if (current_coop.parent_id)
$('#associated_member_name').text(current_coop.parent_name);
else $('#associated_member_name').text(current_coop.parent_name + " ATTENTION à faire manuellement");
} else {
$('#associated_member').hide();
}
chgt_shift_btn.hide();
chgt_shift_btn.off('click', open_shift_choice);
form.find('[name="firstname"]').val(current_coop.firstname);
......
......@@ -31,10 +31,12 @@ urlpatterns = [
url(r'^latest_coop_id/$', views.latest_coop_id),
url(r'^get/([0-9]+)$', views.get),
url(r'^exists/([a-zA-Z0-9_\-\.\+@]+)$', views.exists),
url(r'^is_associated/([0-9]+)$', views.is_associated),
url(r'^get_couchdb_odoo_markers/(.+)$', views.get_couchdb_odoo_markers),
url(r'^menu/$', views.menu),
url(r'^verify_final_state$', views.verify_final_state),
url(r'^update_couchdb_barcodes$', views.update_couchdb_barcodes),
url(r'^add_shares_to_member$', views.add_shares_to_member),
# Borne accueil
url(r'^search/([^\/.]+)/?([0-9]*)', views.search),
url(r'^save_photo/([0-9]+)$', views.save_photo, name='save_photo'),
......@@ -49,11 +51,24 @@ urlpatterns = [
url(r'^easy_validate_shift_presence$', views.easy_validate_shift_presence),
# conso / groupe recherche / socio
url(r'^panel_get_purchases$', views.panel_get_purchases),
# BDM
# BDM
url(r'^save_partner_info$', views.save_partner_info),
# BDM - members admin
url(r'^admin$', admin.admin),
url(r'^admin/?$', admin.admin),
url(r'^admin/manage_makeups$', admin.manage_makeups),
url(r'^admin/manage_shift_registrations$', admin.manage_shift_registrations),
url(r'^admin/manage_regular_shifts$', admin.manage_regular_shifts),
url(r'^get_makeups_members$', admin.get_makeups_members),
url(r'^update_members_makeups$', admin.update_members_makeups),
url(r'^delete_shift_registration$', admin.delete_shift_registration),
url(r'^delete_shift_template_registration$', admin.delete_shift_template_registration),
url(r'^shift_subscription$', admin.shift_subscription),
url(r'^admin/manage_attached$', admin.manage_attached),
url(r'^admin/manage_attached/create_pair$', admin.create_pair),
url(r'^admin/manage_attached/delete_pair$', admin.delete_pair),
url(r'^get_makeups_members$', admin.get_makeups_members),
url(r'^update_members_makeups$', admin.update_members_makeups),
url(r'^get_member_info/(\d+)$', admin.get_member_info),
url(r'^get_attached_members$', admin.get_attached_members),
]
......@@ -8,7 +8,7 @@ from members.models import CagetteMembers
from members.models import CagetteServices
from outils.forms import GenericExportMonthForm
import datetime
default_fields = ['name',
'image_medium']
......@@ -63,6 +63,10 @@ def exists(request, mail):
answer = CagetteMember.exists(mail)
return JsonResponse({'answer': answer})
def is_associated(request, id_parent):
answer = CagetteMember.is_associated(id_parent)
return JsonResponse({'answer': answer})
def getmemberimage(request, id):
m = CagetteMember(id)
call_res = m.get_image()
......@@ -83,21 +87,27 @@ def inscriptions(request, type=1):
"""
template = loader.get_template('members/inscriptions.html')
context = {'type': type, 'title': 'Inscriptions',
'couchdb_server': settings.COUCHDB['url'],
'mag_place_string': settings.MAG_NAME,
'office_place_string': settings.OFFICE_NAME,
'max_begin_hour': settings.MAX_BEGIN_HOUR,
'payment_meanings': settings.SUBSCRIPTION_PAYMENT_MEANINGS,
'force_firstname_hyphen': getattr(settings, 'FORCE_HYPHEN_IN_SUBSCRIPTION_FIRSTNAME', True),
'input_barcode': getattr(settings, 'SUBSCRIPTION_INPUT_BARCODE', False),
'email_domain': getattr(settings, 'EMAIL_DOMAIN', 'lacagette-coop.fr'),
'ask_for_sex': getattr(settings, 'SUBSCRIPTION_ASK_FOR_SEX', False),
'open_on_sunday': getattr(settings, 'OPEN_ON_SUNDAY', False),
'POUCHDB_VERSION': getattr(settings, 'POUCHDB_VERSION', ''),
'max_chq_nb': getattr(settings, 'MAX_CHQ_NB', 12),
'show_ftop_button': getattr(settings, 'SHOW_FTOP_BUTTON', True),
'db': settings.COUCHDB['dbs']['member']}
committees_shift_id = CagetteServices.get_committees_shift_id()
context = {
'type': type, 'title': 'Inscriptions',
'couchdb_server': settings.COUCHDB['url'],
'mag_place_string': settings.MAG_NAME,
'office_place_string': settings.OFFICE_NAME,
'max_begin_hour': settings.MAX_BEGIN_HOUR,
'payment_meanings': settings.SUBSCRIPTION_PAYMENT_MEANINGS,
'force_firstname_hyphen': getattr(settings, 'FORCE_HYPHEN_IN_SUBSCRIPTION_FIRSTNAME', True),
'input_barcode': getattr(settings, 'SUBSCRIPTION_INPUT_BARCODE', False),
'email_domain': getattr(settings, 'EMAIL_DOMAIN', 'lacagette-coop.fr'),
'ask_for_sex': getattr(settings, 'SUBSCRIPTION_ASK_FOR_SEX', False),
'open_on_sunday': getattr(settings, 'OPEN_ON_SUNDAY', False),
'POUCHDB_VERSION': getattr(settings, 'POUCHDB_VERSION', ''),
'max_chq_nb': getattr(settings, 'MAX_CHQ_NB', 12),
'show_ftop_button': getattr(settings, 'SHOW_FTOP_BUTTON', True),
'db': settings.COUCHDB['dbs']['member'],
'ASSOCIATE_MEMBER_SHIFT' : getattr(settings, 'ASSOCIATE_MEMBER_SHIFT', ''),
'prepa_odoo_url' : getattr(settings, 'PREPA_ODOO_URL', '/members/prepa-odoo'),
'committees_shift_id': committees_shift_id,
}
response = HttpResponse(template.render(context, request))
return response
......@@ -237,6 +247,8 @@ def update_couchdb_barcodes(request):
def search(request, needle, shift_id):
"""Search member has been requested."""
search_type = request.GET.get('search_type', "full")
try:
key = int(needle)
k_type = 'barcode_base'
......@@ -247,7 +259,7 @@ def search(request, needle, shift_id):
key = needle
k_type = 'name'
res = CagetteMember.search(k_type, key, shift_id)
res = CagetteMember.search(k_type, key, shift_id, search_type)
return JsonResponse({'res': res})
......@@ -275,6 +287,9 @@ def record_service_presence(request):
mid = int(request.POST.get("mid", 0)) # member id
sid = int(request.POST.get("sid", 0)) # shift id
stid = int(request.POST.get("stid", 0)) # shift_ticket_id
cancel = request.POST.get("cancel") == 'true'
typeAction = str(request.POST.get("type"))
app_env = getattr(settings, 'APP_ENV', "prod")
if (rid > -1 and mid > 0):
overrided_date = ""
......@@ -284,28 +299,31 @@ def record_service_presence(request):
if o_date:
overrided_date = re.sub(r'(%20)',' ', o_date.group(1))
# rid = 0 => C'est un rattrapage, sur le service
if sid > 0 and stid > 0:
# Add member to service and take presence into account
res['rattrapage'] = CagetteServices.record_rattrapage(mid, sid, stid)
if res['rattrapage'] is True:
res['update'] = 'ok'
else:
if (CagetteServices.registration_done(rid, overrided_date) is True):
res['update'] = 'ok'
if(not cancel):
# rid = 0 => C'est un rattrapage, sur le service
if sid > 0 and stid > 0:
# Add member to service and take presence into account
res['rattrapage'] = CagetteServices.record_rattrapage(mid, sid, stid, typeAction)
if res['rattrapage'] is True:
res['update'] = 'ok'
else:
res['update'] = 'ko'
if res['update'] == 'ok':
members = CagetteMember.search('id', mid)
m = members[0]
for k in ['image_medium', 'barcode', 'barcode_base']:
del m[k]
next_shift = {}
if len(m['shifts']) > 0:
next_shift = m['shifts'][0]
del m['shifts']
m['next_shift'] = next_shift
res['member'] = m
if (CagetteServices.registration_done(rid, overrided_date, typeAction) is True):
res['update'] = 'ok'
else:
res['update'] = 'ko'
if res['update'] == 'ok':
members = CagetteMember.search('id', mid)
m = members[0]
for k in ['image_medium', 'barcode', 'barcode_base']:
del m[k]
next_shift = {}
if len(m['shifts']) > 0:
next_shift = m['shifts'][0]
del m['shifts']
m['next_shift'] = next_shift
res['member'] = m
else: CagetteServices.reopen_registration(rid, overrided_date)
except Exception as e:
res['error'] = str(e)
return JsonResponse({'res': res})
......@@ -391,6 +409,21 @@ def panel_get_purchases(request):
response = HttpResponse(message)
return response
def add_shares_to_member(request):
res = {}
try:
data = json.loads(request.body.decode())
partner_id = int(data["partner_id"])
amount = int(data["amount"])
except Exception as e:
res['error'] = "Wrong params"
return JsonResponse(res, safe=False, status=400)
m = CagetteMember(partner_id)
today = datetime.date.today().strftime("%Y-%m-%d")
res = m.create_capital_subscription_invoice(amount, today)
return JsonResponse(res, safe=False)
# # # BDM # # #
def save_partner_info(request):
......
......@@ -44,7 +44,7 @@ class CagetteMembersSpace(models.Model):
['state', '!=', 'replaced'],
['state', '!=', 'replacing'],
]
f = ['create_date', 'date_begin', 'shift_id', 'name', 'state', 'is_late', 'is_makeup']
f = ['create_date', 'date_begin', 'shift_id', 'name', 'state', 'is_late', 'is_makeup','associate_registered']
marshal_none_error = 'cannot marshal None unless allow_none is enabled'
try:
......
......@@ -54,6 +54,13 @@
margin: 3rem 0;
}
#my_info .choose_makeups,
#my_info .unsuscribed_form_link,
#my_info .remove_future_registration {
font-size: 1.8rem;
word-break: normal;
}
#my_info #member_status_action,
#my_info .member_shift_name_area,
#my_info .member_coop_number_area {
......
......@@ -52,7 +52,6 @@
#shifts_list {
flex-direction: column;
display: none;
width: min-content;
max-width: 100%;
white-space: nowrap;
}
......@@ -65,11 +64,38 @@
}
}
.shift_line_container {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
}
.shift_line_extra_actions {
display: flex;
justify-content: flex-start;
flex-wrap: nowrap;
}
@media screen and (max-width:768px) {
.shift_line_container {
flex-direction: column;
}
.shift_line_extra_actions {
width: 100%;
}
.affect_associate_registered {
margin: 0.5rem 0;
}
}
.selectable_shift_line {
min-width: 325px;
display: flex;
align-items: center;
margin-left: 15px;
margin: 0.75rem 0;
border-radius: 5px;
}
......@@ -81,6 +107,35 @@
cursor: not-allowed;
}
.affect_associate_registered {
display: flex;
align-items: center;
border-radius: 5px;
}
@media screen and (min-width:768px) {
.selectable_shift_line {
margin: 0 15px;
}
.affect_associate_registered {
margin-left: 15px;
}
}
.selectable_shift{
margin: 1rem 0;
}
.delete_registration_button {
justify-content: center;
align-items: center;
margin: 0.75rem 15px;
color: #d9534f;
cursor: pointer;
display: none;
}
/* -- Calendar screen, makeups message */
#need_to_select_makeups_message {
......@@ -103,6 +158,21 @@
}
}
/* -- Calendar screen, can delete registrations message */
#can_delete_future_registrations_area {
display: none;
justify-content: center;
align-items: center;
margin: 0 1rem 1rem 1rem;
}
#can_delete_future_registrations_area button {
white-space: normal;
word-break: normal;
margin: 1rem;
}
/* -- Calendar screen, calendar */
#calendar {
......@@ -147,6 +217,19 @@ td{
color: white;
}
.fc-event.shift_booked_makeup {
background-color: #f0ad4e;
cursor: auto;
border-color: #f0ad4e;
}
.fc-event.shift_booked_makeup td {
--fc-list-event-hover-bg-color:#f0ad4e;
}
.fc-list-event.shift_booked_makeup {
color: white;
}
#calendar .fc-list-table {
table-layout: auto;
}
......@@ -194,4 +277,14 @@ td{
#calendar_explaination_button {
max-width: 60%;
margin: 2rem auto 0.5rem auto;
}
/* -- Assign shift modal */
.modal_affect_shift_buttons {
margin: 1rem 0;
}
.assign_shift_button {
margin: 0.25rem;
}
\ No newline at end of file
......@@ -166,6 +166,11 @@ body {
font-size: 1.5rem;
}
.remove_future_registration {
display: none;
white-space: normal;
}
.unsuscribed_form_link {
display: none;
text-decoration: none;
......@@ -335,3 +340,13 @@ body {
.no_content_title {
margin-bottom: 1.5rem;
}
/* - block_actions_for_attached_people is true or false */
.attached-blocked {
display: none;
}
.attached-unblocked {
display: none;
}
function init_faq() {
$("#unsuscribe_form_link_btn").prop("href", unsuscribe_form_link);
$("#unsuscribe_form_link_btn2").prop("href", unsuscribe_form_link);
......@@ -18,8 +17,33 @@ function init_faq() {
$("#helper_unsubscribe_form_link_btn").prop("href", helper_unsubscribe_form_link);
$("#request_form_link_btn2").prop("href", request_form_link);
$("#request_form_link_btn").prop("href", request_form_link);
display_messages_for_attached_people();
}
$(document).on('click', "#shift_exchange_btn", () => {
goto('echange-de-services');
});
\ No newline at end of file
goto('echange-de-services');
});
$(document).on('click', '.accordion', function() {
/* Toggle between adding and removing the "active" class,
to highlight the button that controls the panel */
this.classList.toggle("active");
/* Toggle between hiding and showing the active panel */
var panel = this.nextElementSibling;
if (panel.style.display === "block") {
panel.style.display = "none";
} else {
panel.style.display = "block";
}
});
function display_messages_for_attached_people() {
if (block_actions_for_attached_people === "False") {
$(".attached-unblocked").show();
} else {
$(".attached-blocked").show();
}
}
......@@ -55,7 +55,7 @@ $(document).ready(function() {
toggleHeader();
});
if (partner_data.is_associated_people === "True") {
if (partner_data.is_associated_people === "True" && block_actions_for_attached_people === "True") {
$(".pairs_info").show();
}
});
/**
* Request a 6 month delay
*/
function request_delay() {
return new Promise((resolve) => {
let today = new Date();
const delay_start = today.getFullYear()+'-'+(today.getMonth()+1)+'-'+today.getDate();
let today_plus_six_month = new Date();
today_plus_six_month.setMonth(today_plus_six_month.getMonth()+6);
const diff_time = Math.abs(today_plus_six_month - today);
const diff_days = Math.ceil(diff_time / (1000 * 60 * 60 * 24));
$.ajax({
type: 'POST',
url: "/shifts/request_delay",
dataType:"json",
data: {
verif_token: partner_data.verif_token,
idPartner: partner_data.partner_id,
start_date: delay_start,
duration: diff_days
},
success: function() {
partner_data.cooperative_state = 'delay';
partner_data.date_delay_stop = today_plus_six_month.getFullYear()+'-'+(today_plus_six_month.getMonth()+1)+'-'+today_plus_six_month.getDate();
resolve();
},
error: function(data) {
if (data.status == 403
&& typeof data.responseJSON != 'undefined'
&& data.responseJSON.message === "delays limit reached") {
closeModal();
let msg_template = $("#cant_have_delay_msg_template");
openModal(
msg_template.html(),
() => {
window.location =member_cant_have_delay_form_link;
},
"J'accède au formulaire",
true,
false
);
} else {
err = {msg: "erreur serveur lors de la création du délai", ctx: 'request_delay'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'members_space.home');
closeModal();
alert('Erreur lors de la création du délai.');
}
}
});
});
}
function init_my_shifts_tile() {
if (incoming_shifts.length === 0) {
$("#home_tile_my_services #home_incoming_services").text("Aucun service à venir...");
......
......@@ -66,6 +66,31 @@ function prepare_server_data(data) {
}
}
if (history_item.associate_registered == false || history_item.associate_registered == undefined) {
history_item.associate_registered = "";
} else {
if (partner_data.associated_partner_id != "False") {
if (history_item.associate_registered==="partner") {
history_item.associate_registered = partner_data.name;
} else if (history_item.associate_registered==="associate") {
history_item.associate_registered = partner_data.associated_partner_name;
} else if (history_item.associate_registered==="both") {
history_item.associate_registered = "Les deux";
} else {
history_item.associate_registered = "";
}
} else if (partner_data.parent_id != "False") {
if (history_item.associate_registered==="partner") {
history_item.associate_registered = partner_data.parent_name;
} else if (history_item.associate_registered==="associate") {
history_item.associate_registered = partner_data.name;
} else if (history_item.associate_registered==="both") {
history_item.associate_registered = "Les deux";
} else {
history_item.associate_registered = "";
}
}
}
history_item.details = '';
if (history_item.state === 'excused' || history_item.state === 'absent') {
history_item.details = "Absent.e";
......@@ -87,7 +112,6 @@ function prepare_server_data(data) {
function init_history() {
$(".loading-history").hide();
$("#history").show();
if (partner_history.length === 0) {
$("#history").empty()
.text("Aucun historique... pour l'instant !");
......@@ -103,7 +127,7 @@ function init_history() {
{
data: "shift_name",
title: "<spans class='dt-body-center'>Service</span>",
width: "60%",
width: "50%",
orderable: false
},
{
......@@ -111,6 +135,11 @@ function init_history() {
title: "Détails",
className: "tablet-l desktop",
orderable: false
},
{
data: "associate_registered",
title: "",
orderable: false
}
],
iDisplayLength: -1,
......@@ -129,7 +158,7 @@ function init_history() {
if (cell.text() === "Présent.e") {
$(row).addClass('row_partner_ok');
} else if (cell.text() === "Retard") {
} else if (cell.text().includes("Retard")) {
$(row).addClass('row_partner_late');
} else if (cell.text() === "Absent.e") {
$(row).addClass('row_partner_absent');
......@@ -157,6 +186,28 @@ function init_incoming_shifts() {
for (shift of incoming_shifts) {
let shift_line_template = prepare_shift_line_template(shift.date_begin);
if (partner_data.associated_partner_id != "False") {
if (shift.associate_registered==="partner") {
shift_line_template.find(".shift_line_associate").text(' - '+partner_data.name+'');
} else if (shift.associate_registered==="associate") {
shift_line_template.find(".shift_line_associate").text(' - '+partner_data.associated_partner_name+'');
} else if (shift.associate_registered==="both") {
shift_line_template.find(".shift_line_associate").text(' - Les deux');
} else {
shift_line_template.find(".shift_line_associate").text('A définir');
}
} else if (partner_data.parent_id != "False") {
if (shift.associate_registered==="partner") {
shift_line_template.find(".shift_line_associate").text(' - '+partner_data.parent_name+'');
} else if (shift.associate_registered==="associate") {
shift_line_template.find(".shift_line_associate").text(' - '+partner_data.name+'');
} else if (shift.associate_registered==="both") {
shift_line_template.find(".shift_line_associate").text(' - Les deux');
} else {
shift_line_template.find(".shift_line_associate").text('A définir');
}
}
$("#incoming_shifts").append(shift_line_template.html());
}
}
......
......@@ -2,14 +2,17 @@ var calendar = null,
selected_shift = null,
vw = null;
/* - Logic */
/**
* A partner can exchange shifts if:
* - s.he doesn't have to choose a makeup shift
* - s.he's not an associated partner
* - s.he's is an associated partner who is not blocked in his actions
* @returns boolean
*/
function can_exchange_shifts() {
return partner_data.makeups_to_do == 0 && partner_data.is_associated_people === "False";
return partner_data.makeups_to_do == 0 && (partner_data.is_associated_people === "False" || (partner_data.is_associated_people === "True" && block_actions_for_attached_people === "False"));
}
/**
......@@ -19,9 +22,11 @@ function can_exchange_shifts() {
* @returns boolean
*/
function should_select_makeup() {
return partner_data.makeups_to_do > 0 && partner_data.is_associated_people === "False";
return partner_data.makeups_to_do > 0 || (partner_data.makeups_to_do > 0 && partner_data.is_associated_people === "True" && block_actions_for_attached_people === "False");
}
/* - Server requests */
/**
* Proceed to shift exchange or registration
* @param {int} new_shift_id
......@@ -30,10 +35,19 @@ function add_or_change_shift(new_shift_id) {
if (is_time_to('change_shift')) {
setTimeout(openModal, 100); // loading on
tData = 'idNewShift=' + new_shift_id
+'&idPartner=' + partner_data.partner_id
+ '&shift_type=' + partner_data.shift_type
+ '&verif_token=' + partner_data.verif_token;
if (partner_data.is_associated_people === "False") {
tData = 'idNewShift=' + new_shift_id
+'&idPartner=' + partner_data.partner_id
+ '&shift_type=' + partner_data.shift_type
+ '&verif_token=' + partner_data.verif_token;
} else if (partner_data.is_associated_people === "True" && block_actions_for_attached_people === "False") {
tData = 'idNewShift=' + new_shift_id
+'&idPartner=' + partner_data.parent_id
+ '&shift_type=' + partner_data.shift_type
+ '&verif_token=' + partner_data.parent_verif_token;
} else {
return false;
}
if (selected_shift === null) {
tUrl = '/shifts/add_shift';
......@@ -84,17 +98,38 @@ function add_or_change_shift(new_shift_id) {
// Redraw calendar
calendar.refetchEvents();
} else {
closeModal();
selected_shift = null;
alert(`Une erreur est survenue. ` +
`Il est néanmoins possible que la requête ait abouti, ` +
`veuillez patienter quelques secondes puis vérifier vos services enregistrés.`);
// Refectch shifts anyway, if registration/exchange was still succesful
setTimeout(() => {
load_partner_shifts(partner_data.concerned_partner_id)
.then(init_shifts_list);
}, 300);
}
},
error: function(error) {
closeModal();
selected_shift = null;
if (error.status === 400) {
if (error.status === 400 && 'msg' in error.responseJSON && error.responseJSON.msg === "Old service in less than 24hours.") {
alert(`Désolé ! Le service que tu souhaites échanger démarre dans moins de 24h. ` +
`Afin de faciliter la logistique des services, il n'est plus possible de l'échanger. ` +
`Si tu ne peux vraiment pas venir, tu seras noté.e absent.e à ton service. ` +
`Tu devras alors sélectionner un service de rattrapage sur ton espace membre.`);
`Afin de faciliter la logistique des services, il n'est plus possible de l'échanger. ` +
`Si tu ne peux vraiment pas venir, tu seras noté.e absent.e à ton service. ` +
`Tu devras alors sélectionner un service de rattrapage sur ton espace membre.`);
} else if (error.status === 500 && 'msg' in error.responseJSON && error.responseJSON.msg === "Fail to create shift") {
// TODO differentiate error cases!
alert(`Une erreur est survenue. ` +
`Il est néanmoins possible que la requête ait abouti, ` +
`veuillez patienter quelques secondes puis vérifier vos services enregistrés.`);
} else if (error.status === 400 && 'msg' in error.responseJSON && error.responseJSON.msg === "Bad arguments") {
alert(`Une erreur est survenue. ` +
`Il est néanmoins possible que la requête ait abouti, ` +
`veuillez patienter quelques secondes puis vérifier vos services enregistrés.`);
} else {
alert(`Une erreur est survenue. ` +
`Il est néanmoins possible que la requête ait abouti, ` +
......@@ -109,8 +144,149 @@ function add_or_change_shift(new_shift_id) {
}
});
}
return null;
}
/**
* Send request to delete (cancel) a shift registration.
* @param {Int} shift_registration_id shift registration to cancel
*/
function delete_shift_registration(shift_registration_id) {
if (is_time_to('delete_shift_registration')) {
openModal();
tData = 'idPartner=' + partner_data.concerned_partner_id
+ '&idRegister=' + shift_registration_id
+ '&extra_shift_done=' + partner_data.extra_shift_done;
if (partner_data.is_associated_people === "False") {
tData += '&verif_token=' + partner_data.verif_token;
} else if (partner_data.is_associated_people === "True" && block_actions_for_attached_people === "False") {
tData += '&verif_token=' + partner_data.parent_verif_token;
} else {
return false;
}
$.ajax({
type: 'POST',
url: "/shifts/cancel_shift",
dataType:"json",
data: tData,
timeout: 3000,
success: function() {
partner_data.extra_shift_done -= 1;
// Refetch partner shifts list & update DOM
load_partner_shifts(partner_data.concerned_partner_id)
.then(() => {
init_shifts_list();
if (partner_data.extra_shift_done > 0) {
$(".extra_shift_done").text(partner_data.extra_shift_done);
init_delete_registration_buttons();
} else {
$("#can_delete_future_registrations_area").hide();
$(".delete_registration_button").off();
$(".delete_registration_button").hide();
}
closeModal();
setTimeout(() => {
alert("La présence a bien été annulée !");
}, 100);
});
// Redraw calendar
calendar.refetchEvents();
},
error: function() {
closeModal();
alert("Une erreur est survenue.");
}
});
}
return null;
}
/**
* Proceed affecting a shift registration to a/both member(s) of a pair
* @param {string} partner
* @param {string} shift_id
*/
function affect_shift(partner, shift_id) {
if (is_time_to('affect_shift', 1000)) {
tData = 'idShiftRegistration=' + shift_id
+'&idPartner=' + partner_data.partner_id
+ '&affected_partner=' + partner
+ '&verif_token=' + partner_data.verif_token;
tUrl = '/shifts/affect_shift';
$.ajax({
type: 'POST',
url: tUrl,
dataType:"json",
data: tData,
timeout: 3000,
success: function() {
load_partner_shifts(partner_data.concerned_partner_id)
.then(() => {
init_shifts_list();
modal.find(".btn-modal-ok").show();
closeModal();
});
},
error: function() {
init_shifts_list();
modal.find(".btn-modal-ok").show();
closeModal();
alert(`Une erreur est survenue. ` +
`Il est néanmoins possible que la requête ait abouti, ` +
`veuillez patienter quelques secondes puis vérifier vos services enregistrés.`);
}
});
}
}
/**
* Reset a member extra_shift_done to 0
*/
function offer_extra_shift() {
if (is_time_to('offer_extra_shift')) {
openModal();
$.ajax({
type: 'POST',
url: "/members_space/offer_extra_shift",
dataType:"json",
data: {
partner_id: partner_data.concerned_partner_id
},
timeout: 3000,
success: function() {
partner_data.extra_shift_done -= 1;
$("#can_delete_future_registrations_area").hide();
$(".delete_registration_button").off();
$(".delete_registration_button").hide();
closeModal();
alert("Don de service effectué");
},
error: function() {
closeModal();
alert("Une erreur est survenue");
}
});
}
}
/* - DOM */
function init_shifts_list() {
$(".loading-incoming-shifts").hide();
$("#shifts_list").show();
......@@ -120,7 +296,7 @@ function init_shifts_list() {
} else {
$("#shifts_list").empty();
for (shift of incoming_shifts) {
for (let shift of incoming_shifts) {
let shift_line_template = $("#selectable_shift_line_template");
let datetime_shift_start = new Date(shift.date_begin.replace(/\s/, 'T'));
......@@ -131,18 +307,75 @@ function init_shifts_list() {
shift_line_template.find(".shift_line_date").text(f_date_shift_start);
shift_line_template.find(".shift_line_time").text(datetime_shift_start.toLocaleTimeString("fr-fr", time_options));
// Disable or not
shift_line_template.find(".selectable_shift_line").removeClass("btn--primary");
shift_line_template.find(".selectable_shift_line").removeClass("btn");
shift_line_template.find(".selectable_shift_line").removeClass("btn--warning");
if (!can_exchange_shifts()) {
shift_line_template.find(".selectable_shift_line").removeClass("btn--primary");
shift_line_template.find(".selectable_shift_line").addClass("btn");
shift_line_template.find(".checkbox").prop("disabled", "disabled");
} else {
shift_line_template.find(".selectable_shift_line").removeClass("btn");
shift_line_template.find(".selectable_shift_line").addClass("btn--primary");
shift_line_template.find(".checkbox").prop("disabled", false);
shift_line_template.find(".checkbox").prop("value", shift.id);
if (shift.is_makeup==true) {
shift_line_template.find(".selectable_shift_line").addClass("btn--warning");
shift_line_template.find(".checkbox").prop("disabled", false);
shift_line_template.find(".checkbox").prop("value", shift.id);
} else {
shift_line_template.find(".selectable_shift_line").addClass("btn--primary");
shift_line_template.find(".checkbox").prop("disabled", false);
shift_line_template.find(".checkbox").prop("value", shift.id);
}
}
// Set assign shift button
if (partner_data.associated_partner_id === "False" && partner_data.parent_id === "False") {
shift_line_template.find('.affect_associate_registered').hide();
} else {
if (!can_exchange_shifts()) {
shift_line_template.find('.affect_associate_registered').hide();
} else {
shift_line_template.find('.affect_associate_registered').show();
}
shift_line_template.find('.affect_associate_registered').closest(".shift_line_container")
.attr('id', 'shift_id_'+shift.id);
if (shift.associate_registered==="both") {
shift_line_template.find('.affect_associate_registered').text("Les deux");
shift_line_template.find('.affect_associate_registered').addClass('btn--success');
} else if (shift.associate_registered==="partner") {
shift_line_template.find('.affect_associate_registered').addClass('btn--success');
if (partner_data.associated_partner_id !== "False") {
shift_line_template.find('.affect_associate_registered').text(partner_data.name);
} else {
shift_line_template.find('.affect_associate_registered').text(partner_data.parent_name);
}
} else if (shift.associate_registered==="associate") {
shift_line_template.find('.affect_associate_registered').addClass('btn--success');
if (partner_data.associated_partner_id !== "False") {
shift_line_template.find('.affect_associate_registered').text(partner_data.associated_partner_name);
} else {
shift_line_template.find('.affect_associate_registered').text(partner_data.name);
}
} else {
shift_line_template.find('.affect_associate_registered').text("A déterminer");
shift_line_template.find('.affect_associate_registered').addClass('btn--danger');
}
}
// Set delete registration button if shift isn't a makeup
if (partner_data.extra_shift_done > 0 && shift.is_makeup === false) {
if (shift_line_template.find(".delete_registration_button").length === 0) {
let delete_reg_button_template = $("#delete_registration_button_template");
shift_line_template.find(".shift_line_extra_actions").append(delete_reg_button_template.html());
}
} else {
shift_line_template.find(".delete_registration_button").remove();
}
$("#shifts_list").append(shift_line_template.html());
shift_line_template.find('.affect_associate_registered').removeClass('btn--danger');
shift_line_template.find('.affect_associate_registered').removeClass('btn--success');
}
$(".selectable_shift_line").on("click", function(e) {
......@@ -171,6 +404,46 @@ function init_shifts_list() {
}
}
});
$(".affect_associate_registered").on("click", function() {
// Display modal
let id = $(this).closest(".shift_line_container")
.attr('id')
.split('_')[2];
let modal_template = $("#modal_affect_shift");
if (partner_data.associated_partner_id != "False") {
modal_template.find("#shift_partner").text(partner_data.name);
modal_template.find("#shift_associate").text(partner_data.associated_partner_name);
} else {
modal_template.find("#shift_partner").text(partner_data.parent_name);
modal_template.find("#shift_associate").text(partner_data.name);
}
openModal(
modal_template.html(),
() => {
modal.find(".btn-modal-ok").show();
},
"Valider", true, true,
() => {
modal.find(".btn-modal-ok").show();
}
);
modal.find('#shift_partner').on("click", function() {
affect_shift("partner", id);
});
modal.find('#shift_associate').on("click", function() {
affect_shift("associate", id);
});
modal.find('#shift_both').on("click", function() {
affect_shift("both", id);
});
modal.find(".btn-modal-ok").hide();
});
}
}
......@@ -210,6 +483,22 @@ function init_calendar_page() {
$("#need_to_select_makeups_message").show();
}
if (partner_data.extra_shift_done > 0) {
$(".extra_shift_done").text(partner_data.extra_shift_done);
$("#can_delete_future_registrations_area").css('display', 'flex');
$("#offer_extra_shift").on("click", () => {
openModal(
"<p>Je ne souhaite pas supprimer un service futur.</p>",
offer_extra_shift,
"Confirmer",
false
);
});
$("#delete_future_registration").on("click", init_delete_registration_buttons);
}
let default_initial_view = "";
let header_toolbar = {};
......@@ -257,7 +546,7 @@ function init_calendar_page() {
hiddenDays: hidden_days,
events: '/shifts/get_list_shift_calendar/' + partner_data.concerned_partner_id,
eventClick: function(info) {
if (!$(info.el).hasClass("shift_booked")) {
if (!$(info.el).hasClass("shift_booked") && !$(info.el).hasClass("shift_booked_makeup")) {
const new_shift_id = info.event.id;
// Set new shift
......@@ -401,7 +690,6 @@ function init_read_only_calendar_page() {
const hidden_days = days_to_hide.length > 0 ? $.map(days_to_hide.split(", "), Number) : [];
const calendarEl = document.getElementById('read_only_calendar');
console.log(calendarEl)
calendar = new FullCalendar.Calendar(calendarEl, {
locale: 'fr',
......@@ -435,6 +723,35 @@ function init_read_only_calendar_page() {
calendar.render();
}
function init_delete_registration_buttons() {
$(".delete_registration_button").off();
$(".delete_registration_button").hide();
if (partner_data.extra_shift_done > 0) {
$(".delete_registration_button").on("click", function() {
let shift_name = $(this).closest("div")
.parent().parent()
.find(".shift_line_date")
.text()
.trim();
let shift_id = $(this).closest(".shift_line_container")
.attr('id')
.split('_')[2];
openModal(
`<p>Je m'apprête à supprimer ma présence au service du <b>${shift_name}</b></p>`,
() => {
delete_shift_registration(shift_id);
},
"Confirmer",
false
);
});
$(".delete_registration_button").css('display', 'flex');
}
}
function init_shifts_exchange() {
$(".shifts_exchange_page_content").hide();
vw = window.innerWidth;
......@@ -466,9 +783,9 @@ function init_shifts_exchange() {
$(this).removeClass('active');
});
});
} else if (
partner_data.comite === "True") {
} else if (partner_data.comite === "True") {
let msg_template = $("#comite_template");
$(".comite_content_msg").html(msg_template.html());
$("#comite_content").show();
init_read_only_calendar_page();
......@@ -480,7 +797,6 @@ function init_shifts_exchange() {
$(".select_makeups").on('click', () => {
openModal();
// Create 6 month delay
request_delay()
.then(() => {
......@@ -496,7 +812,17 @@ function init_shifts_exchange() {
}
$(window).smartresize(function() {
vw = window.innerWidth;
init_calendar_page();
// only apply if a width threshold is passed
if (
vw > 992 && window.innerWidth <= 992 ||
vw <= 992 && window.innerWidth > 992 ||
vw > 768 && window.innerWidth <= 768 ||
vw <= 768 && window.innerWidth > 768
) {
vw = window.innerWidth;
init_calendar_page();
} else {
vw = window.innerWidth;
}
});
}
......@@ -131,6 +131,70 @@ function update_content() {
/* - Shifts */
/**
* Request a 6 month delay
*/
function request_delay() {
return new Promise((resolve) => {
let today = new Date();
const delay_start = today.getFullYear()+'-'+(today.getMonth()+1)+'-'+today.getDate();
let today_plus_six_month = new Date();
today_plus_six_month.setMonth(today_plus_six_month.getMonth()+6);
const diff_time = Math.abs(today_plus_six_month - today);
const diff_days = Math.ceil(diff_time / (1000 * 60 * 60 * 24));
$.ajax({
type: 'POST',
url: "/shifts/request_delay",
dataType:"json",
data: {
verif_token: (partner_data.is_associated_people === "True") ? partner_data.parent_verif_token : partner_data.verif_token,
idPartner: partner_data.concerned_partner_id,
start_date: delay_start,
duration: diff_days
},
success: function() {
partner_data.cooperative_state = 'delay';
partner_data.date_delay_stop = today_plus_six_month.getFullYear()+'-'+(today_plus_six_month.getMonth()+1)+'-'+today_plus_six_month.getDate();
resolve();
},
error: function(data) {
if (data.status == 403
&& typeof data.responseJSON != 'undefined'
&& data.responseJSON.message === "delays limit reached") {
closeModal();
let msg_template = $("#cant_have_delay_msg_template");
openModal(
msg_template.html(),
() => {
window.location =member_cant_have_delay_form_link;
},
"J'accède au formulaire",
true,
false
);
} else {
err = {msg: "erreur serveur lors de la création du délai", ctx: 'request_delay'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'members_space.home');
closeModal();
alert('Erreur lors de la création du délai.');
}
}
});
});
}
/**
* Prepare a shift line to insert into the DOM.
* Is used in: Home - My Shifts tile ; My Shifts - Incoming shifts section
*
......@@ -158,7 +222,11 @@ function prepare_shift_line_template(date_begin) {
*/
function init_my_info_data() {
$(".choose_makeups").off();
$(".choose_makeups").hide();
$(".remove_future_registration").off();
$(".remove_future_registration").hide();
$(".unsuscribed_form_link").off();
$(".unsuscribed_form_link").hide();
$(".member_shift_name").text(partner_data.regular_shift_name);
......@@ -199,7 +267,6 @@ function init_my_info_data() {
if (
partner_data.makeups_to_do > 0
&& partner_data.is_associated_people === "False"
&& partner_data.cooperative_state !== 'unsubscribed'
) {
$(".choose_makeups").show();
......@@ -223,12 +290,17 @@ function init_my_info_data() {
}
}
if (partner_data.extra_shift_done > 0) {
$(".remove_future_registration").show();
$(".remove_future_registration").on('click', () => {
goto('echange-de-services');
});
}
$(".member_coop_number").text(partner_data.barcode_base);
}
$(document).ready(function() {
// TODO essayer de ne charger les js que au besoin
$.ajaxSetup({ headers: { "X-CSRFToken": getCookie('csrftoken') } });
// If partner is associated (attached), display the pair's main partner shift data
......@@ -242,7 +314,6 @@ $(document).ready(function() {
// For associated people, their parent name is attached in their display name
let partner_name_split = partner_data.name.split(', ');
partner_data.name = partner_name_split[partner_name_split.length - 1];
base_location = (app_env === 'dev') ? '/members_space/' : '/';
......@@ -257,7 +328,7 @@ $(document).ready(function() {
// debouncing function from John Hann
// http://unscriptable.com/index.php/2009/03/20/debouncing-javascript-methods/
var debounce = function (func, threshold, execAsap) {
var timeout;
var timeout = null;
return function debounced () {
var obj = this, args = arguments;
......@@ -282,4 +353,4 @@ $(document).ready(function() {
return fn ? this.bind('resize', debounce(fn)) : this.trigger(sr);
};
})(jQuery, 'smartresize');
\ No newline at end of file
})(jQuery, 'smartresize');
......@@ -12,5 +12,6 @@ urlpatterns = [
url(r'^faqBDM$', views.faqBDM),
url(r'^no_content$', views.no_content),
url(r'^get_shifts_history$', views.get_shifts_history),
url(r'^offer_extra_shift$', views.offer_extra_shift),
url(r'^.*', views.index) # Urls unknown from the server will redirect to index
]
......@@ -28,6 +28,8 @@ def index(request, exception=None):
context = {
'title': 'Espace Membre',
'COMPANY_LOGO': getattr(settings, 'COMPANY_LOGO', None),
'block_actions_for_attached_people' : getattr(settings, 'BLOCK_ACTIONS_FOR_ATTACHED_PEOPLE', True)
}
template = loader.get_template('members_space/index.html')
......@@ -95,8 +97,16 @@ def index(request, exception=None):
if partnerData["parent_id"] is not False:
partnerData["parent_name"] = partnerData["parent_id"][1]
partnerData["parent_id"] = partnerData["parent_id"][0]
md5_calc = hashlib.md5(partnerData['parent_create_date'].encode('utf-8')).hexdigest()
partnerData['parent_verif_token'] = md5_calc
partnerData['makeups_to_do'] = partnerData['parent_makeups_to_do']
partnerData['date_delay_stop'] = partnerData['parent_date_delay_stop']
partnerData['can_have_delay'] = cs.member_can_have_delay(int(partnerData["parent_id"]))
partnerData['extra_shift_done'] = partnerData["parent_extra_shift_done"]
else:
partnerData["parent_name"] = False
partnerData['can_have_delay'] = cs.member_can_have_delay(int(partner_id))
# look for associated partner for parents
cm = CagetteMember(partner_id)
......@@ -108,9 +118,8 @@ def index(request, exception=None):
if (associated_partner is not None and partnerData["associated_partner_name"].find(str(associated_partner["barcode_base"])) == -1):
partnerData["associated_partner_name"] = str(associated_partner["barcode_base"]) + ' - ' + partnerData["associated_partner_name"]
partnerData['can_have_delay'] = cs.member_can_have_delay(int(partner_id))
m = CagetteMembersSpace()
context['show_faq'] = getattr(settings, 'MEMBERS_SPACE_FAQ_TEMPLATE', 'members_space/faq.html')
partnerData["comite"] = m.is_comite(partner_id)
context['partnerData'] = partnerData
......@@ -177,6 +186,7 @@ def my_info(request):
template = loader.get_template('members_space/my_info.html')
context = {
'title': 'Mes Infos',
'understand_my_status': getattr(settings, 'MEMBERS_SPACE_SHOW_UNDERSTAND_MY_STATUS', True)
}
return HttpResponse(template.render(context, request))
......@@ -197,14 +207,16 @@ def shifts_exchange(request):
return HttpResponse(template.render(context, request))
def faqBDM(request):
template = loader.get_template('members_space/faq.html')
context = {
'title': 'foire aux questions',
}
template_path = getattr(settings, 'MEMBERS_SPACE_FAQ_TEMPLATE', 'members_space/faq.html')
content = ''
if template_path:
template = loader.get_template(template_path)
context = {
'title': 'foire aux questions',
}
content = template.render(context, request)
msettings = MConfig.get_settings('members')
return HttpResponse(template.render(context, request))
return HttpResponse(content)
def no_content(request):
""" Endpoint the front-end will call to load the "No content" page. """
......@@ -225,4 +237,13 @@ def get_shifts_history(request):
date_from = getattr(settings, 'START_DATE_FOR_SHIFTS_HISTORY', '2018-01-01')
res["data"] = m.get_shifts_history(partner_id, limit, offset, date_from)
return JsonResponse(res)
\ No newline at end of file
return JsonResponse(res)
def offer_extra_shift(request):
res = {}
partner_id = int(request.POST['partner_id'])
m = CagetteMember(partner_id)
res = m.update_extra_shift_done(0)
return JsonResponse(res)
/* Comments : */
/* - Screens */
/* -- Sections */
/* - Common */
.page_body{
position: relative;
}
......@@ -9,8 +15,6 @@
right: 0;
}
/* - Common */
.pill {
border-radius: 30px;
min-width: 200px;
......@@ -52,7 +56,8 @@
}
/* - Order selection screen */
#new_order_area {
#new_order_area,
#existing_orders_area {
margin-bottom: 40px;
}
......@@ -163,6 +168,11 @@
border-bottom: 1px solid #004aa6;
}
#common_info_editor_container {
width: 50%;
margin: 15px auto;
}
/* -- Order data */
#order_data_container {
font-size: 1.8rem;
......@@ -173,7 +183,7 @@
}
#order_forms_container {
margin-top: 20px;
margin: 25px 0;
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
......@@ -193,12 +203,12 @@
min-width: 200px;
}
#date_planned_input, #coverage_days_input, #stats_date_period_select {
#date_planned_input, #coverage_days_input, #targeted_amount_input, #percent_adjust_input, #stats_date_period_select {
border-radius: 3px;
}
#coverage_form > div {
display:inline-block;
display:block;
float:left;
}
......@@ -206,11 +216,11 @@
margin-right: 3px;
}
#coverage_days_input, #percent_adjust_input {
#coverage_days_input, #targeted_amount_input, #percent_adjust_input {
display: block;
}
#coverage_days_input {
#coverage_days_input, #targeted_amount_input {
margin-bottom: 3px;
}
......@@ -260,7 +270,8 @@
padding: .5rem .5rem;
}
.supplier_package_qty {
.supplier_package_qty,
.supplier_price {
font-style: italic;
font-size: 1.3rem;
}
......@@ -317,16 +328,18 @@
justify-content: center;
align-items: center;
flex-wrap: wrap;
margin: 30px 0 20px 0;
margin: 15px 0;
position: -webkit-sticky;
position: sticky;
top: 140px;
z-index: 5;
pointer-events: none;
}
.supplier_pill {
background-color: #a0daff;
border: 1px solid #6ea8cc;
background-color: #ffebcd;
border: 2px solid black;
pointer-events: auto;
}
.pill_supplier_name {
......@@ -367,7 +380,7 @@
width: 90%;
}
/* product actions modal*/
/* -- Product actions modal*/
.npa-options {
width: fit-content;
text-align: left;
......@@ -376,6 +389,59 @@
.npa-options label {
display: block;
}
.product_actions_container {
display: flex;
flex-direction: column;
}
.product_actions_section {
width: 100%;
display: flex;
margin: 1em 0;
}
.product_actions_column {
width: 50%;
}
.product_actions_full_column {
width: 100%;
}
.product_actions_column .tooltip {
margin-left: 5px;
}
.product_prices_title {
margin-bottom: 0 !important;
}
.product_prices_area {
margin: 20px 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.product_price_action {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
}
.modal_product_actions_title {
font-weight: bold;
font-size: 2.2rem;
margin-bottom: 10px;
}
.checkbox_action_disabled {
cursor: not-allowed;
opacity: .5;
}
/* - Orders created screen */
.order_created_header {
......
......@@ -9,13 +9,21 @@ var suppliers_list = [],
new_product_supplier_association = {
package_qty: null,
price: null
};
},
qties_values = {},
clicked_order_pill = null,
userAgent = navigator.userAgent;
timerId = null,
info_editor = null;
var dbc = null,
sync = null,
fingerprint = null,
order_doc = {
_id: null,
coverage_days: null,
targeted_amount: null,
stats_date_period: '',
last_update: {
timestamp: null,
......@@ -25,9 +33,11 @@ var dbc = null,
selected_suppliers: [],
selected_rows: []
},
fingerprint = null;
var clicked_order_pill = null;
common_info_doc_name = "common_info",
info_doc = {
_id: null,
content: ""
};
/* - UTILS */
......@@ -43,6 +53,7 @@ function reset_data() {
order_doc = {
_id: null,
coverage_days: null,
targeted_amount: null,
stats_date_period: '',
last_update : {
timestamp: null,
......@@ -116,13 +127,83 @@ function _compute_stats_date_from() {
return val;
}
function debounceFunction(func, delay = 1000) {
clearTimeout(timerId);
timerId = setTimeout(func, delay);
}
/* - PRODUCTS */
var process_new_product_qty = function(input) {
// Remove line coloring on input blur
const row = $(input).closest('tr');
row.removeClass('focused_line');
let val = ($(input).val() == '') ? 0 : $(input).val();
const id_split = $(input).attr('id')
.split('_');
const prod_id = id_split[1];
const supplier_id = id_split[3];
if (val == -1) {
let modal_end_supplier_product_association = $('#templates #modal_end_supplier_product_association');
const product = products.find(p => p.id == prod_id);
modal_end_supplier_product_association.find(".product_name").text(product.name);
const supplier = selected_suppliers.find(s => s.id == supplier_id);
modal_end_supplier_product_association.find(".supplier_name").text(supplier.display_name);
openModal(
modal_end_supplier_product_association.html(),
() => {
if (is_time_to('validate_end_supplier_product_association')) {
end_supplier_product_association(product, supplier);
}
},
'Valider',
false,
true,
() => {
// Reset value in input on cancel
const psi = product.suppliersinfo.find(psi_item => psi_item.supplier_id == supplier_id);
$(input).val(psi.qty);
}
);
} else {
val = parseFloat(val);
// If value is a number
if (!isNaN(val)) {
// Save value
save_product_supplier_qty(prod_id, supplier_id, val);
// Update row
const product = products.find(p => p.id == prod_id);
const new_row_data = prepare_datatable_data([product.id])[0];
products_table.row($(input).closest('tr')).data(new_row_data)
.draw();
debounceFunction(update_cdb_order);
display_total_values();
} else {
$(input).val('');
}
}
};
/**
* Add a product.
*
* @returns -1 if validation failed, 0 otherwise
*/
function add_product() {
const user_input = $("#product_input").val();
......@@ -165,7 +246,7 @@ function add_product() {
res.default_code = ' ';
products.unshift(res);
update_main_screen({'sort_order_dir':'desc'});
update_cdb_order();
debounceFunction(update_cdb_order);
} else {
alert("L'article n'a pas toutes les caractéristiques pour être ajouté.");
}
......@@ -183,50 +264,103 @@ function add_product() {
return 0;
}
function compute_purchase_qty_for_coverage(product, coeff, stock, incoming_qty, daily_conso, days) {
let purchase_qty_for_coverage = 0,
purchase_package_qty_for_coverage = 0;
if (stock == 0 && daily_conso == 0) {
purchase_package_qty_for_coverage = 1;
} else {
purchase_qty_for_coverage = days * daily_conso - stock - incoming_qty + product.minimal_stock;
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;
if (coeff != 1) {
purchase_package_qty_for_coverage *= coeff;
}
}
return Math.ceil(purchase_package_qty_for_coverage); // return Round up to unit for all products
}
function compute_and_affect_product_supplier_quantities(coeff, days) {
for (const [
key,
product
] of Object.entries(products)) {
if ('suppliersinfo' in product && product.suppliersinfo.length > 0) {
// 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;
let purchase_package_qty_for_coverage = compute_purchase_qty_for_coverage(product, coeff, stock, incoming_qty, daily_conso, days);
// Set qty to purchase for first supplier only
products[key].suppliersinfo[0].qty = purchase_package_qty_for_coverage;
}
}
}
/**
* Compute the qty to buy for each product, depending the coverage days.
* Set the computed qty for the first supplier only.
*/
function compute_products_coverage_qties() {
const pc_adjust = $('#percent_adjust_input').val();
let coeff = 1;
if (!isNaN(parseFloat(pc_adjust))) {
coeff = (1 + parseFloat(pc_adjust) /100);
}
return new Promise((resolve) => {
const pc_adjust = $('#percent_adjust_input').val();
let coeff = 1;
if (order_doc.coverage_days != null) {
if (!isNaN(parseFloat(pc_adjust))) {
coeff = (1 + parseFloat(pc_adjust) /100);
}
order_doc.coeff = coeff;
for (const [
key,
product
] 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 + product.minimal_stock;
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;
if (order_doc.coverage_days != null) {
compute_and_affect_product_supplier_quantities(coeff, order_doc.coverage_days);
} else if (order_doc.targeted_amount != null) {
const small_step = 0.1,
max_iter = 182; // Assume that no more than 1/2 year coverage is far enough
let go_on = true,
iter = 0,
days = 1,
step = 1;
//Let's compute the nearst amount, by changing days quantity
while (go_on == true && iter < max_iter) {
order_total_value = 0;
compute_and_affect_product_supplier_quantities(coeff, days);
_compute_total_values_by_supplier();
for (let supplier of selected_suppliers) {
order_total_value += supplier.total_value;
}
let order_total_value_f = parseFloat(order_total_value),
targeted_amount_f = parseFloat(order_doc.targeted_amount);
if (order_doc.coeff != 1) {
purchase_package_qty_for_coverage *= order_doc.coeff;
if (order_total_value_f >= targeted_amount_f) {
if (order_total_value_f != targeted_amount_f && iter < max_iter) {
step = small_step; // we have gone too far, let's go back, using small step
days -= step;
}
} else {
if (step == small_step) {
// amount was above the target, let's compute again with the previous value
go_on = false;
compute_and_affect_product_supplier_quantities(coeff, days + step);
} else {
days += step;
}
}
// 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;
iter++;
}
}
}
resolve();
});
}
/**
......@@ -261,8 +395,12 @@ function check_products_data() {
traditional: true,
contentType: "application/json; charset=utf-8",
success: function(data) {
let loaded_products_ids = products.map(p => p.id);
// Going through products fetched from server
for (let product of data.res.products) {
const p_index = products.findIndex(p => p.id == product.id);
const p_id = product.id;
const p_index = products.findIndex(p => p.id == p_id);
if (p_index === -1) {
// Add product if it wasn't fetched before (made available since last access to order)
......@@ -283,9 +421,41 @@ function check_products_data() {
}
}
}
// Remove fetched product id from loaded products list
const loaded_p_index = loaded_products_ids.indexOf(p_id);
if (loaded_p_index > -1) {
loaded_products_ids.splice(loaded_p_index, 1);
}
}
$('.notifyjs-wrapper').trigger('notify-hide');
/**
* If loaded p_ids are remaining:
* these products were loaded but don't match the conditions to be fetched anymore.
* Remove them.
*/
if (loaded_products_ids.length > 0) {
for (pid of loaded_products_ids) {
const p_index = products.findIndex(p => p.id == pid);
const p_name = products[p_index].name;
products.splice(p_index, 1);
$.notify(
`Produit "${p_name}" retiré de la commande.\nIl a probablement été passé en archivé ou en NPA sur un autre poste.`,
{
globalPosition:"top left",
className: "info",
autoHideDelay: 12000,
clickToHide: false
}
);
}
}
resolve();
},
error: function(data) {
......@@ -346,7 +516,7 @@ function update_product_ref(input_el, p_id, p_index) {
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
success: () => {
update_cdb_order();
debounceFunction(update_cdb_order);
$(".actions_buttons_area .right_action_buttons").notify(
"Référence sauvegardée !",
......@@ -424,7 +594,7 @@ function add_supplier() {
save_supplier_products(supplier, data.res.products);
update_main_screen();
$("#supplier_input").val("");
update_cdb_order();
debounceFunction(update_cdb_order);
closeModal();
},
error: function(data) {
......@@ -460,7 +630,7 @@ function remove_supplier(supplier_id) {
products = products.filter(product => product.suppliersinfo.length > 0);
update_main_screen();
update_cdb_order();
debounceFunction(update_cdb_order);
}
......@@ -503,13 +673,14 @@ function save_supplier_product_association(product, supplier, cell) {
traditional: true,
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
success: () => {
success: (res_data) => {
// Save supplierinfo in product
if (!('suppliersinfo' in product)) {
product.suppliersinfo = [];
}
product.suppliersinfo.push({
id: res_data.res.psi_id,
supplier_id: supplier.id,
package_qty: package_qty,
product_code: false,
......@@ -527,7 +698,7 @@ function save_supplier_product_association(product, supplier, cell) {
products_table.row(row).data(new_row_data)
.draw();
update_cdb_order();
debounceFunction(update_cdb_order);
closeModal();
},
error: function(data) {
......@@ -581,7 +752,7 @@ function end_supplier_product_association(product, supplier) {
// Update table
display_products();
update_cdb_order();
debounceFunction(update_cdb_order);
closeModal();
},
error: function(data) {
......@@ -688,21 +859,44 @@ function _compute_total_values_by_supplier() {
/* - PRODUCT */
function save_products_npa_minimal_stock(product, inputs) {
let actions = {npa: [], minimal_stock: 0, id: product.id, name: product.name};
inputs.each(function (i,e) {
const input = $(e)
if (input.attr('type') == 'checkbox') {
function commit_actions_on_product(product, inputs) {
let actions = {
npa: [],
to_archive: false,
minimal_stock: 0,
qty_available: 0,
id: product.id,
name: product.name,
suppliersinfo: []
};
inputs.each(function (i, e) {
const input = $(e);
if (input.attr('name') == 'npa-actions') {
if (input.prop('checked') == true) {
actions.npa.push(input.val())
actions.npa.push(input.val());
}
} else if (input.attr('name') == "minimal_stock") {
actions.minimal_stock = input.val()
actions.minimal_stock = input.val();
} else if (input.attr('name') == "archive-action") {
if (input.prop('checked') == true && product.incoming_qty === 0) {
actions.to_archive = true;
}
} else if (input.attr('name') == "actual_stock") {
actions.qty_available = parseFloat(input.val());
} else if (input.attr('class') !== undefined && input.attr('class').includes("product_supplier_price")) {
actions.suppliersinfo.push({
supplierinfo_id: parseInt(input.attr('supplierinfo_id')),
price: parseFloat(input.val())
});
}
});
openModal();
$.ajax({
type: "POST",
url: "/products/update_npa_and_minimal_stock",
url: "/products/commit_actions_on_product",
dataType: "json",
traditional: true,
contentType: "application/json; charset=utf-8",
......@@ -710,43 +904,63 @@ function save_products_npa_minimal_stock(product, inputs) {
success: () => {
const index = products.findIndex(p => p.id == product.id);
// Give time for modal to fade
setTimeout(function() {
$(".actions_buttons_area .right_action_buttons").notify(
"Actions enregistrées !",
{
elementPosition:"bottom right",
className: "success",
arrowShow: false
}
);
}, 500);
products[index].minimal_stock = actions.minimal_stock;
if (actions.npa.length > 0) {
// Remove NPA products
if (actions.npa.length > 0 || actions.to_archive === true) {
// Remove NPA & archived products
products.splice(index, 1);
update_main_screen();
update_cdb_order();
debounceFunction(update_cdb_order);
}
closeModal();
check_products_data()
.then(() => {
update_cdb_order();
update_main_screen();
closeModal();
// Give time for modal to fade
setTimeout(function() {
$(".actions_buttons_area .right_action_buttons").notify(
"Actions enregistrées !",
{
elementPosition:"bottom right",
className: "success",
arrowShow: false
}
);
}, 500);
});
},
error: function(data) {
let msg = "erreur serveur lors de la sauvegarde".
msg += ` (product_tmpl_id: ${product.id})`;
err = {msg: msg, ctx: 'save_products_npa_minimal_stock'};
err = {msg: msg, ctx: 'commit_actions_on_product'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
closeModal();
alert('Erreur lors de la sauvegarde de la donnée. Veuillez ré-essayer plus tard.');
update_main_screen();
try {
if (data.responseJSON.code === "archiving_with_incoming_qty") {
alert("Ce produit a des quantités entrantes, vous ne pouvez pas l'archiver.");
} else if (data.responseJSON.code === "error_stock_update") {
alert('Erreur lors de la mise à zéro du stock du produit archivé. Les actions ont bien été réalisées.');
} else {
alert('Erreur lors de la sauvegarde des données. Veuillez ré-essayer plus tard.');
}
} catch (error) {
alert('Erreur lors de la sauvegarde des données. Veuillez ré-essayer plus tard.');
}
check_products_data()
.then(() => {
update_cdb_order();
update_main_screen();
closeModal();
});
}
});
}
......@@ -1160,7 +1374,7 @@ function goto_main_screen(doc) {
check_products_data()
.then(() => {
update_cdb_order();
debounceFunction(update_cdb_order);
update_main_screen();
switch_screen();
});
......@@ -1224,12 +1438,23 @@ function display_suppliers() {
});
}
/**
* Compute data to display in products table
*
* Package qties & prices are related to suppliers,
* so 1 product can have multiple values for these.
* In case of different values, display value under input in supplier column
*
* @param {Object} product
* @returns Object of computed data to add to product object
*/
function _compute_product_data(product) {
let item = {};
/* Supplier related data */
let purchase_qty = 0; // Calculate product's total purchase qty
let p_package_qties = []; // Look for differences in package qties
let p_price = [];
for (let p_supplierinfo of product.suppliersinfo) {
// Preset qty for input if product related to supplier: existing qty or null (null -> qty to be set, display an empty input)
......@@ -1244,6 +1469,7 @@ function _compute_product_data(product) {
// Store temporarily product package qties
p_package_qties.push(p_supplierinfo.package_qty);
p_price.push(p_supplierinfo.price);
}
item.purchase_qty = purchase_qty;
......@@ -1256,13 +1482,19 @@ function _compute_product_data(product) {
}
if (p_package_qties.length == 0 || !p_package_qties.every((val, i, arr) => val === arr[0])) {
// Don't display package qty if no supplierinf or if not all package qties are equals,
// Don't display package qty if no supplierinfo or if not all package qties are equals
item.package_qty = 'X';
} else {
// If all package qties are equals, display it
item.package_qty = p_package_qties[0];
}
if (p_price.length == 0 || !p_price.every((val, i, arr) => val === arr[0])) {
item.price = 'X';
} else {
item.price = p_price[0];
}
/* Coverage related data */
const coverage_days = (order_doc.coverage_days !== null) ? order_doc.coverage_days : 0;
let qty_not_covered = 0;
......@@ -1400,6 +1632,7 @@ function prepare_datatable_columns() {
let content = `<div id="${base_id}_cell_content" class="custom_cell_content">
<input type="number" class="product_qty_input" id="${base_id}_qty_input" min="-1" value=${data}>`;
// Add package qty & price data if they differ between suppliers
if (full.package_qty === 'X') {
let product_data = products.find(p => p.id == full.id);
......@@ -1410,6 +1643,16 @@ function prepare_datatable_columns() {
}
}
if (full.price === 'X') {
let product_data = products.find(p => p.id == full.id);
if (product_data !== undefined) {
let supplierinfo = product_data.suppliersinfo.find(psi => psi.supplier_id == supplier.id);
content += `<span class="supplier_price">Prix HT : ${supplierinfo.price} €</span>`;
}
}
content += `</div>`;
return content;
......@@ -1433,19 +1676,30 @@ function prepare_datatable_columns() {
});
columns.push({
data: "purchase_qty",
title: "Qté Achat",
data: "price",
title: "Prix HT",
className: "dt-body-center",
render: function (data) {
return (data === 'X') ? data : `${data} €`;
},
width: "4%"
});
columns.push({
data: "qty_not_covered",
title: "Besoin non couvert (qté)",
data: "purchase_qty",
title: "Qté Achat",
className: "dt-body-center",
width: "4%"
});
// Not in use for now
// columns.push({
// data: "qty_not_covered",
// title: "Besoin non couvert (qté)",
// className: "dt-body-center",
// width: "4%"
// });
columns.push({
data: "days_covered",
title: "Jours de couverture",
......@@ -1457,7 +1711,7 @@ function prepare_datatable_columns() {
title: ``,
className: "dt-body-center",
orderable: false,
render: function (data) {
render: function () {
return `<button type="button" class="btn--primary product_actions">Actions</button>`;
},
width: "4%"
......@@ -1538,7 +1792,7 @@ function display_products(params) {
}
}
});
products_table.search('');
$('.main').show();
$('#main_content_footer').show();
$('#do_inventory').show();
......@@ -1550,71 +1804,17 @@ function display_products(params) {
row.addClass('focused_line');
});
// Manage data on inputs blur
$('#products_table').on('blur', 'tbody td .product_qty_input', function () {
// Remove line coloring on input blur
const row = $(this).closest('tr');
row.removeClass('focused_line');
let val = ($(this).val() == '') ? 0 : $(this).val();
const id_split = $(this).attr('id')
.split('_');
const prod_id = id_split[1];
const supplier_id = id_split[3];
if (val == -1) {
let modal_end_supplier_product_association = $('#templates #modal_end_supplier_product_association');
const product = products.find(p => p.id == prod_id);
modal_end_supplier_product_association.find(".product_name").text(product.name);
const supplier = selected_suppliers.find(s => s.id == supplier_id);
modal_end_supplier_product_association.find(".supplier_name").text(supplier.display_name);
openModal(
modal_end_supplier_product_association.html(),
() => {
if (is_time_to('validate_end_supplier_product_association')) {
end_supplier_product_association(product, supplier);
}
},
'Valider',
false,
true,
() => {
// Reset value in input on cancel
const psi = product.suppliersinfo.find(psi_item => psi_item.supplier_id == supplier_id);
$(this).val(psi.qty);
}
);
} else {
val = parseFloat(val);
// If value is a number
if (!isNaN(val)) {
// Save value
save_product_supplier_qty(prod_id, supplier_id, val);
// Update row
const product = products.find(p => p.id == prod_id);
const new_row_data = prepare_datatable_data([product.id])[0];
products_table.row($(this).closest('tr')).data(new_row_data)
.draw();
update_cdb_order();
display_total_values();
} else {
$(this).val('');
}
}
})
// Manage data on inputs blur
$('#products_table')
.on('focus', 'tbody td .product_qty_input', function () {
this.select();
})
.on('blur', 'tbody td .product_qty_input', function () {
process_new_product_qty(this);
})
.on('keypress', 'tbody td .product_qty_input', function(e) {
// Validate on Enter pressed
// Validate on Enter pressed
if (e.which == 13) {
$(this).blur();
}
......@@ -1624,15 +1824,18 @@ function display_products(params) {
e.preventDefault();
// On arrow up pressed, focus next row input
let next_input = $(this).closest("tr").prev().find(".product_qty_input");
let next_input = $(this).closest("tr")
.prev()
.find(".product_qty_input");
next_input.focus();
// Scroll to a position where the target input is not hidden by the sticky suppliers container
const suppliers_container_top_offset =
$("#suppliers_container").offset().top
- $(window).scrollTop()
+ $("#suppliers_container").outerHeight();
const next_input_top_offset = next_input.offset().top - $(window).scrollTop();
$("#suppliers_container").offset().top
- $(window).scrollTop()
+ $("#suppliers_container").outerHeight();
const next_input_top_offset = next_input.offset().top - $(window).scrollTop();
if (next_input_top_offset < suppliers_container_top_offset) {
window.scrollTo({
......@@ -1643,38 +1846,72 @@ function display_products(params) {
e.preventDefault();
// On arrow down pressed, focus previous row input
$(this).closest("tr").next().find(".product_qty_input").focus();
$(this).closest("tr")
.next()
.find(".product_qty_input")
.focus();
} else if (e.which == 13) {
e.preventDefault();
// On enter pressed, focus previous row input
$(this).closest("tr").next().find(".product_qty_input").focus();
$(this).closest("tr")
.next()
.find(".product_qty_input")
.focus();
}
})
.on('click', 'tbody td .product_actions', function(e){
// Save / unsave selected row
.on('click', 'tbody td .product_actions', function() {
// Save / unsave selected row
const p_id = products_table.row($(this).closest('tr')).data().id;
const product = products.find(p => p.id == p_id);
let modal_product_actions = $('#templates #modal_product_actions');
modal_product_actions.find(".product_name").text(product.name);
modal_product_actions.find(".actual_stock_input").val(product.qty_available);
const product_can_be_archived = product.incoming_qty === 0;
if (product_can_be_archived == true) {
modal_product_actions.find('input[name="archive-action"]').prop("disabled", false);
modal_product_actions.find('input[name="archive-action"]').closest("label")
.removeClass("checkbox_action_disabled");
} else {
modal_product_actions.find('input[name="archive-action"]').prop("disabled", true);
modal_product_actions.find('input[name="archive-action"]').closest("label")
.addClass("checkbox_action_disabled");
}
//modal_product_actions.find(".product_npa").text(null ? 'Ne Pas Acheter' : 'Peut Être Acheté');
let product_price_action_template = $('#templates #product_price_action_template');
modal_product_actions.find(".product_prices_area").empty();
for (let supplierinfo of product.suppliersinfo) {
let supplier = suppliers_list.find(s => s.id == supplierinfo.supplier_id);
product_price_action_template.find(".supplier_name").text(supplier.display_name);
product_price_action_template.find(".product_supplier_price").attr('supplierinfo_id', supplierinfo.id);
modal_product_actions.find(".product_prices_area").append(product_price_action_template.html());
}
openModal(
modal_product_actions.html(),
() => {
if (is_time_to('validate_product_actions')) {
save_products_npa_minimal_stock(product, modal.find('input'));
commit_actions_on_product(product, modal.find('input'));
}
},
'Valider',
false
);
modal.find('input[name="minimal_stock"]').val(product.minimal_stock)
// Set inputs val after modal is displayed
modal.find('input[name="minimal_stock"]').val(product.minimal_stock);
modal.find('input[name="actual_stock"]').val(product.qty_available);
for (let supplierinfo of product.suppliersinfo) {
modal.find(`input[supplierinfo_id="${supplierinfo.id}"]`).val(supplierinfo.price);
}
});
......@@ -1892,7 +2129,11 @@ function update_main_screen(params) {
} else {
$("#coverage_days_input").val('');
}
if (order_doc.targeted_amount !== null) {
$('#targeted_amount_input').val(order_doc.targeted_amount);
} else {
$('#targeted_amount_input').val('');
}
if (order_doc.coeff && order_doc.coeff != 1) {
$("#percent_adjust_input").val(-Math.ceil((1 - order_doc.coeff) * 100));
}
......@@ -1905,7 +2146,7 @@ function update_main_screen(params) {
}
function display_average_consumption_explanation() {
openModal($('#explanations').html())
openModal($('#explanations').html());
}
/**
* Update DOM display on the order selection screen
......@@ -1919,20 +2160,25 @@ function update_order_selection_screen() {
// Remove listener before recreating them
$(".order_pill").off();
// Reset orders data
let existing_orders_container = $("#existing_orders");
existing_orders_container.empty();
$('#new_order_name').val('');
if (result.rows.length === 0) {
if (
result.rows.length === 0
|| result.rows.length === 0 && result.rows[0].id === common_info_doc_name) {
existing_orders_container.append(`<i>Aucune commande en cours...</i>`);
} else {
for (let row of result.rows) {
let template = $("#templates #order_pill_template");
if (row.id !== common_info_doc_name) {
let template = $("#templates #order_pill_template");
template.find(".pill_order_name").text(row.id);
template.find(".pill_order_name").text(row.id);
existing_orders_container.append(template.html());
existing_orders_container.append(template.html());
}
}
$(".order_pill").on("click", order_pill_on_click);
......@@ -2057,6 +2303,15 @@ function init_pouchdb_sync() {
);
back();
break;
} else if (doc._id === common_info_doc_name) {
init_info_editor();
$.notify(
"Nouveau message dans le bloc d'information !",
{
globalPosition:"top right",
className: "info"
}
);
}
}
......@@ -2072,11 +2327,162 @@ function init_pouchdb_sync() {
});
}
/* - INFO AREA */
/**
* Init the Quill module (Text editor)
* @param {Object} params
*/
function quillify(params) {
info_editor = new Quill(params.id, {
modules: {
toolbar: [
[
{ header: [
1,
2,
false
] }
],
[
'bold',
'italic',
'underline'
],
[
{ 'size': [
'small',
false,
'large',
'huge'
] }
],
[
{ 'color': [] },
{ 'background': [] }
]
]
},
placeholder: "Indiquez ici un message pour le reste de l'équipe",
theme: 'snow'
});
info_editor.root.innerHTML = params.content;
}
/**
* Init object & dom for the info editor.
* Await retrieving content.
*/
async function init_info_editor() {
let info_content = await get_or_create_common_info();
// Reset info editor
info_editor = null;
$("#common_info_editor_container").empty();
$("#common_info_editor_container").append(`<div id="common_info_editor"></div>`);
// Init text editor for Info textarea
let quill_params = {
id: '#common_info_editor',
content: info_content
};
quillify(quill_params);
$("#save_common_info").on("click", function() {
if (is_time_to('save_common_info', 1000)) {
let content = $("#common_info_editor").find('.ql-editor')
.html();
if (content === "<p><br></p>") {
content = "";
}
update_common_info(content);
}
});
}
/**
* Get common info HTML content. If doc doesn't exist, create & return empty string
* @returns String HTML content | ""
*/
function get_or_create_common_info() {
// todo await async
return new Promise((resolve) => {
dbc.get(common_info_doc_name).then((doc) => {
info_doc = doc;
resolve(doc.content);
})
.catch(function (err) {
if (err.status == 404) {
// First access, create
info_doc._id = common_info_doc_name;
dbc.put(info_doc, function callback(err, result) {
if (!err) {
info_doc._rev = result.rev;
} else {
$.notify(
"Erreur lors de l'initialisation du bloc d'informations",
{
globalPosition:"top right",
className: "error"
}
);
console.log(err);
}
resolve("");
});
} else {
$.notify(
"Erreur lors de la récupération du bloc d'informations",
{
globalPosition:"top right",
className: "error"
}
);
console.log(err);
resolve("");
}
});
});
}
/**
* Update couchdb info document with textarea (HTML) content
* @param {String} content
*/
function update_common_info(content) {
info_doc.content = content;
dbc.put(info_doc, function callback(err, result) {
if (!err) {
info_doc._rev = result.rev;
$.notify(
"Bloc d'informations mis à jour",
{
globalPosition:"top right",
className: "success"
}
);
} else {
$.notify(
"Erreur lors de la mise à jour du bloc d'informations",
{
globalPosition:"top right",
className: "error"
}
);
console.log(err);
}
});
}
$(document).ready(function() {
if (coop_is_connected()) {
$('#new_order_form').show();
$('#existing_orders_area').show();
$('#common_info_area').show();
fingerprint = new Fingerprint({canvas: true}).get();
$.ajaxSetup({ headers: { "X-CSRFToken": getCookie('csrftoken') } });
......@@ -2093,18 +2499,28 @@ $(document).ready(function() {
$("#coverage_form").on("submit", function(e) {
e.preventDefault();
if (is_time_to('submit_coverage_form', 1000)) {
let val = $("#coverage_days_input").val();
val = parseInt(val);
let days_val = $("#coverage_days_input").val(),
amount_val = $('#targeted_amount_input').val();
days_val = parseInt(days_val);
amount_val = parseInt(amount_val);
if (isNaN(days_val)) days_val = null;
if (isNaN(amount_val)) amount_val = null;
if (days_val !== null || amount_val !== null) {
order_doc.coverage_days = days_val;
order_doc.targeted_amount = amount_val;
compute_products_coverage_qties()
.then(() => {
debounceFunction(update_cdb_order);
update_main_screen();
});
if (!isNaN(val)) {
order_doc.coverage_days = val;
compute_products_coverage_qties();
update_cdb_order();
update_main_screen();
} else {
$("#coverage_days_input").val(order_doc.coverage_days);
alert(`Valeur non valide pour le nombre de jours de couverture !`);
$("#coverage_days_input").val(order_doc.coverage_days || '');
$('#targeted_amount_input').val(order_doc.targeted_amount || '');
alert("Ni le nombre de jours de couverture, ni le montant à atteindre sont correctement renseignés");
}
}
});
......@@ -2158,10 +2574,13 @@ $(document).ready(function() {
check_products_data()
.then(() => {
compute_products_coverage_qties();
update_main_screen();
update_cdb_order();
closeModal();
compute_products_coverage_qties()
.then(() => {
update_main_screen();
debounceFunction(update_cdb_order);
closeModal();
});
});
}
});
......@@ -2177,7 +2596,7 @@ $(document).ready(function() {
openModal();
check_products_data()
.then(() => {
update_cdb_order();
debounceFunction(update_cdb_order);
update_main_screen();
$("#toggle_action_buttons").click();
closeModal();
......@@ -2279,7 +2698,7 @@ $(document).ready(function() {
return 0;
});
$(document).on("click",".fa-info-circle", display_average_consumption_explanation)
$(document).on("click", ".average_consumption_explanation_icon", display_average_consumption_explanation);
$.datepicker.regional['fr'] = {
monthNames: [
......@@ -2311,11 +2730,13 @@ $(document).ready(function() {
// Order selection screen
update_order_selection_screen();
init_info_editor();
$("#new_order_form").on("submit", function(e) {
e.preventDefault();
if (is_time_to('submit_new_order_form', 1000)) {
create_cdb_order();
}
});
......@@ -2340,8 +2761,6 @@ $(document).ready(function() {
$("#supplier_input").autocomplete({
source: suppliers_list.map(a => a.display_name)
});
},
error: function(data) {
err = {msg: "erreur serveur lors de la récupération des fournisseurs", ctx: 'get_suppliers'};
......@@ -2416,6 +2835,39 @@ $(document).ready(function() {
alert('Erreur lors de la récupération des articles, rechargez la page plus tard');
}
});
$(document).on('click', '.accordion', function() {
/* Toggle between adding and removing the "active" class,
to highlight the button that controls the panel */
this.classList.toggle("active");
/* Toggle between hiding and showing the active panel */
var panel = this.nextElementSibling;
if (panel.style.display === "block") {
panel.style.display = "none";
} else {
panel.style.display = "block";
}
});
if (/Firefox\//.exec(userAgent)) {
// needed to prevent bug using number input arrow to change quantity (https://bugzilla.mozilla.org/show_bug.cgi?id=1012818)
// Have to capture mousedown and mouseup events, instead of using only click event
// Indeed, capturing click only remove the ability to click to have focus on the input to type a number.
$(document).on("mousedown", '[type="number"]', function() {
qties_values[$(this).attr('id')] = $(this).val();
});
$(document).on("mouseup", '[type="number"]', function() {
try {
if ($(this).val() != qties_values[$(this).attr('id')]) {
process_new_product_qty(this);
}
} catch (err) {
console.log(err);
}
});
}
} else {
$('#not_connected_content').show();
}
......
......@@ -70,6 +70,11 @@ class OdooAPI:
return self.models.execute_kw(self.db, self.uid, self.passwd,
entity, 'create', [fields])
def delete(self, entity, ids):
"""Destroy entity instance by given ids."""
return self.models.execute_kw(self.db, self.uid, self.passwd,
entity, 'unlink', [ids])
def execute(self, entity, method, ids, params={}):
return self.models.execute_kw(self.db, self.uid, self.passwd,
entity, method, [ids], params)
......
......@@ -28,6 +28,9 @@
- COMPANY_NAME = 'Les Grains de Sel'
- COMPANY_LOGO = 'https://domaine.name/img/logo.png'
- ADMIN_IDS = [13]
Used to show hidden things. for example, input barcode in shelf adding product (Odoo user id array)
......@@ -66,6 +69,8 @@
- COOP_BARCODE_RULE_ID = 11
- ASSOCIATE_BARCODE_RULE_ID = 12
- FUNDRAISING_CAT_ID = 1
- PARTS_PRICE_UNIT = 10.0
......@@ -126,6 +131,13 @@
La Cagette use False to implement custom rules
- ASSOCIATE_MEMBER_SHIFT = ''
Id number of the associate shift template
- PREPA_ODOO_URL = ''
URL of the "prepa_odoo" page
### Scales and labels files generation
- DAV_PATH = '/data/dav/cagette'
......@@ -311,6 +323,16 @@
Message shown to people when they connect to the Member Space
- MEMBERS_SPACE_FAQ_TEMPLATE = None
If set to None, "FAQ menu" will not be shown. To use a custom content add a template and set it's relative path
- MEMBERS_SPACE_SHOW_UNDERSTAND_MY_STATUS = False
By default, is True. If False, tile showing explanations is not shown
- BLOCK_ACTIONS_FOR_ATTACHED_PEOPLE = False
Attached people can or not change his services
### Reception
- RECEPTION_ADD_ADMIN_MODE = True
......@@ -367,6 +389,12 @@
In members_space history display a special activity about amnistie
### BDM Admin
- BDM_SHOW_FTOP_BUTTON = True (by default)
If True, in BDM Admin manage shift template, on the calendar when subscribing a partner to a shift, "Volant" button is included
### Miscellious
- EXPORT_COMPTA_FORMAT = 'Quadratus'
......
......@@ -6,6 +6,7 @@ from django.http import JsonResponse
from django.http import HttpResponseNotFound
from django.http import HttpResponseForbidden
from django.http import HttpResponseServerError
from django.http import HttpResponseBadRequest
from django.template import loader
from django.shortcuts import render
from django.shortcuts import redirect
......
......@@ -9,7 +9,7 @@ class OdooEntityFieldsForm(forms.Form):
class ExportComptaForm(forms.Form):
mois = forms.DateField(
required=True,
widget=MonthYearWidget()
widget=MonthYearWidget(years=range(datetime.date.today().year-2,datetime.date.today().year+1))
)
#fichier = forms.FileField()
# CHOICES = [('zip', '1 fichier par journal'),('compact', '1 seul fichier')]
......
"""
Delete makeups_to_do for up_to_date members.
Run this script from the project root with:
$ python -m outils.scripts.delete_makeups_for_uptodate_members
"""
import os
from pathlib import Path
from importlib import import_module
import logging
logging.basicConfig(
level=logging.DEBUG,
format='[%(asctime)s] %(levelname)s - %(message)s',
datefmt='%H:%M:%S'
)
logger = logging.getLogger(__file__)
project_path = Path(__file__).resolve().parents[2]
def get_api():
if not os.environ.get('DJANGO_SETTINGS_MODULE'):
os.environ['DJANGO_SETTINGS_MODULE'] = "outils.settings"
module = import_module('outils.common')
return module.OdooAPI()
def get_concerned_users(api):
cond = [
['cooperative_state', '=', 'up_to_date'],
['makeups_to_do', '>', 0]
]
fields = ['id']
return api.search_read('res.partner', cond, fields)
def main():
api = get_api()
concerned_users = get_concerned_users(api)
logger.info('Number of concerned members %i', len(concerned_users))
for user in concerned_users:
logger.debug("Member: %s is concerned", user.get('name'))
api.update('res.partner', user.get('id'), {'makeups_to_do': 0})
logger.debug("Member: %s has no more make ups to do!",
user.get('name'))
new_concerned_users = get_concerned_users(api)
logger.info('Now the number of concerned members %i',
len(new_concerned_users))
if __name__ == "__main__":
main()
SECRET_KEY = 'Mettre_plein_de_caracteres_aleatoires_iezezezeezezci'
ODOO = {
'url': 'http://127.0.0.1:8069'
'url': 'http://127.0.0.1:8069',
'user': 'api',
'passwd': 'xxxxxxxxxxxx',
'db': 'bd_test',
......
......@@ -6,8 +6,9 @@
.b_yellow {background: #fcf3cc;}
.red {color:#FF0000;}
.b_red, .b_less_than_25pc {background:#ff3333 !important;}
.loading {background-image: url("/static/img/ajax-loader.gif"); background-repeat:no-repeat;}
.loading2 {display: none;}
.loading {background-image: url("/static/img/ajax-loader.gif"); background-repeat:no-repeat; background-position: center; background-color: #efefef;}
.loading2 {display: none; position:absolute; top:-20px;}
.loading2-container {position:relative;}
body {background: #fff; margin:5px;}
a, a:active, a:focus,
......@@ -28,6 +29,11 @@ footer { position: fixed;
z-index: 10;
}
.warning_instruction {
font-weight: bold;
font-style: italic;
color: blue;
}
#deconnect, #password_change {float:right; margin-left: 5px;}
/* The Overlay (background) */
......@@ -148,6 +154,10 @@ footer { position: fixed;
width: 230px !important;
}
.tooltip .tooltip-xl {
width: 320px !important;
}
.tooltip .tt_twolines {
top: -15px !important;
}
......
var actions_last_dates = {};
var show_enqueued_messages = function() {
var stored = null;
try {
stored = JSON.parse(localStorage.getItem('enqueued_messages'));
alert(stored.join("\n"))
localStorage.removeItem('enqueued_messages')
} catch (e) {
//no rescue system for the moment
}
};
var enqueue_message_for_next_loading = function(msg) {
try {
let messages = [],
stored = localStorage.getItem('enqueued_messages');
if (stored) {
messages = JSON.parse(stored);
}
messages.push(msg)
localStorage.setItem('enqueued_messages', JSON.stringify(messages));
} catch (e) {
//no rescue system for the moment
}
}
function get_litteral_shift_template_name(name) {
var l_name = '';
......@@ -175,8 +202,8 @@ String.prototype.pad = function(String, len) {
var btns = $('<div/>').addClass('btns');
var btn_ok = $('<button/>').addClass('btn--success');
var btn_nok = $('<button/>').addClass('btn--danger')
var btn_ok = $('<button/>').addClass('btn--success btn-modal-ok');
var btn_nok = $('<button/>').addClass('btn--danger btn-modal-nok')
.attr('id', 'modal_closebtn_bottom')
.text('Fermer');
......@@ -201,6 +228,10 @@ function openModal() {
btn_nok.off('click', closeModal);
btn_ok.off('click', closeModal);
if (btn_ok.hasClass('loading')) {
btn_ok.removeClass('loading');
btn_ok.addClass('btn--success');
}
// If more than one argument, add 'save' button
if (arguments[1]) {
btn_ok.on('click', arguments[1]); // Second argument is callback
......@@ -209,6 +240,14 @@ function openModal() {
// 4th argument: if set and false, validate button doesn't close the modal
if (typeof (arguments[3]) == "undefined" || arguments[3] != false)
btn_ok.on('click', closeModal);
/*
else {
btn_ok.on('click', function() {
$(this).addClass("loading");
$(this).removeClass("btn--success");
})
}
*/
btns.append(btn_ok);
......@@ -460,22 +499,6 @@ for (i = 0; i < acc.length; i++) {
});
}
$(document).on('click', '.accordion', function(){
/* Toggle between adding and removing the "active" class,
to highlight the button that controls the panel */
this.classList.toggle("active");
/* Toggle between hiding and showing the active panel */
var panel = this.nextElementSibling;
if (panel.style.display === "block") {
panel.style.display = "none";
} else {
panel.style.display = "block";
}
console.log(panel)
});
function report_JS_error(e, m) {
try {
$.post('/log_js_error', {module: m, error: JSON.stringify(e)});
......@@ -498,3 +521,5 @@ function isMacUser() {
}
if (isMacUser() && isSafari()) $('.mac-msg').show();
show_enqueued_messages();
\ No newline at end of file
......@@ -35,14 +35,15 @@ function get_shift_name(s_data) {
if (s_data && s_data.week) {
shift_name = weeks_name[s_data.week];
if (s_data.type == 2 && typeof manage_ftop != "undefined" && manage_ftop == true) {
if (s_data.type == 2 && typeof manage_ftop != "undefined" && manage_ftop == true && s_data.id != ASSOCIATE_MEMBER_SHIFT) {
shift_name = 'Volant';
} else if(s_data.id == ASSOCIATE_MEMBER_SHIFT) {
shift_name = 'Binôme';
} else {
shift_name += s_data.day + ' - ' + s_data.begin;
shift_name += ' - ' + s_data.place;
}
}
return shift_name;
}
......@@ -52,8 +53,13 @@ function subscribe_shift(shift_t_id) {
var s_data = shift_templates[shift_t_id].data;
var shift_name = get_shift_name(s_data);
if (committees_shift_id !== undefined && committees_shift_id !== "None" && shift_name === "Volant") {
shift_name = 'des Comités'
}
let msg = 'On inscrit le membre au créneau ' + shift_name
openModal(
'On inscrit le membre au créneau ' + shift_name,
msg,
function() {
closeModal();
current_coop.shift_template = shift_templates[shift_t_id];
......@@ -98,8 +104,8 @@ function single_shift_click() {
}
}
function select_shift_among_compact() {
var clicked = $(this);
function select_shift_among_compact(event, clicked_item = null, subscribe = true) {
var clicked = clicked_item === null ? $(this) : $(clicked_item);
var day = clicked.closest('td').attr('class');
var hour = clicked.closest('tr').data('begin');
var selected = null;
......@@ -128,9 +134,11 @@ function select_shift_among_compact() {
}
}
});
//console.log(worst_score)
if (selected)
if (selected && subscribe === true)
subscribe_shift(selected);
return selected
}
......@@ -324,7 +332,14 @@ function retrieve_and_draw_shift_tempates() {
$.each(shift_templates, function(i, e) {
if (e.data.type == 2 && volant == null) {
volant = e.data.id;
// has comitee shift
if (committees_shift_id !== undefined && committees_shift_id !== "None") {
if (e.data.id == parseInt(committees_shift_id)) {
volant = e.data.id
}
} else {
volant = e.data.id;
}
}
});
......
/*!
Responsive 2.2.2
2014-2018 SpryMedia Ltd - datatables.net/license
Copyright 2014-2021 SpryMedia Ltd.
This source file is free software, available under the following license:
MIT license - http://datatables.net/license/mit
This source file is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
For details please refer to: http://www.datatables.net
Responsive 2.2.9
2014-2021 SpryMedia Ltd - datatables.net/license
*/
(function(d){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(l){return d(l,window,document)}):"object"===typeof exports?module.exports=function(l,j){l||(l=window);if(!j||!j.fn.dataTable)j=require("datatables.net")(l,j).$;return d(j,l,l.document)}:d(jQuery,window,document)})(function(d,l,j,q){function t(a,b,c){var e=b+"-"+c;if(n[e])return n[e];for(var d=[],a=a.cell(b,c).node().childNodes,b=0,c=a.length;b<c;b++)d.push(a[b]);return n[e]=d}function r(a,b,d){var e=b+
"-"+d;if(n[e]){for(var a=a.cell(b,d).node(),d=n[e][0].parentNode.childNodes,b=[],f=0,g=d.length;f<g;f++)b.push(d[f]);d=0;for(f=b.length;d<f;d++)a.appendChild(b[d]);n[e]=q}}var o=d.fn.dataTable,i=function(a,b){if(!o.versionCheck||!o.versionCheck("1.10.10"))throw"DataTables Responsive requires DataTables 1.10.10 or newer";this.s={dt:new o.Api(a),columns:[],current:[]};this.s.dt.settings()[0].responsive||(b&&"string"===typeof b.details?b.details={type:b.details}:b&&!1===b.details?b.details={type:!1}:
b&&!0===b.details&&(b.details={type:"inline"}),this.c=d.extend(!0,{},i.defaults,o.defaults.responsive,b),a.responsive=this,this._constructor())};d.extend(i.prototype,{_constructor:function(){var a=this,b=this.s.dt,c=b.settings()[0],e=d(l).width();b.settings()[0]._responsive=this;d(l).on("resize.dtr orientationchange.dtr",o.util.throttle(function(){var b=d(l).width();b!==e&&(a._resize(),e=b)}));c.oApi._fnCallbackReg(c,"aoRowCreatedCallback",function(e){-1!==d.inArray(!1,a.s.current)&&d(">td, >th",
e).each(function(e){e=b.column.index("toData",e);!1===a.s.current[e]&&d(this).css("display","none")})});b.on("destroy.dtr",function(){b.off(".dtr");d(b.table().body()).off(".dtr");d(l).off("resize.dtr orientationchange.dtr");d.each(a.s.current,function(b,e){!1===e&&a._setColumnVis(b,!0)})});this.c.breakpoints.sort(function(a,b){return a.width<b.width?1:a.width>b.width?-1:0});this._classLogic();this._resizeAuto();c=this.c.details;!1!==c.type&&(a._detailsInit(),b.on("column-visibility.dtr",function(){a._timer&&
clearTimeout(a._timer);a._timer=setTimeout(function(){a._timer=null;a._classLogic();a._resizeAuto();a._resize();a._redrawChildren()},100)}),b.on("draw.dtr",function(){a._redrawChildren()}),d(b.table().node()).addClass("dtr-"+c.type));b.on("column-reorder.dtr",function(){a._classLogic();a._resizeAuto();a._resize()});b.on("column-sizing.dtr",function(){a._resizeAuto();a._resize()});b.on("preXhr.dtr",function(){var e=[];b.rows().every(function(){this.child.isShown()&&e.push(this.id(true))});b.one("draw.dtr",
function(){a._resizeAuto();a._resize();b.rows(e).every(function(){a._detailsDisplay(this,false)})})});b.on("init.dtr",function(){a._resizeAuto();a._resize();d.inArray(false,a.s.current)&&b.columns.adjust()});this._resize()},_columnsVisiblity:function(a){var b=this.s.dt,c=this.s.columns,e,f,g=c.map(function(a,b){return{columnIdx:b,priority:a.priority}}).sort(function(a,b){return a.priority!==b.priority?a.priority-b.priority:a.columnIdx-b.columnIdx}),h=d.map(c,function(e,c){return!1===b.column(c).visible()?
"not-visible":e.auto&&null===e.minWidth?!1:!0===e.auto?"-":-1!==d.inArray(a,e.includeIn)}),m=0;e=0;for(f=h.length;e<f;e++)!0===h[e]&&(m+=c[e].minWidth);e=b.settings()[0].oScroll;e=e.sY||e.sX?e.iBarWidth:0;m=b.table().container().offsetWidth-e-m;e=0;for(f=h.length;e<f;e++)c[e].control&&(m-=c[e].minWidth);var s=!1;e=0;for(f=g.length;e<f;e++){var k=g[e].columnIdx;"-"===h[k]&&(!c[k].control&&c[k].minWidth)&&(s||0>m-c[k].minWidth?(s=!0,h[k]=!1):h[k]=!0,m-=c[k].minWidth)}g=!1;e=0;for(f=c.length;e<f;e++)if(!c[e].control&&
!c[e].never&&!1===h[e]){g=!0;break}e=0;for(f=c.length;e<f;e++)c[e].control&&(h[e]=g),"not-visible"===h[e]&&(h[e]=!1);-1===d.inArray(!0,h)&&(h[0]=!0);return h},_classLogic:function(){var a=this,b=this.c.breakpoints,c=this.s.dt,e=c.columns().eq(0).map(function(a){var b=this.column(a),e=b.header().className,a=c.settings()[0].aoColumns[a].responsivePriority;a===q&&(b=d(b.header()).data("priority"),a=b!==q?1*b:1E4);return{className:e,includeIn:[],auto:!1,control:!1,never:e.match(/\bnever\b/)?!0:!1,priority:a}}),
f=function(a,b){var c=e[a].includeIn;-1===d.inArray(b,c)&&c.push(b)},g=function(d,c,g,k){if(g)if("max-"===g){k=a._find(c).width;c=0;for(g=b.length;c<g;c++)b[c].width<=k&&f(d,b[c].name)}else if("min-"===g){k=a._find(c).width;c=0;for(g=b.length;c<g;c++)b[c].width>=k&&f(d,b[c].name)}else{if("not-"===g){c=0;for(g=b.length;c<g;c++)-1===b[c].name.indexOf(k)&&f(d,b[c].name)}}else e[d].includeIn.push(c)};e.each(function(a,e){for(var c=a.className.split(" "),f=!1,i=0,l=c.length;i<l;i++){var j=d.trim(c[i]);
if("all"===j){f=!0;a.includeIn=d.map(b,function(a){return a.name});return}if("none"===j||a.never){f=!0;return}if("control"===j){f=!0;a.control=!0;return}d.each(b,function(a,b){var d=b.name.split("-"),c=j.match(RegExp("(min\\-|max\\-|not\\-)?("+d[0]+")(\\-[_a-zA-Z0-9])?"));c&&(f=!0,c[2]===d[0]&&c[3]==="-"+d[1]?g(e,b.name,c[1],c[2]+c[3]):c[2]===d[0]&&!c[3]&&g(e,b.name,c[1],c[2]))})}f||(a.auto=!0)});this.s.columns=e},_detailsDisplay:function(a,b){var c=this,e=this.s.dt,f=this.c.details;if(f&&!1!==f.type){var g=
f.display(a,b,function(){return f.renderer(e,a[0],c._detailsObj(a[0]))});(!0===g||!1===g)&&d(e.table().node()).triggerHandler("responsive-display.dt",[e,a,g,b])}},_detailsInit:function(){var a=this,b=this.s.dt,c=this.c.details;"inline"===c.type&&(c.target="td:first-child, th:first-child");b.on("draw.dtr",function(){a._tabIndexes()});a._tabIndexes();d(b.table().body()).on("keyup.dtr","td, th",function(a){a.keyCode===13&&d(this).data("dtr-keyboard")&&d(this).click()});var e=c.target;d(b.table().body()).on("click.dtr mousedown.dtr mouseup.dtr",
"string"===typeof e?e:"td, th",function(c){if(d(b.table().node()).hasClass("collapsed")&&d.inArray(d(this).closest("tr").get(0),b.rows().nodes().toArray())!==-1){if(typeof e==="number"){var g=e<0?b.columns().eq(0).length+e:e;if(b.cell(this).index().column!==g)return}g=b.row(d(this).closest("tr"));c.type==="click"?a._detailsDisplay(g,false):c.type==="mousedown"?d(this).css("outline","none"):c.type==="mouseup"&&d(this).blur().css("outline","")}})},_detailsObj:function(a){var b=this,c=this.s.dt;return d.map(this.s.columns,
function(e,d){if(!e.never&&!e.control)return{title:c.settings()[0].aoColumns[d].sTitle,data:c.cell(a,d).render(b.c.orthogonal),hidden:c.column(d).visible()&&!b.s.current[d],columnIndex:d,rowIndex:a}})},_find:function(a){for(var b=this.c.breakpoints,c=0,e=b.length;c<e;c++)if(b[c].name===a)return b[c]},_redrawChildren:function(){var a=this,b=this.s.dt;b.rows({page:"current"}).iterator("row",function(c,e){b.row(e);a._detailsDisplay(b.row(e),!0)})},_resize:function(){var a=this,b=this.s.dt,c=d(l).width(),
e=this.c.breakpoints,f=e[0].name,g=this.s.columns,h,m=this.s.current.slice();for(h=e.length-1;0<=h;h--)if(c<=e[h].width){f=e[h].name;break}var i=this._columnsVisiblity(f);this.s.current=i;e=!1;h=0;for(c=g.length;h<c;h++)if(!1===i[h]&&!g[h].never&&!g[h].control&&!1===!b.column(h).visible()){e=!0;break}d(b.table().node()).toggleClass("collapsed",e);var k=!1,j=0;b.columns().eq(0).each(function(b,c){!0===i[c]&&j++;i[c]!==m[c]&&(k=!0,a._setColumnVis(b,i[c]))});k&&(this._redrawChildren(),d(b.table().node()).trigger("responsive-resize.dt",
[b,this.s.current]),0===b.page.info().recordsDisplay&&d("td",b.table().body()).eq(0).attr("colspan",j))},_resizeAuto:function(){var a=this.s.dt,b=this.s.columns;if(this.c.auto&&-1!==d.inArray(!0,d.map(b,function(a){return a.auto}))){d.isEmptyObject(n)||d.each(n,function(b){b=b.split("-");r(a,1*b[0],1*b[1])});a.table().node();var c=a.table().node().cloneNode(!1),e=d(a.table().header().cloneNode(!1)).appendTo(c),f=d(a.table().body()).clone(!1,!1).empty().appendTo(c),g=a.columns().header().filter(function(b){return a.column(b).visible()}).to$().clone(!1).css("display",
"table-cell").css("min-width",0);d(f).append(d(a.rows({page:"current"}).nodes()).clone(!1)).find("th, td").css("display","");if(f=a.table().footer()){var f=d(f.cloneNode(!1)).appendTo(c),h=a.columns().footer().filter(function(b){return a.column(b).visible()}).to$().clone(!1).css("display","table-cell");d("<tr/>").append(h).appendTo(f)}d("<tr/>").append(g).appendTo(e);"inline"===this.c.details.type&&d(c).addClass("dtr-inline collapsed");d(c).find("[name]").removeAttr("name");d(c).css("position","relative");
c=d("<div/>").css({width:1,height:1,overflow:"hidden",clear:"both"}).append(c);c.insertBefore(a.table().node());g.each(function(c){c=a.column.index("fromVisible",c);b[c].minWidth=this.offsetWidth||0});c.remove()}},_setColumnVis:function(a,b){var c=this.s.dt,e=b?"":"none";d(c.column(a).header()).css("display",e);d(c.column(a).footer()).css("display",e);c.column(a).nodes().to$().css("display",e);d.isEmptyObject(n)||c.cells(null,a).indexes().each(function(a){r(c,a.row,a.column)})},_tabIndexes:function(){var a=
this.s.dt,b=a.cells({page:"current"}).nodes().to$(),c=a.settings()[0],e=this.c.details.target;b.filter("[data-dtr-keyboard]").removeData("[data-dtr-keyboard]");"number"===typeof e?a.cells(null,e,{page:"current"}).nodes().to$().attr("tabIndex",c.iTabIndex).data("dtr-keyboard",1):("td:first-child, th:first-child"===e&&(e=">td:first-child, >th:first-child"),d(e,a.rows({page:"current"}).nodes()).attr("tabIndex",c.iTabIndex).data("dtr-keyboard",1))}});i.breakpoints=[{name:"desktop",width:Infinity},{name:"tablet-l",
width:1024},{name:"tablet-p",width:768},{name:"mobile-l",width:480},{name:"mobile-p",width:320}];i.display={childRow:function(a,b,c){if(b){if(d(a.node()).hasClass("parent"))return a.child(c(),"child").show(),!0}else{if(a.child.isShown())return a.child(!1),d(a.node()).removeClass("parent"),!1;a.child(c(),"child").show();d(a.node()).addClass("parent");return!0}},childRowImmediate:function(a,b,c){if(!b&&a.child.isShown()||!a.responsive.hasHidden())return a.child(!1),d(a.node()).removeClass("parent"),
!1;a.child(c(),"child").show();d(a.node()).addClass("parent");return!0},modal:function(a){return function(b,c,e){if(c)d("div.dtr-modal-content").empty().append(e());else{var f=function(){g.remove();d(j).off("keypress.dtr")},g=d('<div class="dtr-modal"/>').append(d('<div class="dtr-modal-display"/>').append(d('<div class="dtr-modal-content"/>').append(e())).append(d('<div class="dtr-modal-close">&times;</div>').click(function(){f()}))).append(d('<div class="dtr-modal-background"/>').click(function(){f()})).appendTo("body");
d(j).on("keyup.dtr",function(a){27===a.keyCode&&(a.stopPropagation(),f())})}a&&a.header&&d("div.dtr-modal-content").prepend("<h2>"+a.header(b)+"</h2>")}}};var n={};i.renderer={listHiddenNodes:function(){return function(a,b,c){var e=d('<ul data-dtr-index="'+b+'" class="dtr-details"/>'),f=!1;d.each(c,function(b,c){c.hidden&&(d('<li data-dtr-index="'+c.columnIndex+'" data-dt-row="'+c.rowIndex+'" data-dt-column="'+c.columnIndex+'"><span class="dtr-title">'+c.title+"</span> </li>").append(d('<span class="dtr-data"/>').append(t(a,
c.rowIndex,c.columnIndex))).appendTo(e),f=!0)});return f?e:!1}},listHidden:function(){return function(a,b,c){return(a=d.map(c,function(a){return a.hidden?'<li data-dtr-index="'+a.columnIndex+'" data-dt-row="'+a.rowIndex+'" data-dt-column="'+a.columnIndex+'"><span class="dtr-title">'+a.title+'</span> <span class="dtr-data">'+a.data+"</span></li>":""}).join(""))?d('<ul data-dtr-index="'+b+'" class="dtr-details"/>').append(a):!1}},tableAll:function(a){a=d.extend({tableClass:""},a);return function(b,
c,e){b=d.map(e,function(a){return'<tr data-dt-row="'+a.rowIndex+'" data-dt-column="'+a.columnIndex+'"><td>'+a.title+":</td> <td>"+a.data+"</td></tr>"}).join("");return d('<table class="'+a.tableClass+' dtr-details" width="100%"/>').append(b)}}};i.defaults={breakpoints:i.breakpoints,auto:!0,details:{display:i.display.childRow,renderer:i.renderer.listHidden(),target:0,type:"inline"},orthogonal:"display"};var p=d.fn.dataTable.Api;p.register("responsive()",function(){return this});p.register("responsive.index()",
function(a){a=d(a);return{column:a.data("dtr-index"),row:a.parent().data("dtr-index")}});p.register("responsive.rebuild()",function(){return this.iterator("table",function(a){a._responsive&&a._responsive._classLogic()})});p.register("responsive.recalc()",function(){return this.iterator("table",function(a){a._responsive&&(a._responsive._resizeAuto(),a._responsive._resize())})});p.register("responsive.hasHidden()",function(){var a=this.context[0];return a._responsive?-1!==d.inArray(!1,a._responsive.s.current):
!1});p.registerPlural("columns().responsiveHidden()","column().responsiveHidden()",function(){return this.iterator("column",function(a,b){return a._responsive?a._responsive.s.current[b]:!1},1)});i.version="2.2.2";d.fn.dataTable.Responsive=i;d.fn.DataTable.Responsive=i;d(j).on("preInit.dt.dtr",function(a,b){if("dt"===a.namespace&&(d(b.nTable).hasClass("responsive")||d(b.nTable).hasClass("dt-responsive")||b.oInit.responsive||o.defaults.responsive)){var c=b.oInit.responsive;!1!==c&&new i(b,d.isPlainObject(c)?
c:{})}});return i});
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.findInternal=function(b,k,m){b instanceof String&&(b=String(b));for(var n=b.length,p=0;p<n;p++){var y=b[p];if(k.call(m,y,p,b))return{i:p,v:y}}return{i:-1,v:void 0}};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;$jscomp.ISOLATE_POLYFILLS=!1;
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(b,k,m){if(b==Array.prototype||b==Object.prototype)return b;b[k]=m.value;return b};$jscomp.getGlobal=function(b){b=["object"==typeof globalThis&&globalThis,b,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var k=0;k<b.length;++k){var m=b[k];if(m&&m.Math==Math)return m}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";var $jscomp$lookupPolyfilledValue=function(b,k){var m=$jscomp.propertyToPolyfillSymbol[k];if(null==m)return b[k];m=b[m];return void 0!==m?m:b[k]};
$jscomp.polyfill=function(b,k,m,n){k&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(b,k,m,n):$jscomp.polyfillUnisolated(b,k,m,n))};$jscomp.polyfillUnisolated=function(b,k,m,n){m=$jscomp.global;b=b.split(".");for(n=0;n<b.length-1;n++){var p=b[n];if(!(p in m))return;m=m[p]}b=b[b.length-1];n=m[b];k=k(n);k!=n&&null!=k&&$jscomp.defineProperty(m,b,{configurable:!0,writable:!0,value:k})};
$jscomp.polyfillIsolated=function(b,k,m,n){var p=b.split(".");b=1===p.length;n=p[0];n=!b&&n in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var y=0;y<p.length-1;y++){var z=p[y];if(!(z in n))return;n=n[z]}p=p[p.length-1];m=$jscomp.IS_SYMBOL_NATIVE&&"es6"===m?n[p]:null;k=k(m);null!=k&&(b?$jscomp.defineProperty($jscomp.polyfills,p,{configurable:!0,writable:!0,value:k}):k!==m&&($jscomp.propertyToPolyfillSymbol[p]=$jscomp.IS_SYMBOL_NATIVE?$jscomp.global.Symbol(p):$jscomp.POLYFILL_PREFIX+p,p=
$jscomp.propertyToPolyfillSymbol[p],$jscomp.defineProperty(n,p,{configurable:!0,writable:!0,value:k})))};$jscomp.polyfill("Array.prototype.find",function(b){return b?b:function(k,m){return $jscomp.findInternal(this,k,m).v}},"es6","es3");
(function(b){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(k){return b(k,window,document)}):"object"===typeof exports?module.exports=function(k,m){k||(k=window);m&&m.fn.dataTable||(m=require("datatables.net")(k,m).$);return b(m,k,k.document)}:b(jQuery,window,document)})(function(b,k,m,n){function p(a,c,d){var f=c+"-"+d;if(A[f])return A[f];var g=[];a=a.cell(c,d).node().childNodes;c=0;for(d=a.length;c<d;c++)g.push(a[c]);return A[f]=g}function y(a,c,d){var f=c+"-"+
d;if(A[f]){a=a.cell(c,d).node();d=A[f][0].parentNode.childNodes;c=[];for(var g=0,l=d.length;g<l;g++)c.push(d[g]);d=0;for(g=c.length;d<g;d++)a.appendChild(c[d]);A[f]=n}}var z=b.fn.dataTable,u=function(a,c){if(!z.versionCheck||!z.versionCheck("1.10.10"))throw"DataTables Responsive requires DataTables 1.10.10 or newer";this.s={dt:new z.Api(a),columns:[],current:[]};this.s.dt.settings()[0].responsive||(c&&"string"===typeof c.details?c.details={type:c.details}:c&&!1===c.details?c.details={type:!1}:c&&
!0===c.details&&(c.details={type:"inline"}),this.c=b.extend(!0,{},u.defaults,z.defaults.responsive,c),a.responsive=this,this._constructor())};b.extend(u.prototype,{_constructor:function(){var a=this,c=this.s.dt,d=c.settings()[0],f=b(k).innerWidth();c.settings()[0]._responsive=this;b(k).on("resize.dtr orientationchange.dtr",z.util.throttle(function(){var g=b(k).innerWidth();g!==f&&(a._resize(),f=g)}));d.oApi._fnCallbackReg(d,"aoRowCreatedCallback",function(g,l,h){-1!==b.inArray(!1,a.s.current)&&b(">td, >th",
g).each(function(e){e=c.column.index("toData",e);!1===a.s.current[e]&&b(this).css("display","none")})});c.on("destroy.dtr",function(){c.off(".dtr");b(c.table().body()).off(".dtr");b(k).off("resize.dtr orientationchange.dtr");c.cells(".dtr-control").nodes().to$().removeClass("dtr-control");b.each(a.s.current,function(g,l){!1===l&&a._setColumnVis(g,!0)})});this.c.breakpoints.sort(function(g,l){return g.width<l.width?1:g.width>l.width?-1:0});this._classLogic();this._resizeAuto();d=this.c.details;!1!==
d.type&&(a._detailsInit(),c.on("column-visibility.dtr",function(){a._timer&&clearTimeout(a._timer);a._timer=setTimeout(function(){a._timer=null;a._classLogic();a._resizeAuto();a._resize(!0);a._redrawChildren()},100)}),c.on("draw.dtr",function(){a._redrawChildren()}),b(c.table().node()).addClass("dtr-"+d.type));c.on("column-reorder.dtr",function(g,l,h){a._classLogic();a._resizeAuto();a._resize(!0)});c.on("column-sizing.dtr",function(){a._resizeAuto();a._resize()});c.on("preXhr.dtr",function(){var g=
[];c.rows().every(function(){this.child.isShown()&&g.push(this.id(!0))});c.one("draw.dtr",function(){a._resizeAuto();a._resize();c.rows(g).every(function(){a._detailsDisplay(this,!1)})})});c.on("draw.dtr",function(){a._controlClass()}).on("init.dtr",function(g,l,h){"dt"===g.namespace&&(a._resizeAuto(),a._resize(),b.inArray(!1,a.s.current)&&c.columns.adjust())});this._resize()},_columnsVisiblity:function(a){var c=this.s.dt,d=this.s.columns,f,g=d.map(function(t,v){return{columnIdx:v,priority:t.priority}}).sort(function(t,
v){return t.priority!==v.priority?t.priority-v.priority:t.columnIdx-v.columnIdx}),l=b.map(d,function(t,v){return!1===c.column(v).visible()?"not-visible":t.auto&&null===t.minWidth?!1:!0===t.auto?"-":-1!==b.inArray(a,t.includeIn)}),h=0;var e=0;for(f=l.length;e<f;e++)!0===l[e]&&(h+=d[e].minWidth);e=c.settings()[0].oScroll;e=e.sY||e.sX?e.iBarWidth:0;h=c.table().container().offsetWidth-e-h;e=0;for(f=l.length;e<f;e++)d[e].control&&(h-=d[e].minWidth);var r=!1;e=0;for(f=g.length;e<f;e++){var q=g[e].columnIdx;
"-"===l[q]&&!d[q].control&&d[q].minWidth&&(r||0>h-d[q].minWidth?(r=!0,l[q]=!1):l[q]=!0,h-=d[q].minWidth)}g=!1;e=0;for(f=d.length;e<f;e++)if(!d[e].control&&!d[e].never&&!1===l[e]){g=!0;break}e=0;for(f=d.length;e<f;e++)d[e].control&&(l[e]=g),"not-visible"===l[e]&&(l[e]=!1);-1===b.inArray(!0,l)&&(l[0]=!0);return l},_classLogic:function(){var a=this,c=this.c.breakpoints,d=this.s.dt,f=d.columns().eq(0).map(function(h){var e=this.column(h),r=e.header().className;h=d.settings()[0].aoColumns[h].responsivePriority;
e=e.header().getAttribute("data-priority");h===n&&(h=e===n||null===e?1E4:1*e);return{className:r,includeIn:[],auto:!1,control:!1,never:r.match(/\bnever\b/)?!0:!1,priority:h}}),g=function(h,e){h=f[h].includeIn;-1===b.inArray(e,h)&&h.push(e)},l=function(h,e,r,q){if(!r)f[h].includeIn.push(e);else if("max-"===r)for(q=a._find(e).width,e=0,r=c.length;e<r;e++)c[e].width<=q&&g(h,c[e].name);else if("min-"===r)for(q=a._find(e).width,e=0,r=c.length;e<r;e++)c[e].width>=q&&g(h,c[e].name);else if("not-"===r)for(e=
0,r=c.length;e<r;e++)-1===c[e].name.indexOf(q)&&g(h,c[e].name)};f.each(function(h,e){for(var r=h.className.split(" "),q=!1,t=0,v=r.length;t<v;t++){var B=r[t].trim();if("all"===B){q=!0;h.includeIn=b.map(c,function(w){return w.name});return}if("none"===B||h.never){q=!0;return}if("control"===B||"dtr-control"===B){q=!0;h.control=!0;return}b.each(c,function(w,D){w=D.name.split("-");var x=B.match(new RegExp("(min\\-|max\\-|not\\-)?("+w[0]+")(\\-[_a-zA-Z0-9])?"));x&&(q=!0,x[2]===w[0]&&x[3]==="-"+w[1]?l(e,
D.name,x[1],x[2]+x[3]):x[2]!==w[0]||x[3]||l(e,D.name,x[1],x[2]))})}q||(h.auto=!0)});this.s.columns=f},_controlClass:function(){if("inline"===this.c.details.type){var a=this.s.dt,c=b.inArray(!0,this.s.current);a.cells(null,function(d){return d!==c},{page:"current"}).nodes().to$().filter(".dtr-control").removeClass("dtr-control");a.cells(null,c,{page:"current"}).nodes().to$().addClass("dtr-control")}},_detailsDisplay:function(a,c){var d=this,f=this.s.dt,g=this.c.details;if(g&&!1!==g.type){var l=g.display(a,
c,function(){return g.renderer(f,a[0],d._detailsObj(a[0]))});!0!==l&&!1!==l||b(f.table().node()).triggerHandler("responsive-display.dt",[f,a,l,c])}},_detailsInit:function(){var a=this,c=this.s.dt,d=this.c.details;"inline"===d.type&&(d.target="td.dtr-control, th.dtr-control");c.on("draw.dtr",function(){a._tabIndexes()});a._tabIndexes();b(c.table().body()).on("keyup.dtr","td, th",function(g){13===g.keyCode&&b(this).data("dtr-keyboard")&&b(this).click()});var f=d.target;d="string"===typeof f?f:"td, th";
if(f!==n||null!==f)b(c.table().body()).on("click.dtr mousedown.dtr mouseup.dtr",d,function(g){if(b(c.table().node()).hasClass("collapsed")&&-1!==b.inArray(b(this).closest("tr").get(0),c.rows().nodes().toArray())){if("number"===typeof f){var l=0>f?c.columns().eq(0).length+f:f;if(c.cell(this).index().column!==l)return}l=c.row(b(this).closest("tr"));"click"===g.type?a._detailsDisplay(l,!1):"mousedown"===g.type?b(this).css("outline","none"):"mouseup"===g.type&&b(this).trigger("blur").css("outline","")}})},
_detailsObj:function(a){var c=this,d=this.s.dt;return b.map(this.s.columns,function(f,g){if(!f.never&&!f.control)return f=d.settings()[0].aoColumns[g],{className:f.sClass,columnIndex:g,data:d.cell(a,g).render(c.c.orthogonal),hidden:d.column(g).visible()&&!c.s.current[g],rowIndex:a,title:null!==f.sTitle?f.sTitle:b(d.column(g).header()).text()}})},_find:function(a){for(var c=this.c.breakpoints,d=0,f=c.length;d<f;d++)if(c[d].name===a)return c[d]},_redrawChildren:function(){var a=this,c=this.s.dt;c.rows({page:"current"}).iterator("row",
function(d,f){c.row(f);a._detailsDisplay(c.row(f),!0)})},_resize:function(a){var c=this,d=this.s.dt,f=b(k).innerWidth(),g=this.c.breakpoints,l=g[0].name,h=this.s.columns,e,r=this.s.current.slice();for(e=g.length-1;0<=e;e--)if(f<=g[e].width){l=g[e].name;break}var q=this._columnsVisiblity(l);this.s.current=q;g=!1;e=0;for(f=h.length;e<f;e++)if(!1===q[e]&&!h[e].never&&!h[e].control&&!1===!d.column(e).visible()){g=!0;break}b(d.table().node()).toggleClass("collapsed",g);var t=!1,v=0;d.columns().eq(0).each(function(B,
w){!0===q[w]&&v++;if(a||q[w]!==r[w])t=!0,c._setColumnVis(B,q[w])});t&&(this._redrawChildren(),b(d.table().node()).trigger("responsive-resize.dt",[d,this.s.current]),0===d.page.info().recordsDisplay&&b("td",d.table().body()).eq(0).attr("colspan",v));c._controlClass()},_resizeAuto:function(){var a=this.s.dt,c=this.s.columns;if(this.c.auto&&-1!==b.inArray(!0,b.map(c,function(e){return e.auto}))){b.isEmptyObject(A)||b.each(A,function(e){e=e.split("-");y(a,1*e[0],1*e[1])});a.table().node();var d=a.table().node().cloneNode(!1),
f=b(a.table().header().cloneNode(!1)).appendTo(d),g=b(a.table().body()).clone(!1,!1).empty().appendTo(d);d.style.width="auto";var l=a.columns().header().filter(function(e){return a.column(e).visible()}).to$().clone(!1).css("display","table-cell").css("width","auto").css("min-width",0);b(g).append(b(a.rows({page:"current"}).nodes()).clone(!1)).find("th, td").css("display","");if(g=a.table().footer()){g=b(g.cloneNode(!1)).appendTo(d);var h=a.columns().footer().filter(function(e){return a.column(e).visible()}).to$().clone(!1).css("display",
"table-cell");b("<tr/>").append(h).appendTo(g)}b("<tr/>").append(l).appendTo(f);"inline"===this.c.details.type&&b(d).addClass("dtr-inline collapsed");b(d).find("[name]").removeAttr("name");b(d).css("position","relative");d=b("<div/>").css({width:1,height:1,overflow:"hidden",clear:"both"}).append(d);d.insertBefore(a.table().node());l.each(function(e){e=a.column.index("fromVisible",e);c[e].minWidth=this.offsetWidth||0});d.remove()}},_responsiveOnlyHidden:function(){var a=this.s.dt;return b.map(this.s.current,
function(c,d){return!1===a.column(d).visible()?!0:c})},_setColumnVis:function(a,c){var d=this.s.dt;c=c?"":"none";b(d.column(a).header()).css("display",c);b(d.column(a).footer()).css("display",c);d.column(a).nodes().to$().css("display",c);b.isEmptyObject(A)||d.cells(null,a).indexes().each(function(f){y(d,f.row,f.column)})},_tabIndexes:function(){var a=this.s.dt,c=a.cells({page:"current"}).nodes().to$(),d=a.settings()[0],f=this.c.details.target;c.filter("[data-dtr-keyboard]").removeData("[data-dtr-keyboard]");
"number"===typeof f?a.cells(null,f,{page:"current"}).nodes().to$().attr("tabIndex",d.iTabIndex).data("dtr-keyboard",1):("td:first-child, th:first-child"===f&&(f=">td:first-child, >th:first-child"),b(f,a.rows({page:"current"}).nodes()).attr("tabIndex",d.iTabIndex).data("dtr-keyboard",1))}});u.breakpoints=[{name:"desktop",width:Infinity},{name:"tablet-l",width:1024},{name:"tablet-p",width:768},{name:"mobile-l",width:480},{name:"mobile-p",width:320}];u.display={childRow:function(a,c,d){if(c){if(b(a.node()).hasClass("parent"))return a.child(d(),
"child").show(),!0}else{if(a.child.isShown())return a.child(!1),b(a.node()).removeClass("parent"),!1;a.child(d(),"child").show();b(a.node()).addClass("parent");return!0}},childRowImmediate:function(a,c,d){if(!c&&a.child.isShown()||!a.responsive.hasHidden())return a.child(!1),b(a.node()).removeClass("parent"),!1;a.child(d(),"child").show();b(a.node()).addClass("parent");return!0},modal:function(a){return function(c,d,f){if(d)b("div.dtr-modal-content").empty().append(f());else{var g=function(){l.remove();
b(m).off("keypress.dtr")},l=b('<div class="dtr-modal"/>').append(b('<div class="dtr-modal-display"/>').append(b('<div class="dtr-modal-content"/>').append(f())).append(b('<div class="dtr-modal-close">&times;</div>').click(function(){g()}))).append(b('<div class="dtr-modal-background"/>').click(function(){g()})).appendTo("body");b(m).on("keyup.dtr",function(h){27===h.keyCode&&(h.stopPropagation(),g())})}a&&a.header&&b("div.dtr-modal-content").prepend("<h2>"+a.header(c)+"</h2>")}}};var A={};u.renderer=
{listHiddenNodes:function(){return function(a,c,d){var f=b('<ul data-dtr-index="'+c+'" class="dtr-details"/>'),g=!1;b.each(d,function(l,h){h.hidden&&(b("<li "+(h.className?'class="'+h.className+'"':"")+' data-dtr-index="'+h.columnIndex+'" data-dt-row="'+h.rowIndex+'" data-dt-column="'+h.columnIndex+'"><span class="dtr-title">'+h.title+"</span> </li>").append(b('<span class="dtr-data"/>').append(p(a,h.rowIndex,h.columnIndex))).appendTo(f),g=!0)});return g?f:!1}},listHidden:function(){return function(a,
c,d){return(a=b.map(d,function(f){var g=f.className?'class="'+f.className+'"':"";return f.hidden?"<li "+g+' data-dtr-index="'+f.columnIndex+'" data-dt-row="'+f.rowIndex+'" data-dt-column="'+f.columnIndex+'"><span class="dtr-title">'+f.title+'</span> <span class="dtr-data">'+f.data+"</span></li>":""}).join(""))?b('<ul data-dtr-index="'+c+'" class="dtr-details"/>').append(a):!1}},tableAll:function(a){a=b.extend({tableClass:""},a);return function(c,d,f){c=b.map(f,function(g){return"<tr "+(g.className?
'class="'+g.className+'"':"")+' data-dt-row="'+g.rowIndex+'" data-dt-column="'+g.columnIndex+'"><td>'+g.title+":</td> <td>"+g.data+"</td></tr>"}).join("");return b('<table class="'+a.tableClass+' dtr-details" width="100%"/>').append(c)}}};u.defaults={breakpoints:u.breakpoints,auto:!0,details:{display:u.display.childRow,renderer:u.renderer.listHidden(),target:0,type:"inline"},orthogonal:"display"};var C=b.fn.dataTable.Api;C.register("responsive()",function(){return this});C.register("responsive.index()",
function(a){a=b(a);return{column:a.data("dtr-index"),row:a.parent().data("dtr-index")}});C.register("responsive.rebuild()",function(){return this.iterator("table",function(a){a._responsive&&a._responsive._classLogic()})});C.register("responsive.recalc()",function(){return this.iterator("table",function(a){a._responsive&&(a._responsive._resizeAuto(),a._responsive._resize())})});C.register("responsive.hasHidden()",function(){var a=this.context[0];return a._responsive?-1!==b.inArray(!1,a._responsive._responsiveOnlyHidden()):
!1});C.registerPlural("columns().responsiveHidden()","column().responsiveHidden()",function(){return this.iterator("column",function(a,c){return a._responsive?a._responsive._responsiveOnlyHidden()[c]:!1},1)});u.version="2.2.9";b.fn.dataTable.Responsive=u;b.fn.DataTable.Responsive=u;b(m).on("preInit.dt.dtr",function(a,c,d){"dt"===a.namespace&&(b(c.nTable).hasClass("responsive")||b(c.nTable).hasClass("dt-responsive")||c.oInit.responsive||z.defaults.responsive)&&(a=c.oInit.responsive,!1!==a&&new u(c,
b.isPlainObject(a)?a:{}))});return u});
\ No newline at end of file
......@@ -150,4 +150,17 @@ function get_module_settings() {
}
get_module_settings();
$(document).on('click', '.accordion', function(){
/* Toggle between adding and removing the "active" class,
to highlight the button that controls the panel */
this.classList.toggle("active");
/* Toggle between hiding and showing the active panel */
var panel = this.nextElementSibling;
if (panel.style.display === "block") {
panel.style.display = "none";
} else {
panel.style.display = "block";
}
});
......@@ -189,34 +189,51 @@ class ExportPOS(View):
kept_sessions_id.append(s['id'])
key = y + '-' + m + '-' + d
if not (key in totals):
totals[key] = {'CB': 0, 'CSH': 0, 'CHQ': 0, 'TOTAL': 0}
totals[key] = {'CB': 0,
'CSH': 0,
'CHQ': 0,
'CB_DEJ': 0,
'CHQ_DEJ': 0,
'TOTAL': 0}
sub_total = 0
cb = chq = csh = 0
cb = chq = csh = cbd = chqd = 0
for p in s['payments']:
# p['name'] is a sequence generated string
# Test order is important as CHEQDEJ contains CHEQ for ex.
# p['journal'] could be used but easier to change in Odoo interface
sub_amount = round(p['total_amount'], 2)
if 'CSH' in p['name']:
csh = round(p['total_amount'], 2)
csh = sub_amount
elif 'CHEQDEJ' in p['name']:
chqd = sub_amount
elif 'CHEQ' in p['name']:
chq = round(p['total_amount'], 2)
chq = sub_amount
elif 'CBDEJ' in p['name']:
cbd = sub_amount
elif 'CB' in p['name']:
cb = round(p['total_amount'], 2)
sub_total += round(p['total_amount'], 2)
cb = sub_amount
sub_total += sub_amount
totals[key]['CB'] += cb
totals[key]['CSH'] += csh
totals[key]['CHQ'] += chq
totals[key]['CB_DEJ'] += cbd
totals[key]['CHQ_DEJ'] += chqd
totals[key]['TOTAL'] += round(sub_total, 2)
details_lines.append([mois, s['mm_dates']['min'], s['mm_dates']['min'], s['caisse'], s['name'],
cb, csh, chq, sub_total])
cb, csh, chq, cbd, chqd, sub_total])
wb = Workbook()
ws1 = wb.create_sheet("Totaux " + mois, 0)
ws2 = wb.create_sheet("Détails " + mois, 1)
ws1.append(['date', 'CB', 'CSH', 'CHQ', 'Total'])
ws1.append(['date', 'CB', 'CSH', 'CHQ', 'CB_DEJ', 'CHQ_DEJ', 'Total'])
for day in sorted(totals):
cb = totals[day]['CB']
csh = totals[day]['CSH']
chq = totals[day]['CHQ']
cbd = totals[day]['CB_DEJ']
chqd = totals[day]['CHQ_DEJ']
total = totals[day]['TOTAL']
ws1.append([day, cb, csh, chq, total])
ws2.append(['mois', 'min_date', 'max_date', 'Caisse', 'session', 'CB', 'CSH','CHQ', 'total'])
ws1.append([day, cb, csh, chq, cbd, chqd, total])
ws2.append(['mois', 'min_date', 'max_date', 'Caisse', 'session', 'CB', 'CSH','CHQ', 'CB_DEJ', 'CHQ_DEJ', 'total'])
for row in details_lines:
ws2.append(row)
wb_name = 'export_sessions__' + mois + '.xlsx'
......
......@@ -160,6 +160,7 @@ class CagetteProduct(models.Model):
try:
res["update"] = api.update('product.supplierinfo', psi_id, f)
res["psi_id"] = psi_id
except Exception as e:
res['error'] = str(e)
else:
......@@ -185,6 +186,7 @@ class CagetteProduct(models.Model):
try:
res['create'] = api.create('product.supplierinfo', f)
res['psi_id'] = res['create'] # consistency between update & create res
except Exception as e:
res['error'] = str(e)
......@@ -254,13 +256,24 @@ class CagetteProduct(models.Model):
return res
@staticmethod
def update_npa_and_minimal_stock(data):
"""Update NPA (ne pas acheter) and minimal stock data"""
def commit_actions_on_product(data):
""" Update:
- NPA (ne pas acheter)
- Product is active
- Minimal stock
- price /supplier
"""
res = {}
try:
api = OdooAPI()
f = {'minimal_stock': data['minimal_stock']}
# Minimal & Actual stock, Active
f = {
'minimal_stock': float(data['minimal_stock']),
'active': not data['to_archive']
}
# NPA
if 'simple-npa' in data['npa']:
f['purchase_ok'] = 0
if 'npa-in-name' in data['npa']:
......@@ -279,10 +292,20 @@ class CagetteProduct(models.Model):
f['name'] = re.sub(r'( \[FDS\])', '', current_name)
if len(data['npa']) == 0:
f['purchase_ok'] = 1
res["update"] = api.update('product.template', data['id'], f)
res["update"] = api.update('product.template', int(data['id']), f)
# Update suppliers info
res["update_supplierinfo"] = []
for supplierinfo in data["suppliersinfo"]:
f= {'price': supplierinfo["price"]}
res_update_si = api.update('product.supplierinfo', int(supplierinfo['supplierinfo_id']), f)
res["update_supplierinfo"].append(res_update_si)
except Exception as e:
res["error"] = str(e)
coop_logger.error("update_npa_and_minimal_stock : %s %s", str(e), str(data))
coop_logger.error("commit_actions_on_product : %s %s", str(e), str(data))
return res
class CagetteProducts(models.Model):
"""Initially used to make massive barcode update."""
......@@ -582,7 +605,7 @@ class CagetteProducts(models.Model):
if supplier_ids is not None and len(supplier_ids) > 0:
# Get products/supplier relation
f = ["product_tmpl_id", 'date_start', 'date_end', 'package_qty', 'price', 'name', 'product_code']
f = ["id", "product_tmpl_id", 'date_start', 'date_end', 'package_qty', 'price', 'name', 'product_code']
c = [['name', 'in', [ int(x) for x in supplier_ids]]]
psi = api.search_read('product.supplierinfo', c, f)
......@@ -612,7 +635,7 @@ class CagetteProducts(models.Model):
"product_variant_ids",
"minimal_stock"
]
c = [['id', 'in', ptids], ['purchase_ok', '=', True]]
c = [['id', 'in', ptids], ['purchase_ok', '=', True], ['active', '=', True]]
products_t = api.search_read('product.template', c, f)
filtered_products_t = [p for p in products_t if p["state"] != "end" and p["state"] != "obsolete"]
......@@ -640,6 +663,7 @@ class CagetteProducts(models.Model):
for psi_item in valid_psi:
if psi_item["product_tmpl_id"] is not False and psi_item ["product_tmpl_id"][0] == fp["id"]:
filtered_products_t[i]['suppliersinfo'].append({
'id': int(psi_item["id"]),
'supplier_id': int(psi_item["name"][0]),
'package_qty': psi_item["package_qty"],
'price': psi_item["price"],
......
......@@ -11,7 +11,7 @@ urlpatterns = [
url(r'^update_product_stock$', views.update_product_stock),
url(r'^update_product_purchase_ok$', views.update_product_purchase_ok),
url(r'^update_product_internal_ref$', views.update_product_internal_ref),
url(r'^update_npa_and_minimal_stock$', views.update_npa_and_minimal_stock),
url(r'^commit_actions_on_product$', views.commit_actions_on_product),
url(r'^labels_appli_csv(\/?[a-z]*)$', views.labels_appli_csv, name='labels_appli_csv'),
url(r'^label_print/([0-9]+)/?([0-9\.]*)/?([a-z]*)/?([0-9]*)$', views.label_print),
url(r'^shelf_labels$', views.shelf_labels), # massive print
......
......@@ -100,7 +100,7 @@ def update_product_stock(request):
'products': [p]
}
res['inventory'] = CagetteInventory.update_stock_with_shelf_inventory_data(inventory_data)
res['inventory'] = CagetteInventory.update_products_stock(inventory_data)
return JsonResponse({"res": res})
......@@ -134,13 +134,63 @@ def update_product_internal_ref(request):
else:
return JsonResponse(res, status=403)
def update_npa_and_minimal_stock(request):
def commit_actions_on_product(request):
res = {}
is_connected_user = CagetteUser.are_credentials_ok(request)
if is_connected_user is True:
try:
data = json.loads(request.body.decode())
res = CagetteProduct.update_npa_and_minimal_stock(data)
product_data = CagetteProducts.get_products_for_order_helper(None, [data["id"]])["products"][0]
# Don't allow to archive product if incomin qty > 0
if data["to_archive"] is True and product_data["incoming_qty"] > 0:
res["code"] = "archiving_with_incoming_qty"
return JsonResponse(res, status=500)
res = CagetteProduct.commit_actions_on_product(data)
do_stock_update = False
# If product to archive and stock > 0: do inventory to set stock to 0
if data["to_archive"] is True and product_data["qty_available"] != 0:
p = {
'id': product_data['product_variant_ids'][0], # Need product id
'uom_id': product_data['uom_id'],
'qty': 0
}
inventory_data = {
'name': 'Archivage - ' + product_data['name'],
'products': [p]
}
do_stock_update = True
# Else update actual stock if changed
elif data["qty_available"] != product_data["qty_available"]:
p = {
'id': product_data['product_variant_ids'][0], # Need product id
'uom_id': product_data['uom_id'],
'qty': data["qty_available"]
}
inventory_data = {
'name': 'MAJ stock depuis Aide à la Commande - ' + product_data['name'],
'products': [p]
}
do_stock_update = True
if do_stock_update is True:
try:
res_inventory = CagetteInventory.update_products_stock(inventory_data, 3)
if res_inventory['errors'] or res_inventory['missed']:
res["code"] = "error_stock_update"
res["error"] = res_inventory['errors']
return JsonResponse(res, status=500)
except Exception as e:
res["code"] = "error_stock_update"
return JsonResponse(res, status=500)
except Exception as e:
res['error'] = str(e)
coop_logger.error("Update npa and minimal stock : %s", res['error'])
......
......@@ -14,49 +14,38 @@ class CagetteSales(models.Model):
def get_sales(self, date_from, date_to):
res = []
# Get pos sessions
cond = [['stop_at', '>=', date_from], ['stop_at', '<=', date_to], ['state', '=', "closed"]]
fields = []
sessions = self.o_api.search_read('pos.session', cond, fields)
# Get pos orders
cond = [['date_order', '>=', date_from], ['date_order', '<=', date_to]]
fields = ['partner_id', 'statement_ids', 'name']
orders = self.o_api.search_read('pos.order', cond, fields)
# Get bank statements of these sessions
statements = []
for s in sessions:
statements = statements + s["statement_ids"]
statements_partners = {}
statements_orders = {}
for o in orders:
statements = statements + o["statement_ids"]
for s in o["statement_ids"]:
statements_partners[s] = o["partner_id"][1]
statements_orders[s] = o["name"]
# Get payment lines
cond = [['statement_id', 'in', statements]]
fields = ["partner_id", "amount", "journal_id", "create_date", "date"]
cond = [['id', 'in', statements]]
fields = ["amount", "journal_id", "create_date"]
payments = self.o_api.search_read('account.bank.statement.line', cond, fields, order="create_date ASC", limit=50000)
item = None
try:
for payment in payments:
# POS session can contain payments from another day (closing session on next morning, ...)
if payment["date"] >= date_from and payment["date"] <= date_to:
# If the consecutive payment in the results is from the same partner on the same day, we consider it's the same basket
if item is not None and item["partner_id"][0] == payment["partner_id"][0] and item["date"] == payment["date"]:
res[len(res)-1]["total_amount"] += round(float(payment["amount"]), 2)
res[len(res)-1]["payments"].append({
"amount": round(float(payment["amount"]), 2),
"journal_id": payment["journal_id"]
})
else:
item = {
"partner_id": payment["partner_id"],
res.append({
"partner": statements_partners[payment["id"]],
"create_date": payment["create_date"],
"date": payment["date"],
"pos_order_name": statements_orders[payment["id"]],
"total_amount": round(float(payment["amount"]), 2),
"payments": [
{
"amount": round(float(payment["amount"]), 2),
"journal_id": payment["journal_id"]
}
}
]
}
res.append(item)
})
except Exception as e:
pass
coop_logger.error("get_sales %s", str(e))
return res
......@@ -38,20 +38,23 @@ function display_orders(orders) {
columns:[
{
data:"create_date",
title:"Date de vente",
title:"Date enregistrement",
width: "10%"
},
{
data:"partner_id",
data:"pos_order_name",
title:"Ref. Caisse",
width: "10%"
},
{
data:"partner",
title:"Membre",
width: "50%",
render: function (data) {
return data[1];
}
width: "40%"
},
{
data:"total_amount",
title: "Montant du panier",
title: "Montant dû",
className:"dt-body-center",
render: function (data) {
return parseFloat(data).toFixed(2) + ' €';
......
......@@ -173,7 +173,7 @@ def do_shelf_inventory(request):
return JsonResponse(res, status=500)
# Proceed with inventory
res['inventory'] = CagetteInventory.update_stock_with_shelf_inventory_data(full_inventory_data)
res['inventory'] = CagetteInventory.update_products_stock(full_inventory_data)
full_inventory_data['inventory_id'] = res['inventory']['inv_id']
shelf_data['last_inventory_id'] = res['inventory']['inv_id']
......
......@@ -38,14 +38,28 @@ class CagetteShift(models.Model):
'cooperative_state', 'final_standard_point', 'create_date',
'final_ftop_point', 'shift_type', 'leave_ids', 'makeups_to_do', 'barcode_base',
'street', 'street2', 'zip', 'city', 'mobile', 'phone', 'email',
'is_associated_people', 'parent_id']
'is_associated_people', 'parent_id', 'extra_shift_done']
partnerData = self.o_api.search_read('res.partner', cond, fields, 1)
if partnerData:
partnerData = partnerData[0]
if partnerData['is_associated_people']:
cond = [['id', '=', partnerData['parent_id'][0]]]
fields = ['create_date', 'makeups_to_do', 'date_delay_stop', 'extra_shift_done']
parentData = self.o_api.search_read('res.partner', cond, fields, 1)
if parentData:
partnerData['parent_create_date'] = parentData[0]['create_date']
partnerData['parent_makeups_to_do'] = parentData[0]['makeups_to_do']
partnerData['parent_date_delay_stop'] = parentData[0]['date_delay_stop']
partnerData['parent_extra_shift_done'] = parentData[0]['extra_shift_done']
if partnerData['shift_type'] == 'standard':
partnerData['in_ftop_team'] = False
# Because 'in_ftop_team' doesn't seem to be reset to False in Odoo
cond = [['partner_id.id', '=', id]]
if partnerData['is_associated_people']:
cond = [['partner_id.id', '=', partnerData['parent_id'][0]]]
else:
cond = [['partner_id.id', '=', id]]
fields = ['shift_template_id', 'is_current']
shiftTemplate = self.o_api.search_read('shift.template.registration', cond, fields)
if (shiftTemplate and len(shiftTemplate) > 0):
......@@ -83,7 +97,7 @@ class CagetteShift(models.Model):
def get_shift_partner(self, id):
"""Récupère les shift du membre"""
fields = ['date_begin', 'date_end','final_standard_point',
'shift_id', 'shift_type','partner_id', "id"] # res.partner
'shift_id', 'shift_type','partner_id', "id", "associate_registered", "is_makeup"] # res.partner
cond = [['partner_id.id', '=', id],['state', '=', 'open'],
['date_begin', '>', datetime.datetime.now().isoformat()]]
shiftData = self.o_api.search_read('shift.registration', cond, fields, order ="date_begin ASC")
......@@ -166,10 +180,40 @@ class CagetteShift(models.Model):
if res:
st_r_id = True
return st_r_id
def affect_shift(self, data):
"""Affect shift to partner, his associate or both"""
response = None
# partner_id can be 'associated_people' one, which is never use as shift partner_id reference
# So, let's first retrieved data about the res.partner involved
cond = [['id', '=', int(data['idPartner'])]]
fields = ['parent_id']
partner = self.o_api.search_read('res.partner', cond, fields, 1)
if partner:
if partner[0]['parent_id']:
partner_id = partner[0]['parent_id'][0]
else:
partner_id = int(data['idPartner'])
cond = [['partner_id', '=', partner_id],
['id', '=', int(data['idShiftRegistration'])]]
fields = ['id']
try:
# make sure there is coherence between shift.registration id and partner_id (to avoid forged request)
shit_to_affect = self.o_api.search_read('shift.registration', cond, fields, 1)
if (len(shit_to_affect) == 1):
shift_res = shit_to_affect[0]
fieldsDatas = { "associate_registered":data['affected_partner']}
response = self.o_api.update('shift.registration', [shift_res['id']], fieldsDatas)
except Exception as e:
coop_logger.error("Model affect shift : %s", str(e))
else:
coop_logger.error("Model affect shift nobody found : %s", str(cond))
return response
def cancel_shift(self, idsRegisteur):
def cancel_shift(self, idsRegisteur, origin='memberspace'):
"""Annule un shift"""
fieldsDatas = { "related_shift_state": 'cancel',
"origin": origin,
"state": 'cancel'}
return self.o_api.update('shift.registration', idsRegisteur, fieldsDatas)
......@@ -329,4 +373,8 @@ class CagetteShift(models.Model):
def member_can_have_delay(self, partner_id):
""" Can a member have a delay? """
return self.o_api.execute('res.partner', 'can_have_extension', [partner_id])
\ No newline at end of file
return self.o_api.execute('res.partner', 'can_have_extension', [partner_id])
def update_counter_event(self, fields):
""" Add/remove points """
return self.o_api.create('shift.counter.event', fields)
\ No newline at end of file
......@@ -13,7 +13,9 @@ urlpatterns = [
url(r'^get_test', views.get_test),
# url(r'^get_list', views.get_list),
url(r'^change_shift', views.change_shift),
url(r'^affect_shift', views.affect_shift),
url(r'^add_shift', views.add_shift),
url(r'^cancel_shift', views.cancel_shift),
url(r'^request_delay', views.request_delay),
url(r'^reset_members_positive_points', views.reset_members_positive_points)
]
......@@ -101,8 +101,11 @@ def get_list_shift_calendar(request, partner_id):
use_new_members_space = getattr(settings, 'USE_NEW_MEMBERS_SPACE', False)
listRegisterPartner = []
listMakeUpShift = []
for v in registerPartner:
listRegisterPartner.append(v['id'])
if v['is_makeup']:
listMakeUpShift.append(v['id'])
start = request.GET.get('start')
end = request.GET.get('end')
......@@ -136,7 +139,10 @@ def get_list_shift_calendar(request, partner_id):
if len(l) > 0:
if use_new_members_space is True:
event["classNames"] = ["shift_booked"]
if set(value['registration_ids']) & set(listRegisterPartner) & set(listMakeUpShift):
event["classNames"] = ["shift_booked_makeup"]
else :
event["classNames"] = ["shift_booked"]
else:
event["className"] = "shift_booked"
event["changed"] = False
......@@ -223,15 +229,48 @@ def change_shift(request):
response = {'result': True}
else:
response = {'result': False}
response = {'msg': "Fail to create shift"}
return JsonResponse(response, status=500)
else:
response = {'result': False}
response = {'msg': "Bad arguments"}
return JsonResponse(response, status=400)
return JsonResponse(response)
else:
return HttpResponseForbidden()
else:
return HttpResponseForbidden()
def affect_shift(request):
if 'verif_token' in request.POST:
if Verification.verif_token(request.POST.get('verif_token'), int(request.POST.get('idPartner'))) is True:
cs = CagetteShift()
if 'idShiftRegistration' in request.POST and 'affected_partner' in request.POST:
# if request is made by associated people, idPartner is it's id, not "master" res.partner
# it's will be handled in model's method (affect_shift)
data = {
"idPartner": int(request.POST['idPartner']),
"idShiftRegistration": int(request.POST['idShiftRegistration']),
"affected_partner": request.POST['affected_partner'],
}
st_r_id = None
try:
st_r_id = cs.affect_shift(data)
except Exception as e:
coop_logger.error("affect shift : %s, %s", str(e), str(data))
if st_r_id:
response = {'result': True}
else:
response = {'msg': "Internal Error"}
return JsonResponse(response, status=500)
return(JsonResponse({'result': True}))
else:
response = {'msg': "Bad args"}
return JsonResponse(response, status=400)
else:
return HttpResponseForbidden()
else:
return HttpResponseForbidden()
def add_shift(request):
if 'verif_token' in request.POST:
if Verification.verif_token(request.POST.get('verif_token'), int(request.POST.get('idPartner'))) is True:
......@@ -277,6 +316,37 @@ def add_shift(request):
else:
return HttpResponseForbidden()
def cancel_shift(request):
""" Annule une présence à un shift """
if 'verif_token' in request.POST:
partner_id = int(request.POST.get('idPartner'))
if Verification.verif_token(request.POST.get('verif_token'), partner_id) is True:
cs = CagetteShift()
listRegister = [int(request.POST['idRegister'])]
try:
response = cs.cancel_shift(listRegister)
# decrement extra_shift_done if param exists
if 'extra_shift_done' in request.POST:
target = int(request.POST["extra_shift_done"]) - 1
# extra security
if target < 0:
target = 0
cm = CagetteMember(partner_id)
cm.update_extra_shift_done(target)
return JsonResponse({"res" : 'response'})
except Exception as e:
return JsonResponse({"error" : str(e)}, status=500)
else:
return HttpResponseForbidden()
else:
return HttpResponseForbidden()
def request_delay(request):
if 'verif_token' in request.POST:
if Verification.verif_token(request.POST.get('verif_token'), int(request.POST.get('idPartner'))) is True:
......
......@@ -46,7 +46,7 @@ def do_movement(request):
'products': products
}
res = CagetteInventory.update_stock_with_shelf_inventory_data(inventory_data)
res = CagetteInventory.update_products_stock(inventory_data)
else:
res = CagetteStock.do_stock_movement(data)
......
......@@ -9,15 +9,17 @@
{% endblock %}
{% block content %}
{%if must_identify %}
{% include "common/conn_admin.html" %}
{%endif%}
<div id="admin_connexion_button">
{%if must_identify %}
{% include "common/conn_admin.html" %}
{%endif%}
</div>
<div id="envelop_cashing_error" class="alert--danger clearfix custom_alert" onClick="toggle_error_alert()">
<div style="width: 90%" class="fl txtleft" id="error_alert_txt"></div>
<div style="width: 10%" class="fr txtright"><i class="fas fa-times"></i></div>
</div>
<div id="envelop_cashing_success" class="alert--success clearfix custom_alert" onClick="toggle_success_alert()">
<div style="width: 90%" class="fl txtleft">Enveloppe encaissée !</div>
<div style="width: 90%" class="fl txtleft success_alert_content">Enveloppe encaissée !</div>
<div style="width: 10%" class="fr txtright"><i class="fas fa-times"></i></div>
</div>
<div id="envelop_deletion_success" class="alert--success clearfix custom_alert" onClick="toggle_deleted_alert()">
......@@ -39,13 +41,115 @@
</div>
</section>
<section id="archive_cash">
<hr>
<h2 class="txtcenter">Enveloppes de liquide archivées</h2>
<div id="archive_cash_envelops" class="flex-container flex-column-reverse">
</div>
</section>
<section id="archive_ch">
<hr>
<h2 class="txtcenter">Enveloppes de chèques archivées</h2>
<div id="archive_ch_envelops" class="flex-container flex-column-reverse">
</div>
</section>
</section>
<div id="templates" style="display:none;">
<div id="modal_update_envelop">
<div class="modal_update_envelop_content">
<h3 class="envelop_name"></h3>
<div class="envelop_lines"></div>
<div class="envelop_comments_area">
<p>Commentaires</p>
<textarea class="envelop_comments"></textarea>
</div>
</div>
</div>
<div id="update_envelop_line_template">
<div class="update_envelop_line">
<div class="line_partner_name_container">
<span class="line_number"></span>
<span class="line_partner_name"></span>
</div>
<div class="line_partner_input_container">
<input type="text" class="line_partner_amount" placeholder="Montant">
<i class="fas fa-trash-alt fa-lg delete_envelop_line_icon"></i>
</div>
<div class="deleted_line_through"></div>
</div>
</div>
<div id="modal_add_to_envelop">
<div class="modal_add_to_envelop_content">
<h3>Ajouter un paiement ou des parts sociales</h3>
<h4><i class="envelop_name"></i></h4>
<hr>
<div class="search_member_area">
<h4>Rechercher un membre</h4>
<form class="search_member_form" action="javascript:;" method="post">
<input type="text" class="search_member_input" value="" placeholder="Nom ou numéro du coop..." required>
<button type="submit" class="btn--primary" class="search_member_button">Recherche</button>
</form>
</div>
<div class="search_member_results_area" style="display:none;">
<div class="search_results_text">
<p><i>Choisissez parmi les membres trouvés :</i></p>
</div>
<div class="search_member_results"></div>
</div>
<div class="add_to_envelop_lines_area" style="display:none;">
<div class="add_to_envelop_lines">
</div>
<div class="validation_buttons">
<button class="btn--primary add_payment_button">Ajouter le paiement à l'enveloppe</button>
<button class="btn--primary add_shares_button">Ajouter des parts sociales </button>
</div>
</div>
</div>
</div>
<div id="add_to_envelop_line_template">
<div class="add_to_envelop_line">
<div class="partner_name_container">
<span class="line_partner_name"></span>
</div>
<div class="partner_input_container">
<input type="text" class="line_partner_amount" placeholder="Montant">
<div class="line_partner_amount_error">Le montant doit être un nombre entier.</div>
</div>
</div>
</div>
<div id="modal_confirm_add_payment">
<p>
Vous vous apprêtez à ajouter un paiement de <span class="confirm_add_payment_details amount"></span>
du membre <span class="confirm_add_payment_details member"></span>
à l'enveloppe <span class="confirm_add_payment_details envelop"></span>.
</p>
<p><i>
<i class="fas fa-exclamation-triangle"></i> Avertissement si ce.tte membre a plusieurs factures d'ouvertes.<br/>
Au moment de l'encaissement de l'enveloppe, ce paiement sera lié à la plus vieille facture ouverte de ce membre.
</i></p>
</div>
<div id="modal_confirm_add_shares">
<p>
Vous vous apprêtez à ajouter pour <span class="confirm_add_payment_details amount"></span>€ de parts sociales
au membre <span class="confirm_add_payment_details member"></span>.
</p>
<p>
Le paiement sera ajouté à l'enveloppe <span class="confirm_add_payment_details envelop"></span>.
</p>
</div>
</div>
<script src="{% static "js/pouchdb.min.js" %}"></script>
<script type="text/javascript">
{%if must_identify %}
var must_identify = true
{%endif%}
var must_identify = '{{must_identify}}';
var couchdb_dbname = '{{db}}';
var couchdb_server = '{{couchdb_server}}' + couchdb_dbname;
var dbc = new PouchDB(couchdb_dbname);
......
......@@ -2,66 +2,49 @@
{% load static %}
{% block additionnal_css %}
<link rel="stylesheet" href="{% static 'css/datatables/datatables.min.css' %}">
<link rel="stylesheet" href="{% static 'css/members_admin.css' %}">
<link rel="stylesheet" href="{% static 'jquery-ui-1.12.1/jquery-ui.min.css' %}">
<link rel="stylesheet" href="{% static 'css/admin/bdm_index.css' %}">
{% endblock %}
{% block additionnal_scripts %}
<script type="text/javascript" src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/datatables/datatables.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/admin/bdm_index.js' %}?v="></script>
<script type="text/javascript" src="{% static 'js/notify.min.js' %}?v="></script>
{% endblock %}
{% block content %}
<div class="page_body">
<div class="login_area">
{% include "common/conn_admin.html" %}
</div>
<div class="header txtcenter">
<h1>Bureau des membres</h1>
{% include "common/conn_admin.html" %}
</div>
<div class="page_content">
<section class="tabs autogrid">
<div class="button tab active" id="tab_makeups"><h5>Rattrapages</h5></div>
</section>
<div id="tab_makeups_content" class="tab_content">
<div id="table_top_area">
<h3>Liste des membres devant effectuer un rattrapage</h3>
<div class="table_grouped_action">
<button type="button" class="btn--primary" id="decrement_selected_members_makeups">
-1 rattrapage pour les membres sélectionnés
</button>
</div>
</div>
<div class="table_area">
<table id="makeups_members_table" class="display" cellspacing="0" width="100%"></table>
</div>
<div id="add_members_area">
<div id="add_members_form_area">
<h4>Ou, ajouter un rattrapage à un.e membre</h4>
<form id="search_member_form" action="javascript:;" method="post">
<input type="text" id="search_member_input" value="" placeholder="Nom ou numéro du coop..." required>
<button type="submit" class="btn--primary" id="search_member_button">Recherche</button>
</form>
</div>
<div class="search_member_results_area" style="display:none;">
<div class="search_results_text">
<p><i>Choisissez parmi les membres trouvés :</i></p>
</div>
<div class="search_member_results"></div>
</div>
</div>
<div class="header txtcenter">
<h1>Bienvenue sur l'interface d'administration BDM</h1>
</div>
</div>
<div id="templates" style="display:none;"></div>
<div class="management_type_buttons txtcenter">
<button type="button" class="btn--primary management_type_button" id="manage_makeups_button">
Gestion des rattrapages
<span class="management_type_button_icons"><i class="fas fa-arrow-right"></i></span>
</button><br>
<button type="button" class="btn--primary management_type_button" id="manage_shift_registrations_button">
Gestion des présences
<span class="management_type_button_icons"><i class="fas fa-arrow-right"></i></span>
</button><br>
<button type="button" class="btn--primary management_type_button" id="manage_attached_button">
Gestion des binômes
<span class="management_type_button_icons"><i class="fas fa-arrow-right"></i></span>
</button><br>
<button type="button" class="btn--primary management_type_button" id="manage_regular_shifts_button">
Gestion des créneaux
<span class="management_type_button_icons"><i class="fas fa-arrow-right"></i></span>
</button><br>
<button type="button" class="btn--primary management_type_button" id="manage_leaves_button" disabled>
Gestion des congés
<span class="management_type_button_icons"><i class="fas fa-wrench"></i></span>
{# <span class="management_type_button_icons"><i class="fas fa-arrow-right"></i></span> #}
</button><br>
</div>
</div>
</div>
<script src='{% static "js/all_common.js" %}?v='></script>
<script src='{% static "js/members_admin.js" %}?v='></script>
<script src="{% static "js/all_common.js" %}?v="></script>
{% endblock %}
{% extends "base.html" %}
{% load static %}
{% block additionnal_css %}
<link rel="stylesheet" href="{% static 'css/datatables/datatables.min.css' %}">
<link rel="stylesheet" href="{% static 'css/admin/manage_attached.css' %}">
<link rel="stylesheet" href="{% static 'jquery-ui-1.12.1/jquery-ui.min.css' %}">
{% endblock %}
{% block additionnal_scripts %}
<script type="text/javascript" src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/datatables/datatables.min.js' %}"></script>
{% endblock %}
{% block content %}
<div class="page_body">
<div id="back_to_admin_index">
<button type="button" class="btn--danger"><i class="fas fa-arrow-left"></i>&nbsp; Retour</button>
</div>
<div class="login_area">
{% include "common/conn_admin.html" %}
</div>
<div class="header txtcenter">
<h1>Gestion des Binômes</h1>
</div>
<div class="page_content">
<div class="management_type_buttons txtcenter">
<button type="button" class="btn--primary management_type_button" id="manage_attached_create_pair_button">
Créer un binôme
<span class="management_type_button_icons"></span>
</button><br>
<button type="button" class="btn--primary management_type_button" id="manage_attached_delete_pair_button">
Désolidariser un binôme
<span class="management_type_button_icons"></span>
</button><br>
</div>
</div>
</div>
<script src='{% static "js/all_common.js" %}?v='></script>
<script src='{% static "js/admin/manage_attached.js" %}?v='></script>
<script src='{% static "js/admin/bdm_index.js" %}?v='></script>
{% endblock %}
\ No newline at end of file
{% extends "base.html" %}
{% load static %}
{% block additionnal_css %}
<link rel="stylesheet" href="{% static 'css/datatables/datatables.min.css' %}">
<link rel="stylesheet" href="{% static 'css/admin/manage_attached.css' %}">
<link rel="stylesheet" href="{% static 'jquery-ui-1.12.1/jquery-ui.min.css' %}">
{% endblock %}
{% block additionnal_scripts %}
<script type="text/javascript" src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/datatables/datatables.min.js' %}"></script>
{% endblock %}
{% block content %}
<div id="template" style="display:None;">
<div id="confirmModal">
<h3>Le binôme est sur le point d'être créé</h3>
<br/>
<p>Voulez-vous vraiment créer le binôme avec comme titulaire <b><span id="parentName"></span></b> et comme suppléant <b><span id="childName"></span></b>
<p>Êtes-vous sur de vouloir continuer ?</p>
<hr/>
</div>
</div>
<div class="page_body">
<div id="back_to_admin_index">
<button type="button" class="btn--danger"><i class="fas fa-arrow-left"></i>&nbsp; Retour</button>
</div>
<div class="login_area">
{% include "common/conn_admin.html" %}
</div>
<div class="header txtcenter">
<h1>Gestion des Binômes</h1>
</div>
<div class="page_content">
<div class="tiles_container">
<div class="tile">
<div class="search_member_form_area" id="search_member_form_area">
<h4>Rechercher le.a coopérateur.ice titulaire</h4>
<form autocomplete="off" id="search_member_form" class="search_member_form" action="javascript:;" method="post">
<input name="searchParent" type="text" id="search_member_input" value="" placeholder="Nom ou numéro du coop..." required>
<img id="spinner1" class="spinner" src="{% static 'img/Loading_2.gif' %}" alt="loading" style="display:none;">
</form>
</div>
<div id="parentInfo" style="display:none;">
<div class="tile_title">
<i class="fas fa-user tile_icon"></i>
<span class="member_info member_name"></span>
<br>
<span>Email : </span>
<span class="member_info member_email"></span>
</div>
<div class="tile_content">
<div class="member_status_text_container">
<span>Statut : </span>
<span class="member_info member_status"></span>
<br>
<span>Nombre de rattrapage(s) : </span>
<span class="member_makeups_to_do member_info"></span>
</div>
<div class="member_shift_name_area">
<span>Créneau : </span>
<span class="member_shift_name member_info"></span>
</div>
</div>
</div>
</div>
<div class="create_pair_button">
<button id="createPair" type="button" name="button" disabled>Créer le binôme</button>
</div>
<div class="tile">
<div class="search_member_form_area" id="search_member_form_area">
<h4>Rechercher le.a coopérateur.ice suppléant.e</h4>
<form autocomplete="off" id="search_member_form_child" class="search_member_form" action="javascript:;" method="post">
<input name="searchChild" type="text" id="search_child_input" value="" placeholder="Nom ou numéro du coop..." required>
<img id="spinner2" class="spinner" src="{% static 'img/Loading_2.gif' %}" alt="loading" style="display:none;">
</form>
</div>
<div id="childInfo" style="display:none;">
<div class="tile_title">
<i class="fas fa-user tile_icon"></i>
<span class="member_info member_name"></span>
<br>
<span>Email : </span>
<span class="member_info member_email"></span>
</div>
<div class="tile_content">
<div class="member_status_text_container">
<span>Statut : </span>
<span class="member_info member_status"></span>
<br>
<span>Nombre de rattrapage(s) : </span>
<span class="member_makeups_to_do member_info"></span>
</div>
<div class="member_shift_name_area">
<span>Créneau : </span>
<span class="member_shift_name member_info"></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src='{% static "js/all_common.js" %}?v='></script>
<script src='{% static "js/admin/manage_attached.js" %}?v='></script>
{% endblock %}
{% extends "base.html" %}
{% load static %}
{% block additionnal_css %}
<link rel="stylesheet" href="{% static 'css/datatables/datatables.min.css' %}">
<link rel="stylesheet" href="{% static 'css/admin/manage_attached.css' %}">
<link rel="stylesheet" href="{% static 'jquery-ui-1.12.1/jquery-ui.min.css' %}">
{% endblock %}
{% block additionnal_scripts %}
<script type="text/javascript" src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/datatables/datatables.min.js' %}"></script>
{% endblock %}
{% block content %}
<div id="template" style="display:None;">
<div id="confirmModal">
<h3>Le binôme est sur le point d'être désolidarisé</h3>
<br />
Êtes-vous sûr.e de vouloir désolidariser :
<div>
<div class="attached-members">
<div class="member">
<div class="name">
<strong><span id="parentName"></span></strong> (titulaire)<br />
<i><span id="parentEmail"></span></i>
<div class="select_after_unattached_state">
<input type="checkbox" name="parent_gone" class="after_unattached_state" />
</div>
</div>
</div>
et
<div class="member">
<div class="name">
<strong><span id="childName"></span></strong><br />
<i><span id="childEmail"></span></i>
<div class="select_after_unattached_state">
<input type="checkbox" name="child_gone" class="after_unattached_state" />
</div>
</div>
</div>
</div>
</div>
<p class="warning_instruction">
Si vous souhaitez attribuer le statut "Parti·e" à l'un·e des deux membres, <br/>
cochez la case correspondante.<br/>
Attention, ce statut est réservé aux personnes qui ont demandé à être désinscrit·e·s de leur créneau pour une durée longue ou indéfinie tout en restant membre de La Cagette.
</p>
<hr/>
</div>
</div>
<div class="page_body">
<div id="back_to_admin_index">
<button type="button" class="btn--danger"><i class="fas fa-arrow-left"></i>&nbsp; Retour</button>
</div>
<div class="login_area">
{% include "common/conn_admin.html" %}
</div>
<div class="header txtcenter">
<h1>Gestion des Binômes</h1>
</div>
<div class="page_content">
<div id="subheader">
<h4>Liste des membres en binômes</h4>
</div>
<div class="table_area">
<table id="attached_members_table" class="display" cellspacing="0" width="100%"></table>
</div>
</div>
</div>
<script src='{% static "js/all_common.js" %}?v='></script>
<script src='{% static "js/admin/manage_attached.js" %}?v='></script>
{% endblock %}
{% extends "base.html" %}
{% load static %}
{% block additionnal_css %}
<link rel="stylesheet" href="{% static 'css/datatables/datatables.min.css' %}">
<link rel="stylesheet" href="{% static 'css/admin/manage_makeups.css' %}">
<link rel="stylesheet" href="{% static 'jquery-ui-1.12.1/jquery-ui.min.css' %}">
{% endblock %}
{% block additionnal_scripts %}
<script type="text/javascript" src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/datatables/datatables.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/notify.min.js' %}?v="></script>
{% endblock %}
{% block content %}
<div class="page_body">
<div id="back_to_admin_index">
<button type="button" class="btn--danger"><i class="fas fa-arrow-left"></i>&nbsp; Retour</button>
</div>
<div class="login_area">
{% include "common/conn_admin.html" %}
</div>
<div class="header txtcenter">
<h1>Gestion des Rattrapages</h1>
</div>
<div class="page_content">
<div id="table_top_area">
<h3>Liste des membres devant effectuer un rattrapage</h3>
<div class="table_grouped_action">
<button type="button" class="btn--primary" id="decrement_selected_members_makeups">
-1 rattrapage pour les membres sélectionnés
</button>
</div>
</div>
<div class="table_area">
<table id="makeups_members_table" class="display" cellspacing="0" width="100%"></table>
</div>
<div id="add_members_area">
<div id="add_members_form_area">
<h4>Ou, ajouter un rattrapage à un.e membre</h4>
<form id="search_member_form" action="javascript:;" method="post">
<input type="text" id="search_member_input" value="" placeholder="Nom ou numéro du coop..." required>
<button type="submit" class="btn--primary" id="search_member_button">Recherche</button>
</form>
</div>
<div class="search_member_results_area" style="display:none;">
<div class="search_results_text">
<p><i>Choisissez parmi les membres trouvés :</i></p>
</div>
<div class="search_member_results"></div>
</div>
</div>
</div>
<div id="templates" style="display:none;"></div>
</div>
<script src='{% static "js/all_common.js" %}?v='></script>
<script src='{% static "js/admin/manage_makeups.js" %}?v='></script>
{% endblock %}
{% extends "base.html" %}
{% load static %}
{% block additionnal_css %}
<link rel="stylesheet" href="{% static 'css/datatables/datatables.min.css' %}">
<link rel="stylesheet" href="{% static 'css/admin/manage_regular_shifts.css' %}">
<link rel="stylesheet" href="{% static 'jquery-ui-1.12.1/jquery-ui.min.css' %}">
{% endblock %}
{% block additionnal_scripts %}
<script type="text/javascript" src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/datatables/datatables.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/notify.min.js' %}?v="></script>
{% endblock %}
{% block content %}
<div class="page_body">
<div id="back_to_admin_index">
<button type="button" class="btn--danger"><i class="fas fa-arrow-left"></i>&nbsp; Retour</button>
</div>
<div class="login_area">
{% include "common/conn_admin.html" %}
</div>
<div class="header txtcenter">
<h1>Gestion des Créneaux</h1>
</div>
<div class="page_content">
<div id="search_member_area">
<div id="search_member_form_area">
<h4>Rechercher un.e membre</h4>
<form id="search_member_form" action="javascript:;" method="post">
<input type="text" id="search_member_input" value="" placeholder="Nom ou numéro du coop..." required>
<button type="submit" class="btn--primary" id="search_member_button">Recherche</button>
</form>
</div>
<div class="search_member_results_area" style="display:none;">
<div class="search_results_text">
<p><i>Choisissez parmi les membres trouvés :</i></p>
</div>
<div class="search_member_results"></div>
</div>
</div>
<div id="partner_data_area">
<h4 class="member_name_container">
<i class="fas fa-user member_name_icon"></i>
<span class="member_info member_name"></span>
</h4>
<p class="shift_name_container">Créneau : <span class="member_info member_shift"></span></p>
<p class="status_container">Statut : <span class="member_info member_status"></span></p>
<p class="makeups_container">Nb rattrapage(s) : <span class="member_info member_makeups"></span></p>
<div id="actions_on_member">
<button class="btn--primary" id="remove_shift_template_button">
Désinscrire du créneau
</button>
<button class="btn--primary" id="change_shift_template_button">
Changer de créneau
</button>
<button class="btn--primary" id="subscribe_to_shift_template_button">
Réinscrire à un créneau
</button>
</div>
</div>
</div>
</div>
<div id="shifts_calendar_area">
{% include "members/shift_template_choice.html" %}
</div>
<div id="templates" style="display:none;">
<div id="modal_remove_shift_template">
<p>Voulez vraiment désinscrire ce membre du créneau <span class="shift_template_name"></span> ?</p>
<div class="checkbox_area">
<input type="checkbox" id="permanent_unsuscribe" name="permanent_unsuscribe">
<label for="permanent_unsuscribe">Désinscription définitive</label>
</div>
</div>
<div id="modal_error_change_shift_template">
<h3 class="error_modal_title""">Action impossible</h3>
<p>
Le ou la membre est inscrit.e à un rattrapage sur le créneau choisi (<span class="shift_template_name"></span>), cela empêche de l'inscrire sur ce créneau.
</p>
<p>Vous pouvez essayer de l'inscrire sur ce créneau une autre semaine.</p>
</div>
</div>
</div>
<script src="{% static "js/pouchdb.min.js" %}"></script>
<script type="text/javascript">
var type = 2;
var has_committe_shift = '{{has_committe_shift}}'
var max_begin_hour = '{{max_begin_hour}}'
var couchdb_dbname = '{{db}}';
var couchdb_server = '{{couchdb_server}}' + couchdb_dbname;
var ASSOCIATE_MEMBER_SHIFT = '{{ASSOCIATE_MEMBER_SHIFT}}';
</script>
<script src='{% static "js/common.js" %}?v='></script>
<script src='{% static "js/all_common.js" %}?v='></script>
<script src='{% static "js/admin/manage_regular_shifts.js" %}?v='></script>
{% endblock %}
{% extends "base.html" %}
{% load static %}
{% block additionnal_css %}
<link rel="stylesheet" href="{% static 'css/datatables/datatables.min.css' %}">
<link rel="stylesheet" href="{% static 'css/admin/manage_shift_registrations.css' %}">
<link rel="stylesheet" href="{% static 'jquery-ui-1.12.1/jquery-ui.min.css' %}">
{% endblock %}
{% block additionnal_scripts %}
<script type="text/javascript" src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/datatables/datatables.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/notify.min.js' %}?v="></script>
{% endblock %}
{% block content %}
<div class="page_body">
<div id="back_to_admin_index">
<button type="button" class="btn--danger"><i class="fas fa-arrow-left"></i>&nbsp; Retour</button>
</div>
<div class="login_area">
{% include "common/conn_admin.html" %}
</div>
<div class="header txtcenter">
<h1>Gestion des Présences</h1>
</div>
<div class="page_content">
<div id="search_member_area">
<div id="search_member_form_area">
<h4>Rechercher un.e membre</h4>
<form id="search_member_form" action="javascript:;" method="post">
<input type="text" id="search_member_input" value="" placeholder="Nom ou numéro du coop..." required>
<button type="submit" class="btn--primary" id="search_member_button">Recherche</button>
</form>
</div>
<div class="search_member_results_area" style="display:none;">
<div class="search_results_text">
<p><i>Choisissez parmi les membres trouvés :</i></p>
</div>
<div class="search_member_results"></div>
</div>
</div>
<div id="table_top_area">
<h3>Liste des futurs services de <span id="member_name"></span></h3>
</div>
<div class="table_area">
<table id="member_shifts_table" class="display" cellspacing="0" width="100%"></table>
</div>
</div>
<div id="templates" style="display:none;"></div>
</div>
<script src='{% static "js/all_common.js" %}?v='></script>
<script src='{% static "js/admin/manage_shift_registrations.js" %}?v='></script>
{% endblock %}
......@@ -52,7 +52,7 @@
<div class="row-1 grid-2">
<div class="col-1">
<div class="label">
Biper le badge, ou saisissez le n° de coop. ou le nom
Biper le badge, ou saisir le n° de coop. ou le nom
</div>
Recherche :
<input type="text" name="search_string" autocomplete="off" />
......@@ -112,7 +112,7 @@
<div id="next_shifts">
</div>
</div>
<a class="btn" data-next="first_page">Coopérateur suivant</a>
<a class="btn" data-next="first_page">Coopérateur.rice suivant.e</a>
</div>
<div class="col-1">
<section id="member_advice">
......@@ -138,9 +138,13 @@
</section>
</section>
<section class="grid-6 has-gutter" id="service_entry">
<div class="col-6 row-2">
<div class="col-2 row-2">
<a class="btn btn--primary" data-next="first_page" >Retour accueil</a>
</div>
<div class="col-2 row-2"></div>
<div class="col-2 row-2 login_area">
{% include "common/conn_admin.html" %}
</div>
<div class="col-1"></div>
<div class="col-4">
<h1 class="col-4 txtcenter">Qui es-tu ?</h1>
......@@ -178,9 +182,23 @@
<section id="service_validation" class="col-6 grid-6 has-gutter">
<div class="col-2"></div>
<a class="col-2 btn present">{{CONFIRME_PRESENT_BTN|safe}}</a>
<span class="loading2"><img width="75" src="/static/img/Pedro_luis_romani_ruiz.gif" alt="Chargement en cours...." /></span>
<div class="col-2"></div>
<div class="col-2 loading2-container">
<span class="loading2"><img width="75" src="/static/img/Pedro_luis_romani_ruiz.gif" alt="Chargement en cours...." /></span>
</div>
</section>
<div id="associated_service_validation">
<p class="col-6 txtcenter">Qui est présent à ce service ?</p>
<section class="col-6 grid-5 has-gutter">
<div class="col-1"></div>
<a id="associated_btn" class=" btn present">Membre</a>
<a id="partner_btn" class=" btn present">Associé</a>
<a id="both_btn" class=" btn present">Les deux</a>
<div class="col-1 loading2-container">
<span class="loading2"><img width="75" src="/static/img/Pedro_luis_romani_ruiz.gif" alt="Chargement en cours...." /></span>
</div>
</section>
</div>
</div>
<div class="col-2"></div>
<a class="btn col-2 return" data-next="service_entry">Retour</a>
......@@ -208,7 +226,7 @@
{% endif %}
</section>
<div class="col-2"></div>
<a class="btn col-2" data-next="first_page">Coopérateur suivant</a>
<a class="btn col-2" data-next="first_page">Coopérateur.ice suivant.e</a>
<div class="col-2"></div>
</section>
<section class="grid-6 has-gutter" id="rattrapage_1">
......
......@@ -26,22 +26,29 @@
{% endblock %}
{% block content %}
<nav class="col-6 clearfix nav">
<div id="process_state" class="fl"></div>
<button id="create_new_coop" class="btn--primary fr">Nouvelle inscription</button>
<button id="coop_list_btn" class="btn--info fr" style="display:none;">Liste</button>
<button id="shift_calendar" class="btn--inverse fr">Vue créneaux</button>
<div class="left-nav">
{% if prepa_odoo_url != '' %}
<div id="goto_prepa_odoo" class="fl">
<a class="btn--info" id="goto_prepa_odoo_button" href='{{prepa_odoo_url}}' target='_blank'>Prepa Odoo</a>
</div>
{% endif %}
<div id="process_state_container" class="fl">
<div id="process_state"></div>
</div>
</div>
<button id="create_new_coop" class="btn--primary fr">Nouvelle inscription</button>
<button id="coop_list_btn" class="btn--info fr" style="display:none;">Liste</button>
<button id="shift_calendar" class="btn--inverse fr">Vue créneaux</button>
</nav>
<section class="center" id="new_coop">
<div class="grid-1">
<div class="item-center">
<div class="item-center">
<h2 class="title">
NOUVEAU MEMBRE
</h2>
<form id="coop_create" lang="fr">
<form id="coop_create" lang="fr">
{% if ask_for_sex %}
<p>
{% include "members/sex_input.html" %}
......@@ -65,17 +72,61 @@
<input type="number" min="1" placeholder="Nb de chèques" name="ch_qty" id="ch_qty" style="display:none;"/>
</p>
{% if input_barcode %}
<p>
<input type="text" name="m_barcode" id="m_barcode" maxlength="13" size="13" placeholder="Code barre" autocomplete="off" required/>
</p>
{% endif %}
<button class="btn--primary">Valider</button>
</form>
</div>
<div id="mail_generation">
(*) L'adresse mail étant obligatoire, si le nouveau membre n'en a pas, veuillez en créer une en cliquant sur le bouton suivant : <a class="btn--info" id="generate_email">+</a>
</div>
{% if ASSOCIATE_MEMBER_SHIFT %}
<p id="add_binome" >+ Binomes (facultatif)</p>
<div id="associate_area" style="display:none;">
<div class="choice_button_area d-flex" >
<div id="existing_member_choice" class="member_choice">
A mettre en binome avec un.e membre existant.e
</div>
<div id="new_member_choice" class="member_choice">
A mettre en binome avec un.e nouveau membre
</div>
</div>
<div id="existing_member_choice_action" style="display:none;">
<input type="text" id="search_member_input" value="" placeholder="Nom ou numéro du coop..." >
<div class="btn--primary" id="search_member_button">Recherche</div>
<div class="search_member_results_area" style="display:none;">
<div class="search_results_text">
<p><i>Choisissez parmi les membres trouvés :</i></p>
</div>
<div class="search_member_results"></div>
</div>
<div class="chosen_associate_area" style="display:none;">
<div >
<p><i>Binôme choisi : </i></p>
</div>
<div class="chosen_associate_group">
<span class="chosen_associate"></span>
<i class="fas fa-times remove_binome_icon"></i>
</div>
</div>
</div>
<div id="new_member_choice_action" style="display:none;">
<div >
<div>
<input type="text" id="new_member_input" value="" placeholder="Nom du membre" >
</div>
</div>
</div>
</div>
{% endif %}
<div>
<button class="btn--primary">Valider</button>
</div>
</form>
<div id="mail_generation">
(*) L'adresse mail étant obligatoire, si le nouveau membre n'en a pas, veuillez en créer une en cliquant sur le bouton suivant : <a class="btn--info" id="generate_email">+</a>
</div>
</div>
</section>
{% include "members/shift_template_choice.html" %}
......@@ -90,8 +141,9 @@
<div class="numbox badge"></div>
<p>Pensez à inscrire ce numéro temporaire de coopérateur au crayon de papier sur les deux formulaires papier.</p>
-->
<p id="parent" hidden>En binôme avec : <span id="parentName"></span></p>
<p>Créneau choisi : <span class="shift_template"></span></p>
<p>Prochain service : <span class="next_shift"></span></p>
<p id="next_shift_registration_detail">Prochain service : <span class="next_shift"></span></p>
<button class="btn--primary" id="next_coop">Coopérateur.rice suivant.e !</button>
</div>
......@@ -119,15 +171,18 @@
var couchdb_dbname = '{{db}}';
var couchdb_server = '{{couchdb_server}}' + couchdb_dbname;
var dbc = new PouchDB(couchdb_dbname);
var ASSOCIATE_MEMBER_SHIFT = '{{ASSOCIATE_MEMBER_SHIFT}}';
var sync = PouchDB.sync(couchdb_dbname, couchdb_server, {
live: true,
retry: true,
auto_compaction: false
});
var mag_place_string = '{{mag_place_string}}';
var office_place_string = '{{office_place_string}}'
var max_begin_hour = '{{max_begin_hour}}'
var email_domain = '{{email_domain}}'
var office_place_string = '{{office_place_string}}';
var max_begin_hour = '{{max_begin_hour}}';
var email_domain = '{{email_domain}}';
var committees_shift_id = '{{committees_shift_id}}';
</script>
<script src="{% static "js/all_common.js" %}?v="></script>
<script src="{% static "js/common.js" %}?v="></script>
......
......@@ -60,6 +60,9 @@
<input type="text" name="m_barcode" id="m_barcode" disabled/>
</p>
{% endif %}
<div id="associated_member">
En binôme avec : <span id ="associated_member_name"></span>
</div>
</div>
<p class="buttons">
<button class="btn--success" name="valider">Tout est bon</button>
......
......@@ -31,7 +31,7 @@
<div class="param">
<button type="button" class="accordion btn_faq">
<span class="full_width">000 Gestion de mon créneau: réinscription, changement, désinscription temporaire (absence de moyenne ou longue durée)</span>
<span class="full_width">000 Gestion de mon créneau : réinscription, changement, désinscription temporaire (absence de moyenne ou longue durée)</span>
</button>
<div class="input-container panel">
......@@ -45,10 +45,13 @@
</div>
<div class="grp_text">
<h3><b>Se réinscrire à un créneau:</b></h3>
<p>
<h3><b>Se réinscrire à un créneau :</b></h3>
<p class="attached-blocked">
Si tu es désinscrit.e parce que tu as manqué des services sans faire tes rattrapages il faut te réinscrire à un créneau. Il te faudra tout de même faire tes rattrapages avant d'être à jour de tes services.<br/>
<b>ATTENTION :</b> si tu es en binôme c'est avec le nom du.de la titulaire qu'il faut remplir le formulaire.
</p>
<p class="attached-unblocked">
Si tu es désinscrit.e parce que tu as manqué des services sans faire tes rattrapages il faut te réinscrire à un créneau. Il te faudra tout de même faire tes rattrapages avant d'être à jour de tes services.<br/>
<b>ATTENTION:</b> si tu es en binôme c'est avec le nom du.de la titulaire qu'il faut remplir le formulaire.
</p>
<div class="faq_link_button_area">
<a
......@@ -63,10 +66,13 @@
</div>
</div>
<div class="grp_text">
<h3><b>Changer de créneau:</b></h3>
<p>
<h3><b>Changer de créneau :</b></h3>
<p class="attached-blocked">
Si ton créneau ne te convient plus tu peux demander à le changer.<br/>
<b>ATTENTION :</b> si tu es en binôme c'est avec le nom du.de la titulaire qu'il faut remplir le formulaire.
</p>
<p class="attached-unblocked">
Si ton créneau ne te convient plus tu peux demander à le changer.<br/>
<b>ATTENTION:</b> si tu es en binôme c'est avec le nom du.de la titulaire qu'il faut remplir le formulaire.
</p>
<div class="faq_link_button_area">
<a
......@@ -81,13 +87,18 @@
</div>
</div>
<div class="grp_text">
<h3><b>Se désinscrire de son créneau:</b></h3>
<p>
<h3><b>Se désinscrire de son créneau :</b></h3>
<p class="attached-blocked">
- Si tu as prévu de t'absenter de Montpellier ou que tu vas rencontrer une période durant laquelle tu ne pourras pas faire tes services tu peux te désinscrire de ton créneau. A ton retour il te faudra remplir le formulaire "réinscription à un créneau".<br/>
- Si tu ne souhaites plus participer à la coopérative mais que tu ne souhaites pas démissionner tout de suite, notamment car tu souhaites attendre que la part sociale prenne de la valeur pour te faire rembourser, tu dois te désinscrire de ton créneau. Au moment où tu souhaiteras démissionner il faudra remplir le formulaire du même nom. Tu ne pourras plus faire tes courses à la Cagette.<br/>
<b>ATTENTION:</b> si tu es en binôme et que l'autre personne souhaite continuer à participer il faut remplir le formulaire "se désolidariser de son binôme". Si tu es la personne titulaire du binôme l'autre personne devra remplir le formulaire "se réinscrire à un créneau".
- Si tu ne souhaites plus participer à la coopérative mais que tu ne souhaites pas démissionner tout de suite, notamment car tu souhaites attendre que la part sociale prenne de la valeur pour te faire rembourser, tu dois te désinscrire de ton créneau. Au moment où tu souhaiteras démissionner il faudra remplir le formulaire du même nom. Tu ne pourras plus faire tes courses ici.<br/>
<b>ATTENTION :</b> si tu es en binôme et que l'autre personne souhaite continuer à participer il faut remplir le formulaire "se désolidariser de son binôme". Si tu es la personne titulaire du binôme l'autre personne devra remplir le formulaire "se réinscrire à un créneau".
</p>
<p class="attached-unblocked">
- Si tu as prévu de t'absenter de Montpellier ou que tu vas rencontrer une période durant laquelle tu ne pourras pas faire tes services tu peux te désinscrire de ton créneau. A ton retour il te faudra remplir le formulaire "réinscription à un créneau".<br/>
- Si tu ne souhaites plus participer à la coopérative mais que tu ne souhaites pas démissionner tout de suite, notamment car tu souhaites attendre que la part sociale prenne de la valeur pour te faire rembourser, tu dois te désinscrire de ton créneau. Au moment où tu souhaiteras démissionner il faudra remplir le formulaire du même nom. Tu ne pourras plus faire tes courses à la Cagette.<br/>
<b>ATTENTION :</b> si tu es en binôme nous désolidariseront de fait ton binôme. La personne avec qui tu es en binôme sera inscrite au créneau que vous aviez. Si elle souhaite également se désinscrire de son créneau elle devra remplir le formulaire.
</p>
<div class="faq_link_button_area">
<a
href="javascript:void(0);"
......@@ -102,7 +113,7 @@
</div>
</div>
<button type="button" class="accordion btn_faq">
<span class="full_width" >001 Gestion de mes services: échanger son service, arrivé.e en retard à son service, demande de congés maladie ou parental</span>
<span class="full_width" >001 Gestion de mes services : échanger son service, arrivé.e en retard à son service, demande de congés maladie ou parental</span>
</button>
<div class="input-container panel">
......@@ -117,9 +128,12 @@
<div class="grp_text">
<h3><b>Échanger son service</b></h3>
<p>
<p class="attached-blocked">
Si tu ne peux pas venir effectuer ton service à la date prévue il te faut l'échanger sur ton espace membre.<br/>
<b>ATTENTION :</b> si tu es en binôme c'est via l'espace membre du titulaire du binôme qu'il faut le faire.
</p>
<p class="attached-unblocked">
Si tu ne peux pas venir effectuer ton service à la date prévue il te faut l'échanger sur ton espace membre.<br/>
<b>ATTENTION:</b> si tu es en binôme c'est via l'espace membre du titulaire du binôme qu'il faut le faire.
</p>
<div class="faq_link_button_area">
<a
......@@ -161,33 +175,77 @@
</div>
<div class="grp_text"><h3><b>Les congés maladie</b></h3>
Les congés maladie peuvent être pris en cas d’impossibilité physique de réaliser son service pendant une longue durée qui prend en compte<b> minimum 2 services</b>. Si ton impossibilité est d'un mois il t'est demandé de déplacer ton service.<br />
En cas de rhume nous encourageons les coops à simplement déplacer leur service depuis l’espace membre. La coop a besoin de la participation de chacun.e !<br />
<b>ATTENTION:</b> Compte tenu de la facilité offerte par le statut de binôme, les personnes formant un binôme ne pourront pas<br />
bénéficier de congés maladie. Il sera alors possible de se désolidariser de son binôme en remplissant le formulaire adéquat sur l’espace membre afin d’en bénéficier.<br />
<p class="attached-blocked">
Les congés maladie peuvent être pris en cas d’impossibilité physique de réaliser son service pendant une longue durée qui prend en compte<b> minimum 2 services</b>. Si ton impossibilité est d'un mois il t'est demandé de déplacer ton service.<br />
En cas de rhume nous encourageons les coops à simplement déplacer leur service depuis l’espace membre. La coop a besoin de la participation de chacun.e !<br />
<b>ATTENTION :</b> Compte tenu de la facilité offerte par le statut de binôme, les personnes formant un binôme ne pourront pas<br />
bénéficier de congés maladie. Il sera alors possible de se désolidariser de son binôme en remplissant le formulaire adéquat sur l’espace membre afin d’en bénéficier.<br />
</p>
<p class="attached-unblocked">
Les congés maladie peuvent être pris en cas d’impossibilité physique de réaliser son service pendant une longue durée qui prend en compte<b> minimum 2 services</b>. Si ton impossibilité est d'un mois il t'est demandé de déplacer ton service.<br />
En cas de rhume nous encourageons les coops à simplement déplacer leur service depuis l’espace membre. La coop a besoin de la participation de chacun.e !<br />
</p>
</div>
<div class="grp_text"><h3><b>Les congés parentaux</b></h3>
Lors de la naissance d’un enfant, les coops peuvent continuer de faire leur courses sans faire de services pendant 12 mois.<br />
Si les deux parents font partie de la coopérative, ils peuvent se partager leurs 12 mois comme il l’entendent. Il peuvent prendre par exemple 6 mois chacun.e en même temps ou 8 mois pour l'un.e puis 4 mois pour l'autre.<br />
<br />
Particularités: si les deux parents forment un binôme, i.elles ont alors accès à 6 mois de congé parental simultanément. S’ielles souhaitent répartir le congé différement il faut alors qu’ielles se débinomisent. Si l’un.e des deux parents est binôme avec un.e personne qui n’est pas l’autre parent, ielle doit se débinomiser pour bénéficier du congé parental.<br />
<br />
<p>
Lors de la naissance d’un enfant, les coops peuvent continuer de faire leur courses sans faire de services pendant 12 mois.<br />
Si les deux parents font partie de la coopérative, ils peuvent se partager leurs 12 mois comme il l’entendent. Il peuvent prendre par exemple 6 mois chacun.e en même temps ou 8 mois pour l'un.e puis 4 mois pour l'autre.<br />
</p>
<p>
Particularités : si les deux parents forment un binôme, i.elles ont alors accès à 6 mois de congé parental simultanément. S’ielles souhaitent répartir le congé différement il faut alors qu’ielles se débinomisent. Si l’un.e des deux parents est binôme avec un.e personne qui n’est pas l’autre parent, ielle doit se débinomiser pour bénéficier du congé parental.<br />
</p>
</div>
<div class="grp_text"><h3><b>L'exemption</b></h3>
Si ton état de santé ne te permet pas ou plus de faire tes services et que cette situation ne va pas évoluer ou si tu as 80 ans il est possible de demander une exemption. Cela te permet de ne pas être dans l'obligation de faire tes services tout en pouvant faire tes courses. Cette exemption n'a pas de date de fin, c'est pour cela qu'elle est a demandée dans des cas précis. Aucun justificatif médical n'est demandé, nous nous basons sur la confiance.<br /><br/>
Quelque soit ta demande de congés merci de remplir le formulaire ci dessous:<br />
<div class="faq_link_button_area">
<a
href="javascript:void(0);"
target="_blank"
type="button"
class="btn--primary faq_link_button"
id="sick_leave_form_link_btn"
>
Demande de congés
</a>
</div>
<p>
Si ton état de santé ne te permet pas ou plus de faire tes services et que cette situation ne va pas évoluer ou si tu as 80 ans il est possible de demander une exemption. Cela te permet de ne pas être dans l'obligation de faire tes services tout en pouvant faire tes courses. Cette exemption n'a pas de date de fin, c'est pour cela qu'elle est a demandée dans des cas précis. Aucun justificatif médical n'est demandé, nous nous basons sur la confiance.
</p>
<br />
</div>
<div class="attached-unblocked">
<h3><b>ATTENTION : pour les binômes</b><br /></h3>
<p>
<b>Concernant les congés maladie :</b><br />
- Si un.e des membres du binôme est dans l’incapacité d'effectuer son service sur une période inférieure à deux mois : iel peut déplacer son service ou demander à la personne qui forme son binôme de le faire à sa place.<br />
- Si un.e des membres du binôme est dans l’incapacité d’effectuer son service sur une période supérieure à deux mois : si possible la personne du binôme en capacité de faire ses services remplace la personne qui n’est pas en capacité.<br />
<b>Si cela n’est pas possible :</b><br />
- La personne qui ne peut pas effectuer son service sur une période supérieure à deux mois doit faire une demande de congés maladie en remplissant le formulaire du BDM. Le BDM supprimera alors arbitrairement la moitié des services à venir du binôme sur la période demandée.<br />
<i>Exemple : si je suis en incapacité de d’effectuer mon service durant 6 mois le BDM supprimera 3 services à mon binôme. Si c’est durant 4 mois, 2 services seront supprimés, etc..</i><br />
Ce calcul est basé sur le principe qu’au sein d’un binôme chaque membre fait un service sur deux. Nous supprimons alors les services de la personne en incapacité.<br />
</p>
<p>
<b>Concernant les congés parentaux :</b><br />
<b>Si les deux parents forment le binôme :</b><br />
Le binôme aura droit à 6 mois de congé parental. Durant cette période, iels seront dispensé·e·s de services et pourront faire leurs courses.<br />
<b>Si les deux parents ne forment pas le binôme :</b><br />
- Cas N°1 : un·e seul·e des parents est coopérateur·rice. Iel a droit à 12 mois de congés parental divisé par deux car en binôme. Iel a donc 6 services supprimés.<br />
- Cas N°2 : les deux parents sont coopérateur·rices et l’un·e est en binôme. Le couple parental a droit de se répartir 12 mois de congé. Pour la personne en binôme la durée choisie donnera lieu à la suppression de la moitié des services.<br />
<i>Ex: le couple parental choisit de se répartir de la manière suivante. X qui n’est pas en binôme prend 8 mois de congés et Y qui est en binôme prend 4 mois de congés. X sera donc 8 mois en congés et pour Y il y aura 2 services du binôme supprimés (4 divisé par 2).</i><br />
- Cas N°3 : le couple parental est chacun·e en binôme avec quelqu’un·e d’autre. Pour les deux, la durée de congé choisie donnera lieu à la suppression de la moitié des services du binôme.<br />
<i>Ex. : X prend 8 mois de congé donc son binôme aura 4 services de supprimés et pour Y qui prend 4 mois de congés son binôme aura 2 services supprimés.</i><br />
</p>
<p>
<b>Les demandes d’exemption permanente:</b><br />
Il est possible de demander à être exempté·e de ses services de manière permanente. Cela concerne les personnes de plus de 80 ans ainsi que les personnes dans l’incapacité permanente d’effectuer leur services en magasin.<br />
Dans ce cas, nous invitons le binôme à se désolidariser.<br />
La personne en incapacité devra faire une demande d’exemption.<br />
</p>
</div>
<br />
<p>
Quelque soit ta demande de congés merci de remplir le formulaire ci dessous :
</p>
<div class="faq_link_button_area">
<a
href="javascript:void(0);"
target="_blank"
type="button"
class="btn--primary faq_link_button"
id="sick_leave_form_link_btn"
>
Demande de congés
</a>
</div>
<br />
</div>
<button type="button" class="accordion btn_faq">
<span class="full_width">002 Mon Statut : Je suis en statut "Rattrapage" ou désinscrit.e et je ne peux pas faire mes courses</span>
......@@ -259,24 +317,48 @@
</div>
<button type="button" class="accordion btn_faq">
<span class="full_width" >003 Mon binôme: créer un binôme, se désolidariser de son binôme, changer de binôme</span>
<span class="full_width" >003 Mon binôme : créer un binôme, se désolidariser de son binôme, changer de binôme</span>
</button>
<div class="input-container panel">
<div class="grp_text">
<h3><b>Créer un binôme</b></h3>
Afin de faciliter l’intégration de personnes qui ont des difficultés à rejoindre la Cagette et par mesure de solidarité : Chaque coopérateur·rice peut rattacher un·e autre coopérateur·rice. <br />
Cependant, La Cagette a besoin de forces vives pour pouvoir fonctionner correctement et il est nécessaire qu'un maximum de personnes soient présentes sur les créneaux. <h4><b>C'est pourquoi ce statut de binôme, qui doit rester exceptionnel, est réservé à des personnes qui rencontrent de grandes difficultés organisationnelles dans leur quotidien.</b></h4> Ces difficultés sont à évaluer par la personne concernée. Par exemple : les mères ou les pères célibataires avec enfants, les personnes ayant une charge de travail et /ou des conditions particulières de travail, les aidants, les personnes rencontrant des problèmes de santé... Ce statut est transitoire selon l’évolution des conditions de vie de la personne concernée.
<b>Comprendre le contexte du binôme : Titulaire du créneau et suppléant</b><br />
La procédure de création de binôme concerne deux membres, le titulaire et le suppléant.<br />
<b>Le titulaire </b>est le coopérateur qui va devenir responsable du binôme. Il est responsable de la réalisation des services. Par exemple, si le titulaire n’est pas à jour de ses services, les deux membres du binôme ne pourront plus faire leurs courses. C’est le titulaire qui doit faire la demande de binôme. C'est à partir de son espace membre que les services sont gérés.<br />
<b>Le suppléant</b> est la personne qui va se rattacher au compte du titulaire. Elle n’aura pas d’obligation de faire un service et pourra faire ses courses. Son statut est &quot;désinscrit&quot;, c'est normal. Lorsqu'ielle vient faire un service c'est au nom du de la titulaire du binôme. En revanche, lors du passage en caisse chaque membre du binôme doit donner son propre nom.<br />
Le titulaire et le suppléant peuvent s’organiser comme ils l’entendent pour remplir les obligations du titulaire. Ils peuvent faire un service sur deux ou se répartir l’année en deux semestres, ou bien encore, le titulaire peut faire tous les services et le suppléant aucun. Peu importe, ça les regarde.<br />
<b>Il existe 3 conditions pour créer un binôme :</b><br />
Les deux coopérateur.trice.s doivent justifier que leur situation nécessite un binôme.<br />
Les deux coopérateur.trice.s doivent avoir fait au moins 4 créneaux avant de pouvoir former un binôme,<br />
Les deux coopérateur.trice.s doivent être “à jour” pour former un binôme. Si l’un des membres n’est pas à jour, ses points négatifs peuvent être transférés à l’autre membre. Ainsi, une personne qui a 2 points d’avance peut rattacher à son compte une personne qui a -2 points à son compteur.<br />
<div class="attached-blocked">
<p>
Afin de faciliter l’intégration de personnes qui ont des difficultés à rejoindre le supermarché coopératif et par mesure de solidarité : Chaque coopérateur·rice peut rattacher un·e autre coopérateur·rice. <br />
Cependant, nous avons besoin de forces vives pour pouvoir fonctionner correctement et il est nécessaire qu'un maximum de personnes soient présentes sur les créneaux.
</p>
<h4><b>C'est pourquoi ce statut de binôme, qui doit rester exceptionnel, est réservé à des personnes qui rencontrent de grandes difficultés organisationnelles dans leur quotidien.</b></h4>
<p>
Ces difficultés sont à évaluer par la personne concernée. Par exemple : les mères ou les pères célibataires avec enfants, les personnes ayant une charge de travail et /ou des conditions particulières de travail, les aidants, les personnes rencontrant des problèmes de santé... Ce statut est transitoire selon l’évolution des conditions de vie de la personne concernée.<br />
<b>Comprendre le contexte du binôme : Titulaire du créneau et suppléant</b><br />
La procédure de création de binôme concerne deux membres, le titulaire et le suppléant.<br />
<b>Le titulaire </b>est le coopérateur qui va devenir responsable du binôme. Il est responsable de la réalisation des services. Par exemple, si le titulaire n’est pas à jour de ses services, les deux membres du binôme ne pourront plus faire leurs courses. C’est le titulaire qui doit faire la demande de binôme. C'est à partir de son espace membre que les services sont gérés.<br />
<b>Le suppléant</b> est la personne qui va se rattacher au compte du titulaire. Elle n’aura pas d’obligation de faire un service et pourra faire ses courses. Son statut est &quot;désinscrit&quot;, c'est normal. Lorsqu'ielle vient faire un service c'est au nom du de la titulaire du binôme. En revanche, lors du passage en caisse chaque membre du binôme doit donner son propre nom.<br />
Le titulaire et le suppléant peuvent s’organiser comme ils l’entendent pour remplir les obligations du titulaire. Ils peuvent faire un service sur deux ou se répartir l’année en deux semestres, ou bien encore, le titulaire peut faire tous les services et le suppléant aucun. Peu importe, ça les regarde.<br />
<b>Il existe 3 conditions pour créer un binôme :</b><br />
- Les deux coopérateur.trice.s doivent justifier que leur situation nécessite un binôme.<br />
- Les deux coopérateur.trice.s doivent avoir fait au moins 4 créneaux avant de pouvoir former un binôme,<br />
- Les deux coopérateur.trice.s doivent être “à jour” pour former un binôme. Si l’un des membres n’est pas à jour, ses points négatifs peuvent être transférés à l’autre membre. Ainsi, une personne qui a 2 points d’avance peut rattacher à son compte une personne qui a -2 points à son compteur.<br />
</p>
</div>
<div class="attached-unblocked">
<p>
Afin de faciliter l’intégration de personnes qui ont des difficultés à rejoindre La Cagette par manque de temps, et par mesure de solidarité, il est possible de se mettre en binôme avec la personne de son choix sans condition particulière.<br />
Cependant La Cagette a besoin de forces vives pour pouvoir fonctionner correctement et il est nécessaire qu'il y ait toujours des coopérateur·trices présent·e·s sur les créneaux. C'est pourquoi nous demandons aux personnes qui ont la possibilité de faire un service par mois de ne pas se mettre en binôme. Aussi les binômes pourront être re-soumis à condition s'il s'avère que les créneaux ne sont pas assez remplis pour assurer le bon fonctionnement du projet.
</p>
<h4><b>Comprendre le contexte du binôme :</b></h4>
<p>
Nous nommons “binôme” l’ensemble des deux personnes formant le binôme. Et nous nommons “membre du binôme” les coopérateur·rices à titre individuel.<br />
Il n’existe aucune condition pour devenir binôme.<br />
Si l’un·e des membres du binôme ou les deux ne sont pas à jour de leurs services au moment de la demande de binôme et qu’iels ont des rattrapages à effectuer, ces rattrapages seront reportés sur le binôme.<br />
</p>
<p>
Les deux membres du binôme seront inscrit·e·s au même créneau (ex: semaine A mardi 8h) et se partageront, comme iels le souhaitent, les services à effectuer.
Chacun·e des membres du binôme aura accès à son espace membre avec la liste des services à effectuer et devra noter son nom en face du/des services qu’iel assurera.
Les deux membres du binôme sont solidaires du binôme. Si un service est manqué les deux membres seront informé·e·s qu’il faudra faire un rattrapage. Le statut des membres du binôme sera donc identique.
</p>
</div>
<div class="faq_link_button_area">
<a
href="javascript:void(0);"
......@@ -292,7 +374,7 @@
<div class="grp_text">
<h3><b>Se désolidariser de son binôme</b></h3>
Si pour quelconque raison l&quot;une des deux personnes composant le binôme souhaite le désolidarisé il faut remplir le formulaire qui suit. Par défaut nous inscrirons la personne suppléante sur le même créneau que la personne titulaire. Si cela ne lui convient pas elle devra demander à changer de créneau.<br />
Si pour quelconque raison l'une des deux personnes composant le binôme souhaite le désolidariser il faut remplir le formulaire qui suit. Par défaut nous inscrirons les deux personnes sur le créneau du binôme. Si cela ne lui convient pas elle devra demander à changer de créneau.<br />
<div class="faq_link_button_area">
<a
href="javascript:void(0);"
......@@ -416,7 +498,7 @@
Cela dit, nous en découvrons de nouveaux tous les jours.<br />
Si tu n'as pas su quel formulaire remplir, tu es au bon endroit. <br />
Vas-y dit nous tout !<br /><br />
Attention: si tu souhaites contacter le BDM pour prévenir que tu seras absent-e à ton service cela ne sert à rien! Il te faut déplacer ton service via ton espace membre. Il n'est cependant pas possible d'échanger un service qui commence dans moins de 24h pour des raisons de logistiques. Si tu ne peux pas venir tu seras donc comptabilisé-e absent-e. Tu basculeras en statut "Rattrapage" et ne pourras plus faire tes courses. Il te faudra sélectionner dans ton espace membre un rattrapage à faire dans les 6 prochains mois pour basculer en statut "Délai" et pouvoir faire de nouveau tes courses.<br />
Attention : si tu souhaites contacter le BDM pour prévenir que tu seras absent-e à ton service cela ne sert à rien! Il te faut déplacer ton service via ton espace membre. Il n'est cependant pas possible d'échanger un service qui commence dans moins de 24h pour des raisons de logistiques. Si tu ne peux pas venir tu seras donc comptabilisé-e absent-e. Tu basculeras en statut "Rattrapage" et ne pourras plus faire tes courses. Il te faudra sélectionner dans ton espace membre un rattrapage à faire dans les 6 prochains mois pour basculer en statut "Délai" et pouvoir faire de nouveau tes courses.<br />
Merci de ne pas contacter le Bureau des membres pour cela, il te donnera exactement la même réponse.<br />
<div class="faq_link_button_area">
<a
......@@ -428,23 +510,13 @@
>
Faire une demande au BDM
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
......@@ -10,8 +10,9 @@
<a href="javascript:void(0);" class="nav_item" id="nav_my_info">Mes Infos</a>
<a href="javascript:void(0);" class="nav_item" id="nav_my_shifts">Mes Services</a>
<a href="javascript:void(0);" class="nav_item" id="nav_shifts_exchange">Échange de services</a>
{% if show_faq %}
<a href="javascript:void(0);" class="nav_item" id="nav_faq">Problèmes et Demandes</a>
{% endif %}
<a
href="javascript:void(0);"
target="_blank"
......
......@@ -21,6 +21,9 @@
<button type="button" class="btn--danger choose_makeups">
Je sélectionne mes rattrapages
</button>
<button type="button" class="btn--success remove_future_registration">
J'ai validé un service à deux, je peux supprimer une présence
</button>
</div>
<div class="member_shift_name_area">
<span>Mon créneau : </span>
......
......@@ -31,15 +31,39 @@
<div id="shift_line_template">
<div class="shift_line">
<i class="fas fa-chevron-right shift_line_chevron"></i>
<span class="shift_line_date"></span> - <span class="shift_line_time"></span>
<span class="shift_line_date"></span> - <span class="shift_line_time"></span> <span class="shift_line_associate"> </span>
</div>
</div>
<div id="selectable_shift_line_template">
<div class="selectable_shift_line btn--primary">
<input type="checkbox" class="checkbox">
<div class="selectable_shift_line_text">
<span class="shift_line_date"></span> - <span class="shift_line_time"></span>
<div class="d-flex shift_line_container selectable_shift">
<div class="selectable_shift_line btn--primary">
<input type="checkbox" class="checkbox">
<div class="selectable_shift_line_text">
<span class="shift_line_date"></span> - <span class="shift_line_time"></span>
</div>
</div>
<div class="shift_line_extra_actions">
<div class="affect_associate_registered">
</div>
</div>
</div>
</div>
<div id="delete_registration_button_template">
<div class="delete_registration_button"><i class="fas fa-lg fa-trash"></i></div>
</div>
<div id="modal_affect_shift">
<div>Qui sera présent.e ?</div>
<div class="modal_affect_shift_buttons">
<div id="shift_partner" class="btn--primary assign_shift_button">
</div>
<div id="shift_associate" class=" btn--primary assign_shift_button">
</div>
<div id="shift_both" class=" btn--primary assign_shift_button">
Les deux
</div>
</div>
</div>
......@@ -57,10 +81,12 @@
<div id="calendar_explaination_template">
<h4>Légende du calendrier</h4>
<a class="example-event fc-daygrid-event fc-daygrid-block-event fc-h-event fc-event fc-event-start fc-event-end fc-event-future shift_booked"><div class="fc-event-main"><div class="fc-event-main-frame"><div class="fc-event-time">06:00</div><div class="fc-event-title-container"><div class="fc-event-title fc-sticky">&nbsp;- 9/12</div></div></div></div></a>
<p>Un service colorié en noir : je suis déjà inscrit.e à ce service.</p>
<a class="example-event fc-daygrid-event fc-daygrid-block-event fc-h-event fc-event fc-event-start fc-event-end fc-event-future shift_less_alf"><div class="fc-event-main"><div class="fc-event-main-frame"><div class="fc-event-time">10:45</div><div class="fc-event-title-container"><div class="fc-event-title fc-sticky">&nbsp;- 3/12</div></div></div></div></a>
<p>Un service colorié en bleu : je peux m'inscrire à ce service.</p>
<a class="example-event fc-daygrid-event fc-daygrid-block-event fc-h-event fc-event fc-event-start fc-event-end fc-event-future shift_booked"><div class="fc-event-main"><div class="fc-event-main-frame"><div class="fc-event-time">06:00</div><div class="fc-event-title-container"><div class="fc-event-title fc-sticky">&nbsp;- 9/12</div></div></div></div></a>
<p>Un service colorié en noir : je suis déjà inscrit.e à ce service.</p>
<a class="example-event fc-daygrid-event fc-daygrid-block-event fc-h-event fc-event fc-event-start fc-event-end fc-event-future shift_booked_makeup"><div class="fc-event-main"><div class="fc-event-main-frame"><div class="fc-event-time">13:30</div><div class="fc-event-title-container"><div class="fc-event-title fc-sticky">&nbsp;- 7/12</div></div></div></div></a>
<p>Un service colorié en orange : je suis inscrit.e à un rattrapage sur ce service.</p>
<p>3/12 <i class="arrow_explanation_numbers fas fa-arrow-right"></i> il y a déjà 3 places réservées à ce service sur 12 disponibles.
<b>Plus le chiffre de gauche est petit, plus on a besoin de coopérateurs.rices à ce service !</b></p>
</div>
......@@ -118,12 +144,15 @@
"is_associated_people" : "{{partnerData.is_associated_people}}",
"parent_id" : "{{partnerData.parent_id}}",
"parent_name" : "{{partnerData.parent_name}}",
"parent_verif_token" : "{{partnerData.parent_verif_token}}",
"associated_partner_id" : "{{partnerData.associated_partner_id}}",
"associated_partner_name" : "{{partnerData.associated_partner_name}}",
"verif_token" : "{{partnerData.verif_token}}",
"leave_stop_date": "{{partnerData.leave_stop_date}}",
"comite": "{{partnerData.comite}}"
}
"comite": "{{partnerData.comite}}",
"extra_shift_done": parseInt("{{partnerData.extra_shift_done}}", 10)
};
var block_actions_for_attached_people = '{{block_actions_for_attached_people}}';
</script>
<script src="{% static "js/all_common.js" %}?v="></script>
<script src="{% static "js/members-space-home.js" %}?v="></script>
......
......@@ -25,6 +25,9 @@
<button type="button" class="btn--danger choose_makeups">
Je sélectionne mes rattrapages
</button>
<button type="button" class="btn--success remove_future_registration">
J'ai validé un service à deux, je peux supprimer une présence
</button>
</div>
</div>
</div>
......@@ -102,17 +105,8 @@
</div>
</div>
</div>
<div class="tile full_width_tile">
<div class="tile_title">
Comprendre mon statut
</div>
<div class="my_info_line_middle">
Il existe différents statuts à La Cagette donnant ou non le droit de faire ses courses. Voici un schéma explicatif expliquant le passage d'un statut à un autre. Pour toute question relative aux statuts, rendez&#x2011;vous dans la rubrique <a href='faq'>Problèmes&nbsp;&&nbsp;Demandes</a>.
</div>
<a href="/static/img/diagramme_etat_statut_cooperateurs.png" target=”_blank”>
<img class="status_info_image" src="/static/img/diagramme_etat_statut_cooperateurs.png" alt="diagramme_etat_statut_cooperateurs"/>
</a>
</div>
{% if understand_my_status %}
{% include "members_space/understand_my_status.html" %}
{% endif %}
</div>
</div>
......@@ -35,6 +35,14 @@
<span class="select_makeups_message_block">Je dois les sélectionner dans le calendrier. </span>
<span class="select_makeups_message_block">Je ne peux pas échanger de service tant que je n'ai pas choisi mes rattrapages. </span>
</div>
<div id="can_delete_future_registrations_area">
<button class="btn--success can_delete_future_registrations_button" id="delete_future_registration">
J'ai validé <span class="extra_shift_done"></span> service(s) à deux, je supprime un service futur
</button>
<button class="btn--success can_delete_future_registrations_button" id="offer_extra_shift">
Je souhaite donner <span class="extra_shift_done"></span> service(s) d'avance à la communauté
</button>
</div>
<div id="calendar_top_info">
<div id="partner_shifts_list">
<h4>Liste de mes services :</h4>
......
<div class="tile full_width_tile">
<div class="tile_title">
Comprendre mon statut
</div>
<div class="my_info_line_middle">
Il existe différents statuts à La Cagette donnant ou non le droit de faire ses courses. Voici un schéma explicatif expliquant le passage d'un statut à un autre. Pour toute question relative aux statuts, rendez&#x2011;vous dans la rubrique <a href='faq'>Problèmes&nbsp;&&nbsp;Demandes</a>.
</div>
<a href="/static/img/diagramme_etat_statut_cooperateurs.png" target=”_blank”>
<img class="status_info_image" src="/static/img/diagramme_etat_statut_cooperateurs.png" alt="diagramme_etat_statut_cooperateurs"/>
</a>
</div>
\ No newline at end of file
......@@ -2,34 +2,62 @@
<button type="button" class="accordion" style="width:100%"><label>Comment sont calculées les conso. moyennes / jour ?</label></button>
<div class="txtleft" style="display: none;">
<p>
La fonction qui calcule les consommations moyennes prend en paramètre une date de départ. <br/>
Si elle n'est pas indiquée, la date prise en compte sera "<em>aujourd'hui - nb de jours paramétré dans Odoo</em>".<br/>
La fonction qui calcule les consommations moyennes prend en paramètre soit :
</p>
<ul>
<li>un nombre de jours de couverture</li>
<li>un montant en €</li>
</ul>
<p>
qu'il sera ensuite possible d'ajuster en pourcentage.<br/>
</p>
<p>
Si rien n’est indiqué, le paramètre de calcul pris en compte sera : "<em>aujourd'hui - nb de jours paramétré dans Odoo</em>".<br/>
Le nombre de jours paramétré est actuellement de <strong>{{nb_past_days_to_compute_sales_average}}</strong> jours.<br/>
</p>
<blockquote>
Ce nombre de jours paramétrable se trouve en suivant les menus suivants :<br>
Configuration > Technique > Paramètres > Paramètres systèmes. <br/>
La valeur est définie avec la clef "<em>lacagette_products.nb_past_days_to_compute_sales_average</em>".<br/>
Ce paramètre vaut <strong>{{nb_past_days_to_compute_sales_average}}</strong> au moment du chargement de cette page.<br/>
Les ventes du dimanche sont exclues du calcul.<br/>
</blockquote>
<br>
<p>
<strong>Dans le cas d'un nombre de jours de couverture :</strong>
</p>
<p>
Une requête est faite sur l'ensemble des passages en caisse, de la date de départ à hier, récupérant pour tous les jours de la période (dimanches exclus) les quantités vendues des articles achetés chez le fournisseur.<br/>
Pour chaque article, le nombre de jours considérés pour faire la moyenne est défini commme suit :<br/>
<em>Nb de jours ouvrés de la période - Nb de jours des périodes de rupture</em><br/>
Les périodes de ruptures sont caractérisées par un nombre de jours consécutifs assez important de jours sans vente de l'article.<br/>
Le nombre de jours consécutifs sans vente pour considérer l'article en rupture est actuellement de <strong>{{nb_of_consecutive_non_sale_days_considered_as_break}}</strong><br/>
(c'est le paramètre système avec la clef "<em>lacagette_products.nb_of_consecutive_non_sale_days_considered_as_break</em>").<br/>
Une requête est faite sur l'ensemble des passages en caisse, du Nb jours de couverture à hier, récupérant pour tous les jours de la période (dimanches exclus) les quantités vendues des articles achetés chez le fournisseur.<br/>
Pour chaque article, le nombre de jours considérés pour faire la moyenne est défini comme suit :<br/>
"<em>Nb de jours ouvrés de la période - Nb de jours des périodes de rupture</em>"<br/>
Le nombre de jours consécutifs sans vente pour considérer l'article en rupture est actuellement de <strong>{{nb_of_consecutive_non_sale_days_considered_as_break}}</strong> jours.<br/>
</p>
<blockquote>
Cette période de rupture est paramétrable en suivant les menus suivants :<br>
Configuration > Technique > Paramètres > Paramètres systèmes. <br/>
La valeur est définie avec la clef "<em>lacagette_products.nb_of_consecutive_non_sale_days_considered_as_break</em>".<br/>
</blockquote>
<p>
Pour chaque article, la consommation moyenne par jour est obtenue en divisant la quantité totale vendue sur la période par le nombre de jours significatifs.
</p>
<br>
<p>
<strong>Dans le cas d'un montant en € :</strong>
</p>
<p>
En entrant un montant, la fonction calculera et remplira les quantités d'articles à commander pour être au plus près de ce montant (en étant supérieur ou égal), tout en tenant compte des consommations moyennes des articles.
</p>
</div>
</div>
<div>
<button type="button" class="accordion" style="width:100%"><label>Comment sont calculés les besoins ?</label></button>
<div class="txtleft" style="display: none;">
La quantité à commander pour couvrir les besoins (en jours) est le résultat de :
La quantité à commander pour couvrir les besoins (en jours ou à partir d'un montant en €) est le résultat de :
<p>
(<em>nb_jours</em> <strong>x</strong> <em>conso_moyenne</em>) <strong>-</strong> <em>stock_existant</em> <strong>-</strong> <em>quantités_entrantes</em> <strong>+</strong> <em>stock_minimum</em>
</p>
<p>
Pour plus de précisions, ce résultat peut-être ensuite ajusté en pourcentage.
</p>
</div>
</div>
......@@ -5,12 +5,14 @@
<link rel="stylesheet" href="{% static 'css/datatables/jquery.dataTables.css' %}">
<link rel="stylesheet" href="{% static 'jquery-ui-1.12.1/jquery-ui.min.css' %}">
<link rel="stylesheet" href="{% static 'css/oders_helper_style.css' %}">
<link rel="stylesheet" href="{% static 'quill/quill.snow.css' %}">
{% endblock %}
{% block additionnal_scripts %}
<script type="text/javascript" src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}?v="></script>
<script type="text/javascript" src="{% static 'js/datatables/jquery.dataTables.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/notify.min.js' %}?v="></script>
<script type="text/javascript" src="{% static 'quill/quill.min.js' %}"></script>
{% endblock %}
{% block content %}
......@@ -33,6 +35,13 @@
<h2>Ou, continuer une commande en cours de création</h2>
<div id="existing_orders"></div>
</div>
<div id="common_info_area" style="display:none;">
<h2>Informations</h2>
<div id="common_info_editor_container">
<div id="common_info_editor"></div>
</div>
<button class="btn--primary" id="save_common_info">Sauvegarder</button>
</div>
</div>
<div id="main_content" class="page_content" style="display:none;">
......@@ -41,7 +50,6 @@
<i class="fas fa-arrow-left"></i>&nbsp; Retour
</button>
<div class="right_action_buttons">
<div id="actions_buttons_wrapper">
<button type="button" class='btn--primary' id="toggle_action_buttons">
<span class="button_content">
......@@ -95,10 +103,11 @@
<form action="javascript:;" id="coverage_form" class="order_form_item">
<div class="input-wrapper">
<input type="number" name="coverage_days" id="coverage_days_input" placeholder="Nb jours de couverture" min="1">
<input type="number" name="targeted_amount" id="targeted_amount_input" placeholder="Montant en €" min="1">
<input type="number" name="percent_adjustement" id="percent_adjust_input" placeholder="ajustement en %">
</div>
<div>
<button type="submit" class='btn--primary'>Calculer les besoins</button> <i class='main fa fa-info-circle fa-lg'></i>
<button type="submit" class='btn--primary'>Calculer les besoins</button> <i class='main fa fa-info-circle fa-lg average_consumption_explanation_icon'></i>
</div>
</form>
</div>
......@@ -213,7 +222,7 @@
<p class="remove_order_modal_text">
Vous vous apprêtez à <b style="color: #d9534f;">supprimer</b> cette commande en cours : <span class="remove_order_name"></span>.<br/>
</p>
<p>Êtez-vous sûr ?</p>
<p>Êtez-vous sûr.e ?</p>
<hr/>
</div>
......@@ -224,7 +233,7 @@
Les produits associés uniquement à ce fournisseur seront supprimés du tableau.<br/>
Les données renseignées dans la colonne de ce fournisseur seront perdues.
</p>
<p>Êtez-vous sûr ?</p>
<p>Êtez-vous sûr.e ?</p>
<hr/>
</div>
......@@ -250,7 +259,7 @@
<p>
L'association sera sauvegardée dès que vous aurez cliqué sur "Valider".<br/>
</p>
<p>Êtez-vous sûr ?</p>
<p>Êtez-vous sûr.e ?</p>
<hr/>
</div>
......@@ -263,7 +272,7 @@
<p>
L'association sera supprimée dès que vous aurez cliqué sur "Valider".<br/>
</p>
<p>Êtez-vous sûr ?</p>
<p>Êtez-vous sûr.e ?</p>
<hr/>
</div>
......@@ -271,25 +280,58 @@
<p>
Vous vous apprêtez à créer un inventaire de <span class="inventory_products_count"></span> produits.
</p>
<p>Êtez-vous sûr ?</p>
<p>Êtez-vous sûr.e ?</p>
<hr/>
</div>
<div id="product_price_action_template">
<div class="product_price_action">
<span class="supplier_name"></span>
<input type="number" class="product_supplier_price" name="" value="" />
</div>
</div>
<div id="modal_product_actions">
Actions sur <h3><span class="product_name"></span></h3>
<p>
<h4>NPA</h4>
<div class="npa-options">
<label><input type="checkbox" name="npa-actions" value="simple-npa" /> Mettre le produit en NPA </label>
<label><input type="checkbox" name="npa-actions" value="npa-in-name" /> Mettre le produit en NPA et afficher NPA</label>
<label><input type="checkbox" name="npa-actions" value="fds-in-name" /> Mettre le produit en NPA et afficher FDS</label>
<div class="product_actions_container">
<div class="product_actions_section">
<div class="product_actions_column">
<h4 class="modal_product_actions_title">NPA</h4>
<div class="npa-options">
<label><input type="checkbox" name="npa-actions" value="simple-npa" /> Mettre le produit en NPA </label>
<label><input type="checkbox" name="npa-actions" value="npa-in-name" /> Mettre le produit en NPA et afficher NPA</label>
<label><input type="checkbox" name="npa-actions" value="fds-in-name" /> Mettre le produit en NPA et afficher FDS</label>
</div>
</div>
<div class="product_actions_column">
<h4 class="modal_product_actions_title">Archiver le produit</h4>
<label class="checkbox_action_disabled"><input type="checkbox" name="archive-action" value="archive" disabled /> Archiver </label>
<div class="tooltip">
<i class='main fa fa-info-circle'></i>
<span class="tooltiptext tooltip-xl tt_twolines">
Un produit ne peut pas être archivé si une quantité entrante est prévue.
</span>
</div>
</div>
</div>
</p>
<p>
<h4>Stock minimum</h4>
<input type="number" name="minimal_stock" value="" />
</p>
<div class="product_actions_section">
<div class="product_actions_column">
<h4 class="modal_product_actions_title">Stock minimum</h4>
<input type="number" name="minimal_stock" value="" />
</div>
<div class="product_actions_column">
<h4 class="modal_product_actions_title">Stock réel</h4>
<input type="number" name="actual_stock" value="" />
</div>
</div>
<div class="product_actions_section">
<div class="product_actions_full_column">
<h4 class="modal_product_actions_title product_prices_title">Prix</h4>
<i class="product_prices_title_label">(par fournisseur dans cette commande)</i>
<div class="product_prices_area"></div>
</div>
</div>
</div>
</div>
<div id="modal_create_order">
......
......@@ -3,6 +3,10 @@
{% block content %}
{% if with_shop_header %}
{% include "shop/connect_header.html" %}
{% elif COMPANY_LOGO %}
<div style="width:100%; text-align: center">
<img src="{{COMPANY_LOGO}}" alt="{{COMPANY_NAME}}" width=250 />
</div>
{% endif %}
<div id="connect_form_template" style="text-align:center;">
<form method="POST">
......
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