Commit a13e7d36 by François C.

Merge branch 'dev_cooperatic' of…

Merge branch 'dev_cooperatic' of gl.cooperatic.fr:cooperatic-foodcoops/third-party into dev_cooperatic
parents a8a2e1a9 6f50979a
...@@ -111,6 +111,10 @@ class CagetteEnvelops(models.Model): ...@@ -111,6 +111,10 @@ class CagetteEnvelops(models.Model):
def delete_envelop(self, envelop): def delete_envelop(self, envelop):
return self.c_db.delete(envelop) return self.c_db.delete(envelop)
def archive_envelop(self, envelop):
envelop['archive'] = True
return self.c_db.dbc.update([envelop])
def generate_envelop_display_id(self): def generate_envelop_display_id(self):
"""Generate a unique incremental id to display""" """Generate a unique incremental id to display"""
c_db = CouchDB(arg_db='envelops') c_db = CouchDB(arg_db='envelops')
...@@ -172,7 +176,7 @@ class CagetteEnvelops(models.Model): ...@@ -172,7 +176,7 @@ class CagetteEnvelops(models.Model):
else: else:
# Get the oldest check envelops, limited by the number of checks # Get the oldest check envelops, limited by the number of checks
docs = [] 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) docs.append(item.doc)
# If only 1 check to save # If only 1 check to save
......
#cash, #ch {
margin-top: 15px;
}
.envelop_section { .envelop_section {
margin-bottom: 10px; margin-bottom: 10px;
} }
...@@ -8,12 +12,72 @@ ...@@ -8,12 +12,72 @@
margin-bottom: 15px; margin-bottom: 15px;
} }
#cash_envelops { #cash_envelops, #ch_envelops, #archive_cash_envelops, #archive_ch_envelops {
margin-top: 30px; margin-top: 30px;
} }
#ch_envelops { .update_envelop_button, .delete_envelop_button, .envelop_comment {
margin-top: 30px; margin: 0 0 15px 15px;
}
.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;
}
.line_partner_name_container {
display: flex;
justify-content: flex-start;
align-items: center;
}
.line_partner_name {
text-align: left;
padding: 0 5px;
}
.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;
} }
/* Accordion style */ /* Accordion style */
...@@ -23,7 +87,6 @@ ...@@ -23,7 +87,6 @@
color: #212529; color: #212529;
cursor: pointer; cursor: pointer;
padding: 18px; padding: 18px;
/* width: 80%; */
text-align: left; text-align: left;
border: none; border: none;
outline: none; outline: none;
...@@ -32,7 +95,6 @@ ...@@ -32,7 +95,6 @@
.archive_button { .archive_button {
padding: 18px; padding: 18px;
/* width: 20%; */
} }
hr { hr {
......
var cash_envelops = []; var cash_envelops = [];
var archive_cash_envelops = [];
var ch_envelops = []; var ch_envelops = [];
var archive_ch_envelops = [];
var envelop_to_update = null;
function reset() { function reset() {
$('#cash_envelops').empty(); $('#cash_envelops').empty();
$('#ch_envelops').empty(); $('#ch_envelops').empty();
$('#archive_cash_envelops').empty();
$('#archive_ch_envelops').empty();
archive_cash_envelops = [];
archive_ch_envelops = [];
cash_envelops = []; cash_envelops = [];
ch_envelops = []; ch_envelops = [];
} }
...@@ -12,7 +19,9 @@ function toggle_error_alert() { ...@@ -12,7 +19,9 @@ function toggle_error_alert() {
$('#envelop_cashing_error').toggle(250); $('#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); $('#envelop_cashing_success').toggle(250);
} }
...@@ -20,9 +29,56 @@ function toggle_deleted_alert() { ...@@ -20,9 +29,56 @@ function toggle_deleted_alert() {
$('#envelop_deletion_success').toggle(250); $('#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) { 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 // Calculate envelop total amount
var total_amount = 0; var total_amount = 0;
...@@ -35,15 +91,17 @@ function set_envelop_dom(envelop, envelop_name, envelop_content_id, envelop_inde ...@@ -35,15 +91,17 @@ function set_envelop_dom(envelop, envelop_name, envelop_content_id, envelop_inde
+ '<div class="flex-container">'; + '<div class="flex-container">';
// Allow checking for all cash and first check envelops // 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>' 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 && envelop.canceled) {
new_html += '<button class="accordion w100">' + envelop_name + ' - <i>' + total_amount + '€ (Enveloppe supprimée) </i></button>';
} else { } else {
new_html += '<button class="accordion w100">' + envelop_name + ' - <i>' + total_amount + '€</i></button>'; new_html += '<button class="accordion w100">' + envelop_name + ' - <i>' + total_amount + '€</i></button>';
} }
new_html += '</div>' new_html += '</div>'
+ '<div class="panel panel_' + envelop_content_id + '"><ol id="' + envelop_content_id + '"></ol></div>' + '<div class="panel panel_' + envelop_content_id + '"><ol class="envelop_content_list" id="' + envelop_content_id + '"></ol></div>'
+ '</div>'; + '</div>';
$(new_html).appendTo(envelops_section); $(new_html).appendTo(envelops_section);
...@@ -54,7 +112,7 @@ function set_envelop_dom(envelop, envelop_name, envelop_content_id, envelop_inde ...@@ -54,7 +112,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 + '€'; var content = envelop.envelop_content[node].partner_name + ' : ' + envelop.envelop_content[node].amount + '€';
if ('payment_id' in envelop.envelop_content[node]) { if ('payment_id' in envelop.envelop_content[node]) {
content += " - déjà comptabilisé."; content += " -- paiement comptabilisé.";
} }
var textnode = document.createTextNode(content); // Create a text node var textnode = document.createTextNode(content); // Create a text node
...@@ -64,38 +122,101 @@ function set_envelop_dom(envelop, envelop_name, envelop_content_id, envelop_inde ...@@ -64,38 +122,101 @@ function set_envelop_dom(envelop, envelop_name, envelop_content_id, envelop_inde
} }
let envelop_panel = $(`.panel_${envelop_content_id}`); 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>`);
$(".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'
);
});
}
} }
// 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) { function set_envelops(envelops) {
var cash_index = 0; var cash_index = 0;
var ch_index = 0; var ch_index = 0;
var archive_cash_index = 0;
var archive_ch_index = 0;
reset(); reset();
for (var i= 0; i < envelops.length; i++) { for (var i= 0; i < envelops.length; i++) {
var envelop = envelops[i].doc; 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); cash_envelops.push(envelop);
let split_id = envelop._id.split('_'); let envelop_name = get_envelop_name(envelop);
let envelop_date = split_id[3] + "/" + split_id[2] + "/" + split_id[1];
let envelop_name = 'Enveloppe du ' + envelop_date;
let envelop_content_id = 'content_cash_list_' + cash_index; let envelop_content_id = 'content_cash_list_' + cash_index;
set_envelop_dom(envelop, envelop_name, envelop_content_id, cash_index); set_envelop_dom(envelop, envelop_name, envelop_content_id, cash_index);
cash_index += 1; 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); 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; let envelop_content_id = 'content_ch_list_' + ch_index;
set_envelop_dom(envelop, envelop_name, envelop_content_id, ch_index); set_envelop_dom(envelop, envelop_name, envelop_content_id, ch_index);
ch_index += 1; 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 +246,123 @@ function set_envelops(envelops) { ...@@ -125,18 +246,123 @@ function set_envelops(envelops) {
} }
} }
function delete_envelop(type, index) { /**
if (is_time_to('delete_envelop', 1000)) { * Generate content & set listeners for the modal to update an envelop
openModal(); */
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');
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();
},
'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 : '');
var envelop = null; $(".delete_envelop_line_icon").off("click");
if (type == "cash") { $(".delete_envelop_line_icon").on("click", function() {
envelop = cash_envelops[index]; 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 in couchdb
*/
function update_envelop() {
if (is_time_to('update_envelop', 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();
dbc.put(envelop, function callback(err, result) {
envelop_to_update = null;
if (!err && result !== undefined) {
get_envelops();
toggle_success_alert("Enveloppe modifiée !");
} else { } else {
envelop = ch_envelops[index]; 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) { dbc.put(envelop, function callback(err, result) {
if (!err && result !== undefined) { if (!err && result !== undefined) {
toggle_deleted_alert(); toggle_deleted_alert();
...@@ -150,18 +376,39 @@ function delete_envelop(type, index) { ...@@ -150,18 +376,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) { 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_error').hide();
$('#envelop_cashing_success').hide(); $('#envelop_cashing_success').hide();
// Loading on // Loading on
openModal(); openModal();
if (type == "cash") { let envelop = get_envelop_from_type_index(type, index);
envelop = cash_envelops[index];
} else {
envelop = ch_envelops[index];
}
// Proceed to envelop cashing // Proceed to envelop cashing
$.ajax({ $.ajax({
...@@ -204,7 +451,7 @@ function archive_envelop(type, index) { ...@@ -204,7 +451,7 @@ function archive_envelop(type, index) {
} }
if (display_success_alert) { if (display_success_alert) {
toggle_success_alert(); toggle_success_alert("Enveloppe encaissée !");
} }
}, },
error: function() { error: function() {
...@@ -212,10 +459,14 @@ function archive_envelop(type, index) { ...@@ -212,10 +459,14 @@ function archive_envelop(type, index) {
alert('Erreur serveur. Merci de ne pas ré-encaisser l\'enveloppe qui a causé l\'erreur.'); 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() { function get_envelops() {
dbc.allDocs({ dbc.allDocs({
include_docs: true, include_docs: true,
...@@ -229,20 +480,20 @@ function get_envelops() { ...@@ -229,20 +480,20 @@ function get_envelops() {
}); });
} }
// Hande change in couc db $(document).ready(function() {
sync.on('change', function (info) { if (typeof must_identify == "undefined" || coop_is_connected()) {
get_envelops();
// Hande change in couc db
sync.on('change', function (info) {
// handle change // handle change
if (info.direction == 'pull') { if (info.direction == 'pull') {
get_envelops(); get_envelops();
} }
}).on('error', function (err) { }).on('error', function (err) {
// handle error // handle error
console.log('erreur sync'); console.log('erreur sync');
console.log(err); console.log(err);
}); });
$(document).ready(function() {
if (typeof must_identify == "undefined" || coop_is_connected()) {
get_envelops();
} }
}); });
...@@ -52,7 +52,7 @@ def archive_envelop(request): ...@@ -52,7 +52,7 @@ def archive_envelop(request):
# Immediately save a token than this payment has been saved # Immediately save a token than this payment has been saved
# If an error occurs, this payment won't be saved again # If an error occurs, this payment won't be saved again
envelop['envelop_content'][partner_id]['payment_id'] = res['payment_id'] 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'] envelop['_rev'] = updated_envelop['_rev']
else: else:
# Handling error when saving payment, return data to display error message # Handling error when saving payment, return data to display error message
...@@ -75,8 +75,8 @@ def archive_envelop(request): ...@@ -75,8 +75,8 @@ def archive_envelop(request):
coop_logger.error("Cannot attach payment error message to member : %s", str(e)) coop_logger.error("Cannot attach payment error message to member : %s", str(e))
try: try:
# Delete envelop from couchdb # archive envelop from couchdb
res_envelop = m.delete_envelop(envelop) res_envelop = m.archive_envelop(envelop)
except Exception as e: except Exception as e:
res_envelop = "error" res_envelop = "error"
......
...@@ -368,8 +368,8 @@ class CagetteInventory(models.Model): ...@@ -368,8 +368,8 @@ class CagetteInventory(models.Model):
return {'missed': missed, 'unchanged': unchanged, 'done': done} return {'missed': missed, 'unchanged': unchanged, 'done': done}
@staticmethod @staticmethod
def update_stock_with_shelf_inventory_data(inventory_data): def update_products_stock(inventory_data):
"""Updates Odoo stock after a shelf inventory""" """ Updates Odoo stock after a shelf inventory or another action"""
TWOPLACES = Decimal(10) ** -2 TWOPLACES = Decimal(10) ** -2
api = OdooAPI() api = OdooAPI()
......
...@@ -94,7 +94,7 @@ def do_custom_list_inventory(request): ...@@ -94,7 +94,7 @@ def do_custom_list_inventory(request):
full_inventory_data = CagetteInventory.get_full_inventory_data(inventory_data) full_inventory_data = CagetteInventory.get_full_inventory_data(inventory_data)
# Proceed with inventory # 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 # remove file
CagetteInventory.remove_custom_inv_file(inventory_data['id']) CagetteInventory.remove_custom_inv_file(inventory_data['id'])
......
...@@ -53,9 +53,17 @@ class Command(BaseCommand): ...@@ -53,9 +53,17 @@ class Command(BaseCommand):
byTypeMapFunction = '''function(doc) { byTypeMapFunction = '''function(doc) {
emit(doc.type); emit(doc.type);
}''' }'''
byTypeNotArchiveMapFunction = '''function(doc) {
if(doc.archive != true){
emit(doc.type);
}
}'''
views = { views = {
"by_type": { "by_type": {
"map": byTypeMapFunction "map": byTypeMapFunction
},
"by_type_not_archive": {
"map": byTypeNotArchiveMapFunction
} }
} }
self.createView(dbConn, "index", views) self.createView(dbConn, "index", views)
......
...@@ -508,7 +508,7 @@ class CagetteMember(models.Model): ...@@ -508,7 +508,7 @@ class CagetteMember(models.Model):
stype = shift_template['data']['type'] stype = shift_template['data']['type']
res['shift'] = \ res['shift'] = \
m.create_coop_shift_subscription(shift_t_id, stype) m.create_coop_shift_subscription(shift_t_id, stype)
m.add_first_point(stype) # m.add_first_point(stype) # Not needed anymore
# Update couchdb do with new data # Update couchdb do with new data
try: try:
...@@ -729,6 +729,9 @@ class CagetteMember(models.Model): ...@@ -729,6 +729,9 @@ class CagetteMember(models.Model):
cond = [['barcode', '=', str(key)]] cond = [['barcode', '=', str(key)]]
else: else:
cond = [['name', 'ilike', str(key)]] cond = [['name', 'ilike', str(key)]]
cond.append('|')
cond.append(['is_member', '=', True])
cond.append(['is_associated_people', '=', True])
# cond.append(['cooperative_state', '!=', 'unsubscribed']) # cond.append(['cooperative_state', '!=', 'unsubscribed'])
fields = CagetteMember.m_default_fields fields = CagetteMember.m_default_fields
if not shift_id is None: if not shift_id is None:
...@@ -740,6 +743,7 @@ class CagetteMember(models.Model): ...@@ -740,6 +743,7 @@ class CagetteMember(models.Model):
keep_it = False keep_it = False
if not shift_id is None and len(shift_id) > 0: if not shift_id is None and len(shift_id) > 0:
# Only member registred to shift_id will be returned # 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]]] cond = [['id', '=', m['tmpl_reg_line_ids'][0]]]
fields = ['shift_template_id'] fields = ['shift_template_id']
shift_templ_res = api.search_read('shift.template.registration.line', cond, fields) shift_templ_res = api.search_read('shift.template.registration.line', cond, fields)
...@@ -762,7 +766,7 @@ class CagetteMember(models.Model): ...@@ -762,7 +766,7 @@ class CagetteMember(models.Model):
# member = CagetteMember(m['id'], m['email']) # member = CagetteMember(m['id'], m['email'])
# m['next_shifts'] = member.get_next_shift() # m['next_shifts'] = member.get_next_shift()
if not m['parent_name'] is False: if not m['parent_name'] is False:
m['name'] += ' / ' + m['parent_name'] m['name'] += ' (en binôme avec ' + m['parent_name'] + ')'
del m['parent_name'] del m['parent_name']
members.append(m) members.append(m)
...@@ -1202,7 +1206,7 @@ class CagetteServices(models.Model): ...@@ -1202,7 +1206,7 @@ class CagetteServices(models.Model):
for m in s['members']: for m in s['members']:
for a in associated: for a in associated:
if int(a['parent_id'][0]) == int(m['partner_id'][0]): if int(a['parent_id'][0]) == int(m['partner_id'][0]):
m['partner_id'][1] += ' / ' + a['name'] m['partner_id'][1] += ' en binôme avec ' + a['name']
return services return services
...@@ -1459,4 +1463,3 @@ class CagetteUser(models.Model): ...@@ -1459,4 +1463,3 @@ class CagetteUser(models.Model):
pass pass
return answer return answer
...@@ -72,3 +72,6 @@ h1 .member_name {font-weight: bold;} ...@@ -72,3 +72,6 @@ h1 .member_name {font-weight: bold;}
#member_advice {background: #FFF; color: red;} #member_advice {background: #FFF; color: red;}
.easy_shift_validate {text-align: center; margin-top: 3em;} .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;}
...@@ -62,6 +62,7 @@ var html_elts = { ...@@ -62,6 +62,7 @@ var html_elts = {
real_capture : $('#real_capture'), real_capture : $('#real_capture'),
multi_results : $('#multi_results_preview'), multi_results : $('#multi_results_preview'),
cooperative_state : $('#cooperative_state'), cooperative_state : $('#cooperative_state'),
status_explanation: $('#status_explanation'),
next_shifts : $('#next_shifts') next_shifts : $('#next_shifts')
}; };
...@@ -91,8 +92,13 @@ function fill_member_slide(member) { ...@@ -91,8 +92,13 @@ function fill_member_slide(member) {
} }
html_elts.image_medium.html('<img src="'+img_src+'" width="128" />'); html_elts.image_medium.html('<img src="'+img_src+'" width="128" />');
html_elts.cooperative_state.html(member.cooperative_state); html_elts.cooperative_state.html(member.cooperative_state);
if (member.cooperative_state == 'Désinscrit(e)' || member.cooperative_state == 'Rattrapage') coop_info.addClass('b_red'); if (member.cooperative_state == 'Rattrapage') {
else if (member.cooperative_state == 'En alerte' || member.cooperative_state == 'Délai accordé') coop_info.addClass('b_orange'); 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');
if (member.shifts.length > 0) { if (member.shifts.length > 0) {
html_elts.next_shifts.append('Prochains services : '); html_elts.next_shifts.append('Prochains services : ');
...@@ -141,9 +147,17 @@ function preview_results() { ...@@ -141,9 +147,17 @@ function preview_results() {
for (i in results) { for (i in results) {
if (results[i].is_member != false || results[i].is_associated_people != false) { if (results[i].is_member != false) {
var m = $('<button>').attr('data-i', i) var m = $('<button class="button_is_member">').attr('data-i', i)
.text(results[i].name); .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); html_elts.multi_results.append(m);
} }
......
...@@ -96,14 +96,7 @@ function process_form_submission(event) { ...@@ -96,14 +96,7 @@ function process_form_submission(event) {
closeModal(); closeModal();
if (!err) { if (!err) {
var msg = "Vous êtes maintenant enregistré ! "; window.location.href = em_url;
msg += "<a href='" + em_url + "'>Cliquez ici</a> ";
msg += "pour découvrir l'espace membre";
$('p.intro').remove();
vform.remove();
display_msg_box(msg);
} }
} }
......
...@@ -36,7 +36,7 @@ class CagetteMembersSpace(models.Model): ...@@ -36,7 +36,7 @@ class CagetteMembersSpace(models.Model):
try: try:
cond = [ cond = [
['partner_id', '=', partner_id], ['partner_id', '=', partner_id],
['create_date', '>', date_from], ['date_begin', '>', date_from],
['date_begin', '<', today], ['date_begin', '<', today],
['state', '!=', 'draft'], ['state', '!=', 'draft'],
['state', '!=', 'open'], ['state', '!=', 'open'],
...@@ -88,10 +88,10 @@ class CagetteMembersSpace(models.Model): ...@@ -88,10 +88,10 @@ class CagetteMembersSpace(models.Model):
res = res + [] res = res + []
# Add amnesty line # Add amnesty line
is_amnesty = getattr(settings, 'AMNISTIE_DATE', 'false') is_amnesty = getattr(settings, 'AMNISTIE_DATE', False)
company_code = getattr(settings, 'COMPANY_CODE', '') company_code = getattr(settings, 'COMPANY_CODE', '')
if is_amnesty and company_code == "lacagette": if is_amnesty and company_code == "lacagette":
amnesty={} amnesty = {}
amnesty['is_amnesty'] = True amnesty['is_amnesty'] = True
amnesty['create_date'] = is_amnesty amnesty['create_date'] = is_amnesty
amnesty['date_begin'] = is_amnesty amnesty['date_begin'] = is_amnesty
...@@ -107,7 +107,7 @@ class CagetteMembersSpace(models.Model): ...@@ -107,7 +107,7 @@ class CagetteMembersSpace(models.Model):
paginated_res = res[offset:end_index] paginated_res = res[offset:end_index]
except Exception as e: except Exception as e:
print(str(e)) coop_logger.error("get_shifts_history : %s", str(e))
return paginated_res return paginated_res
\ No newline at end of file
function init_faq() { function init_faq() {
$("#unsuscribe_form_link_btn").prop("href", unsuscribe_form_link); $("#unsuscribe_form_link_btn").prop("href", unsuscribe_form_link);
$("#unsuscribe_form_link_btn2").prop("href", unsuscribe_form_link); $("#unsuscribe_form_link_btn2").prop("href", unsuscribe_form_link);
...@@ -20,6 +19,10 @@ function init_faq() { ...@@ -20,6 +19,10 @@ function init_faq() {
$("#request_form_link_btn").prop("href", request_form_link); $("#request_form_link_btn").prop("href", request_form_link);
} }
$(document).on('click', "#shift_exchange_btn", () => {
goto('echange-de-services');
});
$(document).on('click', '.accordion', function() { $(document).on('click', '.accordion', function() {
/* Toggle between adding and removing the "active" class, /* Toggle between adding and removing the "active" class,
to highlight the button that controls the panel */ to highlight the button that controls the panel */
...@@ -33,8 +36,4 @@ $(document).on('click', '.accordion', function() { ...@@ -33,8 +36,4 @@ $(document).on('click', '.accordion', function() {
} else { } else {
panel.style.display = "block"; panel.style.display = "block";
} }
$("#shift_exchange_btn").on("click", () => {
goto('echange-de-services');
});
}); });
...@@ -91,8 +91,10 @@ function add_or_change_shift(new_shift_id) { ...@@ -91,8 +91,10 @@ function add_or_change_shift(new_shift_id) {
selected_shift = null; selected_shift = null;
if (error.status === 400) { if (error.status === 400) {
alert(`Désolé ! Le service que vous souhaitez échanger démarre dans moins de 24h. ` + alert(`Désolé ! Le service que tu souhaites échanger démarre dans moins de 24h. ` +
`Il n'est plus possible de l'échanger.`); `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 { } else {
alert(`Une erreur est survenue. ` + alert(`Une erreur est survenue. ` +
`Il est néanmoins possible que la requête ait abouti, ` + `Il est néanmoins possible que la requête ait abouti, ` +
...@@ -234,7 +236,7 @@ function init_calendar_page() { ...@@ -234,7 +236,7 @@ function init_calendar_page() {
}; };
} }
const hidden_days = $.map(days_to_hide.split(", "), Number); const hidden_days = days_to_hide.length > 0 ? $.map(days_to_hide.split(", "), Number) : [];
const calendarEl = document.getElementById('calendar'); const calendarEl = document.getElementById('calendar');
...@@ -337,11 +339,107 @@ function init_calendar_page() { ...@@ -337,11 +339,107 @@ function init_calendar_page() {
calendar.render(); calendar.render();
} }
function init_read_only_calendar_page() {
let template_explanations = $("#calendar_explaination_template");
if (vw <= 992) {
$(".loading-calendar").show();
$("#calendar_explaination_area").hide();
$("#calendar_explaination_button").on("click", () => {
openModal(
template_explanations.html(),
closeModal,
"J'ai compris"
);
})
.show();
} else {
$("#calendar_explaination_button").hide();
$("#calendar_explaination_area").html(template_explanations.html())
.show();
}
if (incoming_shifts !== null) {
init_shifts_list();
} else {
load_partner_shifts(partner_data.concerned_partner_id)
.then(init_shifts_list);
}
if (should_select_makeup()) {
$(".makeups_nb").text(partner_data.makeups_to_do);
$("#need_to_select_makeups_message").show();
}
let default_initial_view = "";
let header_toolbar = {};
if (vw <= 768) {
default_initial_view = 'listWeek';
header_toolbar = {
left: 'title',
center: 'listWeek,timeGridDay',
right: 'prev,next today'
};
} else if (vw <=992) {
default_initial_view = 'listWeek';
header_toolbar = {
left: 'title',
center: 'dayGridMonth,listWeek,timeGridDay',
right: 'prev,next today'
};
} else {
default_initial_view = 'dayGridMonth';
header_toolbar = {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,listWeek,timeGridDay'
};
}
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',
initialView: default_initial_view,
headerToolbar: header_toolbar,
buttonText: {
list: "Semaine"
},
eventTimeFormat: {
hour: '2-digit',
minute: '2-digit'
},
allDaySlot: false,
contentHeight: "auto",
eventDisplay: "block",
hiddenDays: hidden_days,
events: '/shifts/get_list_shift_calendar/' + partner_data.concerned_partner_id,
eventDidMount: function() {
// Calendar is hidden at first on mobile to hide header change when data is loaded
$(".loading-calendar").hide();
$("#calendar").show();
if (vw <= 992) {
$(".fc .fc-header-toolbar").addClass("resp-header-toolbar");
} else {
$(".fc .fc-header-toolbar").removeClass("resp-header-toolbar");
}
}
});
calendar.render();
}
function init_shifts_exchange() { function init_shifts_exchange() {
$(".shifts_exchange_page_content").hide(); $(".shifts_exchange_page_content").hide();
vw = window.innerWidth; vw = window.innerWidth;
if (partner_data.cooperative_state === 'unsubscribed') { if (partner_data.cooperative_state === 'unsubscribed' || partner_data.cooperative_state === 'gone') {
$("#unsuscribed_content").show(); $("#unsuscribed_content").show();
$(".unsuscribed_form_link") $(".unsuscribed_form_link")
...@@ -371,9 +469,9 @@ function init_shifts_exchange() { ...@@ -371,9 +469,9 @@ function init_shifts_exchange() {
} else if ( } else if (
partner_data.comite === "True") { partner_data.comite === "True") {
let msg_template = $("#comite_template"); let msg_template = $("#comite_template");
$(".comite_content_msg").html(msg_template.html()); $(".comite_content_msg").html(msg_template.html());
$("#comite_content").show(); $("#comite_content").show();
init_read_only_calendar_page();
} else if (partner_data.cooperative_state === 'suspended' } else if (partner_data.cooperative_state === 'suspended'
&& partner_data.date_delay_stop === 'False') { && partner_data.date_delay_stop === 'False') {
$("#suspended_content .makeups_nb").text(partner_data.makeups_to_do); $("#suspended_content .makeups_nb").text(partner_data.makeups_to_do);
......
...@@ -16,7 +16,8 @@ const possible_cooperative_state = { ...@@ -16,7 +16,8 @@ const possible_cooperative_state = {
alert: "En alerte", alert: "En alerte",
up_to_date: "À jour", up_to_date: "À jour",
unsubscribed: "Désinscrit.e des créneaux", unsubscribed: "Désinscrit.e des créneaux",
delay: "En délai" delay: "En délai",
gone: "Parti.e"
}; };
/* - Data */ /* - Data */
...@@ -177,7 +178,7 @@ function init_my_info_data() { ...@@ -177,7 +178,7 @@ function init_my_info_data() {
$(".delay_date_stop").text(f_date_delay_stop); $(".delay_date_stop").text(f_date_delay_stop);
$(".delay_date_stop_container").show(); $(".delay_date_stop_container").show();
} else if (partner_data.cooperative_state === 'unsubscribed') { } else if (partner_data.cooperative_state === 'unsubscribed' || partner_data.cooperative_state === 'gone') {
$(".member_shift_name").text('X'); $(".member_shift_name").text('X');
$(".unsuscribed_form_link") $(".unsuscribed_form_link")
......
...@@ -12,5 +12,5 @@ urlpatterns = [ ...@@ -12,5 +12,5 @@ urlpatterns = [
url(r'^faqBDM$', views.faqBDM), url(r'^faqBDM$', views.faqBDM),
url(r'^no_content$', views.no_content), url(r'^no_content$', views.no_content),
url(r'^get_shifts_history$', views.get_shifts_history), url(r'^get_shifts_history$', views.get_shifts_history),
url('/*$', views.index), # Urls unknown from the server will redirect to index url(r'^.*', views.index) # Urls unknown from the server will redirect to index
] ]
/* Comments : */
/* - Screens */
/* -- Sections */
/* - Common */
.page_body{ .page_body{
position: relative; position: relative;
} }
...@@ -9,8 +15,6 @@ ...@@ -9,8 +15,6 @@
right: 0; right: 0;
} }
/* - Common */
.pill { .pill {
border-radius: 30px; border-radius: 30px;
min-width: 200px; min-width: 200px;
...@@ -155,6 +159,14 @@ ...@@ -155,6 +159,14 @@
min-height: 45px; min-height: 45px;
} }
#do_inventory {
border-top: 1px solid #004aa6;
}
#refresh_order {
border-top: 1px solid #004aa6;
border-bottom: 1px solid #004aa6;
}
/* -- Order data */ /* -- Order data */
#order_data_container { #order_data_container {
font-size: 1.8rem; font-size: 1.8rem;
...@@ -185,10 +197,27 @@ ...@@ -185,10 +197,27 @@
min-width: 200px; 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; border-radius: 3px;
} }
#coverage_form > div {
display:block;
float:left;
}
#coverage_form .input-wrapper {
margin-right: 3px;
}
#coverage_days_input, #targeted_amount_input, #percent_adjust_input {
display: block;
}
#coverage_days_input, #targeted_amount_input {
margin-bottom: 3px;
}
/* -- Table */ /* -- Table */
#products_table_filter{ #products_table_filter{
text-align: right !important; text-align: right !important;
...@@ -216,6 +245,10 @@ ...@@ -216,6 +245,10 @@
margin-left: 5px; margin-left: 5px;
} }
.main.fa-info-circle {
color: #0275d8;
cursor: pointer;
}
.custom_cell_content { .custom_cell_content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
...@@ -288,7 +321,7 @@ ...@@ -288,7 +321,7 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
margin: 30px 0 20px 0; margin: 15px 0;
position: -webkit-sticky; position: -webkit-sticky;
position: sticky; position: sticky;
top: 140px; top: 140px;
...@@ -296,8 +329,8 @@ ...@@ -296,8 +329,8 @@
} }
.supplier_pill { .supplier_pill {
background-color: #a0daff; background-color: #ffebcd;
border: 1px solid #6ea8cc; border: 2px solid black;
} }
.pill_supplier_name { .pill_supplier_name {
...@@ -338,6 +371,36 @@ ...@@ -338,6 +371,36 @@
width: 90%; width: 90%;
} }
/* -- Product actions modal*/
.npa-options {
width: fit-content;
text-align: left;
margin: auto;
}
.npa-options label {
display: block;
}
.modal_product_actions_section {
margin: 1em 0;
}
.modal_product_actions_section .tooltip {
margin-left: 5px;
}
.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 */ /* - Orders created screen */
.order_created_header { .order_created_header {
...@@ -383,3 +446,11 @@ ...@@ -383,3 +446,11 @@
width: 100%; width: 100%;
padding: 15px; padding: 15px;
} }
/* - Miscellaneous */
footer {
display: none;
}
\ No newline at end of file
...@@ -16,6 +16,7 @@ var dbc = null, ...@@ -16,6 +16,7 @@ var dbc = null,
order_doc = { order_doc = {
_id: null, _id: null,
coverage_days: null, coverage_days: null,
targeted_amount: null,
stats_date_period: '', stats_date_period: '',
last_update: { last_update: {
timestamp: null, timestamp: null,
...@@ -29,7 +30,7 @@ var dbc = null, ...@@ -29,7 +30,7 @@ var dbc = null,
var clicked_order_pill = null; var clicked_order_pill = null;
var timerId;
/* - UTILS */ /* - UTILS */
/** /**
...@@ -43,6 +44,7 @@ function reset_data() { ...@@ -43,6 +44,7 @@ function reset_data() {
order_doc = { order_doc = {
_id: null, _id: null,
coverage_days: null, coverage_days: null,
targeted_amount: null,
stats_date_period: '', stats_date_period: '',
last_update : { last_update : {
timestamp: null, timestamp: null,
...@@ -116,6 +118,11 @@ function _compute_stats_date_from() { ...@@ -116,6 +118,11 @@ function _compute_stats_date_from() {
return val; return val;
} }
function debounceFunction(func, delay = 1000) {
clearTimeout(timerId);
timerId = setTimeout(func, delay);
}
/* - PRODUCTS */ /* - PRODUCTS */
/** /**
...@@ -165,7 +172,7 @@ function add_product() { ...@@ -165,7 +172,7 @@ function add_product() {
res.default_code = ' '; res.default_code = ' ';
products.unshift(res); products.unshift(res);
update_main_screen({'sort_order_dir':'desc'}); update_main_screen({'sort_order_dir':'desc'});
update_cdb_order(); debounceFunction(update_cdb_order);
} else { } else {
alert("L'article n'a pas toutes les caractéristiques pour être ajouté."); alert("L'article n'a pas toutes les caractéristiques pour être ajouté.");
} }
...@@ -183,13 +190,22 @@ function add_product() { ...@@ -183,13 +190,22 @@ function add_product() {
return 0; return 0;
} }
function compute_purchase_qty_for_coverage(product, coeff, stock, incoming_qty, daily_conso, days) {
let purchase_qty_for_coverage = null;
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
* Compute the qty to buy for each product, depending the coverage days. purchase_package_qty_for_coverage = purchase_qty_for_coverage / product.suppliersinfo[0].package_qty;
* Set the computed qty for the first supplier only.
*/ if (coeff != 1) {
function compute_products_coverage_qties() { purchase_package_qty_for_coverage *= coeff;
if (order_doc.coverage_days != null) { }
// return Round up to unit for all products
return Math.ceil(purchase_package_qty_for_coverage);
}
function compute_and_affect_product_supplier_quantities(coeff, days) {
for (const [ for (const [
key, key,
product product
...@@ -201,21 +217,78 @@ function compute_products_coverage_qties() { ...@@ -201,21 +217,78 @@ function compute_products_coverage_qties() {
const stock = product.qty_available; const stock = product.qty_available;
const incoming_qty = product.incoming_qty; const incoming_qty = product.incoming_qty;
const daily_conso = product.daily_conso; const daily_conso = product.daily_conso;
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;
}
}
}
purchase_qty_for_coverage = order_doc.coverage_days * daily_conso - stock - incoming_qty;
purchase_qty_for_coverage = (purchase_qty_for_coverage < 0) ? 0 : purchase_qty_for_coverage;
// Reduce to nb of packages to purchase
purchase_package_qty_for_coverage = purchase_qty_for_coverage / product.suppliersinfo[0].package_qty;
// Round up to unit for all products /**
purchase_package_qty_for_coverage = Math.ceil(purchase_package_qty_for_coverage); * 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() {
return new Promise((resolve) => {
const pc_adjust = $('#percent_adjust_input').val();
let coeff = 1;
if (!isNaN(parseFloat(pc_adjust))) {
coeff = (1 + parseFloat(pc_adjust) /100);
}
order_doc.coeff = coeff;
if (order_doc.coverage_days != null) {
compute_and_affect_product_supplier_quantities(coeff, order_doc.coverage_days);
resolve();
} 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);
// Set qty to purchase for first supplier only if (order_total_value_f >= targeted_amount_f) {
products[key].suppliersinfo[0].qty = purchase_package_qty_for_coverage; 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;
} }
/* console.log(iter)
console.log(order_total_value_f + '/' + targeted_amount_f)
console.log(days)
console.log(go_on)*/
} 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;
} }
} }
iter++;
}
resolve();
}
/* console.log(rder_doc.coverage_days);
console.log(order_doc.targeted_amount)*/
});
} }
/** /**
...@@ -227,7 +300,7 @@ function check_products_data() { ...@@ -227,7 +300,7 @@ function check_products_data() {
if (suppliers_id.length > 0) { if (suppliers_id.length > 0) {
$.notify( $.notify(
"Vérfication des informations produits...", "Vérification des informations produits...",
{ {
globalPosition:"top left", globalPosition:"top left",
className: "info" className: "info"
...@@ -250,8 +323,12 @@ function check_products_data() { ...@@ -250,8 +323,12 @@ function check_products_data() {
traditional: true, traditional: true,
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
success: function(data) { 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) { 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) { if (p_index === -1) {
// Add product if it wasn't fetched before (made available since last access to order) // Add product if it wasn't fetched before (made available since last access to order)
...@@ -272,9 +349,41 @@ function check_products_data() { ...@@ -272,9 +349,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'); $('.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(); resolve();
}, },
error: function(data) { error: function(data) {
...@@ -335,7 +444,7 @@ function update_product_ref(input_el, p_id, p_index) { ...@@ -335,7 +444,7 @@ function update_product_ref(input_el, p_id, p_index) {
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
data: JSON.stringify(data), data: JSON.stringify(data),
success: () => { success: () => {
update_cdb_order(); debounceFunction(update_cdb_order);
$(".actions_buttons_area .right_action_buttons").notify( $(".actions_buttons_area .right_action_buttons").notify(
"Référence sauvegardée !", "Référence sauvegardée !",
...@@ -413,7 +522,7 @@ function add_supplier() { ...@@ -413,7 +522,7 @@ function add_supplier() {
save_supplier_products(supplier, data.res.products); save_supplier_products(supplier, data.res.products);
update_main_screen(); update_main_screen();
$("#supplier_input").val(""); $("#supplier_input").val("");
update_cdb_order(); debounceFunction(update_cdb_order);
closeModal(); closeModal();
}, },
error: function(data) { error: function(data) {
...@@ -449,7 +558,7 @@ function remove_supplier(supplier_id) { ...@@ -449,7 +558,7 @@ function remove_supplier(supplier_id) {
products = products.filter(product => product.suppliersinfo.length > 0); products = products.filter(product => product.suppliersinfo.length > 0);
update_main_screen(); update_main_screen();
update_cdb_order(); debounceFunction(update_cdb_order);
} }
...@@ -516,7 +625,7 @@ function save_supplier_product_association(product, supplier, cell) { ...@@ -516,7 +625,7 @@ function save_supplier_product_association(product, supplier, cell) {
products_table.row(row).data(new_row_data) products_table.row(row).data(new_row_data)
.draw(); .draw();
update_cdb_order(); debounceFunction(update_cdb_order);
closeModal(); closeModal();
}, },
error: function(data) { error: function(data) {
...@@ -570,7 +679,7 @@ function end_supplier_product_association(product, supplier) { ...@@ -570,7 +679,7 @@ function end_supplier_product_association(product, supplier) {
// Update table // Update table
display_products(); display_products();
update_cdb_order(); debounceFunction(update_cdb_order);
closeModal(); closeModal();
}, },
error: function(data) { error: function(data) {
...@@ -677,35 +786,60 @@ function _compute_total_values_by_supplier() { ...@@ -677,35 +786,60 @@ function _compute_total_values_by_supplier() {
/* - PRODUCT */ /* - PRODUCT */
/** function commit_actions_on_product(product, inputs) {
* Update 'purchase_ok' of a product let actions = {
* npa: [],
* @param {int} p_id product id to_archive: false,
* @param {Boolean} npa value to set purchase_ok to minimal_stock: 0,
*/ id: product.id,
function set_product_npa(p_id, npa) { name: product.name
openModal();
const data = {
product_tmpl_id: p_id,
purchase_ok: !npa
}; };
// Fetch supplier products inputs.each(function (i, e) {
const input = $(e);
if (input.attr('name') == 'npa-actions') {
if (input.prop('checked') == true) {
actions.npa.push(input.val());
}
} else if (input.attr('name') == "minimal_stock") {
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;
}
}
});
openModal();
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: "/products/update_product_purchase_ok", url: "/products/commit_actions_on_product",
dataType: "json", dataType: "json",
traditional: true, traditional: true,
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
data: JSON.stringify(data), data: JSON.stringify(actions),
success: () => { success: () => {
const index = products.findIndex(p => p.id == p_id); const index = products.findIndex(p => p.id == product.id);
products[index].minimal_stock = actions.minimal_stock;
if (actions.npa.length > 0 || actions.to_archive === true) {
// Remove NPA & archived products
products.splice(index, 1);
debounceFunction(update_cdb_order);
}
check_products_data()
.then(() => {
update_cdb_order();
update_main_screen();
closeModal();
// Give time for modal to fade // Give time for modal to fade
setTimeout(function() { setTimeout(function() {
$(".actions_buttons_area .right_action_buttons").notify( $(".actions_buttons_area .right_action_buttons").notify(
"Produit passé en NPA !", "Actions enregistrées !",
{ {
elementPosition:"bottom right", elementPosition:"bottom right",
className: "success", className: "success",
...@@ -713,31 +847,41 @@ function set_product_npa(p_id, npa) { ...@@ -713,31 +847,41 @@ function set_product_npa(p_id, npa) {
} }
); );
}, 500); }, 500);
});
// Remove NPA products
products.splice(index, 1);
update_main_screen();
update_cdb_order();
closeModal();
}, },
error: function(data) { error: function(data) {
let msg = "erreur serveur lors de la sauvegarde du NPA". let msg = "erreur serveur lors de la sauvegarde".
msg += ` (product_tmpl_id: ${p_id})`; msg += ` (product_tmpl_id: ${product.id})`;
err = {msg: msg, ctx: 'set_product_npa'}; err = {msg: msg, ctx: 'commit_actions_on_product'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') { if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error; err.msg += ' : ' + data.responseJSON.error;
} }
report_JS_error(err, 'orders'); report_JS_error(err, 'orders');
closeModal(); try {
alert('Erreur lors de la sauvegarde de la donnée. Veuillez ré-essayer plus tard.'); 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(); update_main_screen();
closeModal();
});
} }
}); });
} }
/* - INVENTORY */ /* - INVENTORY */
/** /**
...@@ -1148,7 +1292,7 @@ function goto_main_screen(doc) { ...@@ -1148,7 +1292,7 @@ function goto_main_screen(doc) {
check_products_data() check_products_data()
.then(() => { .then(() => {
update_cdb_order(); debounceFunction(update_cdb_order);
update_main_screen(); update_main_screen();
switch_screen(); switch_screen();
}); });
...@@ -1316,8 +1460,8 @@ function prepare_datatable_columns() { ...@@ -1316,8 +1460,8 @@ function prepare_datatable_columns() {
{ {
data: "id", data: "id",
title: `<div id="table_header_select_all" class="txtcenter"> title: `<div id="table_header_select_all" class="txtcenter">
<span class="select_all_text">Sélectionner</span> <!--<span class="select_all_text">Sélectionner</span>-->
<label for="select_all_products_cb">- Tout</label> <label for="select_all_products_cb">Tout</label>
<input type="checkbox" class="select_product_cb" id="select_all_products_cb" name="select_all_products_cb" value="all"> <input type="checkbox" class="select_product_cb" id="select_all_products_cb" name="select_all_products_cb" value="all">
</div>`, </div>`,
className: "dt-body-center", className: "dt-body-center",
...@@ -1442,12 +1586,11 @@ function prepare_datatable_columns() { ...@@ -1442,12 +1586,11 @@ function prepare_datatable_columns() {
}); });
columns.push({ columns.push({
data: "purchase_ok", title: ``,
title: `NPA`,
className: "dt-body-center", className: "dt-body-center",
orderable: false, orderable: false,
render: function (data) { render: function (data) {
return `<input type="checkbox" class="product_npa_cb" value="purchase_ok" ${data ? '' : 'checked'}>`; return `<button type="button" class="btn--primary product_actions">Actions</button>`;
}, },
width: "4%" width: "4%"
}); });
...@@ -1478,7 +1621,7 @@ function display_products(params) { ...@@ -1478,7 +1621,7 @@ function display_products(params) {
const data = prepare_datatable_data(); const data = prepare_datatable_data();
const columns = prepare_datatable_columns(); const columns = prepare_datatable_columns();
let sort_order_dir = "asc"; let sort_order_dir = "desc";
if (params != undefined && typeof params.sort_order_dir != "undefined") { if (params != undefined && typeof params.sort_order_dir != "undefined") {
sort_order_dir = params.sort_order_dir; sort_order_dir = params.sort_order_dir;
...@@ -1488,10 +1631,11 @@ function display_products(params) { ...@@ -1488,10 +1631,11 @@ function display_products(params) {
columns: columns, columns: columns,
order: [ order: [
[ [
6, // Order by default by first supplier 5, // Order by default by first supplier
sort_order_dir sort_order_dir
] ]
], ],
stateSave: true,
orderClasses: false, orderClasses: false,
aLengthMenu: [ aLengthMenu: [
[ [
...@@ -1594,22 +1738,94 @@ function display_products(params) { ...@@ -1594,22 +1738,94 @@ function display_products(params) {
products_table.row($(this).closest('tr')).data(new_row_data) products_table.row($(this).closest('tr')).data(new_row_data)
.draw(); .draw();
update_cdb_order(); debounceFunction(update_cdb_order);
display_total_values(); display_total_values();
} else { } else {
$(this).val(''); $(this).val('');
} }
} }
}) })
.on('change', 'tbody td .product_qty_input', function () {
// Since data change is saved on blur, set focus on change in case of arrows pressed
$(this).focus();
})
.on('keypress', 'tbody td .product_qty_input', function(e) { .on('keypress', 'tbody td .product_qty_input', function(e) {
if (e.which == 13) {
// Validate on Enter pressed // Validate on Enter pressed
if (e.which == 13) {
$(this).blur(); $(this).blur();
} }
})
.on('keydown', 'tbody td .product_qty_input', function(e) {
if (e.which == 38) {
e.preventDefault();
// On arrow up pressed, focus next row 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();
if (next_input_top_offset < suppliers_container_top_offset) {
window.scrollTo({
top: $(window).scrollTop() - $("#suppliers_container").outerHeight()
});
}
} else if (e.which == 40) {
e.preventDefault();
// On arrow down pressed, focus previous row input
$(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();
}
})
.on('click', 'tbody td .product_actions', function(e) {
// 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);
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");
}
openModal(
modal_product_actions.html(),
() => {
if (is_time_to('validate_product_actions')) {
commit_actions_on_product(product, modal.find('input'));
}
},
'Valider',
false
);
modal.find('input[name="minimal_stock"]').val(product.minimal_stock);
}); });
// Associate product to supplier on click on 'X' in the table // Associate product to supplier on click on 'X' in the table
...@@ -1741,34 +1957,7 @@ function display_products(params) { ...@@ -1741,34 +1957,7 @@ function display_products(params) {
} }
}); });
// Set product is NPA (Ne Pas Acheter)
$('#products_table').on('click', 'tbody td .product_npa_cb', function () {
// Save / unsave selected row
const p_id = products_table.row($(this).closest('tr')).data().id;
const npa = this.checked;
const product = products.find(p => p.id == p_id);
let modal_product_npa = $('#templates #modal_product_npa');
modal_product_npa.find(".product_name").text(product.name);
modal_product_npa.find(".product_npa").text(npa ? 'Ne Pas Acheter' : 'Peut Être Acheté');
openModal(
modal_product_npa.html(),
() => {
if (is_time_to('validate_set_product_npa')) {
set_product_npa(p_id, npa);
}
},
'Valider',
false,
true,
() => {
this.checked = !this.checked;
}
);
});
return 0; return 0;
} }
...@@ -1853,6 +2042,14 @@ function update_main_screen(params) { ...@@ -1853,6 +2042,14 @@ function update_main_screen(params) {
} else { } else {
$("#coverage_days_input").val(''); $("#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));
}
if (order_doc.stats_date_period !== undefined && order_doc.stats_date_period !== null) { if (order_doc.stats_date_period !== undefined && order_doc.stats_date_period !== null) {
$("#stats_date_period_select").val(order_doc.stats_date_period); $("#stats_date_period_select").val(order_doc.stats_date_period);
...@@ -1861,6 +2058,9 @@ function update_main_screen(params) { ...@@ -1861,6 +2058,9 @@ function update_main_screen(params) {
} }
} }
function display_average_consumption_explanation() {
openModal($('#explanations').html());
}
/** /**
* Update DOM display on the order selection screen * Update DOM display on the order selection screen
*/ */
...@@ -2047,18 +2247,28 @@ $(document).ready(function() { ...@@ -2047,18 +2247,28 @@ $(document).ready(function() {
$("#coverage_form").on("submit", function(e) { $("#coverage_form").on("submit", function(e) {
e.preventDefault(); e.preventDefault();
if (is_time_to('submit_coverage_form', 1000)) { if (is_time_to('submit_coverage_form', 1000)) {
let val = $("#coverage_days_input").val(); let days_val = $("#coverage_days_input").val(),
amount_val = $('#targeted_amount_input').val();
val = parseInt(val);
days_val = parseInt(days_val);
if (!isNaN(val)) { amount_val = parseInt(amount_val);
order_doc.coverage_days = val; if (isNaN(days_val)) days_val = null;
compute_products_coverage_qties(); if (isNaN(amount_val)) amount_val = null;
update_cdb_order();
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(); update_main_screen();
})
} else { } else {
$("#coverage_days_input").val(order_doc.coverage_days); $("#coverage_days_input").val(order_doc.coverage_days || '');
alert(`Valeur non valide pour le nombre de jours de couverture !`); $('#targeted_amount_input').val(order_doc.targeted_amount || '');
alert("Ni le nombre de jours de couverture, ni le montant à atteindre sont correctement renseignés")
} }
} }
}); });
...@@ -2112,10 +2322,13 @@ $(document).ready(function() { ...@@ -2112,10 +2322,13 @@ $(document).ready(function() {
check_products_data() check_products_data()
.then(() => { .then(() => {
compute_products_coverage_qties(); compute_products_coverage_qties()
.then(() => {
update_main_screen(); update_main_screen();
update_cdb_order(); debounceFunction(update_cdb_order);
closeModal(); closeModal();
})
}); });
} }
}); });
...@@ -2126,6 +2339,19 @@ $(document).ready(function() { ...@@ -2126,6 +2339,19 @@ $(document).ready(function() {
} }
}); });
$("#refresh_order").on("click", function() {
if (is_time_to('refresh_order', 1000)) {
openModal();
check_products_data()
.then(() => {
debounceFunction(update_cdb_order);
update_main_screen();
$("#toggle_action_buttons").click();
closeModal();
});
}
});
$("#delete_order_button").on("click", function() { $("#delete_order_button").on("click", function() {
if (is_time_to('press_delete_order_button', 1000)) { if (is_time_to('press_delete_order_button', 1000)) {
let modal_remove_order = $('#templates #modal_remove_order'); let modal_remove_order = $('#templates #modal_remove_order');
...@@ -2220,6 +2446,8 @@ $(document).ready(function() { ...@@ -2220,6 +2446,8 @@ $(document).ready(function() {
return 0; return 0;
}); });
$(document).on("click", ".average_consumption_explanation_icon", display_average_consumption_explanation);
$.datepicker.regional['fr'] = { $.datepicker.regional['fr'] = {
monthNames: [ monthNames: [
'Janvier', 'Janvier',
...@@ -2355,6 +2583,21 @@ $(document).ready(function() { ...@@ -2355,6 +2583,21 @@ $(document).ready(function() {
alert('Erreur lors de la récupération des articles, rechargez la page plus tard'); 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";
}
});
} else { } else {
$('#not_connected_content').show(); $('#not_connected_content').show();
} }
......
from outils.common_imports import * from outils.common_imports import *
from outils.for_view_imports import * from outils.for_view_imports import *
from outils.common import OdooAPI
from orders.models import Order, Orders, CagetteSuppliers from orders.models import Order, Orders, CagetteSuppliers
from products.models import CagetteProduct, CagetteProducts from products.models import CagetteProduct, CagetteProducts
...@@ -20,7 +21,9 @@ def helper(request): ...@@ -20,7 +21,9 @@ def helper(request):
'couchdb_server': settings.COUCHDB['url'], 'couchdb_server': settings.COUCHDB['url'],
'db': settings.COUCHDB['dbs']['orders'], 'db': settings.COUCHDB['dbs']['orders'],
'odoo_server': settings.ODOO['url'], 'odoo_server': settings.ODOO['url'],
'metabase_url': getattr(settings, 'ORDERS_HELPER_METABASE_URL', '') 'metabase_url': getattr(settings, 'ORDERS_HELPER_METABASE_URL', ''),
'nb_past_days_to_compute_sales_average': OdooAPI().get_system_param('lacagette_products.nb_past_days_to_compute_sales_average'),
'nb_of_consecutive_non_sale_days_considered_as_break': OdooAPI().get_system_param('lacagette_products.nb_of_consecutive_non_sale_days_considered_as_break')
} }
template = loader.get_template('orders/helper.html') template = loader.get_template('orders/helper.html')
......
...@@ -47,7 +47,7 @@ class OdooAPI: ...@@ -47,7 +47,7 @@ class OdooAPI:
order='id ASC'): order='id ASC'):
"""Main api request, retrieving data according search conditions.""" """Main api request, retrieving data according search conditions."""
fields_and_context = {'fields': fields, fields_and_context = {'fields': fields,
'context': {'lang': 'fr_FR','tz':'Europe/Paris'}, 'context': {'lang': 'fr_FR', 'tz': 'Europe/Paris'},
'limit': limit, 'limit': limit,
'offset': offset, 'offset': offset,
'order': order 'order': order
...@@ -59,8 +59,11 @@ class OdooAPI: ...@@ -59,8 +59,11 @@ class OdooAPI:
def update(self, entity, ids, fields): def update(self, entity, ids, fields):
"""Update entities which have ids, with new fields values.""" """Update entities which have ids, with new fields values."""
context = {
'context': {'lang': 'fr_FR', 'tz': 'Europe/Paris'}
}
return self.models.execute_kw(self.db, self.uid, self.passwd, return self.models.execute_kw(self.db, self.uid, self.passwd,
entity, 'write', [ids, fields]) entity, 'write', [ids, fields], context)
def create(self, entity, fields): def create(self, entity, fields):
"""Create entity instance with given fields values.""" """Create entity instance with given fields values."""
...@@ -74,6 +77,18 @@ class OdooAPI: ...@@ -74,6 +77,18 @@ class OdooAPI:
def authenticate(self, login, password): def authenticate(self, login, password):
return self.common.authenticate(self.db, login, password, {}) return self.common.authenticate(self.db, login, password, {})
def get_system_param(self, key):
value = ''
try:
res = self.search_read('ir.config_parameter',
[['key', '=', key]],
['value'])
if res:
value = res[0]['value']
except Exception as e:
coop_logger.error('get_system_param: (%s) %s', key, str(e))
return value
class CouchDB: class CouchDB:
"""Class to handle interactions with CouchDB""" """Class to handle interactions with CouchDB"""
......
# coding: utf-8
"""Interact with Odoo by python code
Before launching script, launch the following command:
export DJANGO_SETTINGS_MODULE='scripts_settings'
(a file named scripts_settings.py is present in this directory)
"""
#
import sys, getopt, os
sys.path.append(os.path.abspath('../..'))
from outils.common import OdooAPI
import datetime
def main():
"""For coops in alert state, reajust points counter so they get to 0 after adding their makeups to their actual calculated total"""
api = OdooAPI()
cond = [
'|',
'|',
'|',
['cooperative_state','=', 'alert'],
['cooperative_state','=', 'unsubscribed'],
['cooperative_state','=', 'suspended'],
['cooperative_state','=', 'delay']
]
fields = ['id', 'name', 'makeups_to_do', 'cooperative_state']
res = api.search_read('res.partner', cond, fields)
cpt = 0
for p in res:
# Get real points count
cond = [['partner_id','=', p["id"]], ['type','=', 'standard']]
fields = ['point_qty', 'name']
res_counter_event = api.search_read('shift.counter.event', cond, fields)
total_pts = 0
for item in res_counter_event:
total_pts += item['point_qty']
# Get future makeups
cond = [
['name','=', p["name"]],
['shift_type','=', 'standard'],
['is_makeup','=', True],
['date_begin', '>=', datetime.datetime.now().isoformat()]
]
fields = ['id']
res_shift_reg = api.search_read('shift.registration', cond, fields)
final_theoric_pts = total_pts + p['makeups_to_do'] + len(res_shift_reg)
if final_theoric_pts < 0:
cpt += 1
print(p["name"])
print('theoric total : ' + str(final_theoric_pts))
print('>> total_pts : ' + str(total_pts))
print('>> makeups_to_do : ' + str(p['makeups_to_do']))
print('>> nb future makeups : ' + str(len(res_shift_reg)))
"""
For unsubscribed people,
adding a point and going through run_process_target_status may lead them to be suspended
whereas they're not subscribed to any shift.
Adding a fake point will lead odoo to reset Unsuscribed status.
"""
add_second_corrective_pt = p['cooperative_state'] == 'unsubscribed'
# Add/remove points so their final theoric points is 0
points_to_add = -final_theoric_pts
fields = {
'name': "Correction de l'historique de points",
'shift_id': False,
'type': 'standard',
'partner_id': p['id'],
'point_qty': points_to_add
}
api.create('shift.counter.event', fields)
print('===> Pts ajoutés : ' + str(points_to_add))
if add_second_corrective_pt is True:
api.execute('res.partner', 'run_process_target_status', [])
fields = {
'name': "Correction de l'historique - Sécurité pour les désinscrit.es",
'shift_id': False,
'type': 'standard',
'partner_id': p['id'],
'point_qty': 0
}
api.create('shift.counter.event', fields)
print('===> Pt correctif pour désinscrits')
print('--------')
print('Nb de personnes concernées : ' + str(cpt))
if __name__ == "__main__":
main()
\ No newline at end of file
...@@ -148,6 +148,10 @@ footer { position: fixed; ...@@ -148,6 +148,10 @@ footer { position: fixed;
width: 230px !important; width: 230px !important;
} }
.tooltip .tooltip-xl {
width: 320px !important;
}
.tooltip .tt_twolines { .tooltip .tt_twolines {
top: -15px !important; top: -15px !important;
} }
...@@ -194,3 +198,16 @@ footer { position: fixed; ...@@ -194,3 +198,16 @@ footer { position: fixed;
} }
.notifyjs-cancelable-base .buttons {width: 190px; margin: 5px auto;} .notifyjs-cancelable-base .buttons {width: 190px; margin: 5px auto;}
.notifyjs-cancelable-base button {width: 90px;text-align: center; margin: 3px;} .notifyjs-cancelable-base button {width: 90px;text-align: center; margin: 3px;}
button.accordion::after {
content: '\002B';
color: #777;
font-weight: bold;
float: right;
margin-left: 5px;
}
button.accordion.active::after {
content: "\2212";
}
...@@ -30,15 +30,3 @@ input.link {min-width: 50em;} ...@@ -30,15 +30,3 @@ input.link {min-width: 50em;}
overflow: hidden; overflow: hidden;
} }
button.accordion::after {
content: '\002B';
color: #777;
font-weight: bold;
float: right;
margin-left: 5px;
}
button.accordion.active::after {
content: "\2212";
}
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -150,7 +150,6 @@ function get_module_settings() { ...@@ -150,7 +150,6 @@ function get_module_settings() {
} }
get_module_settings(); get_module_settings();
$(document).on('click', '.accordion', function(){ $(document).on('click', '.accordion', function(){
/* Toggle between adding and removing the "active" class, /* Toggle between adding and removing the "active" class,
to highlight the button that controls the panel */ to highlight the button that controls the panel */
...@@ -164,3 +163,4 @@ $(document).on('click', '.accordion', function(){ ...@@ -164,3 +163,4 @@ $(document).on('click', '.accordion', function(){
panel.style.display = "block"; panel.style.display = "block";
} }
}); });
...@@ -189,34 +189,51 @@ class ExportPOS(View): ...@@ -189,34 +189,51 @@ class ExportPOS(View):
kept_sessions_id.append(s['id']) kept_sessions_id.append(s['id'])
key = y + '-' + m + '-' + d key = y + '-' + m + '-' + d
if not (key in totals): 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 sub_total = 0
cb = chq = csh = 0 cb = chq = csh = cbd = chqd = 0
for p in s['payments']: 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']: 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']: 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']: elif 'CB' in p['name']:
cb = round(p['total_amount'], 2) cb = sub_amount
sub_total += round(p['total_amount'], 2) sub_total += sub_amount
totals[key]['CB'] += cb totals[key]['CB'] += cb
totals[key]['CSH'] += csh totals[key]['CSH'] += csh
totals[key]['CHQ'] += chq totals[key]['CHQ'] += chq
totals[key]['CB_DEJ'] += cbd
totals[key]['CHQ_DEJ'] += chqd
totals[key]['TOTAL'] += round(sub_total, 2) totals[key]['TOTAL'] += round(sub_total, 2)
details_lines.append([mois, s['mm_dates']['min'], s['mm_dates']['min'], s['caisse'], s['name'], 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() wb = Workbook()
ws1 = wb.create_sheet("Totaux " + mois, 0) ws1 = wb.create_sheet("Totaux " + mois, 0)
ws2 = wb.create_sheet("Détails " + mois, 1) 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): for day in sorted(totals):
cb = totals[day]['CB'] cb = totals[day]['CB']
csh = totals[day]['CSH'] csh = totals[day]['CSH']
chq = totals[day]['CHQ'] chq = totals[day]['CHQ']
cbd = totals[day]['CB_DEJ']
chqd = totals[day]['CHQ_DEJ']
total = totals[day]['TOTAL'] total = totals[day]['TOTAL']
ws1.append([day, 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', 'total']) ws2.append(['mois', 'min_date', 'max_date', 'Caisse', 'session', 'CB', 'CSH','CHQ', 'CB_DEJ', 'CHQ_DEJ', 'total'])
for row in details_lines: for row in details_lines:
ws2.append(row) ws2.append(row)
wb_name = 'export_sessions__' + mois + '.xlsx' wb_name = 'export_sessions__' + mois + '.xlsx'
......
...@@ -7,6 +7,7 @@ import csv ...@@ -7,6 +7,7 @@ import csv
import tempfile import tempfile
import pymysql.cursors import pymysql.cursors
import datetime import datetime
import re
vcats = [] vcats = []
...@@ -252,10 +253,52 @@ class CagetteProduct(models.Model): ...@@ -252,10 +253,52 @@ class CagetteProduct(models.Model):
return res return res
@staticmethod
def commit_actions_on_product(data):
""" Update:
- NPA (ne pas acheter)
- Product is active
- Minimal stock
"""
res = {}
try:
api = OdooAPI()
# Minimal stock
f = {'minimal_stock': data['minimal_stock']}
# NPA
if 'simple-npa' in data['npa']:
f['purchase_ok'] = 0
if 'npa-in-name' in data['npa']:
# Add [NPA] in product name if needed
f['name'] = data['name'] if ('[NPA]' in data['name']) else data['name'] + " [NPA]"
f['purchase_ok'] = 0
elif '[NPA]' in data['name']:
# Remove [NPA] from name
f['name'] = re.sub(r'( \[NPA\])', '', data['name'])
current_name = data['name'] if ('name' not in f) else f['name']
if 'fds-in-name' in data['npa']:
f['name'] = current_name if '[FDS]' in data['name'] else current_name + " [FDS]"
f['purchase_ok'] = 0
elif '[FDS]' in current_name:
f['name'] = re.sub(r'( \[FDS\])', '', current_name)
if len(data['npa']) == 0:
f['purchase_ok'] = 1
# Active
f["active"] = not data['to_archive']
res["update"] = api.update('product.template', data['id'], f)
except Exception as e:
res["error"] = str(e)
coop_logger.error("update_npa_and_minimal_stock : %s %s", str(e), str(data))
return res
class CagetteProducts(models.Model): class CagetteProducts(models.Model):
"""Initially used to make massive barcode update.""" """Initially used to make massive barcode update."""
@staticmethod @staticmethod
def get_simple_list(): def get_simple_list():
res = [] res = []
...@@ -577,9 +620,10 @@ class CagetteProducts(models.Model): ...@@ -577,9 +620,10 @@ class CagetteProducts(models.Model):
"uom_id", "uom_id",
"purchase_ok", "purchase_ok",
"supplier_taxes_id", "supplier_taxes_id",
"product_variant_ids" "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) 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"] filtered_products_t = [p for p in products_t if p["state"] != "end" and p["state"] != "obsolete"]
......
...@@ -11,6 +11,7 @@ urlpatterns = [ ...@@ -11,6 +11,7 @@ urlpatterns = [
url(r'^update_product_stock$', views.update_product_stock), url(r'^update_product_stock$', views.update_product_stock),
url(r'^update_product_purchase_ok$', views.update_product_purchase_ok), 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_product_internal_ref$', views.update_product_internal_ref),
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'^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'^label_print/([0-9]+)/?([0-9\.]*)/?([a-z]*)/?([0-9]*)$', views.label_print),
url(r'^shelf_labels$', views.shelf_labels), # massive print url(r'^shelf_labels$', views.shelf_labels), # massive print
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
from outils.common_imports import * from outils.common_imports import *
from outils.for_view_imports import * from outils.for_view_imports import *
from members.models import CagetteUser
from products.models import CagetteProduct from products.models import CagetteProduct
from products.models import CagetteProducts from products.models import CagetteProducts
from inventory.models import CagetteInventory from inventory.models import CagetteInventory
...@@ -99,12 +100,14 @@ def update_product_stock(request): ...@@ -99,12 +100,14 @@ def update_product_stock(request):
'products': [p] '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}) return JsonResponse({"res": res})
def update_product_purchase_ok(request): def update_product_purchase_ok(request):
res = {} res = {}
is_connected_user = CagetteUser.are_credentials_ok(request)
if is_connected_user is True:
data = json.loads(request.body.decode()) data = json.loads(request.body.decode())
res = CagetteProduct.update_product_purchase_ok(data["product_tmpl_id"], data["purchase_ok"]) res = CagetteProduct.update_product_purchase_ok(data["product_tmpl_id"], data["purchase_ok"])
...@@ -113,9 +116,13 @@ def update_product_purchase_ok(request): ...@@ -113,9 +116,13 @@ def update_product_purchase_ok(request):
return JsonResponse(res, status=500) return JsonResponse(res, status=500)
else: else:
return JsonResponse({"res": res}) return JsonResponse({"res": res})
else:
return JsonResponse(res, status=403)
def update_product_internal_ref(request): def update_product_internal_ref(request):
res = {} res = {}
is_connected_user = CagetteUser.are_credentials_ok(request)
if is_connected_user is True:
data = json.loads(request.body.decode()) data = json.loads(request.body.decode())
res = CagetteProduct.update_product_internal_ref(data["product_tmpl_id"], data["default_code"]) res = CagetteProduct.update_product_internal_ref(data["product_tmpl_id"], data["default_code"])
...@@ -124,6 +131,57 @@ def update_product_internal_ref(request): ...@@ -124,6 +131,57 @@ def update_product_internal_ref(request):
return JsonResponse(res, status=500) return JsonResponse(res, status=500)
else: else:
return JsonResponse({"res": res}) return JsonResponse({"res": res})
else:
return JsonResponse(res, status=403)
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())
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)
# If stock > 0: do inventory to set stock to 0
if data["to_archive"] is True and product_data["qty_available"] != 0:
try:
p = {
'id': product_data['product_variant_ids'][0], # Need product id
'uom_id': product_data['uom_id'],
'qty': -product_data["qty_available"]
}
inventory_data = {
'name': 'Archivage - ' + product_data['name'],
'products': [p]
}
res_inventory = CagetteInventory.update_products_stock(inventory_data)
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'])
if ('error' in res):
return JsonResponse(res, status=500)
else:
return JsonResponse({"res": res})
else:
return JsonResponse(res, status=403)
def labels_appli_csv(request, params): def labels_appli_csv(request, params):
"""Generate files to put in DAV directory to be retrieved by scales app.""" """Generate files to put in DAV directory to be retrieved by scales app."""
......
...@@ -14,38 +14,29 @@ class CagetteSales(models.Model): ...@@ -14,38 +14,29 @@ class CagetteSales(models.Model):
def get_sales(self, date_from, date_to): def get_sales(self, date_from, date_to):
res = [] res = []
# Get pos sessions # Get pos orders
cond = [['stop_at', '>=', date_from], ['stop_at', '<=', date_to], ['state', '=', "closed"]] cond = [['date_order', '>=', date_from], ['date_order', '<=', date_to]]
fields = [] fields = ['partner_id', 'statement_ids', 'name']
sessions = self.o_api.search_read('pos.session', cond, fields) orders = self.o_api.search_read('pos.order', cond, fields)
# Get bank statements of these sessions # Get bank statements of these sessions
statements = [] statements = []
for s in sessions: statements_partners = {}
statements = statements + s["statement_ids"] 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 # Get payment lines
cond = [['statement_id', 'in', statements]] cond = [['id', 'in', statements]]
fields = ["partner_id", "amount", "journal_id", "create_date", "date"] fields = ["amount", "journal_id", "create_date"]
payments = self.o_api.search_read('account.bank.statement.line', cond, fields, order="create_date ASC", limit=50000) payments = self.o_api.search_read('account.bank.statement.line', cond, fields, order="create_date ASC", limit=50000)
item = None
try: try:
for payment in payments: for payment in payments:
# POS session can contain payments from another day (closing session on next morning, ...) res.append({
if payment["date"] >= date_from and payment["date"] <= date_to: "partner": statements_partners[payment["id"]],
# 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"],
"create_date": payment["create_date"], "create_date": payment["create_date"],
"date": payment["date"], "pos_order_name": statements_orders[payment["id"]],
"total_amount": round(float(payment["amount"]), 2), "total_amount": round(float(payment["amount"]), 2),
"payments": [ "payments": [
{ {
...@@ -53,10 +44,8 @@ class CagetteSales(models.Model): ...@@ -53,10 +44,8 @@ class CagetteSales(models.Model):
"journal_id": payment["journal_id"] "journal_id": payment["journal_id"]
} }
] ]
} })
res.append(item)
except Exception as e: except Exception as e:
pass coop_logger.error("get_sales %s", str(e))
return res return res
...@@ -38,20 +38,23 @@ function display_orders(orders) { ...@@ -38,20 +38,23 @@ function display_orders(orders) {
columns:[ columns:[
{ {
data:"create_date", data:"create_date",
title:"Date de vente", title:"Date enregistrement",
width: "10%" width: "10%"
}, },
{ {
data:"partner_id", data:"pos_order_name",
title:"Ref. Caisse",
width: "10%"
},
{
data:"partner",
title:"Membre", title:"Membre",
width: "50%", width: "40%"
render: function (data) {
return data[1];
}
}, },
{ {
data:"total_amount", data:"total_amount",
title: "Montant du panier", title: "Montant dû",
className:"dt-body-center", className:"dt-body-center",
render: function (data) { render: function (data) {
return parseFloat(data).toFixed(2) + ' €'; return parseFloat(data).toFixed(2) + ' €';
......
...@@ -173,7 +173,7 @@ def do_shelf_inventory(request): ...@@ -173,7 +173,7 @@ def do_shelf_inventory(request):
return JsonResponse(res, status=500) return JsonResponse(res, status=500)
# Proceed with inventory # 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'] full_inventory_data['inventory_id'] = res['inventory']['inv_id']
shelf_data['last_inventory_id'] = res['inventory']['inv_id'] shelf_data['last_inventory_id'] = res['inventory']['inv_id']
......
...@@ -155,6 +155,8 @@ class CagetteShift(models.Model): ...@@ -155,6 +155,8 @@ class CagetteShift(models.Model):
"origin": 'memberspace', "origin": 'memberspace',
"is_makeup": data['is_makeup'], "is_makeup": data['is_makeup'],
"state": 'open'} "state": 'open'}
if shift_type == "standard" and data['is_makeup'] is not True:
fieldsDatas['template_created'] = 1 # It's not true but otherwise, presence add 1 standard point, which is not wanted
st_r_id = self.o_api.create('shift.registration', fieldsDatas) st_r_id = self.o_api.create('shift.registration', fieldsDatas)
except Exception as e: except Exception as e:
...@@ -211,7 +213,7 @@ class CagetteShift(models.Model): ...@@ -211,7 +213,7 @@ class CagetteShift(models.Model):
action = 'create' action = 'create'
# Get partner extension ids # Get partner extension ids
cond = [['id','=',data['idPartner']]] cond = [['id', '=', data['idPartner']]]
fields = ['extension_ids'] fields = ['extension_ids']
partner_extensions = self.o_api.search_read('res.partner', cond, fields) partner_extensions = self.o_api.search_read('res.partner', cond, fields)
response = False response = False
......
...@@ -46,7 +46,7 @@ def do_movement(request): ...@@ -46,7 +46,7 @@ def do_movement(request):
'products': products 'products': products
} }
res = CagetteInventory.update_stock_with_shelf_inventory_data(inventory_data) res = CagetteInventory.update_products_stock(inventory_data)
else: else:
res = CagetteStock.do_stock_movement(data) res = CagetteStock.do_stock_movement(data)
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
<div style="width: 10%" class="fr txtright"><i class="fas fa-times"></i></div> <div style="width: 10%" class="fr txtright"><i class="fas fa-times"></i></div>
</div> </div>
<div id="envelop_cashing_success" class="alert--success clearfix custom_alert" onClick="toggle_success_alert()"> <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 style="width: 10%" class="fr txtright"><i class="fas fa-times"></i></div>
</div> </div>
<div id="envelop_deletion_success" class="alert--success clearfix custom_alert" onClick="toggle_deleted_alert()"> <div id="envelop_deletion_success" class="alert--success clearfix custom_alert" onClick="toggle_deleted_alert()">
...@@ -39,8 +39,49 @@ ...@@ -39,8 +39,49 @@
</div> </div>
</section> </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> </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>
<script src="{% static "js/pouchdb.min.js" %}"></script> <script src="{% static "js/pouchdb.min.js" %}"></script>
<script type="text/javascript"> <script type="text/javascript">
{%if must_identify %} {%if must_identify %}
......
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
<div class="row-1 grid-2"> <div class="row-1 grid-2">
<div class="col-1"> <div class="col-1">
<div class="label"> <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> </div>
Recherche : Recherche :
<input type="text" name="search_string" autocomplete="off" /> <input type="text" name="search_string" autocomplete="off" />
...@@ -107,10 +107,12 @@ ...@@ -107,10 +107,12 @@
</div> </div>
<div id="cooperative_state"> <div id="cooperative_state">
</div> </div>
<div id="status_explanation" style="font-weight:bold;">
</div>
<div id="next_shifts"> <div id="next_shifts">
</div> </div>
</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>
<div class="col-1"> <div class="col-1">
<section id="member_advice"> <section id="member_advice">
...@@ -206,7 +208,7 @@ ...@@ -206,7 +208,7 @@
{% endif %} {% endif %}
</section> </section>
<div class="col-2"></div> <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> <div class="col-2"></div>
</section> </section>
<section class="grid-6 has-gutter" id="rattrapage_1"> <section class="grid-6 has-gutter" id="rattrapage_1">
......
...@@ -107,7 +107,7 @@ ...@@ -107,7 +107,7 @@
<div class="input-container panel"> <div class="input-container panel">
<div class="grp_text"> <div class="grp_text">
<h3><b>Créneau/service c'est quoi la différence?</b></h3> <h3><b>Créneau/service c'est quoi la différence ?</b></h3>
<p> <p>
Un créneau, c'est une plage récurrente, par exemple, tous les jeudi de semaine A de 13h30 à 16h30.<br/> Un créneau, c'est une plage récurrente, par exemple, tous les jeudi de semaine A de 13h30 à 16h30.<br/>
Un service, c'est une plage horaire en particulier, par exemple le jeudi 28 novembre de 13h30 à 16h30. Un service, c'est une plage horaire en particulier, par exemple le jeudi 28 novembre de 13h30 à 16h30.
...@@ -116,7 +116,7 @@ ...@@ -116,7 +116,7 @@
</div> </div>
<div class="grp_text"> <div class="grp_text">
<h3><b>Échanger son service:</b></h3> <h3><b>Échanger son service</b></h3>
<p> <p>
Si tu ne peux pas venir effectuer ton service à la date prévue il te faut l'échanger sur ton espace membre.<br/> 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. <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.
...@@ -134,6 +134,24 @@ ...@@ -134,6 +134,24 @@
</div> </div>
<div class="grp_text"> <div class="grp_text">
<h3><b>Oubli de validation de service</b></h3>
<p>
Si tu as effectué ton service mais que tu as oublié de le valider à l'entrée du magasin, il te faut remplir le formulaire suivant en indiquant la date de ton service.
</p>
<div class="faq_link_button_area">
<a
href="javascript:void(0);"
target="_blank"
type="button"
class="btn--primary faq_link_button"
id="request_form_link_btn2"
>
Oubli validation service
</a>
</div>
</div>
<div class="grp_text">
<h3><b>Demande de congé maladie ou parental</b></h3> <h3><b>Demande de congé maladie ou parental</b></h3>
<p><b> <p><b>
- Si tu t'absentes sur une période de 4 semaines autre que pour une raison de santé ou parentale il t'es demandé de déplacer ton service.<br/> - Si tu t'absentes sur une période de 4 semaines autre que pour une raison de santé ou parentale il t'es demandé de déplacer ton service.<br/>
...@@ -142,9 +160,8 @@ ...@@ -142,9 +160,8 @@
<p></b></p> <p></b></p>
</div> </div>
<div class="grp_text"> <div class="grp_text"><h3><b>Les congés maladie</b></h3>
<b><h3>Les congés maladie:</h3></b> 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 />
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'es 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 /> 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>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 /> 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 />
...@@ -398,7 +415,9 @@ ...@@ -398,7 +415,9 @@
Nous avons créé des formulaires spécifiques pour la plupart des problèmes rencontrés par les membres. Changer de créneau, créer un binôme, ajouter un produit à la gamme, partir en vacances... <br /> Nous avons créé des formulaires spécifiques pour la plupart des problèmes rencontrés par les membres. Changer de créneau, créer un binôme, ajouter un produit à la gamme, partir en vacances... <br />
Cela dit, nous en découvrons de nouveaux tous les jours.<br /> 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 /> Si tu n'as pas su quel formulaire remplir, tu es au bon endroit. <br />
Vas-y dit nous tout !<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 />
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"> <div class="faq_link_button_area">
<a <a
href="javascript:void(0);" href="javascript:void(0);"
......
...@@ -71,7 +71,8 @@ ...@@ -71,7 +71,8 @@
merci de contacter le bureau des membres pour résoudre ce problème en remplissant ce formulaire : </h3> merci de contacter le bureau des membres pour résoudre ce problème en remplissant ce formulaire : </h3>
</div> </div>
<div id="comite_template"> <div id="comite_template">
<h3>Vous êtes inscrit.e dans le service des comités, vous n'avez pas accès au calendrier d'échange des services car vous vous organisez directement avec le responsable du comité. Si vous avez des rattrapages à réaliser, merci de contacter le responsable du comité qui vous aidera à planifier les rattrapages ou trouver une solution</h3> <h3>Vous êtes inscrit.e dans le service des comités, vous n'avez pas accès au calendrier d'échange des services car vous vous organisez directement avec le responsable du comité. Si vous avez des rattrapages à réaliser, merci de contacter le responsable du comité qui vous aidera à planifier les rattrapages ou trouver une solution.</h3>
<h3>Le calendrier ci-dessous est en lecture seule</h3>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
</div> </div>
<div id="comite_content" class="shifts_exchange_page_content"> <div id="comite_content" class="shifts_exchange_page_content">
<div class="comite_content_msg"></div> <div class="comite_content_msg"></div>
<div id="read_only_calendar"></div>
</div> </div>
<div id="suspended_content" class="shifts_exchange_page_content"> <div id="suspended_content" class="shifts_exchange_page_content">
<h3> <h3>
......
<div>
<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/>
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/>
</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/>
</p>
<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>
</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 :
<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>
</div>
</div>
...@@ -55,6 +55,9 @@ ...@@ -55,6 +55,9 @@
<button type="button" class='btn--primary action_button' id="do_inventory" style="display:none;"> <button type="button" class='btn--primary action_button' id="do_inventory" style="display:none;">
Faire un inventaire Faire un inventaire
</button> </button>
<button type="button" class='btn--primary action_button' id="refresh_order">
Rafraîchir la commande
</button>
<button type="button" class='btn--danger action_button' id="delete_order_button"> <button type="button" class='btn--danger action_button' id="delete_order_button">
Supprimer la commande Supprimer la commande
</button> </button>
...@@ -90,8 +93,14 @@ ...@@ -90,8 +93,14 @@
</select> </select>
</form> </form>
<form action="javascript:;" id="coverage_form" class="order_form_item"> <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="coverage_days" id="coverage_days_input" placeholder="Nb jours de couverture" min="1">
<button type="submit" class='btn--primary'>Calculer les besoins</button> <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 average_consumption_explanation_icon'></i>
</div>
</form> </form>
</div> </div>
...@@ -267,14 +276,30 @@ ...@@ -267,14 +276,30 @@
<hr/> <hr/>
</div> </div>
<div id="modal_product_npa"> <div id="modal_product_actions">
<h3>Attention !</h3> Actions sur <h3><span class="product_name"></span></h3>
<p> <div class="modal_product_actions_section">
Vous vous apprêtez à passer le produit <span class="product_name"></span> en <span class="product_npa"></span>.<br/> <h4 class="modal_product_actions_title">NPA</h4>
Dès que vous aurez cliqué sur "Valider", le produit sera retiré du tableau et l'information sera enregistrée dans Odoo. <div class="npa-options">
</p> <label><input type="checkbox" name="npa-actions" value="simple-npa" /> Mettre le produit en NPA </label>
<p>Êtez-vous sûr ?</p> <label><input type="checkbox" name="npa-actions" value="npa-in-name" /> Mettre le produit en NPA et afficher NPA</label>
<hr/> <label><input type="checkbox" name="npa-actions" value="fds-in-name" /> Mettre le produit en NPA et afficher FDS</label>
</div>
</div>
<div class="modal_product_actions_section">
<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 class="modal_product_actions_section">
<h4 class="modal_product_actions_title">Stock minimum</h4>
<input type="number" name="minimal_stock" value="" />
</div>
</div> </div>
<div id="modal_create_order"> <div id="modal_create_order">
...@@ -291,7 +316,9 @@ ...@@ -291,7 +316,9 @@
<br/> <br/>
<hr/> <hr/>
</div> </div>
<div id="explanations">
{% include "orders/explanations.html" %}
</div>
<div id="modal_create_order__supplier_date_planned"> <div id="modal_create_order__supplier_date_planned">
<div class="modal_input_area"> <div class="modal_input_area">
<span class="modal_input_label supplier_name"></span> <span class="modal_input_label supplier_name"></span>
......
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