Commit ad361176 by François C.

Merge branch 'dev_cooperatic' into 'dev_principale'

Intégration des Dev cooperatic (Aide à la commande, Réception avec cache couchDB, possibilité de valider une présence Comité)

See merge request cooperatic-foodcoops/third-party!45
parents e50cbf8a 4a99e4fb
...@@ -97,7 +97,8 @@ SHIFT_INFO = """A la cagette, un service est une plage de trois heures un jour ...@@ -97,7 +97,8 @@ SHIFT_INFO = """A la cagette, un service est une plage de trois heures un jour
PB_INSTRUCTIONS = """Si j'ai un problème, que je suis désinscrit, que je veux changer de créneaux ou quoi que ce soit, merci de vous rendre dans la section \"J'ai un problème\" sur le site web de <a href=\"https://lacagette-coop.fr/?MonEspaceMembre\">La Cagette</a>""" PB_INSTRUCTIONS = """Si j'ai un problème, que je suis désinscrit, que je veux changer de créneaux ou quoi que ce soit, merci de vous rendre dans la section \"J'ai un problème\" sur le site web de <a href=\"https://lacagette-coop.fr/?MonEspaceMembre\">La Cagette</a>"""
ENTRANCE_COME_FOR_SHOPING_MSG = "Hey coucou toi ! Cet été nous sommes plus de <strong>1000 acheteur·euses</strong> pour seulement <strong>300 coopérateur·rice·s</strong> en service. <br />Tu fais tes courses à La Cagette cet été ?<br/> Inscris-toi sur ton espace membre !" ENTRANCE_COME_FOR_SHOPING_MSG = "Hey coucou toi ! Cet été nous sommes plus de <strong>1000 acheteur·euses</strong> pour seulement <strong>300 coopérateur·rice·s</strong> en service. <br />Tu fais tes courses à La Cagette cet été ?<br/> Inscris-toi sur ton espace membre !"
ENTRANCE_EXTRA_BUTTONS_DISPLAY = False
ENTRANCE_EASY_SHIFT_VALIDATE = True
# Members space / shifts # Members space / shifts
UNSUBSCRIBED_MSG = 'Vous êtes désincrit·e, merci de remplir <a href="https://docs.google.com/forms/d/e/1FAIpQLSfPiC2PkSem9x_B5M7LKpoFNLDIz0k0V5I2W3Mra9AnqnQunw/viewform">ce formulaire</a> pour vous réinscrire sur un créneau.<br />Vous pouvez également contacter le Bureau des Membres en remplissant <a href="https://docs.google.com/forms/d/e/1FAIpQLSeZP0m5-EXPVJxEKJk6EjwSyZJtnbiGdYDuAeFI3ENsHAOikg/viewform">ce formulaire</a>' UNSUBSCRIBED_MSG = 'Vous êtes désincrit·e, merci de remplir <a href="https://docs.google.com/forms/d/e/1FAIpQLSfPiC2PkSem9x_B5M7LKpoFNLDIz0k0V5I2W3Mra9AnqnQunw/viewform">ce formulaire</a> pour vous réinscrire sur un créneau.<br />Vous pouvez également contacter le Bureau des Membres en remplissant <a href="https://docs.google.com/forms/d/e/1FAIpQLSeZP0m5-EXPVJxEKJk6EjwSyZJtnbiGdYDuAeFI3ENsHAOikg/viewform">ce formulaire</a>'
CONFIRME_PRESENT_BTN = 'Clique ici pour valider ta présence' CONFIRME_PRESENT_BTN = 'Clique ici pour valider ta présence'
...@@ -106,4 +107,5 @@ RECEPTION_PB = "Ici, vous pouvez signaler toute anomalie lors d'une réception, ...@@ -106,4 +107,5 @@ RECEPTION_PB = "Ici, vous pouvez signaler toute anomalie lors d'une réception,
Merci d'indiquer un maximum d'informations, le nom du produit et son code barre. \ Merci d'indiquer un maximum d'informations, le nom du produit et son code barre. \
Dans le cas de produits déteriorés, merci d'envoyer une photo avec votre téléphone à [Adresse_email]" Dans le cas de produits déteriorés, merci d'envoyer une photo avec votre téléphone à [Adresse_email]"
# display or not column "Autres" in reception process
DISPLAY_COL_AUTRES = False
Received reception data are stored in this directory (could be desactivated if RECEPTION_DATA_BACKUP is set to False in config.py)
\ No newline at end of file
...@@ -52,7 +52,7 @@ class CagetteInventory(models.Model): ...@@ -52,7 +52,7 @@ class CagetteInventory(models.Model):
file_data = json.load(json_file) file_data = json.load(json_file)
date_time = datetime.fromtimestamp(int(filename)) date_time = datetime.fromtimestamp(int(filename))
d = date_time.strftime("%m/%d/%Y, %H:%M") d = date_time.strftime("%d/%m/%Y, %H:%M")
file_data['id'] = int(filename) file_data['id'] = int(filename)
file_data['datetime_created'] = d file_data['datetime_created'] = d
...@@ -113,7 +113,7 @@ class CagetteInventory(models.Model): ...@@ -113,7 +113,7 @@ class CagetteInventory(models.Model):
return file_data['inventory_status'] return file_data['inventory_status']
@staticmethod @staticmethod
def create_custom_inv_file(line_ids, line_type): def create_custom_inv_file(line_ids, line_type, default_partners_id=[]):
res = {} res = {}
try: try:
...@@ -127,36 +127,54 @@ class CagetteInventory(models.Model): ...@@ -127,36 +127,54 @@ class CagetteInventory(models.Model):
api = OdooAPI() api = OdooAPI()
ids = [] ids = []
order = ['', ''] order = ['', '']
user = partner = '' user = ''
fields = ['create_uid', 'product_id', 'partner_id'] partners = []
cond = [['id', 'in', line_ids]] if len(default_partners_id) > 0:
if (line_type == 'cpo'): f = ['name']
model = 'computed.purchase.order.line' c = [['id', 'in', default_partners_id]]
fields += ['computed_purchase_order_id'] partners_name = api.search_read('res.partner', c, f)
for p in partners_name:
partners.append(p['name'])
if line_type == 'product_templates':
fields = ['id']
cond = [['product_tmpl_id', 'in', line_ids]]
model = 'product.product'
user="api"
else: else:
model = 'purchase.order.line' fields = ['create_uid', 'product_id', 'partner_id']
fields += ['order_id'] cond = [['id', 'in', line_ids]]
if (line_type == 'cpo'):
model = 'computed.purchase.order.line'
fields += ['computed_purchase_order_id']
else:
model = 'purchase.order.line'
fields += ['order_id']
lines = api.search_read(model, cond, fields) lines = api.search_read(model, cond, fields)
if len(lines) == len(line_ids): if len(lines) == len(line_ids):
for l in lines: for l in lines:
ids.append(l['product_id'][0]) if line_type == 'product_templates':
user = l['create_uid'][1] ids.append(l['id'])
if (line_type == 'cpo'):
order = l['computed_purchase_order_id']
else: else:
order = l['order_id'] ids.append(l['product_id'][0])
partner = l['partner_id'][1] user = l['create_uid'][1]
if (line_type == 'cpo'):
order = l['computed_purchase_order_id']
else:
order = l['order_id']
partners.append(l['partner_id'][1])
if (line_type == 'cpo'): if (line_type == 'cpo'):
# partner_id isn't defined # partner_id isn't defined
f = ['partner_id'] f = ['partner_id']
c = [['id', '=', int(order[0])]] c = [['id', '=', int(order[0])]]
cpo = api.search_read('computed.purchase.order', c, f) cpo = api.search_read('computed.purchase.order', c, f)
if len(cpo) > 0: if len(cpo) > 0:
partner = cpo[0]['partner_id'][1] partners.append(cpo[0]['partner_id'][1])
file_data = { file_data = {
'order': order[1], 'order': order[1],
'user': user, 'user': user,
'partner': partner, 'partners': partners,
'inventory_status': '', 'inventory_status': '',
'products': ids 'products': ids
} }
......
...@@ -4,6 +4,13 @@ var shelfs_table = null, ...@@ -4,6 +4,13 @@ var shelfs_table = null,
function init_datatable() { function init_datatable() {
// For a smooth migration...
for (const i in lists) {
if (('partners' in lists[i]) === false) {
lists[i]['partners'] = [lists[i]['partner']];
}
}
return $('#lists').DataTable({ return $('#lists').DataTable({
data: lists, // data passed at page loading data: lists, // data passed at page loading
rowId: 'id', rowId: 'id',
...@@ -17,8 +24,16 @@ function init_datatable() { ...@@ -17,8 +24,16 @@ function init_datatable() {
} }
}, },
{ {
data:"partner", data:"partners",
title:"Fournisseur" title:"Fournisseur(s)",
render: function (data) {
res = "";
for (const i in data) {
res += `${data[i]}<br/>`;
}
return res;
}
}, },
{ {
data:"order", data:"order",
......
...@@ -43,7 +43,6 @@ def custom_list_inventory(request, id): ...@@ -43,7 +43,6 @@ def custom_list_inventory(request, id):
products = CagetteInventory.get_custom_list_products(id) products = CagetteInventory.get_custom_list_products(id)
if 'error' in products: if 'error' in products:
print(products)
products['data'] = [] products['data'] = []
context = {'title': 'Inventaire', context = {'title': 'Inventaire',
...@@ -112,10 +111,25 @@ def do_custom_list_inventory(request): ...@@ -112,10 +111,25 @@ def do_custom_list_inventory(request):
def generate_inventory_list(request): def generate_inventory_list(request):
"""Responding to Odoo ajax call (no csrf).""" """Responding to Odoo ajax call (no csrf)."""
res = {} res = {}
default_partners_id = []
try: try:
lines = json.loads(request.POST.get('lines')) lines = json.loads(request.POST.get('lines'))
ltype = request.POST.get('type') ltype = request.POST.get('type')
res = CagetteInventory.create_custom_inv_file(lines, ltype) except Exception as e:
try:
# POST.get() returns None when request from django
data = json.loads(request.body.decode())
lines = data["lines"]
ltype = data["type"]
if "partners_id" in data:
default_partners_id = data["partners_id"]
except Exception as ee:
res['error'] = str(ee)
coop_looger.error("generate_inventory_list : %s", str(e))
return JsonResponse(res, status=500)
try:
res = CagetteInventory.create_custom_inv_file(lines, ltype, default_partners_id)
except Exception as e: except Exception as e:
res['error'] = str(e) res['error'] = str(e)
coop_looger.error("generate_inventory_list : %s", str(e)) coop_looger.error("generate_inventory_list : %s", str(e))
......
...@@ -22,7 +22,7 @@ class CagetteMember(models.Model): ...@@ -22,7 +22,7 @@ class CagetteMember(models.Model):
"""Class to handle cagette Odoo member.""" """Class to handle cagette Odoo member."""
m_default_fields = ['name', 'sex', 'image_medium', 'active', m_default_fields = ['name', 'sex', 'image_medium', 'active',
'barcode_base', 'barcode', 'in_ftop_team', 'barcode_base', 'barcode', 'in_ftop_team',
'is_associated_people', 'is_member', 'is_associated_people', 'is_member', 'shift_type',
'display_ftop_points', 'display_std_points', 'display_ftop_points', 'display_std_points',
'is_exempted', 'cooperative_state', 'date_alert_stop'] 'is_exempted', 'cooperative_state', 'date_alert_stop']
...@@ -1176,6 +1176,44 @@ class CagetteServices(models.Model): ...@@ -1176,6 +1176,44 @@ class CagetteServices(models.Model):
result['service_found'] = False result['service_found'] = False
return result return result
@staticmethod
def easy_validate_shift_presence(coop_id):
"""Add a presence point if the request is valid."""
res = {}
try:
api = OdooAPI()
# let verify coop_id is corresponding to a ftop subscriber
cond = [['id', '=', coop_id]]
fields = ['shift_type']
coop = api.search_read('res.partner', cond, fields)
if coop:
if coop[0]['shift_type'] == 'ftop':
evt_name = getattr(settings, 'ENTRANCE_ADD_PT_EVENT_NAME', 'Validation service comité')
c = [['partner_id', '=', coop_id], ['name', '=', evt_name]]
f = ['create_date']
last_point_mvts = api.search_read('shift.counter.event', c, f,
order ="create_date DESC", limit=1)
ok_for_adding_pt = False
if len(last_point_mvts):
now = datetime.datetime.now()
past = datetime.datetime. strptime(last_point_mvts[0]['create_date'],
'%Y-%m-%d %H:%M:%S')
if (now - past).total_seconds() >= 3600 * 24:
ok_for_adding_pt = True
else:
ok_for_adding_pt = True
if ok_for_adding_pt is True:
res['evt_id'] = CagetteMember(coop_id).add_pts('ftop', 1, evt_name)
else:
res['error'] = "One point has been added less then 24 hours ago"
else:
res['error'] = "Unallowed coop"
else:
res['error'] = "Invalid coop id"
except Exception as e:
coop_logger.error("easy_validate_shift_presence : %s %s", str(coop_id), str(e))
return res
class CagetteUser(models.Model): class CagetteUser(models.Model):
@staticmethod @staticmethod
......
...@@ -58,7 +58,8 @@ h1 .member_name {font-weight: bold;} ...@@ -58,7 +58,8 @@ h1 .member_name {font-weight: bold;}
#member_slide .btn[data-next] #member_slide .btn[data-next]
{margin-top: 35px; background: #449d44; color:#FFF;} {margin-top: 35px; background: #449d44; color:#FFF;}
[data-next="rattrapage_1"] {float:right;color:#171A17 !important; margin-top: 25px;} [data-next="rattrapage_1"] {float:right;color:#171A17 !important; margin-top: 25px;}
#service_en_cours .info a {line-height: 24px; font-size:18px; margin-top:15px;} #service_en_cours .info {font-size: 26px;}
#service_en_cours .info a {line-height: 24px; font-size:14px; margin-top:15px;}
.outside_list a {margin-left:15px;} .outside_list a {margin-left:15px;}
#rattrapage_1 .advice {margin-top:15px;} #rattrapage_1 .advice {margin-top:15px;}
.btn.present {background:#50C878;} .btn.present {background:#50C878;}
...@@ -67,3 +68,5 @@ h1 .member_name {font-weight: bold;} ...@@ -67,3 +68,5 @@ h1 .member_name {font-weight: bold;}
.msg-big {font-size: xx-large; background: #fff; padding:25px; text-align: center;} .msg-big {font-size: xx-large; background: #fff; padding:25px; text-align: center;}
#member_advice {background: #FFF; color: red;} #member_advice {background: #FFF; color: red;}
.easy_shift_validate {text-align: center; margin-top: 3em;}
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
})(jQuery); })(jQuery);
var current_displayed_member = null, var current_displayed_member = null,
operator = null,
results = null, results = null,
loaded_services = null, loaded_services = null,
selected_service = null, selected_service = null,
...@@ -23,6 +24,8 @@ var current_displayed_member = null, ...@@ -23,6 +24,8 @@ var current_displayed_member = null,
rattrapage_ou_volant = null, rattrapage_ou_volant = null,
timeout_counter = null; timeout_counter = null;
var search_button = $('.btn--primary.search'); var search_button = $('.btn--primary.search');
var sm_search_member_button = $('#sm_search_member_button'),
sm_search_member_input = $('#sm_search_member_input');
var loading2 = $('.loading2'); var loading2 = $('.loading2');
var search_field = $('input[name="search_string"]'); var search_field = $('input[name="search_string"]');
var shift_title = $('#current_shift_title'); var shift_title = $('#current_shift_title');
...@@ -36,6 +39,8 @@ var photo_advice = $('#photo_advice'); ...@@ -36,6 +39,8 @@ var photo_advice = $('#photo_advice');
var photo_studio = $('#photo_studio'); var photo_studio = $('#photo_studio');
var coop_info = $('.coop-info'); var coop_info = $('.coop-info');
const missed_begin_msg = $('#missed_begin_msg').html();
let no_pict_msg = $('#no-picture-msg'); let no_pict_msg = $('#no-picture-msg');
var pages = { var pages = {
...@@ -61,6 +66,7 @@ var html_elts = { ...@@ -61,6 +66,7 @@ var html_elts = {
next_shifts : $('#next_shifts') next_shifts : $('#next_shifts')
}; };
var chars = []; //input chars buffer
function fill_member_slide(member) { function fill_member_slide(member) {
no_pict_msg.hide(); no_pict_msg.hide();
...@@ -160,6 +166,8 @@ function canSearch() { ...@@ -160,6 +166,8 @@ function canSearch() {
} }
function search_member(force_search = false) { function search_member(force_search = false) {
chars = []; // to prevent false "as barcode-reader" input
operator = null;
if (canSearch() || force_search) { if (canSearch() || force_search) {
html_elts.member_slide.hide(); html_elts.member_slide.hide();
...@@ -277,7 +285,15 @@ function fill_service_entry(s) { ...@@ -277,7 +285,15 @@ function fill_service_entry(s) {
rattrapage_wanted.show(); rattrapage_wanted.show();
} }
function clean_search_for_easy_validate_zone() {
$('.search_member_results_area').hide();
$('.search_member_results').empty();
sm_search_member_input.val('');
operator = null;
}
function clean_service_entry() { function clean_service_entry() {
clean_search_for_easy_validate_zone();
rattrapage_wanted.hide(); rattrapage_wanted.hide();
shift_title.text(''); shift_title.text('');
shift_members.html(''); shift_members.html('');
...@@ -344,8 +360,8 @@ function get_service_entry_data() { ...@@ -344,8 +360,8 @@ function get_service_entry_data() {
page_title.text('Qui es-tu ?'); page_title.text('Qui es-tu ?');
try { try {
if (rData.res.length == 0) { if (rData.res.length == 0) {
info_place.text('La période pendant laquelle il est possible de s\'enregistrer est close.'); info_place.html(missed_begin_msg);
page_title.text(''); page_title.html('');
} else { } else {
if (rData.res.length > 1) { if (rData.res.length > 1) {
...@@ -665,9 +681,74 @@ html_elts.image_medium.on('click', function() { ...@@ -665,9 +681,74 @@ html_elts.image_medium.on('click', function() {
} }
}); });
$(document).ready(function() { function ask_for_easy_shift_validation() {
var chars = []; //alert("operator = " + JSON.stringify(operator))
msg = "<p>Je suis bien " + operator.name + "<br/> et <br/>je valide mon service 'Comité' </p>";
openModal(msg, function() {
try {
post_form(
'/members/easy_validate_shift_presence',
{
coop_id: operator.id
},
function(err, result) {
if (!err) {
alert("1 point volant vient d'être ajouté.");
clean_search_for_easy_validate_zone();
closeModal();
} else {
if (typeof (err.responseJSON) != "undefined"
&& typeof (err.responseJSON.error) != "undefined") {
alert(err.responseJSON.error);
} else {
console.log(err);
}
}
}
);
} catch (e) {
console.log(e);
}
}, 'Confirmer');
}
// Display the members from the search result (copied from stock_movements)
function display_possible_members() {
$('.search_member_results_area').show();
$('.search_member_results').empty();
if (members_search_results.length > 0) {
for (member of members_search_results) {
let btn_classes = "btn";
if (operator != null && operator.id == member.id) {
btn_classes = "btn--success";
}
// Display results (possible members) as buttons
var member_button = '<button class="' + btn_classes + ' btn_member" member_id="'
+ member.id + '">'
+ member.barcode_base + ' - ' + member.name
+ '</button>';
$('.search_member_results').append(member_button);
// Set action on click on a member button
$('.btn_member').on('click', function() {
for (member of members_search_results) {
if (member.id == $(this).attr('member_id')) {
operator = member;
// Enable validation button when operator is selected
ask_for_easy_shift_validation();
break;
}
}
display_possible_members();
});
}
} else {
$('.search_member_results').html('<p><i>Aucun résultat ! Faites-vous partie d\'un comité ? <br/> Si oui, vérifiez la recherche..</i></p>');
}
}
$(document).ready(function() {
var shopping_entry_btn = $('a[data-next="shopping_entry"]'); var shopping_entry_btn = $('a[data-next="shopping_entry"]');
shopping_entry_btn.on('click', function() { shopping_entry_btn.on('click', function() {
...@@ -708,6 +789,44 @@ $(document).ready(function() { ...@@ -708,6 +789,44 @@ $(document).ready(function() {
init_webcam(); init_webcam();
}); });
$('#sm_search_member_form').submit(function() {
if (is_time_to('search_member', 1000)) {
sm_search_member_button.empty().append(`<i class="fas fa-spinner fa-spin"></i>`);
let search_str = sm_search_member_input.val();
$.ajax({
url: '/members/search/' + search_str,
dataType : 'json',
success: function(data) {
members_search_results = [];
for (member of data.res) {
if (member.shift_type == 'ftop') {
members_search_results.push(member);
}
}
display_possible_members();
},
error: function(data) {
err = {
msg: "erreur serveur lors de la recherche de membres",
ctx: 'easy_validate.search_members'
};
report_JS_error(err, 'members');
$.notify("Erreur lors de la recherche de membre, il faut ré-essayer plus tard...", {
globalPosition:"top right",
className: "error"
});
},
complete: function() {
sm_search_member_button.empty().append(`Recherche`);
}
});
}
});
}); });
Webcam.on('live', function() { Webcam.on('live', function() {
......
...@@ -258,21 +258,21 @@ function save_current_coop(callback) { ...@@ -258,21 +258,21 @@ function save_current_coop(callback) {
sex_error = false; sex_error = false;
if (/([0-9]{2})\/([0-9]{2})\/([0-9]{4})/.exec(birthdate)) { if (/([0-9]{2})\/([0-9]{2})\/([0-9]{4})/.exec(birthdate)) {
try{ try {
var jj = RegExp.$1, var jj = RegExp.$1,
mm = RegExp.$2, mm = RegExp.$2,
aaaa = RegExp.$3; aaaa = RegExp.$3;
let tmp_date = aaaa + "-" + mm + "-" + jj; let tmp_date = aaaa + "-" + mm + "-" + jj;
// try to create a date object // try to create a date object
date_test = new Date(tmp_date); date_test = new Date(tmp_date);
// if date is invalid a correction is apply in date object. Check it // if date is invalid a correction is apply in date object. Check it
// january start at 0, so we add + 1 for the month // january start at 0, so we add + 1 for the month
if ((date_test.getDate() !== parseInt(jj)) || ((date_test.getMonth()+1) !== parseInt(mm)) || (date_test.getFullYear() !== parseInt(aaaa)) || !date_test.isValid()) if ((date_test.getDate() !== parseInt(jj)) || ((date_test.getMonth()+1) !== parseInt(mm)) || (date_test.getFullYear() !== parseInt(aaaa)) || !date_test.isValid()) {
{
birthdate_error = true; birthdate_error = true;
} }
}catch(Exception){ } catch (Exception) {
birthdate_error = true; birthdate_error = true;
} }
......
...@@ -46,6 +46,7 @@ urlpatterns = [ ...@@ -46,6 +46,7 @@ urlpatterns = [
url(r'^remove_data_from_couchdb$', views.remove_data_from_CouchDB), url(r'^remove_data_from_couchdb$', views.remove_data_from_CouchDB),
url(r'^image/([0-9]+)', views.getmemberimage), url(r'^image/([0-9]+)', views.getmemberimage),
url(r'^add_pts_to_everybody/([0-9]+)/([a-zA-Z0-9_ ]+)$', admin.add_pts_to_everybody), url(r'^add_pts_to_everybody/([0-9]+)/([a-zA-Z0-9_ ]+)$', admin.add_pts_to_everybody),
url(r'^easy_validate_shift_presence$', views.easy_validate_shift_presence),
# conso / groupe recherche / socio # conso / groupe recherche / socio
url(r'^panel_get_purchases$', views.panel_get_purchases), url(r'^panel_get_purchases$', views.panel_get_purchases),
] ]
...@@ -24,6 +24,10 @@ def index(request): ...@@ -24,6 +24,10 @@ def index(request):
'WELCOME_SUBTITLE_ENTRANCE_MSG': getattr(settings, 'WELCOME_SUBTITLE_ENTRANCE_MSG', ''), 'WELCOME_SUBTITLE_ENTRANCE_MSG': getattr(settings, 'WELCOME_SUBTITLE_ENTRANCE_MSG', ''),
'ENTRANCE_SHOPPING_BTN': getattr(settings, 'ENTRANCE_SHOPPING_BTN', 'Je viens faire mes courses'), 'ENTRANCE_SHOPPING_BTN': getattr(settings, 'ENTRANCE_SHOPPING_BTN', 'Je viens faire mes courses'),
'ENTRANCE_SERVICE_BTN': getattr(settings, 'ENTRANCE_SERVICE_BTN', 'Je viens faire mon service'), 'ENTRANCE_SERVICE_BTN': getattr(settings, 'ENTRANCE_SERVICE_BTN', 'Je viens faire mon service'),
'ENTRANCE_MISSED_SHIFT_BEGIN_MSG': getattr(settings, 'ENTRANCE_MISSED_SHIFT_BEGIN_MSG',
"La période pendant laquelle il est possible de s'enregistrer est close."),
'ENTRANCE_EASY_SHIFT_VALIDATE_MSG': getattr(settings, 'ENTRANCE_EASY_SHIFT_VALIDATE_MSG',
'Je valide mon service "Comité"'),
'CONFIRME_PRESENT_BTN' : getattr(settings, 'CONFIRME_PRESENT_BTN', 'Présent.e') 'CONFIRME_PRESENT_BTN' : getattr(settings, 'CONFIRME_PRESENT_BTN', 'Présent.e')
} }
for_shoping_msg = getattr(settings, 'ENTRANCE_COME_FOR_SHOPING_MSG', '') for_shoping_msg = getattr(settings, 'ENTRANCE_COME_FOR_SHOPING_MSG', '')
...@@ -33,6 +37,8 @@ def index(request): ...@@ -33,6 +37,8 @@ def index(request):
for_shoping_msg = msettings['msg_accueil']['value'] for_shoping_msg = msettings['msg_accueil']['value']
context['ENTRANCE_COME_FOR_SHOPING_MSG'] = for_shoping_msg context['ENTRANCE_COME_FOR_SHOPING_MSG'] = for_shoping_msg
context['ftop_btn_display'] = getattr(settings, 'ENTRANCE_FTOP_BUTTON_DISPLAY', True) context['ftop_btn_display'] = getattr(settings, 'ENTRANCE_FTOP_BUTTON_DISPLAY', True)
context['extra_btns_display'] = getattr(settings, 'ENTRANCE_EXTRA_BUTTONS_DISPLAY', True)
context['easy_shift_validate'] = getattr(settings, 'ENTRANCE_EASY_SHIFT_VALIDATE', False)
if 'no_picture_member_advice' in msettings: if 'no_picture_member_advice' in msettings:
if len(msettings['no_picture_member_advice']['value']) > 0: if len(msettings['no_picture_member_advice']['value']) > 0:
context['no_picture_member_advice'] = msettings['no_picture_member_advice']['value'] context['no_picture_member_advice'] = msettings['no_picture_member_advice']['value']
...@@ -283,6 +289,22 @@ def record_service_presence(request): ...@@ -283,6 +289,22 @@ def record_service_presence(request):
res['error'] = str(e) res['error'] = str(e)
return JsonResponse({'res': res}) return JsonResponse({'res': res})
def easy_validate_shift_presence(request):
"""Add a presence point if the request is valid."""
res = {}
try:
coop_id = int(request.POST.get("coop_id", "nan"))
res = CagetteServices.easy_validate_shift_presence(coop_id)
except Exception as e:
res['error'] = str(e)
if 'error' in res:
if res['error'] == "One point has been added less then 24 hours ago":
# TODO : use translation (all project wide)
res['error'] = "Vous ne pouvez pas valider plus d'un service par 24h"
return JsonResponse(res, status=500)
else:
return JsonResponse(res, safe=False)
def record_absences(request): def record_absences(request):
return JsonResponse({'res': CagetteServices.record_absences()}) return JsonResponse({'res': CagetteServices.record_absences()})
......
...@@ -222,6 +222,83 @@ class Order(models.Model): ...@@ -222,6 +222,83 @@ class Order(models.Model):
labels_data['total'] += l['product_qty'] labels_data['total'] += l['product_qty']
return labels_data return labels_data
def get_order_attachment_id(self):
res = {}
f = ["id"]
c = [['res_model', '=', 'purchase.order'], ['res_id', '=', self.id], ['type', 'in', ['binary', 'url']]]
try:
attachment = self.o_api.search_read('ir.attachment', c, f)
res = attachment[0]
except Exception as e:
res["id_po"] = self.id
res["error"] = str(e)
return res
@staticmethod
def create(supplier_id, date_planned, order_lines):
order_data = {
"partner_id": int(supplier_id),
"partner_ref": False,
"currency_id": 1,
"date_order": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"origin": "Aide à la commande",
"company_id": 1,
"order_line": [],
"notes": False,
"date_planned": date_planned,
"picking_type_id": 1,
"dest_address_id": False,
"incoterm_id": False,
"payment_term_id": False,
"fiscal_position_id": False,
"message_follower_ids": False,
"message_ids": False
}
for line in order_lines:
order_data["order_line"].append(
[
0,
False,
{
"package_qty": line["package_qty"],
"price_policy": "uom",
"indicative_package": True,
"product_id": line["product_variant_ids"][0],
"name": line["name"],
"date_planned": date_planned,
"account_analytic_id": False,
"product_qty_package":line["product_qty_package"],
"product_qty": line["product_qty"],
"product_uom": line["product_uom"],
"price_unit": line["price_unit"],
"discount": 0,
"taxes_id": [
[
6,
False,
line["supplier_taxes_id"]
]
]
}
]
)
api = OdooAPI()
id_po = api.create('purchase.order', order_data)
res_confirm = api.execute('purchase.order', 'button_confirm', [id_po])
res = {
'id_po': id_po,
'confirm_po': True,
'supplier_id': supplier_id,
'date_planned': date_planned
}
return res
class Orders(models.Model): class Orders(models.Model):
@staticmethod @staticmethod
...@@ -268,3 +345,15 @@ class Orders(models.Model): ...@@ -268,3 +345,15 @@ class Orders(models.Model):
coop_logger.error('Orders get_custom_barcode_labels_to_print(oids) : %s', str(e)) coop_logger.error('Orders get_custom_barcode_labels_to_print(oids) : %s', str(e))
return labels_data return labels_data
class CagetteSuppliers(models.Model):
@staticmethod
def get_suppliers():
api = OdooAPI()
f = ['id', 'name', 'display_name']
c = [['supplier', '=', 1], ['parent_id', '=', False]]
res = api.search_read('res.partner', c, f)
return res
.page_body{
position: relative;
}
.page_content, .login_area {
position: absolute;
top: 0;
left: 0;
right: 0;
}
/* - Common */
.pill {
border-radius: 30px;
min-width: 200px;
min-height: 40px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 5px 30px 5px 30px;
margin: 0 10px 5px 10px;
}
.disabled {
background-color: #c9cbce;
}
.disabled:hover {
background-color: #a1a2a3;
}
/* - Order selection screen */
#new_order_area {
margin-bottom: 40px;
}
#new_order_form {
margin-top: 20px;
}
#existing_orders {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
width: 80%;
margin: 0 auto;
padding-top: 15px;
}
.order_last_update {
font-weight: bold;
}
.order_modified_msg {
font-size: 2rem;
color: #e62720;
}
/* - Main screen */
/* -- Top action button(s) */
#back_to_order_selection {
position: absolute;
}
#main_content .actions_buttons_area {
position: absolute;
width: 100%;
top: 0;
display: flex;
justify-content: space-between;
}
#orders_created .actions_buttons_area {
position: absolute;
width: 100%;
top: 0;
display: flex;
justify-content: flex-start;
}
/* -- Order data */
#order_data_container {
font-size: 1.8rem;
}
#order_data_separator {
margin: 0 10px 0 10px;
}
#order_forms_container {
margin-top: 30px;
display: flex;
justify-content: space-evenly;
}
#supplier_input {
width: 350px;
border-radius: 3px;
}
#date_planned_input, #coverage_days_input {
border-radius: 3px;
}
/* -- Table */
#products_table_filter{
text-align: right !important;
}
#products_table_filter input{
height: 35px;
width: 300px;
border-radius: 3px;
}
#products_table .help {cursor: help;}
#table_header_select_all{
display: flex;
align-content: center;
justify-content: center;
flex-wrap: wrap;
}
.select_all_text {
margin-top: 5px;
margin-bottom: 5px;
}
#table_header_select_all input{
margin-left: 5px;
}
.custom_cell_content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.product_qty_input {
width: 100px;
}
.supplier_package_qty {
font-style: italic;
font-size: 1.3rem;
}
.product_not_from_supplier {
background-color: #e7e9ed;
cursor: pointer;
}
.product_not_from_supplier:hover {
background-color: #c7cace;
}
.product_name, .supplier_name, .product_npa {
font-weight: bold;
}
.select_product_cb, .product_npa_cb {
cursor: pointer;
}
/* -- Footer */
#main_content_footer {
margin: 20px 0 40px 0;
}
#footer_actions {
width: 100%;
display: flex;
justify-content: space-between;
}
/* -- Suppliers list */
#suppliers_container {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
margin: 30px 0 20px 0;
}
.supplier_pill {
background-color: #e7e9edc5;
border: 1px solid black;
}
.pill_supplier_name {
font-weight: bold;
}
.supplier_total_value_container {
font-size: 1.5rem;
}
.remove_supplier_icon {
color: red;
margin-left: 5px;
cursor: pointer;
}
/* -- Attach product to supplier modal */
.modal_input_area {
margin-bottom: 15px;
width: 100%;
display: flex;
justify-content: center;
}
.modal_input_container, .modal_input_label {
width:50%;
margin: 0 15px 0 15px;
}
.modal_input_label {
display: flex;
justify-content: flex-end;
align-items: center;
}
.modal_input {
width: 90%;
}
/* - Orders created screen */
.order_created_header {
margin-top: 5px;
margin-bottom: 40px;
}
#created_orders_area {
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
}
.new_order_item {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 20px;
}
.download_order_file {
margin-top: 10px;
}
.download_order_file_button:hover {
text-decoration: none;
color: white;
}
.download_order_file_button:active {
text-decoration: none;
color: white;
}
.download_order_file_button:focus {
text-decoration: none;
color: white;
}
#recap_delivery_date {
font-weight: bold;
}
.mail_example_container {
display: flex;
flex-direction: column;
align-items: center;
width: 30%;
margin: 0 auto;
}
.mail_type_text {
width: 100%;
}
.mail_example {
background-color: #e7e9ed;
width: 100%;
padding: 15px;
}
\ No newline at end of file
var suppliers_list = [],
selected_suppliers = [],
products_list = [],
products = [],
products_table = null,
selected_rows = [],
product_orders = [],
date_format = "dd/mm/yy",
new_product_supplier_association = {
package_qty: null,
price: null
};
var dbc = null,
sync = null,
order_doc = {
_id: null,
coverage_days: null,
last_update: {
timestamp: null,
fingerprint: null
},
products: [],
selected_suppliers: [],
selected_rows: []
},
fingerprint = null;
var clicked_order_pill = null;
/* - UTILS */
/**
* Reset data that changes between screens
*/
function reset_data() {
products = [];
selected_suppliers = [];
selected_rows = [];
product_orders = [];
order_doc = {
_id: null,
coverage_days: null,
last_update : {
timestamp: null,
fingerprint: null
},
products: [],
selected_suppliers: []
};
new_product_supplier_association = {
package_qty: null,
price: null
};
clicked_order_pill = null;
}
/**
* Difference between two dates
* @param {Date} date1
* @param {Date} date2
* @returns difference object
*/
function dates_diff(date1, date2) {
var diff = {};
var tmp = date2 - date1;
tmp = Math.floor(tmp/1000);
diff.sec = tmp % 60;
tmp = Math.floor((tmp-diff.sec)/60);
diff.min = tmp % 60;
tmp = Math.floor((tmp-diff.min)/60);
diff.hours = tmp % 24;
tmp = Math.floor((tmp-diff.hours)/24);
diff.days = tmp;
return diff;
}
/* - PRODUCTS */
/**
* Add a product.
*
* @returns -1 if validation failed, 0 otherwise
*/
function add_product() {
const user_input = $("#product_input").val();
// Check if user input is a valid article
const product = products_list.find(s => s.display_name === user_input);
if (product === undefined) {
alert("L'article renseigné n'est pas valide.\n"
+ "Veuillez sélectionner un article dans la liste déroulante.");
return -1;
}
const product_exists = products.findIndex(p => p.name === user_input);
if (product_exists !== -1) {
alert("Cet article est déjà dans le tableau.");
$("#product_input").val('');
return -1;
}
$.ajax({
type: 'POST',
url: '/products/get_product_for_order_helper',
data: JSON.stringify([product.tpl_id]),
dataType:"json",
traditional: true,
contentType: "application/json; charset=utf-8",
success: function(data) {
let res = data.products[0];
if (typeof res.id != "undefined") {
res.suppliersinfo = [];
res.default_code = ' ';
products.unshift(res);
update_main_screen({'sort_order_dir':'desc'});
update_cdb_order();
} else {
alert("L'article n'a pas toutes les caractéristiques pour être ajouté.");
}
$("#product_input").val('');
},
error: function(data) {
err = {msg: "erreur serveur lors de la récupération des données liées à l'article", ctx: 'get_product_for_help_order_line'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
alert('Erreur lors de la récupération des informations, réessayer plus tard.');
}
});
return 0;
}
/**
* 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() {
for (const [
key,
product
] of Object.entries(products)) {
if ('suppliersinfo' in product && product.suppliersinfo.length > 0) {
let purchase_qty_for_coverage = null;
// Durée couverture produit = (stock + qté entrante + qté commandée ) / conso quotidienne
const stock = product.qty_available;
const incoming_qty = product.incoming_qty;
const daily_conso = product.daily_conso;
purchase_qty_for_coverage = order_doc.coverage_days * daily_conso - stock - incoming_qty;
purchase_qty_for_coverage = (purchase_qty_for_coverage < 0) ? 0 : purchase_qty_for_coverage;
// Reduce to nb of packages to purchase
purchase_package_qty_for_coverage = purchase_qty_for_coverage / product.suppliersinfo[0].package_qty;
// Round up to unit for all products
purchase_package_qty_for_coverage = Math.ceil(purchase_package_qty_for_coverage);
// Set qty to purchase for first supplier only
products[key].suppliersinfo[0].qty = purchase_package_qty_for_coverage;
}
}
}
/**
* Update order products data in case they have changed.
*/
function check_products_data() {
return new Promise((resolve) => {
const suppliers_id = selected_suppliers.map(s => s.id);
if (suppliers_id.length > 0) {
$.notify(
"Vérfication des informations produits...",
{
globalPosition:"top left",
className: "info"
}
);
clicked_order_pill.find('.pill_order_name').empty()
.append(`<i class="fas fa-spinner fa-spin"></i>`);
$.ajax({
type: 'GET',
url: '/orders/get_supplier_products',
data: {
sids: suppliers_id
},
dataType:"json",
traditional: true,
contentType: "application/json; charset=utf-8",
success: function(data) {
for (let product of data.res.products) {
const p_index = products.findIndex(p => p.id == product.id);
// Override products data with new data (without suppliersinfo so we don't override qty)
const updated_suppliersinfo = product.suppliersinfo;
delete product.suppliersinfo;
products[p_index] = { ...products[p_index], ...product };
// Update suppliers info
for (let psi_index in products[p_index].suppliersinfo) {
const updated_psi = updated_suppliersinfo.find(psi => psi.supplier_id == products[p_index].suppliersinfo[psi_index].supplier_id);
if (updated_psi !== undefined) {
products[p_index].suppliersinfo[psi_index].package_qty = updated_psi.package_qty;
products[p_index].suppliersinfo[psi_index].price = updated_psi.price;
}
}
}
$('.notifyjs-wrapper').trigger('notify-hide');
resolve();
},
error: function(data) {
err = {msg: "erreur serveur lors de la vérification des données des articles", ctx: 'check_products_data'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
alert(`Erreur lors de la vérification des données des articles. Certaines données peuvent être erronées`);
$('.notifyjs-wrapper').trigger('notify-hide');
// Don't block process if this call fails
resolve();
}
});
} else {
resolve();
}
});
}
/* - SUPPLIERS */
/**
* Add a supplier to the selected suppliers list.
*
* @returns -1 if validation failed, 0 otherwise
*/
function add_supplier() {
const user_input = $("#supplier_input").val();
// Check if user input is a valid supplier
let supplier = suppliers_list.find(s => s.display_name === user_input);
if (supplier === undefined) {
alert("Le fournisseur renseigné n'est pas valide.\n"
+ "Veuillez sélectionner un fournisseur dans la liste déroulante.");
return -1;
}
const supplier_selected = selected_suppliers.find(s => s.display_name === user_input);
if (supplier_selected !== undefined) {
alert("Ce fournisseur est déjà sélectionné.");
return -1;
}
openModal();
supplier.total_value = 0;
selected_suppliers.push(supplier);
let url = "/orders/get_supplier_products";
url += "?sids=" + encodeURIComponent(supplier.id);
// Fetch supplier products
$.ajax({
type: 'GET',
url: url,
dataType:"json",
traditional: true,
contentType: "application/json; charset=utf-8",
success: function(data) {
save_supplier_products(supplier, data.res.products);
update_main_screen();
$("#supplier_input").val("");
update_cdb_order();
closeModal();
},
error: function(data) {
err = {msg: "erreur serveur lors de la récupération des produits", ctx: 'get_supplier_products'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
closeModal();
alert('Erreur lors de la récupération des produits, réessayer plus tard.');
}
});
return 0;
}
/**
* Remove a supplier from the selected list & its associated products
*
* @param {int} supplier_id
*/
function remove_supplier(supplier_id) {
// Remove from suppliers list
selected_suppliers = selected_suppliers.filter(supplier => supplier.id != supplier_id);
// Remove the supplier from the products suppliers list
for (const i in products) {
products[i].suppliersinfo = products[i].suppliersinfo.filter(supplier => supplier.supplier_id != supplier_id);
}
// Remove products only associated to this product
products = products.filter(product => product.suppliersinfo.length > 0);
update_main_screen();
update_cdb_order();
}
/**
* Send to server the association product-supplier
*
* @param {object} product
* @param {object} supplier
* @param {node} cell product's row in datatable
*/
function save_supplier_product_association(product, supplier, cell) {
openModal();
$('.new_product_supplier_price').off();
$('.new_product_supplier_package_pty').off();
const package_qty = parseFloat(new_product_supplier_association.package_qty);
const price = parseFloat(new_product_supplier_association.price);
// If value is a number
if (isNaN(package_qty) || isNaN(price)) {
closeModal();
alert(`Les champs "Prix" et "Colisage" doivent être remplis et valides. L'association n'a pas été sauvegardée.`);
return -1;
}
const data = {
product_tmpl_id: product.id,
supplier_id: supplier.id,
package_qty: package_qty,
price: price
};
// Send request to create association
$.ajax({
type: "POST",
url: "/orders/associate_supplier_to_product",
dataType: "json",
traditional: true,
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
success: () => {
// Save supplierinfo in product
if (!('suppliersinfo' in product)) {
product.suppliersinfo = [];
}
product.suppliersinfo.push({
supplier_id: supplier.id,
package_qty: package_qty,
price: price
});
// Save relation locally
save_supplier_products(supplier, [product]);
// Update table
$(cell).removeClass("product_not_from_supplier");
const row = $(cell).closest("tr");
const new_row_data = prepare_datatable_data([product.id])[0];
products_table.row(row).data(new_row_data)
.draw();
update_cdb_order();
closeModal();
},
error: function(data) {
let msg = "erreur serveur lors de la sauvegarde de l'association product/supplier".
msg += ` (product_tmpl_id: ${product.id}; supplier_id: ${supplier.id})`;
err = {msg: msg, ctx: 'save_supplier_product_association'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
closeModal();
alert('Erreur lors de la sauvegarde de l\'association. Veuillez ré-essayer plus tard.');
}
});
return 0;
}
/**
* When products are fetched, save them and the relation with the supplier.
* If product already saved, add the supplier to its suppliers list.
* Else, add product with supplier.
*
* @param {object} supplier
* @param {array} new_products
*/
function save_supplier_products(supplier, new_products) {
for (np of new_products) {
let index = products.findIndex(p => p.id === np.id);
if (index === -1) {
products.push(np);
} else {
// Prevent adding ducplicate supplierinfo
let index_existing_supplierinfo = products[index].suppliersinfo.findIndex(psi => psi.supplier_id == supplier.id);
if (index_existing_supplierinfo === -1) {
np_supplierinfo = np.suppliersinfo[0];
products[index].suppliersinfo.push(np_supplierinfo);
}
}
}
}
/**
* Save the quantity set for a product/supplier
*
* @param {int} prod_id
* @param {int} supplier_id
* @param {float} val
*/
function save_product_supplier_qty(prod_id, supplier_id, val) {
for (const i in products) {
if (products[i].id == prod_id) {
for (const j in products[i].suppliersinfo) {
if (products[i].suppliersinfo[j].supplier_id == supplier_id) {
products[i].suppliersinfo[j].qty = val;
break;
}
}
}
}
}
/**
* Look in the 'suppliers' property of a product
*
* @param {object} product
* @param {object} supplier
* @returns boolean
*/
function is_product_related_to_supplier(product, supplier) {
return product.suppliersinfo.find(s => s.supplier_id === supplier.id) !== undefined;
}
/**
* Calculate the total value purchased for all supplier
*/
function _compute_total_values_by_supplier() {
// Reinit
for (let s of selected_suppliers) {
s.total_value = 0;
}
for (let p of products) {
for (let supinfo of p.suppliersinfo) {
let supplier_index = selected_suppliers.findIndex(s => s.id == supinfo.supplier_id);
let product_supplier_value = ('qty' in supinfo) ? supinfo.qty * supinfo.package_qty * supinfo.price : 0;
selected_suppliers[supplier_index].total_value += product_supplier_value;
}
}
}
/* - PRODUCT */
/**
* Update 'purchase_ok' of a product
*
* @param {int} p_id product id
* @param {Boolean} npa value to set purchase_ok to
*/
function set_product_npa(p_id, npa) {
openModal();
const data = {
product_tmpl_id: p_id,
purchase_ok: !npa
};
// Fetch supplier products
$.ajax({
type: "POST",
url: "/products/update_product_purchase_ok",
dataType: "json",
traditional: true,
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
success: () => {
const index = products.findIndex(p => p.id == p_id);
// Give time for modal to fade
setTimeout(function() {
$.notify(
"Produit passé en NPA !",
{
globalPosition:"top right",
className: "success"
}
);
}, 500);
// Remove NPA products
products.splice(index, 1);
update_main_screen();
update_cdb_order();
closeModal();
},
error: function(data) {
let msg = "erreur serveur lors de la sauvegarde du NPA".
msg += ` (product_tmpl_id: ${p_id})`;
err = {msg: msg, ctx: 'set_product_npa'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
closeModal();
alert('Erreur lors de la sauvegarde de la donnée. Veuillez ré-essayer plus tard.');
update_main_screen();
}
});
}
/* - INVENTORY */
/**
* Create an inventory with the selected lines in the table
*/
function generate_inventory() {
if (products_table !== null) {
const selected_data = products_table.rows('.selected').data();
if (selected_data.length == 0) {
alert("Veuillez sélectionner les produits à inventorier en cochant les cases sur la gauche du tableau.");
} else {
data = {
lines: [],
partners_id: [],
type: 'product_templates'
};
for (var i = 0; i < selected_data.length; i++) {
const product = products.find(p => p.id == selected_data[i].id);
data.lines.push(product.id);
for (const supplierinfo of product.suppliersinfo) {
if (data.partners_id.indexOf(supplierinfo.supplier_id) === -1) {
data.partners_id.push(supplierinfo.supplier_id);
}
}
}
let modal_create_inventory = $('#templates #modal_create_inventory');
modal_create_inventory.find(".inventory_products_count").text(data.lines.length);
openModal(
modal_create_inventory.html(),
() => {
if (is_time_to('validate_generate_inventory')) {
$('#do_inventory').empty()
.append(`<i class="fas fa-spinner fa-spin"></i>`);
$.ajax({
type: "POST",
url: "/inventory/generate_inventory_list",
dataType: "json",
traditional: true,
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
success: () => {
unselect_all_rows();
// Give time for modal to fade
setTimeout(function() {
$('#do_inventory').empty()
.append(`Faire un inventaire`);
$('#do_inventory').notify(
"Inventaire créé !",
{
globalPosition:"bottom center",
className: "success"
}
);
}, 200);
},
error: function(data) {
$('#do_inventory').empty()
.append(`Faire un inventaire`);
let msg = "erreur serveur lors de la création de l'inventaire".
err = {msg: msg, ctx: 'generate_inventory'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
alert("Erreur lors de la création de l'inventaire. Réessayez plus tard.");
}
});
}
},
'Valider'
);
}
}
}
/* - ORDER */
/**
* Event fct: on click on an order button
*/
function order_pill_on_click() {
if (is_time_to('order_pill_on_click', 1000)) {
clicked_order_pill = $(this);
let order_name_container = clicked_order_pill.find('.pill_order_name');
let doc_id = $(order_name_container).text();
dbc.get(doc_id).then((doc) => {
if (doc.last_update.fingerprint !== fingerprint) {
time_diff = dates_diff(new Date(doc.last_update.timestamp), new Date());
diff_str = ``;
if (time_diff.days !== 0) {
diff_str += `${time_diff.days} jour(s), `;
}
if (time_diff.hours !== 0) {
diff_str += `${time_diff.hours} heure(s), `;
}
if (time_diff.min !== 0) {
diff_str += `${time_diff.min} min, `;
}
diff_str += `${time_diff.sec}s`;
let modal_order_access = $('#templates #modal_order_access');
modal_order_access.find(".order_last_update").text(diff_str);
openModal(
modal_order_access.html(),
() => {
if (is_time_to('validate_access_order')) {
goto_main_screen(doc);
}
},
'Valider'
);
} else {
goto_main_screen(doc);
}
})
.catch(function (err) {
if (err.status == 404) {
$.notify(
"Cette commande n'existe plus.",
{
globalPosition:"top right",
className: "error"
}
);
update_order_selection_screen();
} else {
alert('Erreur lors de la récupération de la commande. Si l\'erreur persiste, contactez un administrateur svp.');
}
console.log(err);
});
}
}
/**
* Create an order in couchdb if the name doesn't exist
*/
function create_cdb_order() {
const order_name = $("#new_order_name").val();
order_doc._id = order_name;
order_doc.last_update = {
timestamp: Date.now(),
fingerprint: fingerprint
};
dbc.put(order_doc, function callback(err, result) {
if (!err) {
order_doc._rev = result.rev;
update_main_screen();
switch_screen();
} else {
if (err.status == 409) {
alert("Une commande porte déjà ce nom !");
}
console.log(err);
}
});
}
/**
* Update order data of an existing order in couchdb
*/
function update_cdb_order() {
order_doc.products = products;
order_doc.selected_suppliers = selected_suppliers;
// Save that current user last updated the order
order_doc.last_update = {
timestamp: Date.now(),
fingerprint: fingerprint
};
return new Promise((resolve) => {
dbc.put(order_doc, function callback(err, result) {
if (!err && result !== undefined) {
order_doc._rev = result.rev;
resolve();
} else {
alert("Erreur lors de la sauvegarde de la commande... Si l'erreur persiste contactez un administrateur svp.");
console.log(err);
resolve();
}
});
});
}
/**
* Create the Product Orders in Odoo
*/
function create_orders() {
let orders_data = {
"suppliers_data": {}
};
// Get planned delivery date for each supplier before hiding the modal
for (let supplier of selected_suppliers) {
// Get planned date from modal
let supplier_date_planned = $(`#date_planned_supplier_${supplier.id}`).val();
let formatted_date = null;
if (supplier_date_planned !== '') {
if (date_format === "dd/mm/yy") {
// Change format [dd/mm/yy] to ISO [yy-mm-dd]
formatted_date = supplier_date_planned
.split('/')
.reverse()
.join('-') + ' 00:00:00';
} else {
formatted_date = supplier_date_planned + ' 00:00:00';
}
} else {
// Default date : tomorrow
let date_object = new Date();
date_object.setDate(date_object.getDate() + 1);
// Get ISO format bare string
formatted_date = date_object.toISOString().replace('T', ' ')
.split('.')[0];
}
// Create an entry for this supplier
orders_data.suppliers_data[supplier.id] = {
date_planned: formatted_date,
lines: []
};
}
openModal();
// Prepare data: get products where a qty is set
for (let p of products) {
for (let p_supplierinfo of p.suppliersinfo) {
// If a qty is set for a supplier for a product
if ('qty' in p_supplierinfo && p_supplierinfo.qty != 0) {
const supplier_id = p_supplierinfo.supplier_id;
orders_data.suppliers_data[supplier_id].lines.push({
'package_qty': p_supplierinfo.package_qty,
'product_id': p.id,
'name': p.name,
'product_qty_package': p_supplierinfo.qty,
'product_qty': p_supplierinfo.qty * p_supplierinfo.package_qty,
'product_uom': p.uom_id[0],
'price_unit': p_supplierinfo.price,
'supplier_taxes_id': p.supplier_taxes_id,
'product_variant_ids': p.product_variant_ids
});
}
}
}
$.ajax({
type: "POST",
url: "/orders/create_orders",
dataType: "json",
traditional: true,
contentType: "application/json; charset=utf-8",
data: JSON.stringify(orders_data),
success: (result) => {
$('#created_orders_area').empty();
// Display new orders
for (let new_order of result.res.created) {
const supplier_name = suppliers_list.find(s => s.id == new_order.supplier_id).display_name;
const date_planned = new_order.date_planned
.split(' ')[0]
.split('-')
.reverse()
.join('/');
product_orders.push({
'id': new_order.id_po,
'supplier_id': new_order.supplier_id,
'supplier_name': supplier_name
});
let new_order_template = $("#templates #new_order_item_template");
new_order_template.find(".new_order_supplier_name").text(supplier_name);
new_order_template.find(".new_order_po").text(`PO${new_order.id_po}`);
new_order_template.find(".new_order_date_planned").text(`Date de livraison prévue: ${date_planned}`);
new_order_template.find(".download_order_file_button").attr('id', `download_attachment_${new_order.id_po}`);
$('#created_orders_area').append(new_order_template.html());
}
// Prepare buttons to download order attachment
get_order_attachments();
// Clear data
order_doc._deleted = true;
update_cdb_order().then(() => {
update_order_selection_screen();
});
reset_data();
switch_screen('orders_created');
closeModal();
},
error: function(data) {
let msg = "erreur serveur lors de la création des product orders";
err = {msg: msg, ctx: 'save_supplier_product_association', data: orders_data};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
closeModal();
alert('Erreur lors de la création des commandes. Veuillez ré-essayer plus tard.');
}
});
return 0;
}
/**
* Get the PO attachment id.
* Display download button when fetch is succesful.
* The file might not be created soon enough, so try again after 10s if error server
*/
function get_order_attachments() {
if (product_orders.length > 0) {
let po_ids = product_orders.map(po => po.id);
$.ajax({
type: 'GET',
url: "/orders/get_orders_attachment",
data: {
'po_ids': po_ids
},
dataType:"json",
traditional: true,
contentType: "application/json; charset=utf-8",
success: function(data) {
for (let res_po of data.res) {
$(`#download_attachment_${res_po.id_po}`).attr('href', `${odoo_server}/web/content/${res_po.id_attachment}?download=true`);
}
$('#created_orders_area .download_order_file_loading').hide();
$('#created_orders_area .download_order_file_button').show();
},
error: function() {
$.notify(
"Échec de la récupération du lien de téléchargement des fichiers. Nouvelle tentative dans 10s.",
{
globalPosition:"top right",
className: "error"
}
);
setTimeout(get_order_attachments, 10000);
}
});
}
}
/* - DISPLAY */
function goto_main_screen(doc) {
order_doc = doc;
products = order_doc.products;
selected_suppliers = order_doc.selected_suppliers;
check_products_data()
.then(() => {
update_cdb_order();
update_main_screen();
switch_screen();
});
}
function back() {
reset_data();
update_order_selection_screen();
switch_screen('order_selection');
}
/**
* Create a string to represent a supplier column in product data
* @returns String
*/
function supplier_column_name(supplier) {
const supplier_id = ('supplier_id' in supplier) ? supplier.supplier_id : supplier.id;
return `qty_supplier_${supplier_id}`;
}
/**
* Display the selected suppliers
*/
function display_suppliers() {
let supplier_container = $("#suppliers_container");
$("#suppliers_container").empty();
$(".remove_supplier_icon").off();
for (let supplier of selected_suppliers) {
let template = $("#templates #supplier_pill_template");
template.find(".pill_supplier_name").text(supplier.display_name);
template.find(".supplier_pill").attr('id', `pill_supplier_${supplier.id}`);
template.find(".remove_supplier_icon").attr('id', `remove_supplier_${supplier.id}`);
supplier_container.append(template.html());
}
$(".remove_supplier_icon").on("click", function() {
const el_id = $(this).attr('id')
.split('_');
const supplier_id = el_id[el_id.length-1];
const clicked_supplier = selected_suppliers.find(s => s.id == supplier_id);
let modal_remove_supplier = $('#templates #modal_remove_supplier');
modal_remove_supplier.find(".supplier_name").text(clicked_supplier.display_name);
openModal(
modal_remove_supplier.html(),
() => {
if (is_time_to('validate_remove_supplier')) {
remove_supplier(supplier_id);
}
},
'Valider'
);
});
}
function _compute_product_data(product) {
let item = {};
/* Supplier related data */
let purchase_qty = 0; // Calculate product's total purchase qty
let p_package_qties = []; // Look for differences in package qties
for (let p_supplierinfo of product.suppliersinfo) {
// Preset qty for input if product related to supplier: existing qty or null (null -> qty to be set, display an empty input)
let supplier_qty = ("qty" in p_supplierinfo) ? p_supplierinfo.qty : null;
item[supplier_column_name(p_supplierinfo)] = supplier_qty;
// Update product's total qty to buy if qty set for this supplier
if (supplier_qty !== null) {
purchase_qty += +parseFloat(supplier_qty * p_supplierinfo.package_qty).toFixed(2);
}
// Store temporarily product package qties
p_package_qties.push(p_supplierinfo.package_qty);
}
item.purchase_qty = purchase_qty;
// If product not related to supplier, set qty for this supplier to false (false -> don't display any input)
for (supplier of selected_suppliers) {
if (!is_product_related_to_supplier(product, supplier)) {
item[supplier_column_name(supplier)] = false;
}
}
if (p_package_qties.length == 0 || !p_package_qties.every((val, i, arr) => val === arr[0])) {
// Don't display package qty if no supplierinf or if not all package qties are equals,
item.package_qty = 'X';
} else {
// If all package qties are equals, display it
item.package_qty = p_package_qties[0];
}
/* Coverage related data */
if (order_doc.coverage_days !== null) {
let qty_not_covered = 0;
let days_covered = 0;
if (product.daily_conso !== 0) {
qty_not_covered = product.daily_conso * order_doc.coverage_days - product.qty_available - product.incoming_qty - purchase_qty;
qty_not_covered = -Math.ceil(qty_not_covered); // round up, so if a value is not fully covered display it
qty_not_covered = (qty_not_covered > 0) ? 0 : qty_not_covered; // only display qty not covered (neg value)
days_covered = (product.qty_available + product.incoming_qty + purchase_qty) / product.daily_conso;
days_covered = Math.floor(days_covered);
}
item.qty_not_covered = qty_not_covered;
item.days_covered = days_covered;
} else {
item.qty_not_covered = 'X';
item.days_covered = 'X';
}
return item;
}
/**
* @param {array} product_ids if set, return formatted data for these products only
* @returns Array of formatted data for datatable data setup
*/
function prepare_datatable_data(product_ids = []) {
let data = [];
let products_to_format = [];
if (product_ids.length > 0) {
products_to_format = products.filter(p => product_ids.includes(p.id));
} else {
products_to_format = products;
}
for (product of products_to_format) {
const item = {
id: product.id,
name: product.name,
default_code: product.default_code,
incoming_qty: +parseFloat(product.incoming_qty).toFixed(3), // '+' removes unecessary zeroes at the end
qty_available: +parseFloat(product.qty_available).toFixed(3),
daily_conso: product.daily_conso,
purchase_ok: product.purchase_ok,
uom: product.uom_id[1],
stats: `Ecart type: ${product.sigma} / Jours sans vente: ${Math.round((product.vpc) * 100)}%`
};
const computed_data = _compute_product_data(product);
const full_item = { ...item, ...computed_data };
data.push(full_item);
}
return data;
}
/**
* @returns Array of formatted data for datatable columns setup
*/
function prepare_datatable_columns() {
let columns = [
{
data: "id",
title: `<div id="table_header_select_all" class="txtcenter">
<span class="select_all_text">Sélectionner</span>
<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">
</div>`,
className: "dt-body-center",
orderable: false,
render: function (data) {
return `<input type="checkbox" class="select_product_cb" id="select_product_${data}" value="${data}">`;
},
width: "4%"
},
{
data: "default_code",
title: "Ref",
width: "6%",
render: function (data) {
return (data === false) ? "" : data;
}
},
{
data: "name",
title: "Produit"
},
{
data: "qty_available",
title: "Stock",
className: "dt-body-center",
width: "4%"
},
{
data: "incoming_qty",
title: "Quantité entrante",
className: "dt-body-center",
width: "4%"
},
{
data: "daily_conso",
title: "Conso moy /jour",
render: function (data, type, full) {
return '<div class="help" title="' + full.stats+ '">' + data + '</div>';
},
className: "dt-body-center",
width: "6%"
}
];
for (const supplier of selected_suppliers) {
columns.push({
data: supplier_column_name(supplier),
title: supplier.display_name,
width: "10%",
className: `dt-body-center supplier_input_cell`,
render: (data, type, full) => {
const base_id = `product_${full.id}_supplier_${supplier.id}`;
if (data === false) {
return `<div id="${base_id}_cell_content" class="custom_cell_content">X</div>`;
} else {
let content = `<div id="${base_id}_cell_content" class="custom_cell_content">
<input type="number" class="product_qty_input" id="${base_id}_qty_input" min="0" value=${data}>`;
if (full.package_qty === 'X') {
let product_data = products.find(p => p.id == full.id);
if (product_data !== undefined) {
let supplierinfo = product_data.suppliersinfo.find(psi => psi.supplier_id == supplier.id);
content += `<span class="supplier_package_qty">Colisage : ${supplierinfo.package_qty}</span>`;
}
}
content += `</div>`;
return content;
}
}
});
}
columns.push({
data: "package_qty",
title: "Colisage",
className: "dt-body-center",
width: "4%"
});
columns.push({
data: "uom",
title: "UDM",
className: "dt-body-center",
width: "4%"
});
columns.push({
data: "purchase_qty",
title: "Qté Achat",
className: "dt-body-center",
width: "4%"
});
columns.push({
data: "qty_not_covered",
title: "Besoin non couvert (qté)",
className: "dt-body-center",
width: "4%"
});
columns.push({
data: "days_covered",
title: "Jours de couverture",
className: "dt-body-center",
width: "4%"
});
columns.push({
data: "purchase_ok",
title: `NPA`,
className: "dt-body-center",
orderable: false,
render: function (data) {
return `<input type="checkbox" class="product_npa_cb" value="purchase_ok" ${data ? '' : 'checked'}>`;
},
width: "4%"
});
return columns;
}
/**
* Display the Datatable containing the products
*/
function display_products(params) {
if (products.length == 0) {
$('.main').hide();
$('#main_content_footer').hide();
$('#do_inventory').hide();
return -1;
}
// Empty datatable if it already exists
if (products_table) {
products_table.clear().destroy();
$('#products_table').empty();
}
const data = prepare_datatable_data();
const columns = prepare_datatable_columns();
let sort_order_dir = "asc";
if (params != undefined && typeof params.sort_order_dir != "undefined") {
sort_order_dir = params.sort_order_dir;
}
products_table = $('#products_table').DataTable({
data: data,
columns: columns,
order: [
[
6, // Order by default by first supplier
sort_order_dir
]
],
stripeClasses: [], // Remove datatable cells coloring
orderClasses: false,
aLengthMenu: [
[
25,
50,
100,
200,
-1
],
[
25,
50,
100,
200,
"Tout"
]
],
iDisplayLength: -1,
scrollX: true,
language: {url : '/static/js/datatables/french.json'},
createdRow: function(row) {
for (const cell_node of row.cells) {
const cell = $(cell_node);
if (cell.hasClass("supplier_input_cell")) {
if (cell.text() == "X") {
cell.addClass('product_not_from_supplier');
}
}
}
}
});
$('.main').show();
$('#main_content_footer').show();
$('#do_inventory').show();
// On inputs change
$('#products_table').on('change', 'tbody td .product_qty_input', function () {
let val = ($(this).val() == '') ? 0 : $(this).val();
val = parseFloat(val);
// If value is a number
if (!isNaN(val)) {
const id_split = $(this).attr('id')
.split('_');
const prod_id = id_split[1];
const supplier_id = id_split[3];
// Save value
save_product_supplier_qty(prod_id, supplier_id, val);
// Update row
const product = products.find(p => p.id == prod_id);
const new_row_data = prepare_datatable_data([product.id])[0];
products_table.row($(this).closest('tr')).data(new_row_data)
.draw();
update_cdb_order();
display_total_values();
} else {
$(this).val('');
}
});
// Associate product to supplier on click on 'X' in the table
$('#products_table').on('click', 'tbody .product_not_from_supplier', function () {
// Get supplier & product id
const el_id = $(this).children()
.first()
.attr('id')
.split('_');
const product_id = el_id[1];
const supplier_id = el_id[3];
const product = products.find(p => p.id == product_id);
const supplier = selected_suppliers.find(s => s.id == supplier_id);
let modal_attach_product_to_supplier = $('#templates #modal_attach_product_to_supplier');
modal_attach_product_to_supplier.find(".product_name").text(product.name);
modal_attach_product_to_supplier.find(".supplier_name").text(supplier.display_name);
openModal(
modal_attach_product_to_supplier.html(),
() => {
if (is_time_to('validate_save_supplier_product_association')) {
save_supplier_product_association(product, supplier, this);
}
},
'Valider',
false
);
// Find existing price in another supplierinfo
let default_price = null;
let default_package_qty = 1; // Default package qty is 1
for (let psi of product.suppliersinfo) {
if ('price' in psi && psi.price !== null) {
default_price = psi.price;
}
if ('package_qty' in psi && psi.package_qty !== null) {
default_package_qty = psi.package_qty;
}
}
// Set default value for price & package qty for new supplierinfo
$(".new_product_supplier_package_pty").val(default_package_qty);
$(".new_product_supplier_price").val(default_price); // Default price is existing price for other supplier, or none if no other
new_product_supplier_association = {
package_qty: default_package_qty,
price: default_price
};
$('.new_product_supplier_price').on('input', function () {
new_product_supplier_association.price = $(this).val();
});
$('.new_product_supplier_package_pty').on('input', function () {
new_product_supplier_association.package_qty = $(this).val();
});
});
// Select row(s) on checkbox change
$(products_table.table().header()).on('click', 'th #select_all_products_cb', function () {
if (this.checked) {
selected_rows = [];
products_table.rows().every(function() {
const node = $(this.node());
node.addClass('selected');
node.find(".select_product_cb").first()
.prop("checked", true);
// Save selected rows in case the table is updated
selected_rows.push(this.data().id);
return 0;
});
} else {
unselect_all_rows();
}
});
$('#products_table').on('click', 'tbody td .select_product_cb', function () {
$(this).closest('tr')
.toggleClass('selected');
// Save / unsave selected row
const p_id = products_table.row($(this).closest('tr')).data().id;
if (this.checked) {
selected_rows.push(p_id);
} else {
const i = selected_rows.findIndex(id => id == p_id);
selected_rows.splice(i, 1);
}
});
// 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;
}
/**
* Unselect all rows from datatable.
*/
function unselect_all_rows() {
products_table.rows().every(function() {
const node = $(this.node());
node.removeClass('selected');
node.find(".select_product_cb").first()
.prop("checked", false);
return 0;
});
selected_rows = [];
}
/**
* Display the total values for each supplier & the global total value
*/
function display_total_values() {
_compute_total_values_by_supplier();
let order_total_value = 0;
for (let supplier of selected_suppliers) {
$(`#pill_supplier_${supplier.id}`).find('.supplier_total_value')
.text(parseFloat(supplier.total_value).toFixed(2));
order_total_value += supplier.total_value;
}
order_total_value = parseFloat(order_total_value).toFixed(2);
$('#order_total_value').text(order_total_value);
}
/**
* Update DOM display on main screen
*/
function update_main_screen(params) {
// Remove listener before recreating them
$('#products_table').off('change', 'tbody td .product_qty_input');
$('#products_table').off('click', 'tbody .product_not_from_supplier');
$('#products_table').off('click', 'thead th #select_all_products_cb');
$('#products_table').off('click', 'tbody td .select_product_cb');
$(".remove_supplier_icon").off();
$(".order_name_container").text(order_doc._id);
display_suppliers();
display_products(params);
display_total_values();
// Re-select previously selected rows
if (selected_rows.length > 0) {
products_table.rows().every(function() {
if (selected_rows.includes(this.data().id)) {
const node = $(this.node());
node.addClass('selected');
node.find(".select_product_cb").first()
.prop("checked", true);
}
return 0;
});
}
$("#select_all_products_cb").prop("checked", false);
if (order_doc.coverage_days !== null) {
$("#coverage_days_input").val(order_doc.coverage_days);
} else {
$("#coverage_days_input").val('');
}
}
/**
* Update DOM display on the order selection screen
*/
function update_order_selection_screen() {
dbc.allDocs({
include_docs: true
}).then(function (result) {
// Remove listener before recreating them
$(".order_pill").off();
let existing_orders_container = $("#existing_orders");
existing_orders_container.empty();
$('#new_order_name').val('');
if (result.rows.length === 0) {
existing_orders_container.append(`<i>Aucune commande en cours...</i>`);
} else {
for (let row of result.rows) {
let template = $("#templates #order_pill_template");
template.find(".pill_order_name").text(row.id);
existing_orders_container.append(template.html());
}
$(".order_pill").on("click", order_pill_on_click);
}
})
.catch(function (err) {
alert('Erreur lors de la synchronisation des commandes. Vous pouvez créer une nouvelle commande.');
console.log(err);
});
}
/**
* Switch between screens
* @param {String} direction target screen : order_selection | main_screen | orders_created
* @param {String} from source screen : order_selection | main_screen | orders_created
*/
function switch_screen(direction = 'main_screen', from = 'main_screen') {
if (direction === 'orders_created') {
$('#main_content').hide();
$('#orders_created').show();
} else {
// Animated transition
let oldBox = null;
let newBox = null;
let outerWidth = null;
if (direction === 'main_screen') {
oldBox = $("#select_order_content");
newBox = $("#main_content");
outerWidth = oldBox.outerWidth(true);
} else {
if (from === 'orders_created') {
oldBox = $("#orders_created");
} else {
oldBox = $("#main_content");
}
newBox = $("#select_order_content");
outerWidth = - oldBox.outerWidth(true);
}
// Display the new box and place it on the right of the screen
newBox.css({ "left": outerWidth + "px", "right": -outerWidth + "px", "display": "" });
// Make the old content slide to the left
oldBox.animate({ "left": -outerWidth + "px", "right": outerWidth + "px" }, 800, function() {
// Hide old content after animation
oldBox.css({ "left": "", "right": "", "display": "none" });
});
// Slide new box to regular place
newBox.animate({ "left": "", "right": "" }, 800);
}
}
/**
* Init the PouchDB local database & sync
*/
function init_pouchdb_sync() {
dbc = new PouchDB(couchdb_dbname);
sync = PouchDB.sync(couchdb_dbname, couchdb_server, {
live: true,
retry: true,
auto_compaction: true,
revs_limit: 1
});
sync.on('change', function (info) {
if (info.direction === "pull") {
for (const doc of info.change.docs) {
if (order_doc._id === doc._id && (order_doc._rev !== doc._rev || doc._deleted === true)) {
// If current order was modified somewhere else
$.notify(
"Un autre navigateur est en train de modifier cette commande !",
{
globalPosition:"top right",
className: "error"
}
);
back();
break;
}
}
update_order_selection_screen();
}
}).on('error', function (err) {
if (err.status === 409) {
alert("Une erreur de synchronisation s'est produite, la commande a sûrement été modifiée sur un autre navigateur. Vous allez être redirigé.e.");
back();
}
console.log('erreur sync');
console.log(err);
});
}
$(document).ready(function() {
if (coop_is_connected()) {
$('#new_order_form').show();
$('#existing_orders_area').show();
fingerprint = new Fingerprint({canvas: true}).get();
$.ajaxSetup({ headers: { "X-CSRFToken": getCookie('csrftoken') } });
openModal();
init_pouchdb_sync();
// Main screen
$("#coverage_form").on("submit", function(e) {
e.preventDefault();
if (is_time_to('submit_coverage_form', 1000)) {
let val = $("#coverage_days_input").val();
val = parseInt(val);
if (!isNaN(val)) {
order_doc.coverage_days = val;
compute_products_coverage_qties();
update_cdb_order();
update_main_screen();
} else {
$("#coverage_days_input").val(order_doc.coverage_days);
alert(`Valeur non valide pour le nombre de jours de couverture !`);
}
}
});
$("#supplier_form").on("submit", function(e) {
e.preventDefault();
if (is_time_to('add_product', 1000)) {
add_supplier();
}
});
$("#product_form").on("submit", function(e) {
e.preventDefault();
if (is_time_to('add_product', 1000)) {
add_product();
}
});
$("#do_inventory").on("click", function() {
if (is_time_to('generate_inventory', 1000)) {
generate_inventory();
}
});
$('#back_to_order_selection_from_main').on('click', function() {
if (is_time_to('back_to_order_selection_from_main', 1000)) {
back();
}
});
$('#create_orders').on('click', function() {
if (is_time_to('create_orders', 1000)) {
let modal_create_order = $('#templates #modal_create_order');
modal_create_order.find('.suppliers_date_planned_area').empty();
for (let supplier of selected_suppliers) {
let supplier_date_planned_template = $('#templates #modal_create_order__supplier_date_planned');
supplier_date_planned_template.find(".supplier_name").text(supplier.display_name);
supplier_date_planned_template.find(".modal_input_container").attr('id', `container_date_planned_supplier_${supplier.id}`);
modal_create_order.find('.suppliers_date_planned_area').append(supplier_date_planned_template.html());
}
openModal(
modal_create_order.html(),
() => {
if (is_time_to('validate_create_orders')) {
create_orders();
}
},
'Valider',
false
);
// Add id to input once modal is displayed
for (let supplier of selected_suppliers) {
$(`#modal #container_date_planned_supplier_${supplier.id}`).find(".supplier_date_planned")
.attr('id', `date_planned_supplier_${supplier.id}`);
}
$("#modal .supplier_date_planned")
.datepicker({
defaultDate: "+1d",
minDate: new Date()
})
.on('change', function() {
try {
// When date input changes, try to read date
$.datepicker.parseDate(date_format, $(this).val());
} catch {
alert('Date invalide');
$(this).val('');
}
});
}
return 0;
});
$.datepicker.regional['fr'] = {
monthNames: [
'Janvier',
'Fevrier',
'Mars',
'Avril',
'Mai',
'Juin',
'Juillet',
'Aout',
'Septembre',
'Octobre',
'Novembre',
'Decembre'
],
dayNamesMin: [
'Di',
'Lu',
'Ma',
'Me',
'Je',
'Ve',
'Sa'
],
dateFormat: date_format
};
$.datepicker.setDefaults($.datepicker.regional['fr']);
// Order selection screen
update_order_selection_screen();
$("#new_order_form").on("submit", function(e) {
e.preventDefault();
if (is_time_to('submit_new_order_form', 1000)) {
create_cdb_order();
}
});
// Orders created screen
$('#back_to_order_selection_from_orders_created').on('click', function() {
if (is_time_to('back_to_order_selection_from_orders_created', 1000)) {
switch_screen('order_selection', 'orders_created');
}
});
// Get suppliers
$.ajax({
type: 'GET',
url: "/orders/get_suppliers",
dataType:"json",
traditional: true,
contentType: "application/json; charset=utf-8",
success: function(data) {
suppliers_list = data.res;
// Set up autocomplete on supplier input
$("#supplier_input").autocomplete({
source: suppliers_list.map(a => a.display_name)
});
},
error: function(data) {
err = {msg: "erreur serveur lors de la récupération des fournisseurs", ctx: 'get_suppliers'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
closeModal();
alert('Erreur lors de la récupération des fournisseurs, rechargez la page plus tard');
}
});
//Get products
var accentMap = {
"á": "a",
"à": "a",
"â": "a",
"é": "e",
"è": "e",
"ê": "e",
"ë": "e",
"ç": "c",
"ù": "u",
"ü": "u",
"ö": "o"
};
var normalize = function(term) {
var ret = "";
for (var i = 0; i < term.length; i++) {
ret += accentMap[ term.charAt(i) ] || term.charAt(i);
}
return ret;
};
$.ajax({
type: 'GET',
url: "/products/simple_list",
dataType:"json",
traditional: true,
contentType: "application/json; charset=utf-8",
success: function(data) {
products_list = data.list;
// Set up autocomplete on product input
$("#product_input").autocomplete({
source: function(request, response) {
var matcher = new RegExp($.ui.autocomplete.escapeRegex(request.term), "i");
response($.grep(products_list.map(a => a.display_name), function(value) {
value = value.label || value.value || value;
return matcher.test(value) || matcher.test(normalize(value));
}));
},
position: {collision: "flip" }
});
closeModal();
},
error: function(data) {
err = {msg: "erreur serveur lors de la récupération des articles", ctx: 'get_products'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
closeModal();
alert('Erreur lors de la récupération des articles, rechargez la page plus tard');
}
});
} else {
$('#not_connected_content').show();
}
});
...@@ -8,5 +8,11 @@ urlpatterns = [ ...@@ -8,5 +8,11 @@ urlpatterns = [
url(r'^export/([0-9]+)', views.export_one), url(r'^export/([0-9]+)', views.export_one),
url(r'^export/([a-z]+)', views.export_regex), url(r'^export/([a-z]+)', views.export_regex),
url(r'^get_pdf_labels$', views.get_pdf_labels), url(r'^get_pdf_labels$', views.get_pdf_labels),
url(r'^print_product_labels$', views.print_product_labels) url(r'^print_product_labels$', views.print_product_labels),
url(r'^helper$', views.helper),
url(r'^get_suppliers$', views.get_suppliers),
url(r'^get_supplier_products$', views.get_supplier_products),
url(r'^associate_supplier_to_product$', views.associate_supplier_to_product),
url(r'^create_orders$', views.create_orders),
url(r'^get_orders_attachment$', views.get_orders_attachment),
] ]
from outils.common_imports import * from outils.common_imports import *
from outils.for_view_imports import * from outils.for_view_imports import *
from orders.models import Order, Orders from orders.models import Order, Orders, CagetteSuppliers
from products.models import CagetteProduct from products.models import CagetteProduct, CagetteProducts
from openpyxl import Workbook from openpyxl import Workbook
from openpyxl.writer.excel import save_virtual_workbook from openpyxl.writer.excel import save_virtual_workbook
import datetime
def as_text(value): return str(value) if value is not None else "" def as_text(value): return str(value) if value is not None else ""
def index(request): def index(request):
return HttpResponse('Orders') return HttpResponse('Orders')
def helper(request):
context = {
'title': 'Aide à la commande',
'couchdb_server': settings.COUCHDB['url'],
'db': settings.COUCHDB['dbs']['orders'],
'odoo_server': settings.ODOO['url']
}
template = loader.get_template('orders/helper.html')
return HttpResponse(template.render(context, request))
def get_suppliers(request):
""" Get suppliers list """
res = {}
try:
res = CagetteSuppliers.get_suppliers()
except Exception as e:
res["error"] = str(e)
return JsonResponse(res, status=500)
return JsonResponse({'res': res})
def get_supplier_products(request):
""" Get supplier products """
suppliers_id = request.GET.getlist('sids', '')
res = CagetteProducts.get_products_for_order_helper(suppliers_id)
if 'error' in res:
return JsonResponse(res, status=500)
else:
return JsonResponse({'res': res})
def associate_supplier_to_product(request):
""" This product is now supplied by this supplier """
res = {}
try:
data = json.loads(request.body.decode())
res = CagetteProduct.associate_supplier_to_product(data)
except Exception as e:
res["error"] = str(e)
return JsonResponse(res, status=500)
return JsonResponse({'res': res})
def create_orders(request):
""" Create products orders """
res = { "created": [] }
try:
data = json.loads(request.body.decode())
# suppliers id are keys in request data
for supplier_id in data["suppliers_data"].keys():
supplier_data = data["suppliers_data"][supplier_id]
res_created = Order.create(
supplier_id,
supplier_data["date_planned"],
supplier_data["lines"]
)
res["created"].append(res_created)
except Exception as e:
res["error"] = str(e)
return JsonResponse(res, status=500)
return JsonResponse({'res': res})
def get_orders_attachment(request):
""" Get order attachment: order file created after PO is finalized """
res = []
po_ids = request.GET.getlist('po_ids')
for id_po in po_ids:
m = Order(int(id_po))
attachment = m.get_order_attachment_id()
if 'error' in attachment:
res.append(attachment)
else:
res.append({
'id_po': id_po,
'id_attachment': attachment["id"]
})
for item in res:
if 'error' in item:
return JsonResponse(res, status=500)
return JsonResponse({'res': res})
def export_one(request, oid): def export_one(request, oid):
msg = '' msg = ''
try: try:
...@@ -20,7 +115,6 @@ def export_one(request, oid): ...@@ -20,7 +115,6 @@ def export_one(request, oid):
order = Order(oid) order = Order(oid)
order_data = order.export() order_data = order.export()
if ('success' in order_data) and (order_data['success'] is True): if ('success' in order_data) and (order_data['success'] is True):
import datetime
now = datetime.datetime.now() now = datetime.datetime.now()
taxes = 0 taxes = 0
company_name = '' company_name = ''
......
...@@ -228,6 +228,22 @@ ...@@ -228,6 +228,22 @@
- ENTRANCE_SERVICE_BTN = "…faire <b>mon service 🤝" - ENTRANCE_SERVICE_BTN = "…faire <b>mon service 🤝"
- ENTRANCE_EXTRA_BUTTONS_DISPLAY = False (no button is shown above shift coop. list) (True if not set)
- ENTRANCE_EASY_SHIFT_VALIDATE = False (default value)
When set to True allow coop to identify and have 1 point (only if FTOP)
- ENTRANCE_ADD_PT_EVENT_NAME = 'Add 1 point name throught easy validate' (default : 'Validation service comité'')
- ENTRANCE_MISSED_SHIFT_BEGIN_MSG (default : "")
This message is dispayed when time to register is last
- ENTRANCE_EASY_SHIFT_VALIDATE_MSG (default = 'Je valide mon service "Comité"')
(makes sens if ENTRANCE_EASY_SHIFT_VALIDATE is True)
### Member space ### Member space
- EM_URL = '' - EM_URL = ''
......
...@@ -99,6 +99,7 @@ STATICFILES_DIRS = ( ...@@ -99,6 +99,7 @@ STATICFILES_DIRS = (
"website/static", "website/static",
"shop/static", "shop/static",
"shelfs/static", "shelfs/static",
"orders/static",
# "tests/static" # "tests/static"
) )
......
...@@ -19,7 +19,9 @@ COUCHDB = { ...@@ -19,7 +19,9 @@ COUCHDB = {
'member': 'coops', 'member': 'coops',
'inventory': 'inventory', 'inventory': 'inventory',
'envelops': 'envelop', 'envelops': 'envelop',
'shop': 'shopping_carts' 'shop': 'shopping_carts',
'orders': 'orders_test',
'reception': 'reception_test',
} }
} }
......
...@@ -59,10 +59,9 @@ function post_form(url, data, callback) { ...@@ -59,10 +59,9 @@ function post_form(url, data, callback) {
$.post(ajax_params) $.post(ajax_params)
.done(function(rData) { .done(function(rData) {
callback(null, rData); callback(null, rData);
}) })
.fail(function(err) { .fail(function(err) {
console.log(err); callback(err, {});
}); });
} }
......
...@@ -6,6 +6,7 @@ from outils.common import OdooAPI ...@@ -6,6 +6,7 @@ from outils.common import OdooAPI
import csv import csv
import tempfile import tempfile
import pymysql.cursors import pymysql.cursors
import datetime
vcats = [] vcats = []
...@@ -126,10 +127,66 @@ class CagetteProduct(models.Model): ...@@ -126,10 +127,66 @@ class CagetteProduct(models.Model):
res = api.create('product.supplier.shortage', f) res = api.create('product.supplier.shortage', f)
return res return res
@staticmethod
def associate_supplier_to_product(data):
api = OdooAPI()
product_tmpl_id = data["product_tmpl_id"]
partner_id = data["supplier_id"]
package_qty = data["package_qty"]
price = data["price"]
f = ["id", "standard_price", "purchase_ok"]
c = [['product_tmpl_id', '=', product_tmpl_id]]
res_products = api.search_read('product.product', c, f)
product = res_products[0]
f = {
'product_tmpl_id' : product_tmpl_id,
'product_id' : product["id"],
'name' : partner_id,
'product_purchase_ok': product["purchase_ok"],
'price': price,
'base_price': price,
'package_qty': package_qty,
'sequence': 1000 # lowest priority for the new suppliers
}
res = api.create('product.supplierinfo', f)
return res
@staticmethod
def update_product_purchase_ok(product_tmpl_id, purchase_ok):
api = OdooAPI()
res = {}
f = {
'purchase_ok': purchase_ok
}
try:
res["update"] = api.update('product.template', product_tmpl_id, f)
except Exception as e:
res["error"] = str(e)
print(str(e))
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():
res = []
api = OdooAPI()
try:
res = api.execute('lacagette.products', 'get_simple_list', {'only_purchase_ok': True})
except Exception as e:
coop_logger.error("Odoo api products simple list : %s", str(e))
return res
@staticmethod
def __unique_product_list(plist): def __unique_product_list(plist):
# 276 # 276
upl = {} upl = {}
...@@ -387,6 +444,99 @@ class CagetteProducts(models.Model): ...@@ -387,6 +444,99 @@ class CagetteProducts(models.Model):
bc_map[bc] = bc bc_map[bc] = bc
return bc_map return bc_map
@staticmethod
def get_template_products_sales_average(params={}):
api = OdooAPI()
res = {}
try:
res = api.execute('lacagette.products', 'get_template_products_sales_average', params)
except Exception as e:
coop_logger.error('get_template_products_sales_average %s (%s)', str(e), str(params))
res["error"] = str(e)
return res
@staticmethod
def get_products_for_order_helper(supplier_ids, pids = []):
"""
One of the two parameters must be not empty.
Get products by supplier if one or more supplier_id is set.
If supplier_ids is empty, get products specified in pids. In this case, suppliers info won't be fetched.
"""
api = OdooAPI()
res = {}
# todo : try with no result
try:
today = datetime.date.today().strftime("%Y-%m-%d")
if supplier_ids is not None and len(supplier_ids) > 0:
# Get products/supplier relation
f = ["product_tmpl_id", 'date_start', 'date_end', 'package_qty', 'price', 'name']
c = [['name', 'in', [ int(x) for x in supplier_ids]]]
psi = api.search_read('product.supplierinfo', c, f)
# Filter valid data
ptids = []
for p in psi:
if (p["product_tmpl_id"] is not False
and (p["date_start"] is False or p["date_end"] is not False and p["date_start"] <= today)
and (p["date_end"] is False or p["date_end"] is not False and p["date_end"] >= today)):
ptids.append(p["product_tmpl_id"][0])
else:
ptids = [ int(x) for x in pids ]
# Get products templates
f = [
"id",
"state",
"name",
"default_code",
"qty_available",
"incoming_qty",
"uom_id",
"purchase_ok",
"supplier_taxes_id",
"product_variant_ids"
]
c = [['id', 'in', ptids], ['purchase_ok', '=', True]]
products_t = api.search_read('product.template', c, f)
filtered_products_t = [p for p in products_t if p["state"] != "end" and p["state"] != "obsolete"]
sales_average_params = {
'ids': ptids,
# 'from': '2019-04-10',
# 'to': '2019-08-10',
}
sales = CagetteProducts.get_template_products_sales_average(sales_average_params)
if 'list' in sales and len(sales['list']) > 0:
sales = sales['list']
else:
sales = []
# Add supplier data to product data
for i, fp in enumerate(filtered_products_t):
if supplier_ids is not None and len(supplier_ids) > 0:
psi_item = next(item for item in psi if item["product_tmpl_id"] is not False and item["product_tmpl_id"][0] == fp["id"])
filtered_products_t[i]['suppliersinfo'] = [{
'supplier_id': int(psi_item["name"][0]),
'package_qty': psi_item["package_qty"],
'price': psi_item["price"]
}]
for s in sales:
if s["id"] == fp["id"]:
filtered_products_t[i]['daily_conso'] = s["average_qty"]
filtered_products_t[i]['sigma'] = s["sigma"]
filtered_products_t[i]['vpc'] = s["vpc"]
res["products"] = filtered_products_t
except Exception as e:
coop_logger.error('get_products_for_order_helper %s (%s)', str(e))
res["error"] = str(e)
return res
class OFF(models.Model): class OFF(models.Model):
"""OpenFoodFact restricted DB queries.""" """OpenFoodFact restricted DB queries."""
......
...@@ -37,6 +37,7 @@ IFCBarcodes = { ...@@ -37,6 +37,7 @@ IFCBarcodes = {
try { try {
let price = parseFloat(value); let price = parseFloat(value);
if (currency == 'FF') if (currency == 'FF')
price = price / 6.55957; price = price / 6.55957;
...@@ -131,6 +132,7 @@ IFCBarcodes = { ...@@ -131,6 +132,7 @@ IFCBarcodes = {
if (product_data !== null) { if (product_data !== null) {
p_uom = (this.uoms)[product_data[this.keys.uom_id]]; p_uom = (this.uoms)[product_data[this.keys.uom_id]];
let qty = 1; let qty = 1;
if (encoded_value.length > 0 && !isNaN(encoded_value)) { if (encoded_value.length > 0 && !isNaN(encoded_value)) {
qty = 0; //if no rule is found it will advise user that there is a problem qty = 0; //if no rule is found it will advise user that there is a problem
/* /*
...@@ -149,7 +151,8 @@ IFCBarcodes = { ...@@ -149,7 +151,8 @@ IFCBarcodes = {
} else { } else {
let list_price = product_data[this.keys.list_price]; let list_price = product_data[this.keys.list_price];
let currency = null; let currency = null;
if (pattern_type == 'FF_price_to_weight') currency = 'FF'
if (pattern_type == 'FF_price_to_weight') currency = 'FF';
qty = parseFloat(this.get_quantity_eq_to_encoded_price(encoded_value, list_price, currency)); qty = parseFloat(this.get_quantity_eq_to_encoded_price(encoded_value, list_price, currency));
} }
......
...@@ -4,9 +4,12 @@ from . import views ...@@ -4,9 +4,12 @@ from . import views
urlpatterns = [ urlpatterns = [
url(r'^$', views.home), url(r'^$', views.home),
url(r'^simple_list$', views.get_simple_list),
url(r'^get_product_for_order_helper$', views.get_product_for_order_helper),
url(r'^get_product_data$', views.get_product_data), url(r'^get_product_data$', views.get_product_data),
url(r'^get_products_stdprices$', views.get_products_stdprices), url(r'^get_products_stdprices$', views.get_products_stdprices),
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'^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
......
...@@ -26,6 +26,32 @@ def home(request): ...@@ -26,6 +26,32 @@ def home(request):
return HttpResponse(template.render(context, request)) return HttpResponse(template.render(context, request))
def get_simple_list(request):
res = {}
try:
res = CagetteProducts.get_simple_list()
except Exception as e:
coop_logger.error("Get products simple list : %s", str(e))
res['error'] = str(e)
if ('error' in res):
return JsonResponse(res, status=500)
else:
return JsonResponse(res, safe=False)
def get_product_for_order_helper(request):
res = {}
try:
pids = json.loads(request.body.decode())
res = CagetteProducts.get_products_for_order_helper(None, pids)
except Exception as e:
coop_logger.error("get_product_for_help_order_line : %s", str(e))
res['error'] = str(e)
if ('error' in res):
return JsonResponse(res, status=500)
else:
return JsonResponse(res, safe=False)
def get_product_data(request): def get_product_data(request):
barcode = request.GET['barcode'] barcode = request.GET['barcode']
res = CagetteProduct.get_product_from_barcode(barcode) res = CagetteProduct.get_product_from_barcode(barcode)
...@@ -75,6 +101,16 @@ def update_product_stock(request): ...@@ -75,6 +101,16 @@ def update_product_stock(request):
return JsonResponse({"res": res}) return JsonResponse({"res": res})
def update_product_purchase_ok(request):
res = {}
data = json.loads(request.body.decode())
res = CagetteProduct.update_product_purchase_ok(data["product_tmpl_id"], data["purchase_ok"])
if ('error' in res):
return JsonResponse(res, status=500)
else:
return JsonResponse({"res": res})
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."""
......
...@@ -29,14 +29,16 @@ class CagetteReception(models.Model): ...@@ -29,14 +29,16 @@ class CagetteReception(models.Model):
if res and len(res) > 0: if res and len(res) > 0:
for r in res: for r in res:
pids.append(int(r['purchase_id'][0])) pids.append(int(r['purchase_id'][0]))
if len(pids): if len(pids):
f=["id","name","date_order", "partner_id", "date_planned", "amount_untaxed", "amount_total", "x_reception_status"] f=["id","name","date_order", "partner_id", "date_planned", "amount_untaxed", "amount_total", "x_reception_status"]
# Only get orders that need to be treated in Reception # Only get orders that need to be treated in Reception
c=[['id', 'in', pids], ["x_reception_status", "in", [False, 'qty_valid']]] c = [['id', 'in', pids], ["x_reception_status", "in", [False, 'qty_valid', 'valid_pending', 'br_valid']]]
orders = api.search_read('purchase.order', c, f)
orders = api.search_read('purchase.order', c, f)
except Exception as e: except Exception as e:
print (str(e)) print(str(e))
return orders return orders
def get_order_unprocessable_products(id_po): def get_order_unprocessable_products(id_po):
...@@ -61,7 +63,7 @@ class CagetteReception(models.Model): ...@@ -61,7 +63,7 @@ class CagetteReception(models.Model):
"""Update purchase.order.line with qty data and/or price""" """Update purchase.order.line with qty data and/or price"""
result = None result = None
try: try:
f={} f = {}
if updateType == "qty_valid" or updateType == "br_valid": if updateType == "qty_valid" or updateType == "br_valid":
f['package_qty'] = pakageQty f['package_qty'] = pakageQty
...@@ -74,7 +76,8 @@ class CagetteReception(models.Model): ...@@ -74,7 +76,8 @@ class CagetteReception(models.Model):
res = self.o_api.update('purchase.order.line', idOnLine, f) res = self.o_api.update('purchase.order.line', idOnLine, f)
result = True result = True
except: except Exception as e:
coop_logger.error("update_line : %s (fields values = %s, line_id = %s)",str(e), str(f), str(idOnLine))
result = False result = False
return result return result
......
...@@ -11,6 +11,10 @@ input[type="number"] { ...@@ -11,6 +11,10 @@ input[type="number"] {
} }
/* INDEX */ /* INDEX */
.group_line {
margin-bottom: 5px;
}
#orders tbody tr { #orders tbody tr {
cursor: pointer; cursor: pointer;
} }
...@@ -31,6 +35,15 @@ input[type="number"] { ...@@ -31,6 +35,15 @@ input[type="number"] {
margin-bottom: 1em; margin-bottom: 1em;
} }
.order_last_update {
font-weight: bold;
}
.order_modified_msg {
font-size: 2rem;
color: #e62720;
}
/* PRODUITS */ /* PRODUITS */
.page_body { .page_body {
height: 100%; height: 100%;
......
var order = { var orders = [],
order = {
'id' : null 'id' : null
}, },
table_orders = null, table_orders = null,
callback_update = false, callback_update = false,
callback_report = false, callback_report = false,
selection_type = null, selection_type = null,
saved_groups = []; order_groups = {
_id: 'grouped_orders',
groups: []
},
dbc = null,
sync = null,
fingerprint = null;
/* UTILS */ /* UTILS */
// Wait for both ajax callbacks for reloading to avoid a js error /**
// -> reloading when ajax call not answered causes a popup to appear, can be confusing * Difference between two dates
function reload() { * @param {Date} date1
if (callback_update && callback_report) * @param {Date} date2
document.location.reload(); * @returns difference object
} */
function dates_diff(date1, date2) {
var diff = {};
var tmp = date2 - date1;
function goto(id) { tmp = Math.floor(tmp/1000);
document.location.href = "produits/" + id; diff.sec = tmp % 60;
}
/* tmp = Math.floor((tmp-diff.sec)/60);
* Go to Products page for an existing group diff.min = tmp % 60;
* params :
* i : index of group in 'saved_groups' array
*/
function group_goto(i) {
// Make sure all group's orders are saved in local storage
for (j in saved_groups[i]) {
set_local_storage(saved_groups[i][j]);
}
// go to one of group's order Products page tmp = Math.floor((tmp-diff.min)/60);
goto(saved_groups[i][0].id); diff.hours = tmp % 24;
}
/* tmp = Math.floor((tmp-diff.hours)/24);
* Set local storage for given order diff.days = tmp;
*/
function set_local_storage(order_data) {
if (Modernizr.localstorage) {
var stored_order = JSON.parse(localStorage.getItem('order_' + order_data.id));
// Set local storage if key doesn't exist return diff;
if (stored_order == null) {
localStorage.setItem("order_" + order_data.id, JSON.stringify(order_data));
}
}
} }
/* /**
* Remove from local storage orders that have a wrong status * Wait for both ajax callbacks for reloading to avoid a js error
* (-> order has been updated elsewhere) * -> reloading when ajax call not answered causes a popup to appear, which can be confusing
*/ */
function clean_local_storage() { function reload() {
var stored_order = null; if (callback_update && callback_report)
document.location.reload();
// Loop through local storage }
for (key of Object.keys(localStorage)) {
if (key.startsWith('order_')) {
stored_order = JSON.parse(localStorage.getItem(key));
// Loop through orders in table to find match
var i = 0;
var found = false;
while (i < table_orders.rows().data().length && !found) { /**
var uptodate_order = table_orders.rows(i).data()[0]; * Check for concurent access to same order before going to reception page.
* @param {Int} id
*/
function check_before_goto(id) {
const order_doc_id = 'order_' + id;
// If status in local storage is wrong dbc.get(order_doc_id).then((doc) => {
if (stored_order.id == uptodate_order.id if (doc.last_update.fingerprint !== null && doc.last_update.fingerprint !== fingerprint) {
&& stored_order.reception_status != uptodate_order.reception_status) { time_diff = dates_diff(new Date(doc.last_update.timestamp), new Date());
diff_str = ``;
// Remove from local storage if (time_diff.days !== 0) {
localStorage.removeItem("order_" + uptodate_order.id); diff_str += `${time_diff.days} jour(s), `;
}
if (time_diff.hours !== 0) {
diff_str += `${time_diff.hours} heure(s), `;
}
if (time_diff.min !== 0) {
diff_str += `${time_diff.min} min, `;
}
diff_str += `${time_diff.sec}s`;
// Evolution: warn user (order modified elsewhere, local data has been deleted) let modal_order_access = $('#templates #modal_order_access');
found = true;
}
i++; modal_order_access.find(".order_last_update").text(diff_str);
}
if (!found) { openModal(
// Remove too if order isn't in server data modal_order_access.html(),
localStorage.removeItem("order_" + stored_order.id); () => {
} goto(id);
},
'Valider'
);
} else {
goto(id);
} }
} })
.catch((err) => {
console.log(err);
});
} }
function create_groups_from_server_data() { function goto(id) {
// Get array of stored grouped orders document.location.href = "produits/" + id;
var grouped_orders = JSON.parse(localStorage.getItem('grouped_orders')); }
// Create if not exists /**
if (grouped_orders == null) { * Go to Products page for an existing group
grouped_orders = []; * @param {int} group_index index of group in groups array
} else { */
// Remove from server data groups already in local storage function group_goto(group_index) {
for (stored_group of grouped_orders) { // Make sure a couchdb document exists for all group's orders
for (sg_order_item of stored_group) { for (let i in order_groups.groups[group_index]) {
for (i in server_stored_groups) { let order_data = null;
if (server_stored_groups[i].includes(sg_order_item)) {
server_stored_groups.splice(i, 1); // Find order data
break; for (let order of orders) {
} if (order.id == order_groups.groups[group_index][i]) {
} order_data = order;
} }
} }
create_order_doc(order_data);
} }
// Add server groups to stored groups // go to first order
grouped_orders = grouped_orders.concat(server_stored_groups); check_before_goto(order_groups.groups[group_index][0]);
localStorage.setItem('grouped_orders', JSON.stringify(grouped_orders));
} }
/* /**
* If there are groups in local storage, extract them from the table, set the groups actions. * Create a couchdb document for an order if it doesn't exist
* @param {Object} order_data
* @param {Boolean} goto if true, go to order page
*/ */
function extract_grouped_orders() { function create_order_doc(order_data, go_to_order = false) {
var saved_grouped_orders = JSON.parse(localStorage.getItem('grouped_orders')); const order_doc_id = 'order_' + order_data.id;
var groups_to_delete = []; // indexes
// if there are grouped orders
if (saved_grouped_orders != null) {
// for each group
for (group_index in saved_grouped_orders) {
var g = [];
// for each order in group
for (group_element_id of saved_grouped_orders[group_index]) {
// Look for order in datatable
for (var i = 0; i < table_orders.rows().data().length; i++) {
if (group_element_id == table_orders.rows(i).data()[0].id) {
var order = table_orders.rows(i).data()[0];
g.push(order);
// remove raw from table dbc.get(order_doc_id).then(() => {
table_orders.rows(i).remove() if (go_to_order === true) {
.draw(); check_before_goto(order_data.id);
}
})
.catch(function (err) {
// Create if doesn't exist
if (err.status === 404) {
let order_doc = { ...order_data };
order_doc._id = order_doc_id;
order_doc.last_update = {
timestamp: Date.now(),
fingerprint: fingerprint
};
dbc.put(order_doc).then(() => {
if (go_to_order === true) {
goto(order_data.id);
} }
} })
} .catch((err) => {
error = {
// No order found, delete group and skip the rest msg: 'Erreur dans la creation de la commande dans couchdb',
if (g.length == 0) { ctx: 'create_order_doc',
groups_to_delete.push(group_index); details: err
continue; };
} report_JS_error(error, 'reception');
console.log(error);
// Display group });
document.getElementById("container_groups").hidden = false;
var group_row = "<ul> <li> Commandes de ";
for (i in g) {
if (i == g.length-1) { // last element of list
group_row += "<b>" + g[i].partner + "</b> du " + g[i].date_order + " : ";
} else {
group_row += "<b>" + g[i].partner + "</b> du " + g[i].date_order + ", ";
}
} }
});
if (g[0].reception_status == 'False') {
group_row += "<button class='btn--primary' onClick='group_goto("
+ saved_groups.length
+ ")'>Compter les produits</button>";
} else {
group_row += "<button class='btn--success' onClick='group_goto("
+ saved_groups.length
+ ")'>Mettre à jour les prix</button>";
}
group_row += "</li>";
$('#groups_items').append(group_row);
saved_groups.push(g);
}
}
if (groups_to_delete.length > 0) {
for (index of groups_to_delete) {
saved_grouped_orders.splice(index, 1);
}
localStorage.setItem('grouped_orders', JSON.stringify(saved_grouped_orders));
}
} }
/* ACTIONS */ /* ACTIONS */
// Validate all prices of an order /**
* Validate all prices of an order
*/
function validatePrices() { function validatePrices() {
// Loading on // Loading on
openModal(); openModal();
...@@ -210,9 +187,19 @@ function validatePrices() { ...@@ -210,9 +187,19 @@ function validatePrices() {
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
data: JSON.stringify(update_data), data: JSON.stringify(update_data),
success: function() { success: function() {
localStorage.removeItem("order_" + order["id"]); // Remove order
callback_update = true; dbc.get(`order_${order['id']}`).then((doc) => {
reload(); return dbc.remove(doc);
})
.then(() => {
callback_update = true;
reload();
})
.catch((err) => {
// No doc found
console.log(err);
reload();
});
}, },
error: function() { error: function() {
closeModal(); closeModal();
...@@ -251,91 +238,117 @@ function validatePrices() { ...@@ -251,91 +238,117 @@ function validatePrices() {
}); });
} }
/**
// Action fired when orders are grouped (new group) * Action fired when orders are grouped (new group)
* @returns
*/
function group_action() { function group_action() {
var pswd = prompt('Merci de demander à un.e salarié.e le mot de passe pour fusionner ces commandes.'); let pswd = prompt('Merci de demander à un.e salarié.e le mot de passe pour fusionner ces commandes.');
if (pswd == merge_orders_pswd) { // Minimum security level // Minimum security level
// Use local storage to pass order data to next page if (pswd == merge_orders_pswd) {
if (Modernizr.localstorage) { let selected_data = table_orders.rows('.selected').data();
var selected_data = table_orders.rows('.selected').data(); let group_ids = [];
var group_ids = [];
// Select orders id for (let i = 0; i < selected_data.length; i++) {
for (var i = 0; i < selected_data.length; i++) { // Select group orders id
group_ids.push(selected_data[i].id); group_ids.push(selected_data[i].id);
}
// Notify server that group is created // Create doc for each group order if doesn't exist
$.ajax({ create_order_doc(selected_data[i]);
type: "POST", }
url: "/reception/save_order_group",
dataType: "json",
traditional: true,
contentType: "application/json; charset=utf-8",
data: JSON.stringify(group_ids),
success: function() {
var min_id = 9999999;
for (var i = 0; i < selected_data.length; i++) {
// get smallest id
if (selected_data[i].id < min_id) {
min_id = selected_data[i].id;
}
// Add each order to local storage group_ids.sort();
set_local_storage(selected_data[i]);
}
// Get array of grouped orders // Save group
var grouped_orders = JSON.parse(localStorage.getItem('grouped_orders')); order_groups.groups.push(group_ids);
dbc.put(order_groups, (err) => {
if (!err) {
goto(group_ids[0]);
} else {
alert("Une erreur est survenue lors de la création du groupe. Veuillez ré-essayer plus tard svp.");
console.log(err);
}
});
// Create if not exists } else if (pswd == null) {
if (grouped_orders == null) { return;
grouped_orders = []; } else {
} alert('Mauvais mot de passe !');
}
}
// Add group /* DISPLAY */
grouped_orders.push(group_ids);
// store grouped orders array /**
localStorage.setItem('grouped_orders', JSON.stringify(grouped_orders)); * Display the order groups.
* Remove the grouped orders from the order table to prevent grouping in multiple groups.
*/
function display_grouped_orders() {
if (table_orders !== null) {
$('#groups_items').empty();
let groups_display_content = "<ul>";
for (let group_index in order_groups.groups) {
let group_orders = [];
// Extract every order in the groups from the orders table
for (group_order_id of order_groups.groups[group_index]) {
// Look for order in datatable"
for (let i = 0; i < table_orders.rows().data().length; i++) {
if (group_order_id == table_orders.rows(i).data()[0].id) {
var order = table_orders.rows(i).data()[0];
// Go to products page of order with smallest id group_orders.push(order);
goto(min_id);
}, // remove table row
error: function(data) { table_orders.rows(i).remove()
if (data != null && data.status == 409) { .draw();
alert("Un groupe a déjà été formé sur un autre poste "
+ "avec au moins l'une des commandes sélectionnées. Merci de rafraichir la page.");
} }
} }
}); }
// Display group
document.getElementById("container_groups").hidden = false;
let group_row = `<li class="group_line"> Commandes de `;
} else { for (let i in group_orders) {
alert("Le local storage n'est pas disponible. Merci de contacter un.e salarié.e !"); if (i == group_orders.length-1) { // last element of list
} group_row += "<b>" + group_orders[i].partner + "</b> du " + group_orders[i].date_order + " : ";
} else {
group_row += "<b>" + group_orders[i].partner + "</b> du " + group_orders[i].date_order + ", ";
}
}
} else if (pswd == null) { if (group_orders[0].reception_status == 'False') {
return; group_row += "<button class='btn--primary' onClick='group_goto("
} else { + group_index
alert('Mauvais mot de passe !'); + ")'>Compter les produits</button>";
} else {
group_row += "<button class='btn--success' onClick='group_goto("
+ group_index
+ ")'>Mettre à jour les prix</button>";
}
group_row += "</li>";
groups_display_content += group_row;
}
$('#container_groups').show();
$('#groups_items').append(groups_display_content);
} }
} }
/**
$(document).ready(function() { * Display the main orders table
openModal(); */
function display_orders_table() {
$.ajaxSetup({ headers: { "X-CSRFToken": getCookie('csrftoken') } }); if (table_orders) {
table_orders.clear().destroy();
// Set date format for DataTable so date ordering can work $('#orders').empty();
$.fn.dataTable.moment('D/M/Y'); }
table_orders = $('#orders').DataTable({ table_orders = $('#orders').DataTable({
ajax: "get_list_orders", data: orders,
columns:[ columns:[
{ {
data:"id", data:"id",
...@@ -374,17 +387,40 @@ $(document).ready(function() { ...@@ -374,17 +387,40 @@ $(document).ready(function() {
title:"Statut", title:"Statut",
className:"dt-body-center", className:"dt-body-center",
render: function (data) { render: function (data) {
switch (data) { switch (data) {
case 'qty_valid': case 'qty_valid':
return "<span class='btn--success'>Mettre à jour les prix</span>"; return "<span class='btn--success'>Mettre à jour les prix</span>";
case 'br_valid': case 'br_valid':
return "<span class='btn'><i class='far fa-check-circle'></i> Réception OK</span>"; return "<span class='btn'><i class='far fa-check-circle'></i> Réception OK</span>";
default: case 'False':
return "<span class='btn--primary'>Compter les produits</span>"; return "<span class='btn--primary'>Compter les produits</span>";
case 'done':
return "<span class='btn'><i class='far fa-check-circle'></i> Terminé</span>";
case 'uprice_valid':
return "<span class='btn--primary'>Mise à jour du prix OK</span>";
case "valid_pending":
return "<span class='btn--info'>En attente de validation</span>";
case 'legacy':
return "<span class='btn--success'>Legacy</span>";
case 'error_pack_op':
return "<span class='btn--danger'>Erreur pack operations</span>";
case 'error_transfer':
return "<span class='btn--danger'>Erreur de transfert</span>";
case 'error_picking':
return "<span class='btn--danger'>Erreur validation quantité</span>";
case '/error_uprice':
return "<span class='btn--danger'>Erreur mise à jour du prix</span>";
default:
return "<span class='btn--warning'>Status inconnu : " + data + "</span>";
} }
}, },
width: "20%" width: "20%"
} } //error_transfert ou error_pack_op
], ],
dom: 'rtip', dom: 'rtip',
order: [ order: [
...@@ -394,13 +430,7 @@ $(document).ready(function() { ...@@ -394,13 +430,7 @@ $(document).ready(function() {
] ]
], ],
iDisplayLength: 25, iDisplayLength: 25,
language: {url : '/static/js/datatables/french.json'}, language: {url : '/static/js/datatables/french.json'}
initComplete: function() { // After data is loaded
clean_local_storage();
create_groups_from_server_data();
extract_grouped_orders();
closeModal();
}
}); });
// Set rows event on click // Set rows event on click
...@@ -459,12 +489,10 @@ $(document).ready(function() { ...@@ -459,12 +489,10 @@ $(document).ready(function() {
document.getElementById("group_action").hidden = true; document.getElementById("group_action").hidden = true;
} }
} else if (this.cellIndex == 4) { // Click on last cell button -> go to products page } else if (this.cellIndex == 4) { // Click on last cell button -> go to products page
// Extra security if order with a different status gets lost in here // Click action only for specific reception status
if (row_data.reception_status == "qty_valid" || row_data.reception_status == "False") { if (row_data.reception_status == "qty_valid" || row_data.reception_status == "False") {
// Use local storage to pass order data to next page // Use couchdb to pass order data to next page
set_local_storage(row_data); create_order_doc(row_data, true);
goto(row_data.id);
} }
} else if (this.cellIndex == 3 && row_data.reception_status == "qty_valid") { } else if (this.cellIndex == 3 && row_data.reception_status == "qty_valid") {
// If 'update prices' step, click on before-last cell -> validate all prices // If 'update prices' step, click on before-last cell -> validate all prices
...@@ -479,6 +507,112 @@ $(document).ready(function() { ...@@ -479,6 +507,112 @@ $(document).ready(function() {
.search(jQuery.fn.DataTable.ext.type.search.string(this.value)) .search(jQuery.fn.DataTable.ext.type.search.string(this.value))
.draw(); .draw();
}); });
}
$(document).ready(function() {
openModal();
$.ajaxSetup({ headers: { "X-CSRFToken": getCookie('csrftoken') } });
fingerprint = new Fingerprint({canvas: true}).get();
// Init couchdb
dbc = new PouchDB(couchdb_dbname),
sync = PouchDB.sync(couchdb_dbname, couchdb_server, {
live: true,
retry: true,
auto_compaction: false
});
// On distant changes
sync.on('change', function (info) {
// If important data changed somewhere else, update local data
let need_to_reload = false;
if (info.direction === "pull") {
for (let doc of info.change.docs) {
if (doc._id === "grouped_orders") {
// If groups doc changed, update local groups
need_to_reload = true;
order_groups = doc;
} else if ("_deleted" in doc && doc._deleted === true) {
// If order was deleted, delete it locally
try {
const deleted_order_id = parseInt(doc._id.split('_')[1]);
let index = orders.findIndex(order => order.id == deleted_order_id);
if (index !== -1) {
orders.splice(index, 1);
need_to_reload = true;
}
} catch (error) {
console.log(error);
}
} else {
// Find updated order in local orders & update it if reception status changed
let index = orders.findIndex(order => order.id == doc.id);
if (index !== -1 && orders[index].reception_status !== doc.reception_status) {
orders[index] = doc;
need_to_reload = true;
break;
}
}
}
}
if (need_to_reload) {
display_orders_table();
display_grouped_orders();
}
}).on('error', function (err) {
console.log(err);
});
// Get or create order groups doc
dbc.get("grouped_orders").then((doc) => {
order_groups = doc;
})
.catch(function (err) {
console.log(err);
if (err.status === 404) {
// Create if doesn't exist
dbc.put(order_groups, (err, result) => {
if (!err) {
order_groups._rev = result.rev;
} else {
console.log("document pour les groupes déjà créé");
console.log(err);
}
});
}
});
// Set date format for DataTable so date ordering can work
$.fn.dataTable.moment('D/M/Y');
// Get orders
$.ajax({
type: 'GET',
url: "/reception/get_list_orders",
dataType:"json",
traditional: true,
contentType: "application/json; charset=utf-8",
success: function(data) {
orders = data.data;
display_orders_table();
display_grouped_orders();
closeModal();
},
error: function(data) {
err = {msg: "erreur serveur lors de la récupération des commandes", ctx: 'get_list_orders'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
closeModal();
alert('Erreur lors de la récupération des commandes, rechargez la page plus tard.');
}
});
}); });
...@@ -13,9 +13,9 @@ Sémantiquement, ici : ...@@ -13,9 +13,9 @@ Sémantiquement, ici :
* Associative array of current order(s) * Associative array of current order(s)
* If more than 1 element: group of orders * If more than 1 element: group of orders
* If 1 element: single order * If 1 element: single order
* -> check for Object.keys(orders).length to know if we're in a group case
*/ */
var orders = {}, var orders = {},
is_group = false,
group_ids = []; group_ids = [];
var reception_status = null, var reception_status = null,
...@@ -32,6 +32,9 @@ var reception_status = null, ...@@ -32,6 +32,9 @@ var reception_status = null,
updateType = "", // step 1: qty_valid; step2: br_valid updateType = "", // step 1: qty_valid; step2: br_valid
barcodes = null; // Barcodes stored locally barcodes = null; // Barcodes stored locally
var dbc = null,
sync = null,
fingerprint = null;
/* UTILS */ /* UTILS */
...@@ -108,12 +111,56 @@ function select_product_from_bc(barcode) { ...@@ -108,12 +111,56 @@ function select_product_from_bc(barcode) {
return 0; return 0;
} }
/**
* Update couchdb order
* @param {int} order_id
*/
function update_distant_order(order_id) {
orders[order_id].last_update = {
timestamp: Date.now(),
fingerprint: fingerprint
};
dbc.put(orders[order_id], (err, result) => {
if (!err && result !== undefined) {
orders[order_id]._rev = result.rev;
} else {
alert("Erreur lors de la sauvegarde de la commande... Si l'erreur persiste contactez un administrateur svp.");
console.log(err);
}
});
}
/**
* Update distant orders with local data
* @param {int} order_id
*/
function update_distant_orders() {
for (let order_id in orders) {
orders[order_id].last_update = {
timestamp: Date.now(),
fingerprint: fingerprint
};
}
dbc.bulkDocs(Object.values(orders)).then((response) => {
// Update rev of current orders after their update
for (let doc of response) {
let order_id = doc.id.split('_')[1];
orders[order_id]._rev = doc.rev;
}
})
.catch((err) => {
console.log(err);
});
}
/* INIT */ /* INIT */
// Get order(s) data from server // Get order(s) data from server
function fetch_data() { function fetch_data() {
try { try {
$.ajaxSetup({ headers: { "X-CSRFToken": getCookie('csrftoken') } });
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: '../get_orders_lines', url: '../get_orders_lines',
...@@ -194,7 +241,7 @@ function initLists() { ...@@ -194,7 +241,7 @@ function initLists() {
document.getElementById("valid_all_uprices").disabled = false; document.getElementById("valid_all_uprices").disabled = false;
} }
// Set lists with local storage content // Set processed and to_process lists based on saved data
for (var i = 0; i < updatedProducts.length; i++) { for (var i = 0; i < updatedProducts.length; i++) {
let product = updatedProducts[i]; let product = updatedProducts[i];
...@@ -271,7 +318,8 @@ function initLists() { ...@@ -271,7 +318,8 @@ function initLists() {
title:"Autres", title:"Autres",
defaultContent: "<select class='select_product_action'><option value=''></option><option value='supplier_shortage'>Rupture fournisseur</option></select>", defaultContent: "<select class='select_product_action'><option value=''></option><option value='supplier_shortage'>Rupture fournisseur</option></select>",
className:"dt-body-center", className:"dt-body-center",
orderable: false orderable: false,
visible: display_autres === "True"
} }
], ],
rowId : "product_id.0", rowId : "product_id.0",
...@@ -287,7 +335,6 @@ function initLists() { ...@@ -287,7 +335,6 @@ function initLists() {
dom: 'lrtip', // Remove the search input from that table dom: 'lrtip', // Remove the search input from that table
language: {url : '/static/js/datatables/french.json'} language: {url : '/static/js/datatables/french.json'}
}); });
// Init table for processed content // Init table for processed content
table_processed = $('#table_processed').DataTable({ table_processed = $('#table_processed').DataTable({
data: list_processed, data: list_processed,
...@@ -323,8 +370,18 @@ function initLists() { ...@@ -323,8 +370,18 @@ function initLists() {
{ {
data:"product_qty", data:"product_qty",
title:"Qté", title:"Qté",
className:"dt-body-center", className:"dt-head-center dt-body-center",
visible: (reception_status == "False") visible: (reception_status == "False"),
render: function (data, type, full) {
let disp = [
full.product_qty,
(full.old_qty !== undefined)?full.old_qty:full.product_qty
].join("/");
return disp;
},
orderable: false
}, },
{ {
data:"price_unit", data:"price_unit",
...@@ -342,6 +399,7 @@ function initLists() { ...@@ -342,6 +399,7 @@ function initLists() {
title:"Autres", title:"Autres",
className:"dt-body-center", className:"dt-body-center",
orderable: false, orderable: false,
visible: display_autres === "True",
render: function (data, type, full) { render: function (data, type, full) {
let disabled = (full.supplier_shortage) ? "disabled" : ''; let disabled = (full.supplier_shortage) ? "disabled" : '';
...@@ -376,17 +434,18 @@ function initLists() { ...@@ -376,17 +434,18 @@ function initLists() {
$('#table_to_process tbody').on('click', 'a#toProcess_line_valid', function () { $('#table_to_process tbody').on('click', 'a#toProcess_line_valid', function () {
if (is_time_to('reception_direct_valid_order_line', 500)) { if (is_time_to('reception_direct_valid_order_line', 500)) {
try { try {
var row = table_to_process.row($(this).parents('tr')); let row = table_to_process.row($(this).parents('tr'));
var data = row.data(); let data = row.data();
add_to_processed(data); add_to_processed(data);
remove_from_toProcess(row, data); remove_from_toProcess(row, data);
// Update local storage of product's order // Update product's order
if (!orders[data.id_po]['valid_products']) if (!orders[data.id_po]['valid_products']) {
orders[data.id_po]['valid_products'] = []; orders[data.id_po]['valid_products'] = [];
}
orders[data.id_po]['valid_products'].push(data['id']); orders[data.id_po]['valid_products'].push(data['id']);
localStorage.setItem("order_" + data.id_po, JSON.stringify(orders[data.id_po])); update_distant_order(data.id_po);
// Reset search // Reset search
document.getElementById('search_input').value = ''; document.getElementById('search_input').value = '';
...@@ -711,8 +770,8 @@ function set_supplier_shortage(row, product, from_processed = false) { ...@@ -711,8 +770,8 @@ function set_supplier_shortage(row, product, from_processed = false) {
} }
add_to_processed(product); add_to_processed(product);
// Update local storage of product's order // Update product's order
localStorage.setItem("order_" + product.id_po, JSON.stringify(orders[product.id_po])); update_distant_order(product.id_po);
// Reset search // Reset search
document.getElementById('search_input').value = ''; document.getElementById('search_input').value = '';
...@@ -788,9 +847,12 @@ function clearLineEdition() { ...@@ -788,9 +847,12 @@ function clearLineEdition() {
/** /**
* Update a product info : qty or unit price * Update a product info : qty or unit price
* If 'value' is set, use it as new value * @param {Object} productToEdit
* @param {Float} value if set, use it as new value
* @param {Boolean} batch if true, don't update couchdb data here
* @returns
*/ */
function editProductInfo (productToEdit, value = null) { function editProductInfo (productToEdit, value = null, batch = false) {
try { try {
// Check if the product is already in the 'updated' list // Check if the product is already in the 'updated' list
var index = searchUpdatedProduct(); var index = searchUpdatedProduct();
...@@ -856,16 +918,16 @@ function editProductInfo (productToEdit, value = null) { ...@@ -856,16 +918,16 @@ function editProductInfo (productToEdit, value = null) {
if (firstUpdate) { if (firstUpdate) {
updatedProducts.push(productToEdit); updatedProducts.push(productToEdit);
/* Update local storage of product order */
// Create 'updated_products' list in order if not exists // Create 'updated_products' list in order if not exists
if (!orders[productToEdit.id_po]['updated_products']) if (!orders[productToEdit.id_po]['updated_products']) {
orders[productToEdit.id_po]['updated_products'] = []; orders[productToEdit.id_po]['updated_products'] = [];
}
// Add product to order's updated products if first update // Add product to order's updated products if first update
orders[productToEdit.id_po]['updated_products'].push(productToEdit); orders[productToEdit.id_po]['updated_products'].push(productToEdit);
// May have been directly validated then updated from processed list // May have been directly validated then updated from processed list
// -> then: remove from 'valid_products' list // -> remove from 'valid_products' list
for (i in orders[productToEdit.id_po]['valid_products']) { for (i in orders[productToEdit.id_po]['valid_products']) {
if (orders[productToEdit.id_po]['valid_products'][i] == productToEdit['id']) { if (orders[productToEdit.id_po]['valid_products'][i] == productToEdit['id']) {
orders[productToEdit.id_po]['valid_products'].splice(i, 1); orders[productToEdit.id_po]['valid_products'].splice(i, 1);
...@@ -881,8 +943,10 @@ function editProductInfo (productToEdit, value = null) { ...@@ -881,8 +943,10 @@ function editProductInfo (productToEdit, value = null) {
} }
} }
// Update local storage of product order if (batch === false) {
localStorage.setItem("order_" + productToEdit.id_po, JSON.stringify(orders[productToEdit.id_po])); // Update product order
update_distant_order(productToEdit.id_po);
}
if(addition){ if(addition){
let row = table_processed.row($('#'+productToEdit.product_id[0])); let row = table_processed.row($('#'+productToEdit.product_id[0]));
...@@ -914,16 +978,17 @@ function setAllQties() { ...@@ -914,16 +978,17 @@ function setAllQties() {
table_to_process.rows().every(function () { table_to_process.rows().every(function () {
var data = this.data(); var data = this.data();
editProductInfo(data, 0); editProductInfo(data, 0, true);
return true; return true;
}); });
list_to_process = []; list_to_process = [];
table_to_process.rows().remove() table_to_process.rows().remove()
.draw(); .draw();
}
// Batch update orders
update_distant_orders();
}
/* ACTIONS */ /* ACTIONS */
...@@ -1015,7 +1080,6 @@ function pre_send(type) { ...@@ -1015,7 +1080,6 @@ function pre_send(type) {
function data_validation() { function data_validation() {
openModal(); openModal();
$.ajaxSetup({ headers: { "X-CSRFToken": getCookie('csrftoken') } });
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: "../data_validation", url: "../data_validation",
...@@ -1057,6 +1121,7 @@ function send() { ...@@ -1057,6 +1121,7 @@ function send() {
// Loading on // Loading on
openModal(); openModal();
/* Prepare data for orders update */
// Only send to server the updated lines // Only send to server the updated lines
var update_data = { var update_data = {
update_type: updateType, update_type: updateType,
...@@ -1070,7 +1135,8 @@ function send() { ...@@ -1070,7 +1135,8 @@ function send() {
// for each updated product, add it to its order list // for each updated product, add it to its order list
for (i in updatedProducts) { for (i in updatedProducts) {
// if product was in different orders
/* ---> The following part concerns products found in different orders */
if ('other_orders_data' in updatedProducts[i]) { if ('other_orders_data' in updatedProducts[i]) {
// for each other order of product // for each other order of product
for (other_order_data of updatedProducts[i].other_orders_data) { for (other_order_data of updatedProducts[i].other_orders_data) {
...@@ -1127,13 +1193,44 @@ function send() { ...@@ -1127,13 +1193,44 @@ function send() {
orders[other_order_data.id_po]['updated_products'].push(product_copy); orders[other_order_data.id_po]['updated_products'].push(product_copy);
} }
} }
/* <--- */
// Add product to order's prod list // Add product to order's prod list
prod_order_id = updatedProducts[i].id_po; prod_order_id = updatedProducts[i].id_po;
update_data.orders[prod_order_id]['po'].push(updatedProducts[i]); update_data.orders[prod_order_id]['po'].push(updatedProducts[i]);
} }
$.ajaxSetup({ headers: { "X-CSRFToken": getCookie('csrftoken') } }); /* Create the error report */
// Send changes between items to process and processed items
var error_report_data = {
'group_amount_total' : 0,
'update_type' : updateType,
'updated_products' : updatedProducts,
'user_comments': user_comments,
'orders' : []
};
for (let i in orders) {
error_report_data.group_amount_total += orders[i].amount_total;
error_report_data.orders.push(orders[i]);
}
// Send request for error report
$.ajax({
type: "POST",
url: "../save_error_report",
dataType: "json",
traditional: true,
contentType: "application/json; charset=utf-8",
data: JSON.stringify(error_report_data),
success: function() {},
error: function() {
closeModal();
alert('Erreur dans l\'envoi du rapport.');
}
});
/* Update orders */
$.ajax({ $.ajax({
type: "PUT", type: "PUT",
url: "../update_orders", url: "../update_orders",
...@@ -1145,8 +1242,9 @@ function send() { ...@@ -1145,8 +1242,9 @@ function send() {
closeModal(); closeModal();
try { try {
// If step 1 (counting), open pop-up with procedure explanation // If step 1 (counting)
if (reception_status == "False") { if (reception_status == "False") {
/* Open pop-up with procedure explanation */
var barcodes_to_print = false; var barcodes_to_print = false;
// Select products with local barcode and without barcode, when qty > 0 // Select products with local barcode and without barcode, when qty > 0
...@@ -1176,9 +1274,9 @@ function send() { ...@@ -1176,9 +1274,9 @@ function send() {
} }
// Set order(s) name in popup DOM // Set order(s) name in popup DOM
if (Object.keys(orders).length == 1) { // Single order if (Object.keys(orders).length === 1) { // Single order
document.getElementById("order_ref").innerHTML = orders[Object.keys(orders)[0]].name; document.getElementById("order_ref").innerHTML = orders[Object.keys(orders)[0]].name;
} else { } else { // group
document.getElementById("success_order_name_container").hidden = true; document.getElementById("success_order_name_container").hidden = true;
document.getElementById("success_orders_name_container").hidden = false; document.getElementById("success_orders_name_container").hidden = false;
...@@ -1203,13 +1301,35 @@ function send() { ...@@ -1203,13 +1301,35 @@ function send() {
openModal( openModal(
$('#modal_qtiesValidated').html(), $('#modal_qtiesValidated').html(),
function() { back,
document.location.href = "/reception";
},
'Retour à la liste des commandes', 'Retour à la liste des commandes',
true, true,
false false
); );
/* Not last step: update distant data */
for (let order_id in orders) {
// Save current step updated data
orders[order_id].previous_steps_data = {};
orders[order_id].previous_steps_data[reception_status] = {
updated_products: orders[order_id].updated_products || []
};
orders[order_id].reception_status = updateType;
// Unlock order
orders[order_id].last_update = {
timestamp: null,
fingerprint: null
};
// Delete temp data
delete orders[order_id].valid_products;
delete orders[order_id].updated_products;
}
dbc.bulkDocs(Object.values(orders)).catch((err) => {
console.log(err);
});
} else { } else {
// Print etiquettes with new prices // Print etiquettes with new prices
if (updatedProducts.length > 0) { if (updatedProducts.length > 0) {
...@@ -1218,49 +1338,48 @@ function send() { ...@@ -1218,49 +1338,48 @@ function send() {
openModal( openModal(
$('#templates #modal_pricesValidated').html(), $('#templates #modal_pricesValidated').html(),
function() { back,
document.location.href = "/reception";
},
'Retour à la liste des commandes', 'Retour à la liste des commandes',
true, true,
false false
); );
}
// Go back to to_process list if modal closed /* Last step: Clear distant data */
$('#modal_closebtn_top').on('click', function () { // Delete orders doc
document.location.href = "/reception"; for (let order_id in orders) {
}); orders[order_id]._deleted = true;
}
// Remove orders group
dbc.get("grouped_orders").then((doc) => {
let couchdb_update_data = Object.values(orders);
$('#modal_closebtn_bottom').on('click', function () { // We're in a group, remove it & update groups doc
document.location.href = "/reception"; if (Object.keys(orders).length > 1) {
}); let groups_doc = doc;
// Clear local storage before leaving let first_order_id = parseInt(Object.keys(orders)[0]);
for (order_id in orders) {
localStorage.removeItem("order_" + order_id);
}
// Delete group(s) for (let i in groups_doc.groups) {
if (is_group) { if (groups_doc.groups[i].includes(first_order_id)) {
var grouped_orders = JSON.parse(localStorage.getItem('grouped_orders')); groups_doc.groups.splice(i, 1);
break;
// Remove all groups containing these orders
for (order_id in orders) {
search:
for (var h = 0; i < grouped_orders.length; h++) {
for (var j = 0; j < grouped_orders[h].length; j++) {
if (grouped_orders[h][j] == order_id) {
grouped_orders.splice(h);
break search;
} }
} }
couchdb_update_data.push(groups_doc);
} }
}
localStorage.setItem('grouped_orders', JSON.stringify(grouped_orders)); return dbc.bulkDocs(couchdb_update_data);
})
.catch(function (err) {
console.log(err);
});
} }
// Back if modal closed
$('#modal_closebtn_top').on('click', back);
$('#modal_closebtn_bottom').on('click', back);
} catch (ee) { } catch (ee) {
err = {msg: ee.name + ' : ' + ee.message, ctx: 'callback update_orders'}; err = {msg: ee.name + ' : ' + ee.message, ctx: 'callback update_orders'};
console.error(err); console.error(err);
...@@ -1272,34 +1391,6 @@ function send() { ...@@ -1272,34 +1391,6 @@ function send() {
alert('Erreur lors de la sauvegarde des données.'); alert('Erreur lors de la sauvegarde des données.');
} }
}); });
// Send changes between items to process and processed items
var updates = {
'group_amount_total' : 0,
'update_type' : updateType,
'updated_products' : updatedProducts,
'user_comments': user_comments,
'orders' : []
};
for (i in orders) {
updates.group_amount_total += orders[i].amount_total;
updates.orders.push(orders[i]);
}
$.ajax({
type: "POST",
url: "../save_error_report",
dataType: "json",
traditional: true,
contentType: "application/json; charset=utf-8",
data: JSON.stringify(updates),
success: function(data) {},
error: function() {
closeModal();
alert('Erreur dans l\'envoi du rapport.');
}
});
} catch (e) { } catch (e) {
err = {msg: e.name + ' : ' + e.message, ctx: 'send'}; err = {msg: e.name + ' : ' + e.message, ctx: 'send'};
console.error(err); console.error(err);
...@@ -1327,17 +1418,19 @@ function confirm_all_left_is_good() { ...@@ -1327,17 +1418,19 @@ function confirm_all_left_is_good() {
} else { } else {
value = data.price_unit; value = data.price_unit;
} }
editProductInfo(data, value); editProductInfo(data, value, true);
return true; return true;
}); });
list_to_process = []; list_to_process = [];
table_to_process.rows().remove() table_to_process.rows().remove()
.draw(); .draw();
// Batch update orders
update_distant_orders();
closeModal(); closeModal();
} }
/* TODO: upgrade modal
-> disable background scrolling*/
function openFAQ() { function openFAQ() {
openModal($("div#modal_FAQ_content").html(), function() {}, 'Compris !', true, false); openModal($("div#modal_FAQ_content").html(), function() {}, 'Compris !', true, false);
} }
...@@ -1349,6 +1442,7 @@ function openErrorReport() { ...@@ -1349,6 +1442,7 @@ function openErrorReport() {
// this is necessary because default behavior is overwritten by the listener defined in jquery.pos.js; // this is necessary because default behavior is overwritten by the listener defined in jquery.pos.js;
$("#error_report").keypress(function(e) { $("#error_report").keypress(function(e) {
var key = e.keyCode; var key = e.keyCode;
if (key === 13) { if (key === 13) {
this.value += "\n"; this.value += "\n";
} }
...@@ -1364,10 +1458,10 @@ function openErrorReport() { ...@@ -1364,10 +1458,10 @@ function openErrorReport() {
function saveErrorReport() { function saveErrorReport() {
user_comments = document.getElementById("error_report").value; user_comments = document.getElementById("error_report").value;
// Save comment in local storage, in all orders // Save comments in all orders
for (order_id of Object.keys(orders)) { for (order_id of Object.keys(orders)) {
orders[order_id].user_comments = user_comments; orders[order_id].user_comments = user_comments;
localStorage.setItem("order_" + order_id, JSON.stringify(orders[order_id])); update_distant_order(order_id);
} }
document.getElementById("search_input").focus(); document.getElementById("search_input").focus();
...@@ -1379,199 +1473,91 @@ var get_barcodes = async function() { ...@@ -1379,199 +1473,91 @@ var get_barcodes = async function() {
}; };
$(document).ready(function() {
$.ajaxSetup({ headers: { "X-CSRFToken": getCookie('csrftoken') } });
// Load barcodes
get_barcodes();
// Get Route parameter
var pathArray = window.location.pathname.split('/');
var id = pathArray[pathArray.length-1];
// Disable alert errors from datatables
$.fn.dataTable.ext.errMode = 'none';
// Listen for errors in tables with custom behavior
$('#table_to_process').on('error.dt', function (e, settings, techNote, message) {
var err_msg = message;
try {
var split = message.split(" ");
var row_number = null;
for (var i = 0; i < split.length; i++) {
if (split[i] == "row")
row_number = split[i+1];
}
row_number = row_number.replace(',', '');
var row_data = $('#table_to_process').DataTable()
.row(row_number)
.data();
err_msg += " - Order id: " + row_data.id_po;
err_msg += " - Product: " + row_data.product_id[1];
} catch (e) {
console.log(e);
}
err = {msg: err_msg, ctx: 'datatable: table to_process'};
console.error(err);
report_JS_error(err, 'reception');
});
$('#table_processed').on('error.dt', function (e, settings, techNote, message) {
var err_msg = message;
try { /**
var split = message.split(" "); * Init the page according to order(s) data (texts, colors, events...)
var row_number = null; *
* @param {Array} partners_display_data
for (var i = 0; i < split.length; i++) { */
if (split[i] == "row") function init_dom(partners_display_data) {
row_number = split[i+1]; // Back button
} $('#back_button').on('click', function () {
// Liberate current orders
row_number = row_number.replace(',', ''); for (let order_id in orders) {
var row_data = $('#table_processed').DataTable() orders[order_id].last_update = {
.row(row_number) timestamp: null,
.data(); fingerprint: null
};
err_msg += " - Order id: " + row_data.id_po;
err_msg += " - Product: " + row_data.product_id[1];
} catch (e) {
console.log(e);
} }
err = {msg: err_msg, ctx: 'datatable: table processed'}; dbc.bulkDocs(Object.values(orders)).then((response) => {
console.error(err); back();
report_JS_error(err, 'reception'); })
.catch((err) => {
console.log(err);
});
}); });
try { // Grouped orders
// Get order info from local storage (it should be there if process followed) if (Object.keys(orders).length > 1) {
if (Modernizr.localstorage) { $('#partner_name').html(Object.keys(orders).length + " commandes");
// Look for current order in grouped orders in local storage
var grouped_orders = JSON.parse(localStorage.getItem('grouped_orders'));
if (grouped_orders != null) {
for (group of grouped_orders) {
for (group_element_id of group) {
if (group_element_id == id) {
// We're in a group!
is_group = true;
group_ids = group;
}
}
}
}
// if not in group, add current order to group
if (group_ids.length == 0) {
group_ids.push(id);
}
var stored_order = null;
var display_partners_name = [];
// for each order in order group
for (order_id of group_ids) {
// Get order data from local storage
stored_order = JSON.parse(localStorage.getItem('order_' + order_id));
// Add order to order list
if (stored_order != null) {
orders[order_id] = stored_order;
//Add each order's already updated and validated products to common list // Display order data for each order
if (stored_order["updated_products"]) var msg = "";
updatedProducts = updatedProducts.concat(stored_order["updated_products"]);
if (stored_order["valid_products"]) for (display_partner_data of partners_display_data) {
validProducts = validProducts.concat(stored_order["valid_products"]); if (msg != "") {
msg += ", ";
// Prepare data to display in 'partner name' area
display_partners_name.push(stored_order['partner'] + ' du ' + stored_order['date_order']);
}
} }
msg += display_partner_data;
// Set current reception status: take first order's
reception_status = orders[Object.keys(orders)[0]].reception_status;
// Load user comments from local storage, get it from first order
user_comments = orders[Object.keys(orders)[0]].user_comments || "";
} }
$('#container_multiple_partners').append('<h6> ' + msg + '</h6>');
} else {
$('#partner_name').html(orders[Object.keys(orders)[0]].partner);
}
// Fetch orders data /* Set DOM according to reception status */
fetch_data(); if (reception_status == "qty_valid") { // Step 2
// Header
if (is_group) { document.getElementById('header_step_two').classList.add('step_two_active');
$('#partner_name').html(Object.keys(orders).length + " commandes"); var check_icon = document.createElement('i');
// Display order data for each order
var msg = "";
for (display_partner_data of display_partners_name) {
if (msg != "") {
msg += ", ";
}
msg += display_partner_data;
}
$('#container_multiple_partners').append('<h6> ' + msg + '</h6>');
} else {
$('#partner_name').html(orders[Object.keys(orders)[0]].partner);
}
/* Set DOM according to reception status */
if (reception_status == "qty_valid") { // Step 2
// Header
document.getElementById('header_step_two').classList.add('step_two_active');
var check_icon = document.createElement('i');
check_icon.className = 'far fa-check-circle';
document.getElementById('header_step_one_content').appendChild(check_icon);
// Products lists containers check_icon.className = 'far fa-check-circle';
document.getElementById('container_left').style.border = "3px solid #0275D8"; // container qty_checked document.getElementById('header_step_one_content').appendChild(check_icon);
document.getElementById('container_right').style.border = "3px solid #5CB85C"; // container processed items
document.getElementById('header_container_left').innerHTML = "Prix à mettre à jour";
document.getElementById('header_container_right').innerHTML = "Prix mis à jour";
// Edition // Products lists containers
document.getElementById('edition_header').innerHTML = "Editer les prix"; document.getElementById('container_left').style.border = "3px solid #0275D8"; // container qty_checked
document.getElementById('edition_input_label').innerHTML = "Prix unit."; document.getElementById('container_right').style.border = "3px solid #5CB85C"; // container processed items
document.getElementById('header_container_left').innerHTML = "Prix à mettre à jour";
document.getElementById('header_container_right').innerHTML = "Prix mis à jour";
// Validation buttons // Edition
document.getElementById("valid_all").innerHTML = "<button class='btn--danger full_width_button' id='valid_all_uprices' onclick=\"openModal($('#templates #modal_no_prices').html(), confirmPricesAllValid, 'Confirmer', false);\" disabled>Pas de prix sur le bon de livraison</button>"; document.getElementById('edition_header').innerHTML = "Editer les prix";
document.getElementById("validation_button").innerHTML = "<button class='btn--success full_width_button' id='valid_uprice' onclick=\"pre_send('br_valid')\" disabled>Valider la mise à jour des prix</button>"; document.getElementById('edition_input_label').innerHTML = "Prix unit.";
// Modal content after validation // Validation buttons
$("#modal_pricesValidated").load("/reception/reception_pricesValidated"); document.getElementById("valid_all").innerHTML = "<button class='btn--danger full_width_button' id='valid_all_uprices' onclick=\"openModal($('#templates #modal_no_prices').html(), confirmPricesAllValid, 'Confirmer', false);\" disabled>Pas de prix sur le bon de livraison</button>";
} else if (reception_status == "False") { // Step 1 document.getElementById("validation_button").innerHTML = "<button class='btn--success full_width_button' id='valid_uprice' onclick=\"pre_send('br_valid')\" disabled>Valider la mise à jour des prix</button>";
document.getElementById('header_step_one').classList.add('step_one_active');
document.getElementById('container_left').style.border = "3px solid #212529"; // container products to process // Modal content after validation
document.getElementById('container_right').style.border = "3px solid #0275D8"; // container qty_checked $("#modal_pricesValidated").load("/reception/reception_pricesValidated");
document.getElementById('header_container_left').innerHTML = "Produits à compter"; } else if (reception_status == "False") { // Step 1
document.getElementById('header_container_right').innerHTML = "Produits déjà comptés"; document.getElementById('header_step_one').classList.add('step_one_active');
document.getElementById('edition_header').innerHTML = "Editer les quantités"; document.getElementById('container_left').style.border = "3px solid #212529"; // container products to process
document.getElementById('edition_input_label').innerHTML = "Qté"; document.getElementById('container_right').style.border = "3px solid #0275D8"; // container qty_checked
document.getElementById('header_container_left').innerHTML = "Produits à compter";
document.getElementById('header_container_right').innerHTML = "Produits déjà comptés";
document.getElementById("valid_all").innerHTML = "<button class='btn--danger full_width_button' id='valid_all_qties' onclick=\"openModal($('#templates #modal_no_qties').html(), setAllQties, 'Confirmer');\" disabled>Il n'y a plus de produits à compter</button>"; document.getElementById('edition_header').innerHTML = "Editer les quantités";
document.getElementById("validation_button").innerHTML = "<button class='btn--primary full_width_button' id='valid_qty' onclick=\"pre_send('qty_valid')\" disabled>Valider le comptage des produits</button>"; document.getElementById('edition_input_label').innerHTML = "Qté";
$("#modal_qtiesValidated").load("/reception/reception_qtiesValidated"); document.getElementById("valid_all").innerHTML = "<button class='btn--danger full_width_button' id='valid_all_qties' onclick=\"openModal($('#templates #modal_no_qties').html(), setAllQties, 'Confirmer');\" disabled>Il n'y a plus de produits à compter</button>";
} else { document.getElementById("validation_button").innerHTML = "<button class='btn--primary full_width_button' id='valid_qty' onclick=\"pre_send('qty_valid')\" disabled>Valider le comptage des produits</button>";
// Extra security, shouldn't get in here
document.location.href = "/reception";
}
} catch (e) {
err = {msg: e.name + ' : ' + e.message, ctx: 'page init'};
console.error(err);
report_JS_error(err, 'reception');
alert("Erreur au chargement de cette commande. Vous allez être redirigé."); $("#modal_qtiesValidated").load("/reception/reception_qtiesValidated");
} else {
// Extra security, shouldn't get in here: reception status not valid
back(); back();
} }
...@@ -1594,16 +1580,6 @@ $(document).ready(function() { ...@@ -1594,16 +1580,6 @@ $(document).ready(function() {
$('#edition_input').on('focus', function () { $('#edition_input').on('focus', function () {
$(this).on('wheel.disableScroll', function (e) { $(this).on('wheel.disableScroll', function (e) {
e.preventDefault(); e.preventDefault();
/*
Option to possibly enable page scrolling when mouse over the input, but :
- deltaY is not in pixels in Firefox
- movement not fluid on other browsers
var scrollTo = (e.originalEvent.deltaY) + $(document.documentElement).scrollTop();
$(document.documentElement).scrollTop(scrollTo);
-> other option to allow scrolling would be to loose input focus with blur(): not acceptable
*/
}); });
}) })
.on('blur', function () { .on('blur', function () {
...@@ -1692,4 +1668,191 @@ $(document).ready(function() { ...@@ -1692,4 +1668,191 @@ $(document).ready(function() {
.draw(); .draw();
select_product_from_bc(barcode); select_product_from_bc(barcode);
}); });
}
$(document).ready(function() {
$.ajaxSetup({ headers: { "X-CSRFToken": getCookie('csrftoken') } });
fingerprint = new Fingerprint({canvas: true}).get();
// Load barcodes
get_barcodes();
// Get Route parameter
let pathArray = window.location.pathname.split('/');
let id = pathArray[pathArray.length-1];
// Init couchdb
dbc = new PouchDB(couchdb_dbname),
sync = PouchDB.sync(couchdb_dbname, couchdb_server, {
live: true,
retry: true,
auto_compaction: false
});
sync.on('change', function (info) {
if (info.direction === "pull") {
for (const doc of info.change.docs) {
// Redirect if one of the current order is being modified somewhere else
if (String(doc.id) in orders && orders[doc.id]._rev !== doc._rev) {
alert("Un autre navigateur est en train de modifier cette commande ! Vous allez être redirigé.e.");
back();
}
}
}
}).on('error', function (err) {
if (err.status === 409) {
alert("Une erreur de synchronisation s'est produite, la commande a sûrement été modifiée sur un autre navigateur. Vous allez être redirigé.e.");
back();
}
console.log('erreur sync');
console.log(err);
});
// Disable alert errors from datatables
$.fn.dataTable.ext.errMode = 'none';
// Listen for errors in tables with custom behavior
$('#table_to_process').on('error.dt', function (e, settings, techNote, message) {
var err_msg = message;
try {
var split = message.split(" ");
var row_number = null;
for (var i = 0; i < split.length; i++) {
if (split[i] == "row")
row_number = split[i+1];
}
row_number = row_number.replace(',', '');
var row_data = $('#table_to_process').DataTable()
.row(row_number)
.data();
err_msg += " - Order id: " + row_data.id_po;
err_msg += " - Product: " + row_data.product_id[1];
} catch (e) {
console.log(e);
}
err = {msg: err_msg, ctx: 'datatable: table to_process'};
console.error(err);
report_JS_error(err, 'reception');
});
$('#table_processed').on('error.dt', function (e, settings, techNote, message) {
var err_msg = message;
try {
var split = message.split(" ");
var row_number = null;
for (var i = 0; i < split.length; i++) {
if (split[i] == "row")
row_number = split[i+1];
}
row_number = row_number.replace(',', '');
var row_data = $('#table_processed').DataTable()
.row(row_number)
.data();
err_msg += " - Order id: " + row_data.id_po;
err_msg += " - Product: " + row_data.product_id[1];
} catch (e) {
console.log(e);
}
err = {msg: err_msg, ctx: 'datatable: table processed'};
console.error(err);
report_JS_error(err, 'reception');
});
/* Get order info from couchdb */
// Get order groups
let order_groups = [];
dbc.get("grouped_orders").then((doc) => {
order_groups = doc.groups;
for (let group of order_groups) {
for (group_order_id of group) {
if (group_order_id == id) {
// We're in a group!
group_ids = group;
}
}
}
// if not in group, add current order to group
if (group_ids.length == 0) {
group_ids.push(id);
}
let partners_display_data = [];
dbc.allDocs({
include_docs: true
}).then(function (result) {
// for each order in the group
for (let order_id of group_ids) {
// find order
let order = result.rows.find(el => el.id == 'order_' + order_id);
order = order.doc;
orders[order_id] = order;
// Add each order's already updated and validated products to common list
if (order["updated_products"]) {
updatedProducts = updatedProducts.concat(order["updated_products"]);
}
if (order["valid_products"]) {
validProducts = validProducts.concat(order["valid_products"]);
}
// Prepare data to display in 'partner name' area
partners_display_data.push(order['partner'] + ' du ' + order['date_order']);
}
// Set current reception status: take first order's
reception_status = orders[Object.keys(orders)[0]].reception_status;
// Load saved user comments, get it from first order
user_comments = orders[Object.keys(orders)[0]].user_comments || "";
// Indicate that these orders are used in this navigator
update_distant_orders();
// Fetch orders data
fetch_data();
init_dom(partners_display_data);
})
.catch(function (e) {
let msg = ('message' in e && 'name' in e) ? e.name + ' : ' + e.message : '';
err = {msg, ctx: 'page init - get orders from couchdb', details: e};
console.error(err);
report_JS_error(err, 'reception');
// Should be there, redirect
alert("Erreur au chargement de cette commande. Vous allez être redirigé.");
back();
});
})
.catch(function (e) {
let msg = ('message' in e && 'name' in e) ? e.name + ' : ' + e.message : '';
err = {msg, ctx: 'page init - get grouped orders', details: e};
console.error(err);
report_JS_error(err, 'reception');
// Should be there, redirect
alert("Erreur au chargement de cette commande. Vous allez être redirigé.");
back();
});
}); });
...@@ -16,6 +16,5 @@ urlpatterns = [ ...@@ -16,6 +16,5 @@ urlpatterns = [
url(r'^reception_qtiesValidated', views.reception_qtiesValidated), url(r'^reception_qtiesValidated', views.reception_qtiesValidated),
url(r'^reception_pricesValidated', views.reception_pricesValidated), url(r'^reception_pricesValidated', views.reception_pricesValidated),
# url(r'^update_order_status/([0-9]+)$', views.tmp_update_order_status), # url(r'^update_order_status/([0-9]+)$', views.tmp_update_order_status),
url(r'^po_process_picking$', views.po_process_picking), url(r'^po_process_picking$', views.po_process_picking)
url(r'^save_order_group$', views.save_order_group)
] ]
...@@ -26,22 +26,19 @@ def as_text(value): ...@@ -26,22 +26,19 @@ def as_text(value):
def home(request): def home(request):
"""Page de selection de la commande suivant un fournisseurs""" """Page de selection de la commande suivant un fournisseurs"""
if 'reception' in settings.COUCHDB['dbs']:
context = {
'title': 'Reception',
'merge_orders_pswd': getattr(settings, 'RECEPTION_MERGE_ORDERS_PSWD', 'makeastop'),
'couchdb_server': settings.COUCHDB['url'],
'db': settings.COUCHDB['dbs']['reception'],
'POUCHDB_VERSION': getattr(settings, 'POUCHDB_VERSION', '')
}
template = loader.get_template('reception/index.html')
# Get grouped orders stored on the server return HttpResponse(template.render(context, request))
try: else:
with open('temp/grouped_order.json', 'r') as json_file: return HttpResponse("Need to configure reception couchdb db in settings_secret.py")
saved_groups = json.load(json_file)
except Exception:
saved_groups = []
context = {
'title': 'Reception',
'merge_orders_pswd': settings.RECEPTION_MERGE_ORDERS_PSWD,
'server_stored_groups' : saved_groups
}
template = loader.get_template('reception/index.html')
return HttpResponse(template.render(context, request))
def get_list_orders(request): def get_list_orders(request):
...@@ -75,8 +72,14 @@ def get_list_orders(request): ...@@ -75,8 +72,14 @@ def get_list_orders(request):
def produits(request, id): def produits(request, id):
""" Gets Order details """ """ Gets Order details """
context = {'title': 'Réception des produits', context = {
"TOOLS_SERVER": settings.TOOLS_SERVER} 'title': 'Réception des produits',
"TOOLS_SERVER": settings.TOOLS_SERVER,
'couchdb_server': settings.COUCHDB['url'],
'db': settings.COUCHDB['dbs']['reception'],
'POUCHDB_VERSION': getattr(settings, 'POUCHDB_VERSION', ''),
"DISPLAY_AUTRES": getattr(settings, 'DISPLAY_COL_AUTRES', True),
}
fixed_barcode_prefix = '0490' fixed_barcode_prefix = '0490'
if hasattr(settings, 'RECEPTION_PB'): if hasattr(settings, 'RECEPTION_PB'):
...@@ -131,40 +134,6 @@ def data_validation(request): ...@@ -131,40 +134,6 @@ def data_validation(request):
coop_logger.error("Orders data validation : %s", str(e)) coop_logger.error("Orders data validation : %s", str(e))
return JsonResponse({'error': str(e)}, status=500) return JsonResponse({'error': str(e)}, status=500)
def save_order_group(request):
"""
When an order group is created, save it to force group these orders later.
Raise an error if one of the orders is already in a group.
"""
order_ids = json.loads(request.body.decode())
try:
try:
# Check if any of the orders attempted to be grouped is already in a group
with open('temp/grouped_order.json', 'r') as json_file:
saved_groups = json.load(json_file)
for order_id in order_ids:
for group in saved_groups:
if order_id in group:
# Found in a group, stop
msg = 'One of the orders is already in a group'
return JsonResponse({'message': msg}, status=409)
except Exception:
saved_groups = []
# All good, save group
with open('temp/grouped_order.json', 'w+') as json_file:
saved_groups.append(order_ids)
json.dump(saved_groups, json_file)
msg = 'Group saved'
return JsonResponse({'message': msg})
except Exception as e:
print(str(e))
return JsonResponse({'message': str(e)}, status=500)
def update_orders(request): def update_orders(request):
"""Update orders lines: quantity and unit prices""" """Update orders lines: quantity and unit prices"""
...@@ -177,6 +146,16 @@ def update_orders(request): ...@@ -177,6 +146,16 @@ def update_orders(request):
if request.is_ajax(): if request.is_ajax():
if request.method == 'PUT': if request.method == 'PUT':
data = json.loads(request.body.decode()) data = json.loads(request.body.decode())
if getattr(settings, 'RECEPTION_DATA_BACKUP', True) is True:
try:
file_name = ''
for order_id, order in data['orders'].items():
file_name += str(order_id) + '_'
file_name += data['update_type'] + '_' + str(round(time.time() * 1000)) + '.json'
with open('data/receptions_backup/' + file_name, 'w') as data_file:
json.dump(data, data_file)
except Exception as ef:
coop_logger.error("Enable to save data : %s (data = %s)",str(ef), str(data))
answer_data = {} answer_data = {}
print_labels = True print_labels = True
if hasattr(settings, 'RECEPTION_SHELF_LABEL_PRINT'): if hasattr(settings, 'RECEPTION_SHELF_LABEL_PRINT'):
...@@ -232,6 +211,8 @@ def update_orders(request): ...@@ -232,6 +211,8 @@ def update_orders(request):
except KeyError: except KeyError:
coop_logger.info("No line to update.") coop_logger.info("No line to update.")
except Exception as e:
coop_logger.error("update_orders : %s", str(e))
answer_data[order_id]['order_data'] = order answer_data[order_id]['order_data'] = order
answer_data[order_id]['errors'] = errors answer_data[order_id]['errors'] = errors
...@@ -243,6 +224,7 @@ def update_orders(request): ...@@ -243,6 +224,7 @@ def update_orders(request):
# Remove order's group # Remove order's group
try: try:
# TODO remove from couchdb orders & group (here?)
if os.path.exists('temp/grouped_order.json'): if os.path.exists('temp/grouped_order.json'):
with open('temp/grouped_order.json', 'r') as json_file: with open('temp/grouped_order.json', 'r') as json_file:
saved_groups = json.load(json_file) saved_groups = json.load(json_file)
...@@ -258,7 +240,9 @@ def update_orders(request): ...@@ -258,7 +240,9 @@ def update_orders(request):
except Exception as e: except Exception as e:
# no saved groups # no saved groups
print(str(e)) print(str(e))
# TODO else if first step, save first step data (here?)
else:
coop_logger.error("update_orders errors : %s", str(errors))
rep = JsonResponse(answer_data, safe=False) rep = JsonResponse(answer_data, safe=False)
return rep return rep
...@@ -266,7 +250,7 @@ def update_orders(request): ...@@ -266,7 +250,7 @@ def update_orders(request):
# """ Method used for tests purposes: Reset an order status """ # """ Method used for tests purposes: Reset an order status """
# m = CagetteReception(id_po) # m = CagetteReception(id_po)
# m.update_order_status(id_po, False) # m.update_order_status(id_po, False)
#
# return JsonResponse({'id_po': id_po}) # return JsonResponse({'id_po': id_po})
def save_error_report(request): def save_error_report(request):
...@@ -285,7 +269,7 @@ def save_error_report(request): ...@@ -285,7 +269,7 @@ def save_error_report(request):
orders_partner = "" orders_partner = ""
group_ids = [] group_ids = []
for i, order in enumerate(data['orders']) : for i, order in enumerate(data['orders']) :
# list of temp files: 1 report per reception # list of temp files: 1 report per order & group
data['orders'][i]['temp_file_name'] = "temp/" + order['name'] + "_rapport-reception_temp.xlsx" data['orders'][i]['temp_file_name'] = "temp/" + order['name'] + "_rapport-reception_temp.xlsx"
group_ids.append(order['id']) group_ids.append(order['id'])
...@@ -376,7 +360,7 @@ def save_error_report(request): ...@@ -376,7 +360,7 @@ def save_error_report(request):
# Create report with data from steps 1 & 2 # Create report with data from steps 1 & 2
else: elif data['update_type'] == 'br_valid':
for order in data['orders']: for order in data['orders']:
# Read step 1 data from temp file # Read step 1 data from temp file
data_qties = {} data_qties = {}
...@@ -408,7 +392,8 @@ def save_error_report(request): ...@@ -408,7 +392,8 @@ def save_error_report(request):
# Clear step 1 temp file # Clear step 1 temp file
os.remove(order['temp_file_name']) os.remove(order['temp_file_name'])
except: except:
data_comment_s1 = "Rapport de la première étape absent !" data_comment_s1 = "Données de la première étape absentes !"
# Add data from step 2 # Add data from step 2
data_full = [] data_full = []
error_total = 0 error_total = 0
...@@ -469,6 +454,7 @@ def save_error_report(request): ...@@ -469,6 +454,7 @@ def save_error_report(request):
# no updated products, do nothing # no updated products, do nothing
print("Error while updating products") print("Error while updating products")
print(exp) print(exp)
# Add remaining products, the ones edited only in step 1 # Add remaining products, the ones edited only in step 1
for product in data_qties.values(): for product in data_qties.values():
item = { item = {
......
...@@ -63,10 +63,12 @@ function display_orders(orders) { ...@@ -63,10 +63,12 @@ function display_orders(orders) {
orderable: false, orderable: false,
render: function (data) { render: function (data) {
let res = '<ul>'; let res = '<ul>';
for (p of data) { for (p of data) {
res += `<li>${p.journal_id[1]} : ${p.amount} €</li>` res += `<li>${p.journal_id[1]} : ${p.amount} €</li>`;
} }
res += "</ul>" res += "</ul>";
return res; return res;
} }
} }
...@@ -97,6 +99,7 @@ function get_sales() { ...@@ -97,6 +99,7 @@ function get_sales() {
openModal(); openModal();
var url = "/sales/get_sales"; var url = "/sales/get_sales";
url += '?from=' + encodeURIComponent(from_datepicker.val()); url += '?from=' + encodeURIComponent(from_datepicker.val());
url += '&to=' + encodeURIComponent(to_datepicker.val()); url += '&to=' + encodeURIComponent(to_datepicker.val());
...@@ -189,7 +192,7 @@ $(document).ready(function() { ...@@ -189,7 +192,7 @@ $(document).ready(function() {
enable_validation(); enable_validation();
}); });
$( "#sales_form" ).submit(function( event ) { $("#sales_form").submit(function(event) {
event.preventDefault(); event.preventDefault();
get_sales(); get_sales();
}); });
......
...@@ -10,10 +10,6 @@ def index(request): ...@@ -10,10 +10,6 @@ def index(request):
context = {'title': 'Export de ventes'} context = {'title': 'Export de ventes'}
template = loader.get_template('sales/index.html') template = loader.get_template('sales/index.html')
# m = CagetteSales()
# sales = m.get_sales()
# print(sales)
return HttpResponse(template.render(context, request)) return HttpResponse(template.render(context, request))
def get_sales(request): def get_sales(request):
......
...@@ -1067,16 +1067,18 @@ var shouldCategoryBeShown = function (cat_id) { ...@@ -1067,16 +1067,18 @@ var shouldCategoryBeShown = function (cat_id) {
answer = false; answer = false;
} }
if (typeof cat_nb_pdts != "undefined") { if (typeof cat_nb_pdts != "undefined") {
let list = cat_nb_pdts.list; let list = cat_nb_pdts.list;
let cat_ids = Object.keys(list).map(x => parseInt(x,10)); let cat_ids = Object.keys(list).map(x => parseInt(x, 10));
//cat_ids is now an array of category ids which have product //cat_ids is now an array of category ids which have product
if (cat_ids.indexOf(cat_id) < 0) { if (cat_ids.indexOf(cat_id) < 0) {
// cat_id is corresponding to a category which have no product // cat_id is corresponding to a category which have no product
answer = false; answer = false;
} }
} }
return answer; return answer;
} };
var appendChildrenCatToMenu = function (catdiv, children) { var appendChildrenCatToMenu = function (catdiv, children) {
var ul = catdiv.find('ul'); var ul = catdiv.find('ul');
...@@ -1400,28 +1402,29 @@ var showSentCart = function() { ...@@ -1400,28 +1402,29 @@ var showSentCart = function() {
content = $('<div>'), content = $('<div>'),
table = $('<table>'); table = $('<table>');
let header = $('<tr><th>Article</th><th>Qté</th><th>Prix Total (T.T.C)</th></tr>'); let header = $('<tr><th>Article</th><th>Qté</th><th>Prix Total (T.T.C)</th></tr>');
let bottom_msg = $('<p>').html("<em>Contenu non modifiable.</em>") let bottom_msg = $('<p>').html("<em>Contenu non modifiable.</em>");
table.append(header); table.append(header);
$.each(my_sent_orders, function(i,e) { $.each(my_sent_orders, function(i, e) {
if (e._id == id) { if (e._id == id) {
$.each(e.products, function (j,p) { $.each(e.products, function (j, p) {
let tr = $('<tr>'), let tr = $('<tr>'),
name = $('<td>').text(p.name), name = $('<td>').text(p.name),
qty = $('<td>').text(p.qty), qty = $('<td>').text(p.qty),
total = $('<td>').text(p.total) total = $('<td>').text(p.total);
tr.append(name); tr.append(name);
tr.append(qty); tr.append(qty);
tr.append(total); tr.append(total);
table.append(tr); table.append(tr);
}) });
} }
}) });
content.append(table); content.append(table);
content.append(bottom_msg); content.append(bottom_msg);
displayMsg(content.html()); displayMsg(content.html());
} };
var destroySentCart = function() { var destroySentCart = function() {
var clicked = $(this); var clicked = $(this);
......
...@@ -318,6 +318,7 @@ function fetch_product_from_bc(barcode) { ...@@ -318,6 +318,7 @@ function fetch_product_from_bc(barcode) {
if (p_existing !== null) { if (p_existing !== null) {
without_consent_update_product(p_existing, product.qty); without_consent_update_product(p_existing, product.qty);
return 0; return 0;
} else { } else {
add_product(product); add_product(product);
......
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
lists = {{lists|safe}} var lists = {{lists|safe}}
</script> </script>
<script src="{% static "js/all_common.js" %}?v="></script> <script src="{% static "js/all_common.js" %}?v="></script>
<script src="{% static "js/common.js" %}?v="></script> <script src="{% static "js/common.js" %}?v="></script>
......
...@@ -137,13 +137,16 @@ ...@@ -137,13 +137,16 @@
<div class="col-1"></div> <div class="col-1"></div>
<div class="col-4"> <div class="col-4">
<h1 class="col-4 txtcenter">Qui es-tu ?</h1> <h1 class="col-4 txtcenter">Qui es-tu ?</h1>
<section class="outside_list"> {% if extra_btns_display %}
<a class="btn b_orange" data-next="rattrapage_1" data-type="rattrapage">Je viens faire un rattrapage</a> <section class="outside_list">
{% if ftop_btn_display %} <a class="btn b_orange" data-next="rattrapage_1" data-type="rattrapage">Je viens faire un rattrapage</a>
<a class="btn b_yellow" data-next="rattrapage_1" data-type="volant">Je suis volant</a> {% if ftop_btn_display %}
<a class="btn b_yellow" data-next="rattrapage_1" data-type="volant">Je suis volant</a>
{% endif %}
</section>
{% endif %} {% endif %}
</section>
</div> </div>
<div class="col-1"></div> <div class="col-1"></div>
<div class="col-1"></div> <div class="col-1"></div>
<section id="service_en_cours" class="col-4"> <section id="service_en_cours" class="col-4">
...@@ -153,6 +156,12 @@ ...@@ -153,6 +156,12 @@
<div id="current_shift_members"> <div id="current_shift_members">
</div> </div>
{% if easy_shift_validate %}
<div class="easy_shift_validate">
<p>{{ENTRANCE_EASY_SHIFT_VALIDATE_MSG|safe}}</p>
{% include "members/member_selection.html" %}
</div>
{% endif %}
</section> </section>
</section> </section>
...@@ -227,6 +236,9 @@ ...@@ -227,6 +236,9 @@
<input type="text" placeholder="crop_height" id="img_crop_height" /><br /> <input type="text" placeholder="crop_height" id="img_crop_height" /><br />
<button type="button" id="init_webcam">Cam</button> <button type="button" id="init_webcam">Cam</button>
</section> </section>
<div style="display:none;">
<p id="missed_begin_msg">{{ENTRANCE_MISSED_SHIFT_BEGIN_MSG|safe}}</p>
</div>
<script src="{% static "js/all_common.js" %}?v="></script> <script src="{% static "js/all_common.js" %}?v="></script>
<script src="{% static "js/members.js" %}?v="></script> <script src="{% static "js/members.js" %}?v="></script>
{% endblock %} {% endblock %}
<form id="sm_search_member_form" action="javascript:;" method="post">
<input type="text" id="sm_search_member_input" value="" placeholder="Votre nom ou numéro..." required>
<button type="submit" class="btn--primary" id="sm_search_member_button">Recherche</button>
</form>
<div class="search_member_results_area" style="display:none;">
<div>
<p>Vous êtes (cliquez sur votre nom) :</p>
</div>
<div class="search_member_results"></div>
</div>
\ No newline at end of file
{% extends "base.html" %}
{% load static %}
{% block additionnal_css %}
<link rel="stylesheet" href="{% static 'css/datatables/jquery.dataTables.css' %}">
<link rel="stylesheet" href="{% static 'jquery-ui-1.12.1/jquery-ui.min.css' %}">
<link rel="stylesheet" href="{% static 'css/oders_helper_style.css' %}">
{% endblock %}
{% block additionnal_scripts %}
<script type="text/javascript" src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}?v="></script>
<script type="text/javascript" src="{% static 'js/datatables/jquery.dataTables.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/notify.min.js' %}?v="></script>
{% endblock %}
{% block content %}
<div class="page_body">
<div id="select_order_content" class="page_content txtcenter">
<div class="login_area">
{% include "common/conn_admin.html" %}
</div>
<div id="new_order_area">
<h2>Créer une nouvelle commande</h2>
<div class="txtcenter" id="not_connected_content" style="display:none;">
<p>Vous devez vous connecter avec un compte Odoo pour accéder au module d'aide à la commande.</p>
</div>
<form id="new_order_form" style="display:none;">
<input type="text" id="new_order_name" placeholder="Nom de la commande...">
<button type="submit" class="btn btn--primary">C'est parti !</button>
</form>
</div>
<div id="existing_orders_area" style="display:none;">
<h2>Ou, continuer une commande en cours de création</h2>
<div id="existing_orders"></div>
</div>
</div>
<div id="main_content" class="page_content" style="display:none;">
<div class="actions_buttons_area">
<button type="button" class="btn--danger" id="back_to_order_selection_from_main">
<i class="fas fa-arrow-left"></i>&nbsp; Retour
</button>
<div class="rights_buttons">
<button type="button" class='btn--primary' id="do_inventory" style="display:none;">
Faire un inventaire
</button>
</div>
</div>
<div class="header txtcenter">
<h1>Aide à la commande</h1>
<p id="order_data_container">
Commande : <span class="order_name_container"></span>
<span id="order_data_separator"> | </span>
Total HT : <span id="order_total_value">0</span>
</p>
</div>
<div class="txtcenter" id="order_forms_container">
<form action="javascript:;" id="coverage_form">
<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>
</form>
<form action="javascript:;" id="supplier_form">
<input type="text" name="supplier" id="supplier_input" placeholder="Rechercher un fournisseur par son nom">
<button type="submit" class='btn--primary'>Ajouter le fournisseur</button>
</form>
</div>
<div id="suppliers_container"></div>
<div class="main" style="display:none;">
<div class="table_area">
<table id="products_table" class="display" cellspacing="0" width="100%"></table>
</div>
</div>
<div id="main_content_footer" style="display:none;">
<div id="footer_actions">
<div class="add_product_container">
<div id="product_form_container">
<form action="javascript:;" id="product_form">
<input type="text" name="article" id="product_input" placeholder="Rechercher un article">
<button type="submit" class='btn--primary'>Ajouter l'article</button>
</form>
</div>
</div>
<button type="button" class='btn--primary' id="create_orders">
Générer les commandes
</button>
</div>
</div>
</div>
<div id="orders_created" class="page_content" style="display:none;">
<div class="actions_buttons_area">
<button type="button" class="btn--danger" id="back_to_order_selection_from_orders_created">
<i class="fas fa-arrow-left"></i>&nbsp; Retour
</button>
</div>
<div class="order_created_header txtcenter">
<h2>Commandes créées !</h2>
</div>
<div id="created_orders_area"></div>
<br/><br/><hr/><br/>
<div class="mail_example_container">
<p class="mail_type_text">Mail type :</p>
<div class="mail_example">
Objet : Cde Cagette JJ/MM<br/>
<br/>
Bonjour XXXXXXX,<br/>
<br/>
Voici la commande de La Cagette pour le XX/XX/XX.<br/>
<br/>
Merci d'avance,<br/>
Bonne journée
</div>
</div>
</div>
<div id="templates" style="display:none;">
<div id="supplier_pill_template">
<div class="pill supplier_pill">
<div class="supplier_name_container">
<span class="pill_supplier_name"></span>
<i class="fas fa-times remove_supplier_icon"></i>
</div>
<div class="supplier_total_value_container">
Total: <span class="supplier_total_value">0</span>
</div>
</div>
</div>
<div id="order_pill_template">
<div class="pill order_pill btn btn--primary">
<span class="pill_order_name"></span>
</div>
</div>
<div id="new_order_item_template">
<div class="new_order_item">
<h3 class="new_order_supplier_name"></h3>
<h3 class="new_order_po"></h3>
<h4 class="new_order_date_planned"></h4>
<div class='download_order_file'>
<i class="fas fa-spinner fa-spin download_order_file_loading"></i>
<a class='btn--success download_order_file_button' style="display:none;" href="#">
Télécharger le fichier de commande
</a>
</div>
</div>
</div>
<div id="modal_order_access">
<h3>Attention !</h3>
<br/>
<p class="order_modified_msg">
Un autre navigateur a modifié cette commande il y a <span class="order_last_update"></span>.
</p><br/>
<p>
Si quelqu'un d'autre que vous est à l'origine de la modification et que celle-ci est récente,
nous conseillons fortement de ne pas accéder à la commande afin d'éviter les conflits.
</p><br/>
<p>Voulez-vous quand même y accéder ?</p>
<hr/>
</div>
<div id="modal_remove_supplier">
<h3>Attention !</h3>
<p>
Vous vous apprêtez à supprimer le fournisseur <span class="supplier_name"></span> de la sélection.<br/>
Les produits associés uniquement à ce fournisseur seront supprimés du tableau.<br/>
Les données renseignées dans la colonne de ce fournisseur seront perdues.
</p>
<p>Êtez-vous sûr ?</p>
<hr/>
</div>
<div id="modal_attach_product_to_supplier">
<h3>Attention !</h3>
<p>
Vous vous apprêtez à associer le produit <span class="product_name"></span> au fournisseur <span class="supplier_name"></span>.<br/>
</p>
<br/>
<div class="modal_input_area">
<span class="modal_input_label">Prix du produit chez ce fournisseur: </span>
<div class="modal_input_container">
<input type="number" class="modal_input new_product_supplier_price" >
</div>
</div>
<div class="modal_input_area">
<span class="modal_input_label">Colisage chez ce fournisseur: </span>
<div class="modal_input_container">
<input type="number" class="modal_input new_product_supplier_package_pty">
</div>
</div>
<br/>
<p>
L'association sera sauvegardée dès que vous aurez cliqué sur "Valider".<br/>
</p>
<p>Êtez-vous sûr ?</p>
<hr/>
</div>
<div id="modal_create_inventory">
<p>
Vous vous apprêtez à créer un inventaire de <span class="inventory_products_count"></span> produits.
</p>
<p>Êtez-vous sûr ?</p>
<hr/>
</div>
<div id="modal_product_npa">
<h3>Attention !</h3>
<p>
Vous vous apprêtez à passer le produit <span class="product_name"></span> en <span class="product_npa"></span>.<br/>
Dès que vous aurez cliqué sur "Valider", le produit sera retiré du tableau et l'information sera enregistrée dans Odoo.
</p>
<p>Êtez-vous sûr ?</p>
<hr/>
</div>
<div id="modal_create_order">
<h3>Dernière étape...</h3>
<br/>
<p>
Vous vous apprêtez à générer les commandes à partir des données rentrées dans le tableau.
</p>
<p>
Vous pouvez rentrer une date de livraison prévue pour chaque fournisseur (optionnel, la date par défaut sera celle de demain).
</p>
<br/>
<div class="suppliers_date_planned_area"></div>
<br/>
<hr/>
</div>
<div id="modal_create_order__supplier_date_planned">
<div class="modal_input_area">
<span class="modal_input_label supplier_name"></span>
<div class="modal_input_container">
<input type="text" class="modal_input supplier_date_planned" placeholder="Date de livraison prévue">
</div>
</div>
</div>
</div>
</div>
<script src="{% static "js/pouchdb.min.js" %}"></script>
<script type="text/javascript">
var couchdb_dbname = '{{db}}';
var couchdb_server = '{{couchdb_server}}' + couchdb_dbname;
var odoo_server = '{{odoo_server}}';
</script>
<script src="{% static "js/all_common.js" %}?v="></script>
<script type="text/javascript" src="{% static 'js/orders_helper.js' %}?v="></script>
{% endblock %}
...@@ -11,7 +11,6 @@ ...@@ -11,7 +11,6 @@
<script type="text/javascript" src="{% static 'js/datatables/dataTables.plugins.js' %}"></script> <script type="text/javascript" src="{% static 'js/datatables/dataTables.plugins.js' %}"></script>
<script type="text/javascript" src="{% static 'js/moment.min.js' %}"></script> <script type="text/javascript" src="{% static 'js/moment.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/datatables/dataTables.plugin.moment_sorting.js' %}"></script> <script type="text/javascript" src="{% static 'js/datatables/dataTables.plugin.moment_sorting.js' %}"></script>
<script type="text/javascript" src="{% static 'js/reception_index.js' %}?v="></script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
...@@ -31,7 +30,6 @@ ...@@ -31,7 +30,6 @@
<div id="groups_items"></div> <div id="groups_items"></div>
</div> </div>
<br>
<div id="grouped_action"> <div id="grouped_action">
<button type="button" class='btn--primary' id='group_action' onclick="group_action()" hidden>Compter les produits des commandes sélectionnées</button> <button type="button" class='btn--primary' id='group_action' onclick="group_action()" hidden>Compter les produits des commandes sélectionnées</button>
</div> </div>
...@@ -48,13 +46,30 @@ ...@@ -48,13 +46,30 @@
<p>Êtez-vous sûr ?</p> <p>Êtez-vous sûr ?</p>
<hr /> <hr />
</div> </div>
<div id="modal_order_access">
<h3>Attention !</h3>
<br/>
<p class="order_modified_msg">
Un autre navigateur a commencé à réceptionner cette commande il y a <span class="order_last_update"></span>.
</p><br/>
<p>
Si quelqu'un d'autre que vous est à l'origine de cette opération et que celle-ci est récente,
nous conseillons fortement de ne pas accéder à la commande afin d'éviter les conflits.
</p><br/>
<p>Voulez-vous quand même y accéder ?</p>
<hr/>
</div>
</div> </div>
<br/> <br/>
<script src="{% static "js/all_common.js" %}?v="></script> <script src="{% static "js/all_common.js" %}?v="></script>
<script src="{% static 'js/pouchdb.min'|add:POUCHDB_VERSION|add:'.js' %}"></script>
<script type="text/javascript"> <script type="text/javascript">
var merge_orders_pswd = '{{merge_orders_pswd}}'; var merge_orders_pswd = '{{merge_orders_pswd}}';
var server_stored_groups = {{server_stored_groups}}; var couchdb_dbname = '{{db}}';
var couchdb_server = '{{couchdb_server}}' + couchdb_dbname;
</script> </script>
<script src="{% static "js/common.js" %}?v="></script> <script type="text/javascript" src="{% static 'js/reception_index.js' %}?v="></script>
{% endblock %} {% endblock %}
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
{% endif %} {% endif %}
<div class="page_body"> <div class="page_body">
<header class="flex-container"> <header class="flex-container">
<button class="right btn--danger" onclick="back()">Retour</button> <button class="right btn--danger" id="back_button">Retour</button>
<div class="w33 arrow-block txtcenter" id="header_step_one"> <div class="w33 arrow-block txtcenter" id="header_step_one">
<h4 id="header_step_one_content">Produits à compter </h4> <h4 id="header_step_one_content">Produits à compter </h4>
</div> </div>
...@@ -180,9 +180,14 @@ ...@@ -180,9 +180,14 @@
</div> </div>
<br/> <br/>
</div> </div>
<script src="{% static 'js/pouchdb.min'|add:POUCHDB_VERSION|add:'.js' %}"></script>
<script type="text/javascript"> <script type="text/javascript">
var tools_server = '{{TOOLS_SERVER}}' var tools_server = '{{TOOLS_SERVER}}'
var fixed_barcode_prefix = '{{FIXED_BARCODE_PREFIX}}' var fixed_barcode_prefix = '{{FIXED_BARCODE_PREFIX}}'
var couchdb_dbname = '{{db}}';
var couchdb_server = '{{couchdb_server}}' + couchdb_dbname;
var display_autres = "{{DISPLAY_AUTRES}}";
</script> </script>
<script src="{% static "js/all_common.js" %}?v="></script> <script src="{% static "js/all_common.js" %}?v="></script>
<script src='{% static "js/barcodes.js" %}?v='></script> <script src='{% static "js/barcodes.js" %}?v='></script>
......
...@@ -32,8 +32,8 @@ ...@@ -32,8 +32,8 @@
<li>vous avez raté un service et vous ne l'avez pas rattrapé dans les 4 semaines qui ont suivis,</li> <li>vous avez raté un service et vous ne l'avez pas rattrapé dans les 4 semaines qui ont suivis,</li>
<li>vous avez été désinscrit.e pour avoir raté 3 services et venez d'être réinscrit.e.</li> <li>vous avez été désinscrit.e pour avoir raté 3 services et venez d'être réinscrit.e.</li>
</ul> </ul>
<p>Heureusement, vous pouvez demander une extension !</p> <p>Heureusement, vous pouvez demander un délai !</p>
<p>Une extension vous donne un délais de quatre semaines supplémentaires pour rattraper le service manqué (ou les deux services manqués).Vous pouvez demander jusqu'à 6 extensions d'affilé.</p> <p>Cela vous donne quatre semaines supplémentaires pour rattraper le service manqué (ou les deux services manqués).Vous pouvez demander jusqu'à 6 délais d'affilé.</p>
<p>En pratique, ça signifie que vous avez jusqu'à 24 semaines pour rattraper un service raté !</p> <p>En pratique, ça signifie que vous avez jusqu'à 24 semaines pour rattraper un service raté !</p>
<p>Si vous ratez un troisième service, vous serez désinscrit.e, vous n'aurez plus accès au magasin et votre place sera libérée pour un.e autre membre.</p> <p>Si vous ratez un troisième service, vous serez désinscrit.e, vous n'aurez plus accès au magasin et votre place sera libérée pour un.e autre membre.</p>
<br/> <br/>
...@@ -44,12 +44,12 @@ ...@@ -44,12 +44,12 @@
Merci de passer au magasin pour régulariser la situation. Merci de passer au magasin pour régulariser la situation.
</strong> </strong>
{% else %} {% else %}
Vous pouvez demander une extension en cliquant ici : <a class='btn--primary' onclick="request_delay()">Demander une extension</a> Vous pouvez demander un délai en cliquant ici : <a class='btn--primary' onclick="request_delay()">Demander un délai</a>
{% endif %} {% endif %}
</p> </p>
</div> </div>
<div id="no_delay" hidden> <div id="no_delay" hidden>
<p>Vous ne pouvez plus demander d'extensions, merci de passer au magasin pour vous régulariser.</p> <p>Vous ne pouvez plus demander de délai, merci de passer au magasin pour vous régulariser.</p>
</div> </div>
<p> <p>
</div> </div>
......
...@@ -83,16 +83,7 @@ ...@@ -83,16 +83,7 @@
<hr /> <hr />
<section class="set_member_to_movement"> <section class="set_member_to_movement">
<p>Avant de valider l'opération, merci de nous dire qui vous êtes :</p> <p>Avant de valider l'opération, merci de nous dire qui vous êtes :</p>
<form id="sm_search_member_form" action="javascript:;" method="post"> {% include "members/member_selection.html" %}
<input type="text" id="sm_search_member_input" value="" placeholder="Votre nom ou numéro..." required>
<button type="submit" class="btn--primary" id="sm_search_member_button">Recherche</button>
</form>
<div class="search_member_results_area" style="display:none;">
<div>
<p>Vous êtes (cliquez sur votre nom) :</p>
</div>
<div class="search_member_results"></div>
</div>
</section> </section>
<hr /> <hr />
</div> </div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment