Commit 07250cbf by Damien Moulard

fix merge conflict with dev

parents 15c034c5 7a0149bc
Pipeline #1283 passed with stage
in 1 minute 39 seconds
## Add odoo user login button
- In the template, include "conn_admin.html" as following :
```
{% block content %}
{% include "common/conn_admin.html" %}
```
- In the JS code, use the following pattern (for example) :
```
$(document).ready(function() {
if (coop_is_connected()) {
show content
}
}
```
...@@ -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'
...@@ -111,3 +112,5 @@ DISPLAY_COL_AUTRES = False ...@@ -111,3 +112,5 @@ DISPLAY_COL_AUTRES = False
# Should block service exchange if old service is happening in less than 24h # Should block service exchange if old service is happening in less than 24h
BLOCK_SERVICE_EXCHANGE_24H_BEFORE = True BLOCK_SERVICE_EXCHANGE_24H_BEFORE = True
# URL to the metabase dashboard for orders helper
ORDERS_HELPER_METABASE_URL = "https://metabase.lacagette-coop.fr/dashboard/16"
...@@ -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 = {
...@@ -162,6 +167,7 @@ function canSearch() { ...@@ -162,6 +167,7 @@ function canSearch() {
function search_member(force_search = false) { function search_member(force_search = false) {
chars = []; // to prevent false "as barcode-reader" input 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();
...@@ -279,7 +285,15 @@ function fill_service_entry(s) { ...@@ -279,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('');
...@@ -346,8 +360,8 @@ function get_service_entry_data() { ...@@ -346,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) {
...@@ -667,6 +681,73 @@ html_elts.image_medium.on('click', function() { ...@@ -667,6 +681,73 @@ html_elts.image_medium.on('click', function() {
} }
}); });
function ask_for_easy_shift_validation() {
//alert("operator = " + JSON.stringify(operator))
msg = "<p>Je suis bien " + operator.name + "<br/> et <br/>je valide mon service 'Comité' </p>";
openModal(msg, function() {
try {
post_form(
'/members/easy_validate_shift_presence',
{
coop_id: operator.id
},
function(err, result) {
if (!err) {
alert("1 point volant vient d'être ajouté.");
clean_search_for_easy_validate_zone();
closeModal();
} else {
if (typeof (err.responseJSON) != "undefined"
&& typeof (err.responseJSON.error) != "undefined") {
alert(err.responseJSON.error);
} else {
console.log(err);
}
}
}
);
} catch (e) {
console.log(e);
}
}, 'Confirmer');
}
// Display the members from the search result (copied from stock_movements)
function display_possible_members() {
$('.search_member_results_area').show();
$('.search_member_results').empty();
if (members_search_results.length > 0) {
for (member of members_search_results) {
let btn_classes = "btn";
if (operator != null && operator.id == member.id) {
btn_classes = "btn--success";
}
// Display results (possible members) as buttons
var member_button = '<button class="' + btn_classes + ' btn_member" member_id="'
+ member.id + '">'
+ member.barcode_base + ' - ' + member.name
+ '</button>';
$('.search_member_results').append(member_button);
// Set action on click on a member button
$('.btn_member').on('click', function() {
for (member of members_search_results) {
if (member.id == $(this).attr('member_id')) {
operator = member;
// Enable validation button when operator is selected
ask_for_easy_shift_validation();
break;
}
}
display_possible_members();
});
}
} else {
$('.search_member_results').html('<p><i>Aucun résultat ! Faites-vous partie d\'un comité ? <br/> Si oui, vérifiez la recherche..</i></p>');
}
}
$(document).ready(function() { $(document).ready(function() {
var shopping_entry_btn = $('a[data-next="shopping_entry"]'); var shopping_entry_btn = $('a[data-next="shopping_entry"]');
...@@ -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() {
......
...@@ -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()})
......
...@@ -258,6 +258,11 @@ class Order(models.Model): ...@@ -258,6 +258,11 @@ class Order(models.Model):
} }
for line in order_lines: for line in order_lines:
product_line_name = line["name"]
if "product_code" in line and line["product_code"] is not False:
product_code = str(line["product_code"])
product_line_name = "[" + product_code + "] " + product_line_name
order_data["order_line"].append( order_data["order_line"].append(
[ [
0, 0,
...@@ -267,7 +272,7 @@ class Order(models.Model): ...@@ -267,7 +272,7 @@ class Order(models.Model):
"price_policy": "uom", "price_policy": "uom",
"indicative_package": True, "indicative_package": True,
"product_id": line["product_variant_ids"][0], "product_id": line["product_variant_ids"][0],
"name": line["name"], "name": product_line_name,
"date_planned": date_planned, "date_planned": date_planned,
"account_analytic_id": False, "account_analytic_id": False,
"product_qty_package":line["product_qty_package"], "product_qty_package":line["product_qty_package"],
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
position: relative; position: relative;
} }
.page_content { .page_content, .login_area {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 5px 30px 5px 30px; padding: 7px 30px 7px 30px;
margin: 0 10px 5px 10px; margin: 0 10px 5px 10px;
} }
...@@ -31,6 +31,26 @@ ...@@ -31,6 +31,26 @@
background-color: #a1a2a3; background-color: #a1a2a3;
} }
.link_as_button:hover {
text-decoration: none;
color: white;
}
.link_as_button:active {
text-decoration: none;
color: white;
}
.link_as_button:focus {
text-decoration: none;
color: white;
}
.remove_order_modal_text {
font-size: 2rem;
}
.remove_order_name {
font-weight: bold;
}
/* - Order selection screen */ /* - Order selection screen */
#new_order_area { #new_order_area {
margin-bottom: 40px; margin-bottom: 40px;
...@@ -50,6 +70,25 @@ ...@@ -50,6 +70,25 @@
padding-top: 15px; padding-top: 15px;
} }
.order_pill {
flex-direction: row;
}
.pill_order_name {
flex: 3 0 auto;
}
.remove_order_icon {
flex: 0 1 auto;
color: #912930;
margin-left: 5px;
cursor: pointer;
z-index: 2;
transform: scale(1.2);
}
.remove_order_icon:hover {
color: #e62720;
}
.order_last_update { .order_last_update {
font-weight: bold; font-weight: bold;
} }
...@@ -82,6 +121,40 @@ ...@@ -82,6 +121,40 @@
justify-content: flex-start; justify-content: flex-start;
} }
.right_action_buttons {
display: flex;
}
#actions_buttons_wrapper {
position: relative;
margin-right: 5px;
}
#toggle_action_buttons {
width: 250px;
position: relative;
}
.toggle_action_buttons_icon {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 15px;
}
#actions_buttons_container {
position: absolute;
display: flex;
flex-direction: column;
width: 250px;
display: none;
}
.action_button {
width: 100%;
min-height: 45px;
}
/* -- Order data */ /* -- Order data */
#order_data_container { #order_data_container {
font-size: 1.8rem; font-size: 1.8rem;
...@@ -92,17 +165,27 @@ ...@@ -92,17 +165,27 @@
} }
#order_forms_container { #order_forms_container {
margin-top: 30px; margin-top: 20px;
display: flex; display: flex;
flex-wrap: wrap;
justify-content: space-evenly; justify-content: space-evenly;
} }
.order_form_item {
margin-top: 10px;
}
#supplier_input { #supplier_input {
width: 350px; width: 350px;
border-radius: 3px; border-radius: 3px;
} }
#date_planned_input, #coverage_days_input { #stats_date_period_select {
margin-left: 5px;
min-width: 200px;
}
#date_planned_input, #coverage_days_input, #stats_date_period_select {
border-radius: 3px; border-radius: 3px;
} }
...@@ -144,13 +227,26 @@ ...@@ -144,13 +227,26 @@
width: 100px; width: 100px;
} }
.product_ref_input {
padding: .5rem .5rem;
}
.supplier_package_qty { .supplier_package_qty {
font-style: italic; font-style: italic;
font-size: 1.3rem; font-size: 1.3rem;
} }
.product_not_from_supplier { .product_not_from_supplier {
background-color: #e7e9ed; background-color: #e8ebf0;
cursor: pointer;
}
.product_not_from_supplier:hover {
background-color: #d3d7db;
}
.product_ref_cell:hover {
background-color: #d3d7db;
cursor: pointer; cursor: pointer;
} }
...@@ -162,6 +258,18 @@ ...@@ -162,6 +258,18 @@
cursor: pointer; cursor: pointer;
} }
.focused_line {
background-color: #76cf71 !important;
}
.dataTables_scrollHead {
position: sticky !important;
position: -webkit-sticky !important;
top: 0;
z-index: 3;
background-color: white;
}
/* -- Footer */ /* -- Footer */
#main_content_footer { #main_content_footer {
...@@ -181,19 +289,24 @@ ...@@ -181,19 +289,24 @@
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
margin: 30px 0 20px 0; margin: 30px 0 20px 0;
position: -webkit-sticky;
position: sticky;
top: 140px;
z-index: 5;
} }
.supplier_pill { .supplier_pill {
background-color: #e7e9edc5; background-color: #a0daff;
border: 1px solid black; border: 1px solid #6ea8cc;
} }
.pill_supplier_name { .pill_supplier_name {
font-weight: bold; font-weight: bold;
} }
.supplier_total_value_container { .supplier_data {
font-size: 1.5rem; font-size: 1.5rem;
display: flex;
} }
.remove_supplier_icon { .remove_supplier_icon {
...@@ -249,19 +362,6 @@ ...@@ -249,19 +362,6 @@
margin-top: 10px; 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 { #recap_delivery_date {
font-weight: bold; font-weight: bold;
} }
......
...@@ -16,6 +16,7 @@ var dbc = null, ...@@ -16,6 +16,7 @@ var dbc = null,
order_doc = { order_doc = {
_id: null, _id: null,
coverage_days: null, coverage_days: null,
stats_date_period: '',
last_update: { last_update: {
timestamp: null, timestamp: null,
fingerprint: null fingerprint: null
...@@ -42,6 +43,7 @@ function reset_data() { ...@@ -42,6 +43,7 @@ function reset_data() {
order_doc = { order_doc = {
_id: null, _id: null,
coverage_days: null, coverage_days: null,
stats_date_period: '',
last_update : { last_update : {
timestamp: null, timestamp: null,
fingerprint: null fingerprint: null
...@@ -81,6 +83,39 @@ function dates_diff(date1, date2) { ...@@ -81,6 +83,39 @@ function dates_diff(date1, date2) {
return diff; return diff;
} }
/**
* Compute the date from which to calculate stats of sells,
* depending on the selected parameter.
*
* @returns String value of the date, ISO format
*/
function _compute_stats_date_from() {
let val = '';
if (order_doc.stats_date_period !== '') {
let date = new Date();
switch (order_doc.stats_date_period) {
case '1week':
date.setDate(date.getDate() - 7);
break;
case '2weeks':
date.setDate(date.getDate() - 14);
break;
default:
break;
}
let day = ("0" + date.getDate()).slice(-2);
let month = ("0" + (date.getMonth() +1)).slice(-2);
let year = date.getFullYear();
val = `${year}-${month}-${day}`;
}
return val;
}
/* - PRODUCTS */ /* - PRODUCTS */
/** /**
...@@ -110,15 +145,21 @@ function add_product() { ...@@ -110,15 +145,21 @@ function add_product() {
return -1; return -1;
} }
let data = {
pids: [product.tpl_id],
stats_from: _compute_stats_date_from()
};
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: '/products/get_product_for_order_helper', url: '/products/get_product_for_order_helper',
data: JSON.stringify([product.tpl_id]), data: JSON.stringify(data),
dataType:"json", dataType:"json",
traditional: true, traditional: true,
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
success: function(data) { success: function(data) {
let res = data.products[0]; let res = data.products[0];
if (typeof res.id != "undefined") { if (typeof res.id != "undefined") {
res.suppliersinfo = []; res.suppliersinfo = [];
res.default_code = ' '; res.default_code = ' ';
...@@ -148,29 +189,31 @@ function add_product() { ...@@ -148,29 +189,31 @@ function add_product() {
* Set the computed qty for the first supplier only. * Set the computed qty for the first supplier only.
*/ */
function compute_products_coverage_qties() { function compute_products_coverage_qties() {
for (const [ if (order_doc.coverage_days != null) {
key, for (const [
product key,
] of Object.entries(products)) { product
if ('suppliersinfo' in product && product.suppliersinfo.length > 0) { ] of Object.entries(products)) {
let purchase_qty_for_coverage = null; 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; // Durée couverture produit = (stock + qté entrante + qté commandée ) / conso quotidienne
const incoming_qty = product.incoming_qty; const stock = product.qty_available;
const daily_conso = product.daily_conso; 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; 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; // 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); // 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; // Set qty to purchase for first supplier only
products[key].suppliersinfo[0].qty = purchase_package_qty_for_coverage;
}
} }
} }
} }
...@@ -187,17 +230,21 @@ function check_products_data() { ...@@ -187,17 +230,21 @@ function check_products_data() {
"Vérfication des informations produits...", "Vérfication des informations produits...",
{ {
globalPosition:"top left", globalPosition:"top left",
className: "warning" className: "info"
} }
); );
clicked_order_pill.find('.pill_order_name').empty().append(`<i class="fas fa-spinner fa-spin"></i>`); if (clicked_order_pill != null) {
clicked_order_pill.find('.pill_order_name').empty()
.append(`<i class="fas fa-spinner fa-spin"></i>`);
}
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: '/orders/get_supplier_products', url: '/orders/get_supplier_products',
data: { data: {
sids: suppliers_id sids: suppliers_id,
stats_from: _compute_stats_date_from()
}, },
dataType:"json", dataType:"json",
traditional: true, traditional: true,
...@@ -206,19 +253,24 @@ function check_products_data() { ...@@ -206,19 +253,24 @@ function check_products_data() {
for (let product of data.res.products) { for (let product of data.res.products) {
const p_index = products.findIndex(p => p.id == product.id); const p_index = products.findIndex(p => p.id == product.id);
// Override products data with new data (without suppliersinfo so we don't override qty) if (p_index === -1) {
const updated_suppliersinfo = product.suppliersinfo; // Add product if it wasn't fetched before (made available since last access to order)
delete product.suppliersinfo; products.push(product);
products[p_index] = { ...products[p_index], ...product }; } else {
// Save old product suppliersinfo to keep user qty inputs
// Update suppliers info const old_suppliersinfo = [...products[p_index].suppliersinfo];
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); // Update product data
if (updated_psi !== undefined) { products[p_index] = product;
products[p_index].suppliersinfo[psi_index].package_qty = updated_psi.package_qty;
products[p_index].suppliersinfo[psi_index].price = updated_psi.price;
}
// Re-set qties
for (let psi_index in products[p_index].suppliersinfo) {
const old_psi = old_suppliersinfo.find(psi => psi.supplier_id == products[p_index].suppliersinfo[psi_index].supplier_id);
if (old_psi !== undefined && old_psi.qty !== undefined) {
products[p_index].suppliersinfo[psi_index].qty = old_psi.qty;
}
}
} }
} }
...@@ -244,6 +296,73 @@ function check_products_data() { ...@@ -244,6 +296,73 @@ function check_products_data() {
}); });
} }
/**
* Update the product internal reference ('default_code')
*
* @param {HTMLElement} input_el
* @param {int} p_id
* @param {int} p_index
*/
function update_product_ref(input_el, p_id, p_index) {
const val = $(input_el).val();
const existing_val = products[p_index].default_code.replace("[input]", "");
products[p_index].default_code = val;
const row = $(input_el).closest('tr');
const new_row_data = prepare_datatable_data([p_id])[0];
products_table.row(row).data(new_row_data)
.draw();
$('#products_table')
.off('blur', 'tbody .product_ref_input')
.off('keypress', 'tbody .product_ref_input');
// Update in backend if value changed
if (existing_val !== val) {
const data = {
'product_tmpl_id': p_id,
'default_code': val
};
// Send request to create association
$.ajax({
type: "POST",
url: "/products/update_product_internal_ref",
dataType: "json",
traditional: true,
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
success: () => {
update_cdb_order();
$(".actions_buttons_area .right_action_buttons").notify(
"Référence sauvegardée !",
{
elementPosition:"bottom right",
className: "success",
arrowShow: false
}
);
},
error: function(data) {
let msg = "erreur serveur lors de la sauvegarde de la référence";
msg += ` (product_tmpl_id: ${product.id}`;
err = {msg: msg, ctx: 'update_product_ref'};
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 sauvegarde de la référence dans Odoo. Veuillez recharger la page et ré-essayer plus tard.');
}
});
}
}
/* - SUPPLIERS */ /* - SUPPLIERS */
...@@ -255,9 +374,9 @@ function check_products_data() { ...@@ -255,9 +374,9 @@ function check_products_data() {
function add_supplier() { function add_supplier() {
const user_input = $("#supplier_input").val(); 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); let supplier = suppliers_list.find(s => s.display_name === user_input);
// Check if user input is a valid supplier
if (supplier === undefined) { if (supplier === undefined) {
alert("Le fournisseur renseigné n'est pas valide.\n" alert("Le fournisseur renseigné n'est pas valide.\n"
+ "Veuillez sélectionner un fournisseur dans la liste déroulante."); + "Veuillez sélectionner un fournisseur dans la liste déroulante.");
...@@ -275,20 +394,22 @@ function add_supplier() { ...@@ -275,20 +394,22 @@ function add_supplier() {
openModal(); openModal();
supplier.total_value = 0;
selected_suppliers.push(supplier);
let url = "/orders/get_supplier_products";
url += "?sids=" + encodeURIComponent(supplier.id);
// Fetch supplier products // Fetch supplier products
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: url, url: "/orders/get_supplier_products",
data: {
sids: [supplier.id],
stats_from: _compute_stats_date_from()
},
dataType:"json", dataType:"json",
traditional: true, traditional: true,
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
success: function(data) { success: function(data) {
supplier.total_value = 0;
supplier.total_packages = 0;
selected_suppliers.push(supplier);
save_supplier_products(supplier, data.res.products); save_supplier_products(supplier, data.res.products);
update_main_screen(); update_main_screen();
$("#supplier_input").val(""); $("#supplier_input").val("");
...@@ -380,6 +501,7 @@ function save_supplier_product_association(product, supplier, cell) { ...@@ -380,6 +501,7 @@ function save_supplier_product_association(product, supplier, cell) {
product.suppliersinfo.push({ product.suppliersinfo.push({
supplier_id: supplier.id, supplier_id: supplier.id,
package_qty: package_qty, package_qty: package_qty,
product_code: false,
price: price price: price
}); });
...@@ -398,8 +520,9 @@ function save_supplier_product_association(product, supplier, cell) { ...@@ -398,8 +520,9 @@ function save_supplier_product_association(product, supplier, cell) {
closeModal(); closeModal();
}, },
error: function(data) { error: function(data) {
let msg = "erreur serveur lors de la sauvegarde de l'association product/supplier". let msg = "erreur serveur lors de la sauvegarde de l'association product/supplier";
msg += ` (product_tmpl_id: ${product.id}; supplier_id: ${supplier.id})`;
msg += ` (product_tmpl_id: ${product.id}; supplier_id: ${supplier.id})`;
err = {msg: msg, ctx: 'save_supplier_product_association'}; err = {msg: msg, ctx: 'save_supplier_product_association'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') { if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
...@@ -416,6 +539,59 @@ function save_supplier_product_association(product, supplier, cell) { ...@@ -416,6 +539,59 @@ function save_supplier_product_association(product, supplier, cell) {
} }
/** /**
* Send to server the deletion of association product-supplier
*
* @param {object} product
* @param {object} supplier
*/
function end_supplier_product_association(product, supplier) {
openModal();
const data = {
product_tmpl_id: product.id,
supplier_id: supplier.id
};
// Send request to create association
$.ajax({
type: "POST",
url: "/orders/end_supplier_product_association",
dataType: "json",
traditional: true,
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
success: () => {
// Remove relation locally
let p_index = products.findIndex(p => p.id == product.id);
let psi_index = product.suppliersinfo.findIndex(psi => psi.supplier_id == supplier.id);
products[p_index].suppliersinfo.splice(psi_index, 1);
// Update table
display_products();
update_cdb_order();
closeModal();
},
error: function(data) {
let msg = "erreur serveur lors de la suppression de l'association product/supplier".
msg += ` (product_tmpl_id: ${product.id}; supplier_id: ${supplier.id})`;
err = {msg: msg, ctx: 'end_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 suppression de l\'association. Veuillez ré-essayer plus tard.');
}
});
return 0;
}
/**
* When products are fetched, save them and the relation with the supplier. * When products are fetched, save them and the relation with the supplier.
* If product already saved, add the supplier to its suppliers list. * If product already saved, add the supplier to its suppliers list.
* Else, add product with supplier. * Else, add product with supplier.
...@@ -430,11 +606,13 @@ function save_supplier_products(supplier, new_products) { ...@@ -430,11 +606,13 @@ function save_supplier_products(supplier, new_products) {
if (index === -1) { if (index === -1) {
products.push(np); products.push(np);
} else { } else {
// Prevent adding ducplicate supplierinfo // Prevent adding duplicate supplierinfo
let index_existing_supplierinfo = products[index].suppliersinfo.findIndex(psi => psi.supplier_id == supplier.id); let index_existing_supplierinfo = products[index].suppliersinfo.findIndex(psi => psi.supplier_id == supplier.id);
if (index_existing_supplierinfo === -1) { if (index_existing_supplierinfo === -1) {
np_supplierinfo = np.suppliersinfo[0]; // Find the right supplierinfo in new product
let np_supplierinfo = np.suppliersinfo.find(psi => psi.supplier_id == supplier.id);
products[index].suppliersinfo.push(np_supplierinfo); products[index].suppliersinfo.push(np_supplierinfo);
} }
} }
...@@ -479,14 +657,20 @@ function _compute_total_values_by_supplier() { ...@@ -479,14 +657,20 @@ function _compute_total_values_by_supplier() {
// Reinit // Reinit
for (let s of selected_suppliers) { for (let s of selected_suppliers) {
s.total_value = 0; s.total_value = 0;
s.total_packages = 0;
} }
for (let p of products) { for (let p of products) {
for (let supinfo of p.suppliersinfo) { for (let supinfo of p.suppliersinfo) {
let supplier_index = selected_suppliers.findIndex(s => s.id == supinfo.supplier_id); let supplier_index = selected_suppliers.findIndex(s => s.id == supinfo.supplier_id);
// Value
let product_supplier_value = ('qty' in supinfo) ? supinfo.qty * supinfo.package_qty * supinfo.price : 0; let product_supplier_value = ('qty' in supinfo) ? supinfo.qty * supinfo.package_qty * supinfo.price : 0;
selected_suppliers[supplier_index].total_value += product_supplier_value; selected_suppliers[supplier_index].total_value += product_supplier_value;
// Packages
selected_suppliers[supplier_index].total_packages += ('qty' in supinfo) ? supinfo.qty : 0;
} }
} }
} }
...@@ -520,11 +704,12 @@ function set_product_npa(p_id, npa) { ...@@ -520,11 +704,12 @@ function set_product_npa(p_id, npa) {
// Give time for modal to fade // Give time for modal to fade
setTimeout(function() { setTimeout(function() {
$.notify( $(".actions_buttons_area .right_action_buttons").notify(
"Produit passé en NPA !", "Produit passé en NPA !",
{ {
globalPosition:"top right", elementPosition:"bottom right",
className: "success" className: "success",
arrowShow: false
} }
); );
}, 500); }, 500);
...@@ -590,7 +775,8 @@ function generate_inventory() { ...@@ -590,7 +775,8 @@ function generate_inventory() {
modal_create_inventory.html(), modal_create_inventory.html(),
() => { () => {
if (is_time_to('validate_generate_inventory')) { if (is_time_to('validate_generate_inventory')) {
$('#do_inventory').empty().append(`<i class="fas fa-spinner fa-spin"></i>`); $('#toggle_action_buttons .button_content').empty()
.append(`<i class="fas fa-spinner fa-spin"></i>`);
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: "/inventory/generate_inventory_list", url: "/inventory/generate_inventory_list",
...@@ -600,29 +786,31 @@ function generate_inventory() { ...@@ -600,29 +786,31 @@ function generate_inventory() {
data: JSON.stringify(data), data: JSON.stringify(data),
success: () => { success: () => {
unselect_all_rows(); unselect_all_rows();
// Give time for modal to fade // Give time for modal to fade
setTimeout(function() { setTimeout(function() {
$('#do_inventory').empty().append(`Faire un inventaire`); $('#toggle_action_buttons .button_content').empty()
$('#do_inventory').notify( .append(`Actions`);
$('#toggle_action_buttons').notify(
"Inventaire créé !", "Inventaire créé !",
{ {
globalPosition:"bottom center", elementPosition:"bottom center",
className: "success" className: "success"
} }
); );
}, 200); }, 200);
}, },
error: function(data) { error: function(data) {
$('#do_inventory').empty().append(`Faire un inventaire`); $('#do_inventory').empty()
.append(`Faire un inventaire`);
let msg = "erreur serveur lors de la création de l'inventaire". let msg = "erreur serveur lors de la création de l'inventaire".
err = {msg: msg, ctx: 'generate_inventory'}; err = {msg: msg, ctx: 'generate_inventory'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') { if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error; err.msg += ' : ' + data.responseJSON.error;
} }
report_JS_error(err, 'orders'); report_JS_error(err, 'orders');
alert("Erreur lors de la création de l'inventaire. Réessayez plus tard."); alert("Erreur lors de la création de l'inventaire. Réessayez plus tard.");
} }
}); });
...@@ -644,12 +832,12 @@ function order_pill_on_click() { ...@@ -644,12 +832,12 @@ function order_pill_on_click() {
clicked_order_pill = $(this); clicked_order_pill = $(this);
let order_name_container = clicked_order_pill.find('.pill_order_name'); let order_name_container = clicked_order_pill.find('.pill_order_name');
let doc_id = $(order_name_container).text(); let doc_id = $(order_name_container).text();
dbc.get(doc_id).then((doc) => { dbc.get(doc_id).then((doc) => {
if (doc.last_update.fingerprint !== fingerprint) { if (doc.last_update.fingerprint !== fingerprint) {
time_diff = dates_diff(new Date(doc.last_update.timestamp), new Date()); time_diff = dates_diff(new Date(doc.last_update.timestamp), new Date());
diff_str = ``; diff_str = ``;
if (time_diff.days !== 0) { if (time_diff.days !== 0) {
diff_str += `${time_diff.days} jour(s), `; diff_str += `${time_diff.days} jour(s), `;
} }
...@@ -660,11 +848,11 @@ function order_pill_on_click() { ...@@ -660,11 +848,11 @@ function order_pill_on_click() {
diff_str += `${time_diff.min} min, `; diff_str += `${time_diff.min} min, `;
} }
diff_str += `${time_diff.sec}s`; diff_str += `${time_diff.sec}s`;
let modal_order_access = $('#templates #modal_order_access'); let modal_order_access = $('#templates #modal_order_access');
modal_order_access.find(".order_last_update").text(diff_str); modal_order_access.find(".order_last_update").text(diff_str);
openModal( openModal(
modal_order_access.html(), modal_order_access.html(),
() => { () => {
...@@ -724,6 +912,8 @@ function create_cdb_order() { ...@@ -724,6 +912,8 @@ function create_cdb_order() {
/** /**
* Update order data of an existing order in couchdb * Update order data of an existing order in couchdb
*
* @returns Promise resolved after update is complete
*/ */
function update_cdb_order() { function update_cdb_order() {
order_doc.products = products; order_doc.products = products;
...@@ -752,6 +942,28 @@ function update_cdb_order() { ...@@ -752,6 +942,28 @@ function update_cdb_order() {
} }
/** /**
* Delete an order in couchdb.
*
* @returns Promise resolved after delete is complete
*/
function delete_cdb_order() {
order_doc._deleted = true;
return new Promise((resolve, reject) => {
dbc.put(order_doc, function callback(err, result) {
if (!err && result !== undefined) {
resolve();
} else {
alert("Erreur lors de la suppression de la commande... Si l'erreur persiste contactez un administrateur svp.");
console.log(err);
reject(new Error("Error while deleting order"));
}
});
});
}
/**
* Create the Product Orders in Odoo * Create the Product Orders in Odoo
*/ */
function create_orders() { function create_orders() {
...@@ -778,17 +990,19 @@ function create_orders() { ...@@ -778,17 +990,19 @@ function create_orders() {
} else { } else {
// Default date : tomorrow // Default date : tomorrow
let date_object = new Date(); let date_object = new Date();
date_object.setDate(date_object.getDate() + 1); date_object.setDate(date_object.getDate() + 1);
// Get ISO format bare string // Get ISO format bare string
formatted_date = date_object.toISOString().replace('T', ' ').split('.')[0]; formatted_date = date_object.toISOString().replace('T', ' ')
.split('.')[0];
} }
// Create an entry for this supplier // Create an entry for this supplier
orders_data.suppliers_data[supplier.id] = { orders_data.suppliers_data[supplier.id] = {
date_planned: formatted_date, date_planned: formatted_date,
lines: [] lines: []
} };
} }
openModal(); openModal();
...@@ -799,6 +1013,7 @@ function create_orders() { ...@@ -799,6 +1013,7 @@ function create_orders() {
// If a qty is set for a supplier for a product // If a qty is set for a supplier for a product
if ('qty' in p_supplierinfo && p_supplierinfo.qty != 0) { if ('qty' in p_supplierinfo && p_supplierinfo.qty != 0) {
const supplier_id = p_supplierinfo.supplier_id; const supplier_id = p_supplierinfo.supplier_id;
const product_code = p_supplierinfo.product_code;
orders_data.suppliers_data[supplier_id].lines.push({ orders_data.suppliers_data[supplier_id].lines.push({
'package_qty': p_supplierinfo.package_qty, 'package_qty': p_supplierinfo.package_qty,
...@@ -809,7 +1024,8 @@ function create_orders() { ...@@ -809,7 +1024,8 @@ function create_orders() {
'product_uom': p.uom_id[0], 'product_uom': p.uom_id[0],
'price_unit': p_supplierinfo.price, 'price_unit': p_supplierinfo.price,
'supplier_taxes_id': p.supplier_taxes_id, 'supplier_taxes_id': p.supplier_taxes_id,
'product_variant_ids': p.product_variant_ids 'product_variant_ids': p.product_variant_ids,
'product_code': product_code
}); });
} }
} }
...@@ -823,6 +1039,8 @@ function create_orders() { ...@@ -823,6 +1039,8 @@ function create_orders() {
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
data: JSON.stringify(orders_data), data: JSON.stringify(orders_data),
success: (result) => { success: (result) => {
$('#created_orders_area').empty();
// Display new orders // Display new orders
for (let new_order of result.res.created) { for (let new_order of result.res.created) {
const supplier_name = suppliers_list.find(s => s.id == new_order.supplier_id).display_name; const supplier_name = suppliers_list.find(s => s.id == new_order.supplier_id).display_name;
...@@ -853,13 +1071,14 @@ function create_orders() { ...@@ -853,13 +1071,14 @@ function create_orders() {
get_order_attachments(); get_order_attachments();
// Clear data // Clear data
order_doc._deleted = true; delete_cdb_order().finally(() => {
update_cdb_order().then(() => { // Continue with workflow anyway
update_order_selection_screen(); update_order_selection_screen().then(() => {
}) reset_data();
reset_data(); switch_screen('orders_created');
switch_screen('orders_created'); closeModal();
closeModal(); });
});
}, },
error: function(data) { error: function(data) {
let msg = "erreur serveur lors de la création des product orders"; let msg = "erreur serveur lors de la création des product orders";
...@@ -932,7 +1151,7 @@ function goto_main_screen(doc) { ...@@ -932,7 +1151,7 @@ function goto_main_screen(doc) {
update_cdb_order(); update_cdb_order();
update_main_screen(); update_main_screen();
switch_screen(); switch_screen();
}) });
} }
function back() { function back() {
...@@ -978,6 +1197,7 @@ function display_suppliers() { ...@@ -978,6 +1197,7 @@ function display_suppliers() {
const clicked_supplier = selected_suppliers.find(s => s.id == supplier_id); const clicked_supplier = selected_suppliers.find(s => s.id == supplier_id);
let modal_remove_supplier = $('#templates #modal_remove_supplier'); let modal_remove_supplier = $('#templates #modal_remove_supplier');
modal_remove_supplier.find(".supplier_name").text(clicked_supplier.display_name); modal_remove_supplier.find(".supplier_name").text(clicked_supplier.display_name);
openModal( openModal(
...@@ -1032,25 +1252,21 @@ function _compute_product_data(product) { ...@@ -1032,25 +1252,21 @@ function _compute_product_data(product) {
} }
/* Coverage related data */ /* Coverage related data */
if (order_doc.coverage_days !== null) { const coverage_days = (order_doc.coverage_days !== null) ? order_doc.coverage_days : 0;
let qty_not_covered = 0; let qty_not_covered = 0;
let days_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;
days_covered = qty_not_covered / product.daily_conso;
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 = -Math.ceil(days_covered); if (product.daily_conso !== 0) {
} qty_not_covered = product.daily_conso * coverage_days - product.qty_available - product.incoming_qty - purchase_qty;
qty_not_covered = -Math.ceil(qty_not_covered); // round up: display values that are not fully covered
qty_not_covered = (qty_not_covered > 0) ? 0 : qty_not_covered; // only display qty not covered (neg value)
item.qty_not_covered = qty_not_covered; days_covered = (product.qty_available + product.incoming_qty + purchase_qty) / product.daily_conso;
item.days_covered = days_covered; days_covered = Math.floor(days_covered);
} else {
item.qty_not_covered = 'X';
item.days_covered = 'X';
} }
item.qty_not_covered = qty_not_covered;
item.days_covered = days_covered;
return item; return item;
} }
...@@ -1114,9 +1330,20 @@ function prepare_datatable_columns() { ...@@ -1114,9 +1330,20 @@ function prepare_datatable_columns() {
{ {
data: "default_code", data: "default_code",
title: "Ref", title: "Ref",
width: "6%", width: "8%",
render: function (data) { render: function (data, type, full) {
return (data === false) ? "" : data; if (data === false) {
return "";
} else if (data.includes("[input]")) {
let val = data.replace("[input]", "");
return `<div class="custom_cell_content">
<input type="text" class="product_ref_input" id="${full.id}_ref_input" value="${val}">
</div>`;
} else {
return data;
}
} }
}, },
{ {
...@@ -1159,7 +1386,7 @@ function prepare_datatable_columns() { ...@@ -1159,7 +1386,7 @@ function prepare_datatable_columns() {
return `<div id="${base_id}_cell_content" class="custom_cell_content">X</div>`; return `<div id="${base_id}_cell_content" class="custom_cell_content">X</div>`;
} else { } else {
let content = `<div id="${base_id}_cell_content" class="custom_cell_content"> 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}>`; <input type="number" class="product_qty_input" id="${base_id}_qty_input" min="-1" value=${data}>`;
if (full.package_qty === 'X') { if (full.package_qty === 'X') {
let product_data = products.find(p => p.id == full.id); let product_data = products.find(p => p.id == full.id);
...@@ -1224,7 +1451,7 @@ function prepare_datatable_columns() { ...@@ -1224,7 +1451,7 @@ function prepare_datatable_columns() {
}, },
width: "4%" width: "4%"
}); });
return columns; return columns;
} }
...@@ -1240,8 +1467,11 @@ function display_products(params) { ...@@ -1240,8 +1467,11 @@ function display_products(params) {
return -1; return -1;
} }
// Empty datatable if it already exists // If datatable already exists, empty & clear events
if (products_table) { if (products_table) {
$(products_table.table().header()).off();
$('#products_table').off();
products_table.clear().destroy(); products_table.clear().destroy();
$('#products_table').empty(); $('#products_table').empty();
} }
...@@ -1262,23 +1492,36 @@ function display_products(params) { ...@@ -1262,23 +1492,36 @@ function display_products(params) {
sort_order_dir sort_order_dir
] ]
], ],
stripeClasses: [], // Remove datatable cells coloring
orderClasses: false, orderClasses: false,
aLengthMenu: [ aLengthMenu: [
[25, 50, 100, 200, -1], [
[25, 50, 100, 200, "Tout"] 25,
50,
100,
200,
-1
],
[
25,
50,
100,
200,
"Tout"
]
], ],
iDisplayLength: -1, iDisplayLength: -1,
scrollX: true, scrollX: true,
language: {url : '/static/js/datatables/french.json'}, language: {url : '/static/js/datatables/french.json'},
createdRow: function(row) { createdRow: function(row) {
for (const cell_node of row.cells) { for (var i = 0; i < row.cells.length; i++) {
const cell_node = row.cells[i];
const cell = $(cell_node); const cell = $(cell_node);
if (cell.hasClass("supplier_input_cell")) { if (cell.hasClass("supplier_input_cell") && cell.text() === "X") {
if (cell.text() == "X") { cell.addClass('product_not_from_supplier');
cell.addClass('product_not_from_supplier'); } else if (i === 1) {
} // Column at index 1 is product reference
cell.addClass('product_ref_cell');
} }
} }
} }
...@@ -1288,35 +1531,86 @@ function display_products(params) { ...@@ -1288,35 +1531,86 @@ function display_products(params) {
$('#main_content_footer').show(); $('#main_content_footer').show();
$('#do_inventory').show(); $('#do_inventory').show();
// On inputs change // Color line on input focus
$('#products_table').on('change', 'tbody td .product_qty_input', function () { $('#products_table').on('focus', 'tbody td .product_qty_input', function () {
let val = ($(this).val() == '') ? 0 : $(this).val(); const row = $(this).closest('tr');
row.addClass('focused_line');
});
// Manage data on inputs blur
$('#products_table').on('blur', 'tbody td .product_qty_input', function () {
// Remove line coloring on input blur
const row = $(this).closest('tr');
val = parseFloat(val); row.removeClass('focused_line');
// If value is a number let val = ($(this).val() == '') ? 0 : $(this).val();
if (!isNaN(val)) {
const id_split = $(this).attr('id') const id_split = $(this).attr('id')
.split('_'); .split('_');
const prod_id = id_split[1]; const prod_id = id_split[1];
const supplier_id = id_split[3]; const supplier_id = id_split[3];
// Save value if (val == -1) {
save_product_supplier_qty(prod_id, supplier_id, val); let modal_end_supplier_product_association = $('#templates #modal_end_supplier_product_association');
// Update row
const product = products.find(p => p.id == prod_id); 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) modal_end_supplier_product_association.find(".product_name").text(product.name);
.draw(); const supplier = selected_suppliers.find(s => s.id == supplier_id);
update_cdb_order(); modal_end_supplier_product_association.find(".supplier_name").text(supplier.display_name);
display_total_values();
openModal(
modal_end_supplier_product_association.html(),
() => {
if (is_time_to('validate_end_supplier_product_association')) {
end_supplier_product_association(product, supplier);
}
},
'Valider',
false,
true,
() => {
// Reset value in input on cancel
const psi = product.suppliersinfo.find(psi_item => psi_item.supplier_id == supplier_id);
$(this).val(psi.qty);
}
);
} else { } else {
$(this).val(''); val = parseFloat(val);
// If value is a number
if (!isNaN(val)) {
// Save value
save_product_supplier_qty(prod_id, supplier_id, val);
// Update row
const product = products.find(p => p.id == prod_id);
const new_row_data = prepare_datatable_data([product.id])[0];
products_table.row($(this).closest('tr')).data(new_row_data)
.draw();
update_cdb_order();
display_total_values();
} else {
$(this).val('');
}
} }
}); })
.on('change', 'tbody td .product_qty_input', function () {
// Since data change is saved on blur, set focus on change in case of arrows pressed
$(this).focus();
})
.on('keypress', 'tbody td .product_qty_input', function(e) {
if (e.which == 13) {
// Validate on Enter pressed
$(this).blur();
}
});
// Associate product to supplier on click on 'X' in the table // Associate product to supplier on click on 'X' in the table
$('#products_table').on('click', 'tbody .product_not_from_supplier', function () { $('#products_table').on('click', 'tbody .product_not_from_supplier', function () {
...@@ -1376,6 +1670,41 @@ function display_products(params) { ...@@ -1376,6 +1670,41 @@ function display_products(params) {
new_product_supplier_association.package_qty = $(this).val(); new_product_supplier_association.package_qty = $(this).val();
}); });
}); });
// Display input on click on product ref cell
$('#products_table').on('click', 'tbody .product_ref_cell', function () {
if ($(this).find('input').length === 0) {
const row = $(this).closest('tr');
const p_id = products_table.row(row).data().id;
const p_index = products.findIndex(p => p.id === p_id);
const existing_ref = products[p_index].default_code === false ? '' : products[p_index].default_code;
products[p_index].default_code = "[input]" + existing_ref;
const new_row_data = prepare_datatable_data([p_id])[0];
products_table.row(row).data(new_row_data)
.draw();
let ref_input = $(`#${p_id}_ref_input`);
ref_input.focus();
ref_input.select();
$('#products_table')
.on('blur', 'tbody .product_ref_input', function () {
update_product_ref(this, p_id, p_index);
})
.on('keypress', 'tbody .product_ref_input', function(e) {
// Validate on Enter pressed
if (e.which == 13) {
update_product_ref(this, p_id, p_index);
}
});
}
});
// Select row(s) on checkbox change // Select row(s) on checkbox change
$(products_table.table().header()).on('click', 'th #select_all_products_cb', function () { $(products_table.table().header()).on('click', 'th #select_all_products_cb', function () {
if (this.checked) { if (this.checked) {
...@@ -1448,6 +1777,8 @@ function display_products(params) { ...@@ -1448,6 +1777,8 @@ function display_products(params) {
* Unselect all rows from datatable. * Unselect all rows from datatable.
*/ */
function unselect_all_rows() { function unselect_all_rows() {
$("#select_all_products_cb").prop("checked", false);
products_table.rows().every(function() { products_table.rows().every(function() {
const node = $(this.node()); const node = $(this.node());
...@@ -1468,9 +1799,14 @@ function display_total_values() { ...@@ -1468,9 +1799,14 @@ function display_total_values() {
_compute_total_values_by_supplier(); _compute_total_values_by_supplier();
let order_total_value = 0; let order_total_value = 0;
for (let supplier of selected_suppliers) { for (let supplier of selected_suppliers) {
$(`#pill_supplier_${supplier.id}`).find('.supplier_total_value').text(supplier.total_value); $(`#pill_supplier_${supplier.id}`).find('.supplier_total_value')
.text(parseFloat(supplier.total_value).toFixed(2));
order_total_value += supplier.total_value; order_total_value += supplier.total_value;
$(`#pill_supplier_${supplier.id}`).find('.supplier_total_packages')
.text(+parseFloat(supplier.total_packages).toFixed(2));
} }
order_total_value = parseFloat(order_total_value).toFixed(2); order_total_value = parseFloat(order_total_value).toFixed(2);
...@@ -1482,8 +1818,11 @@ function display_total_values() { ...@@ -1482,8 +1818,11 @@ function display_total_values() {
*/ */
function update_main_screen(params) { function update_main_screen(params) {
// Remove listener before recreating them // Remove listener before recreating them
$('#products_table').off('focus', 'tbody td .product_qty_input');
$('#products_table').off('blur', 'tbody td .product_qty_input');
$('#products_table').off('change', 'tbody td .product_qty_input'); $('#products_table').off('change', 'tbody td .product_qty_input');
$('#products_table').off('click', 'tbody .product_not_from_supplier'); $('#products_table').off('click', 'tbody .product_not_from_supplier');
$('#products_table').off('click', 'tbody .product_ref_cell');
$('#products_table').off('click', 'thead th #select_all_products_cb'); $('#products_table').off('click', 'thead th #select_all_products_cb');
$('#products_table').off('click', 'tbody td .select_product_cb'); $('#products_table').off('click', 'tbody td .select_product_cb');
$(".remove_supplier_icon").off(); $(".remove_supplier_icon").off();
...@@ -1514,39 +1853,91 @@ function update_main_screen(params) { ...@@ -1514,39 +1853,91 @@ function update_main_screen(params) {
} else { } else {
$("#coverage_days_input").val(''); $("#coverage_days_input").val('');
} }
if (order_doc.stats_date_period !== undefined && order_doc.stats_date_period !== null) {
$("#stats_date_period_select").val(order_doc.stats_date_period);
} else {
$("#stats_date_period_select").val('');
}
} }
/** /**
* Update DOM display on the order selection screen * Update DOM display on the order selection screen
*/ */
function update_order_selection_screen() { function update_order_selection_screen() {
dbc.allDocs({ return new Promise((resolve) => {
include_docs: true dbc.allDocs({
}).then(function (result) { include_docs: true
// Remove listener before recreating them })
$(".order_pill").off(); .then(function (result) {
// Remove listener before recreating them
let existing_orders_container = $("#existing_orders"); $(".order_pill").off();
existing_orders_container.empty();
$('#new_order_name').val('');
if (result.rows.length === 0) { let existing_orders_container = $("#existing_orders");
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()); existing_orders_container.empty();
} $('#new_order_name').val('');
$(".order_pill").on("click", order_pill_on_click); if (result.rows.length === 0) {
} existing_orders_container.append(`<i>Aucune commande en cours...</i>`);
}) } else {
.catch(function (err) { for (let row of result.rows) {
alert('Erreur lors de la synchronisation des commandes. Vous pouvez créer une nouvelle commande.'); let template = $("#templates #order_pill_template");
console.log(err);
}); template.find(".pill_order_name").text(row.id);
existing_orders_container.append(template.html());
}
$(".order_pill").on("click", order_pill_on_click);
$(".remove_order_icon").on("click", function(e) {
e.preventDefault();
e.stopImmediatePropagation();
order_name_container = $(this).prev()[0];
let order_id = $(order_name_container).text();
let modal_remove_order = $('#templates #modal_remove_order');
modal_remove_order.find(".remove_order_name").text(order_id);
openModal(
modal_remove_order.html(),
() => {
if (is_time_to('validate_remove_order')) {
dbc.get(order_id).then((doc) => {
order_doc = doc;
delete_cdb_order().then(() => {
update_order_selection_screen().then(() => {
reset_data();
setTimeout(function() {
$.notify(
"Commande supprimée !",
{
globalPosition:"top left",
className: "success"
}
);
}, 500);
});
})
.catch(() => {
console.log("error deleting order");
});
});
}
},
'Valider'
);
});
}
resolve();
})
.catch(function (err) {
alert('Erreur lors de la synchronisation des commandes. Vous pouvez créer une nouvelle commande.');
console.log(err);
});
});
} }
/** /**
...@@ -1637,243 +2028,334 @@ function init_pouchdb_sync() { ...@@ -1637,243 +2028,334 @@ function init_pouchdb_sync() {
$(document).ready(function() { $(document).ready(function() {
fingerprint = new Fingerprint({canvas: true}).get(); if (coop_is_connected()) {
$.ajaxSetup({ headers: { "X-CSRFToken": getCookie('csrftoken') } }); $('#new_order_form').show();
$('#existing_orders_area').show();
openModal(); fingerprint = new Fingerprint({canvas: true}).get();
$.ajaxSetup({ headers: { "X-CSRFToken": getCookie('csrftoken') } });
init_pouchdb_sync(); openModal();
// Main screen init_pouchdb_sync();
$("#coverage_form").on("submit", function(e) {
e.preventDefault(); // Main screen
if (is_time_to('submit_coverage_form', 1000)) { if (metabase_url !== '') {
let val = $("#coverage_days_input").val(); $('#access_metabase').show();
}
val = parseInt(val);
$("#coverage_form").on("submit", function(e) {
if (!isNaN(val)) { e.preventDefault();
order_doc.coverage_days = val; if (is_time_to('submit_coverage_form', 1000)) {
compute_products_coverage_qties(); let val = $("#coverage_days_input").val();
update_cdb_order();
update_main_screen(); 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 !`);
}
}
});
$("#toggle_action_buttons").on("click", function() {
if ($('#actions_buttons_container').is(":visible")) {
$('#actions_buttons_container').hide();
$('.toggle_action_buttons_icon').empty()
.append('<i class="fas fa-chevron-down"></i>');
} else { } else {
$("#coverage_days_input").val(order_doc.coverage_days); $('#actions_buttons_container').show();
alert(`Valeur non valide pour le nombre de jours de couverture !`); $('.toggle_action_buttons_icon').empty()
.append('<i class="fas fa-chevron-up"></i>');
} }
} });
});
// Close dropdown menu on click outside
$(document).click(function(event) {
let target = $(event.target);
if (
!target.closest('#actions_buttons_wrapper').length
&& $('#actions_buttons_container').is(":visible")
) {
$('#actions_buttons_container').hide();
$('.toggle_action_buttons_icon').empty()
.append('<i class="fas fa-chevron-down"></i>');
}
});
$("#supplier_form").on("submit", function(e) { $("#supplier_form").on("submit", function(e) {
e.preventDefault(); e.preventDefault();
if (is_time_to('add_product', 1000)) { if (is_time_to('add_product', 1000)) {
add_supplier(); add_supplier();
} }
}); });
$("#product_form").on("submit", function(e) { $("#product_form").on("submit", function(e) {
e.preventDefault(); e.preventDefault();
if (is_time_to('add_product', 1000)) { if (is_time_to('add_product', 1000)) {
add_product(); add_product();
} }
}); });
$("#do_inventory").on("click", function() { $("#stats_date_period_select").on("change", function(e) {
if (is_time_to('generate_inventory', 1000)) { e.preventDefault();
generate_inventory(); if (is_time_to('change_stats_date_period', 1000)) {
} openModal();
});
$('#back_to_order_selection_from_main').on('click', function() { order_doc.stats_date_period = $(this).val();
if (is_time_to('back_to_order_selection_from_main', 1000)) {
back();
}
});
$('#create_orders').on('click', function() { check_products_data()
if (is_time_to('create_orders', 1000)) { .then(() => {
let modal_create_order = $('#templates #modal_create_order'); compute_products_coverage_qties();
modal_create_order.find('.suppliers_date_planned_area').empty(); update_main_screen();
update_cdb_order();
for (let supplier of selected_suppliers) { closeModal();
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( $("#do_inventory").on("click", function() {
modal_create_order.html(), if (is_time_to('generate_inventory', 1000)) {
() => { generate_inventory();
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'] = { $("#delete_order_button").on("click", function() {
monthNames: [ if (is_time_to('press_delete_order_button', 1000)) {
'Janvier', let modal_remove_order = $('#templates #modal_remove_order');
'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 modal_remove_order.find(".remove_order_name").text(order_doc._id);
update_order_selection_screen();
$("#new_order_form").on("submit", function(e) { openModal(
e.preventDefault(); modal_remove_order.html(),
if (is_time_to('submit_new_order_form', 1000)) { () => {
create_cdb_order(); if (is_time_to('validate_remove_order')) {
} delete_cdb_order().then(() => {
}); update_order_selection_screen().then(() => {
reset_data();
switch_screen('order_selection');
setTimeout(function() {
$.notify(
"Commande supprimée !",
{
globalPosition:"top left",
className: "success"
}
);
}, 500);
});
})
.catch(() => {
console.log("error deleting order");
});
}
},
'Valider'
);
}
// 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 $('#back_to_order_selection_from_main').on('click', function() {
$.ajax({ if (is_time_to('back_to_order_selection_from_main', 1000)) {
type: 'GET', back();
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 $('#create_orders').on('click', function() {
$("#supplier_input").autocomplete({ if (is_time_to('create_orders', 1000)) {
source: suppliers_list.map(a => a.display_name) let modal_create_order = $('#templates #modal_create_order');
});
modal_create_order.find('.suppliers_date_planned_area').empty();
}, for (let supplier of selected_suppliers) {
error: function(data) { let supplier_date_planned_template = $('#templates #modal_create_order__supplier_date_planned');
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') { supplier_date_planned_template.find(".supplier_name").text(supplier.display_name);
err.msg += ' : ' + data.responseJSON.error; 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('');
}
});
} }
report_JS_error(err, 'orders');
closeModal(); return 0;
alert('Erreur lors de la récupération des fournisseurs, rechargez la page plus tard'); });
}
});
//Get products $.datepicker.regional['fr'] = {
var accentMap = { monthNames: [
"á": "a", 'Janvier',
"à": "a", 'Fevrier',
"â": "a", 'Mars',
"é": "e", 'Avril',
"è": "e", 'Mai',
"ê": "e", 'Juin',
"ë": "e", 'Juillet',
"ç": "c", 'Aout',
"ù": "u", 'Septembre',
"ü": "u", 'Octobre',
"ö": "o" 'Novembre',
}; 'Decembre'
],
dayNamesMin: [
'Di',
'Lu',
'Ma',
'Me',
'Je',
'Ve',
'Sa'
],
dateFormat: date_format
};
$.datepicker.setDefaults($.datepicker.regional['fr']);
var normalize = function(term) { // Order selection screen
var ret = ""; update_order_selection_screen();
for (var i = 0; i < term.length; i++) { $("#new_order_form").on("submit", function(e) {
ret += accentMap[ term.charAt(i) ] || term.charAt(i); e.preventDefault();
} if (is_time_to('submit_new_order_form', 1000)) {
create_cdb_order();
}
});
return ret; // 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');
}
});
$.ajax({ // Get suppliers
type: 'GET', $.ajax({
url: "/products/simple_list", type: 'GET',
dataType:"json", url: "/orders/get_suppliers",
traditional: true, dataType:"json",
contentType: "application/json; charset=utf-8", traditional: true,
success: function(data) { contentType: "application/json; charset=utf-8",
products_list = data.list; success: function(data) {
suppliers_list = data.res;
// Set up autocomplete on product input // Set up autocomplete on supplier input
$("#product_input").autocomplete({ $("#supplier_input").autocomplete({
source: function(request, response) { source: suppliers_list.map(a => a.display_name)
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)); },
})); error: function(data) {
}, err = {msg: "erreur serveur lors de la récupération des fournisseurs", ctx: 'get_suppliers'};
position: {collision: "flip" } if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
}); err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
closeModal(); closeModal();
}, alert('Erreur lors de la récupération des fournisseurs, rechargez la page plus tard');
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(); //Get products
alert('Erreur lors de la récupération des articles, rechargez la page plus tard'); 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();
}
}); });
...@@ -13,6 +13,7 @@ urlpatterns = [ ...@@ -13,6 +13,7 @@ urlpatterns = [
url(r'^get_suppliers$', views.get_suppliers), url(r'^get_suppliers$', views.get_suppliers),
url(r'^get_supplier_products$', views.get_supplier_products), url(r'^get_supplier_products$', views.get_supplier_products),
url(r'^associate_supplier_to_product$', views.associate_supplier_to_product), url(r'^associate_supplier_to_product$', views.associate_supplier_to_product),
url(r'^end_supplier_product_association$', views.end_supplier_product_association),
url(r'^create_orders$', views.create_orders), url(r'^create_orders$', views.create_orders),
url(r'^get_orders_attachment$', views.get_orders_attachment), url(r'^get_orders_attachment$', views.get_orders_attachment),
] ]
...@@ -19,7 +19,8 @@ def helper(request): ...@@ -19,7 +19,8 @@ def helper(request):
'title': 'Aide à la commande', 'title': 'Aide à la commande',
'couchdb_server': settings.COUCHDB['url'], 'couchdb_server': settings.COUCHDB['url'],
'db': settings.COUCHDB['dbs']['orders'], 'db': settings.COUCHDB['dbs']['orders'],
'odoo_server': settings.ODOO['url'] 'odoo_server': settings.ODOO['url'],
'metabase_url': getattr(settings, 'ORDERS_HELPER_METABASE_URL', '')
} }
template = loader.get_template('orders/helper.html') template = loader.get_template('orders/helper.html')
...@@ -42,7 +43,8 @@ def get_supplier_products(request): ...@@ -42,7 +43,8 @@ def get_supplier_products(request):
""" Get supplier products """ """ Get supplier products """
suppliers_id = request.GET.getlist('sids', '') suppliers_id = request.GET.getlist('sids', '')
res = CagetteProducts.get_products_for_order_helper(suppliers_id) stats_from = request.GET.get('stats_from')
res = CagetteProducts.get_products_for_order_helper(suppliers_id, [], stats_from)
if 'error' in res: if 'error' in res:
return JsonResponse(res, status=500) return JsonResponse(res, status=500)
...@@ -52,15 +54,29 @@ def get_supplier_products(request): ...@@ -52,15 +54,29 @@ def get_supplier_products(request):
def associate_supplier_to_product(request): def associate_supplier_to_product(request):
""" This product is now supplied by this supplier """ """ This product is now supplied by this supplier """
res = {} res = {}
try:
data = json.loads(request.body.decode()) data = json.loads(request.body.decode())
res = CagetteProduct.associate_supplier_to_product(data) res = CagetteProduct.associate_supplier_to_product(data)
except Exception as e:
res["error"] = str(e) if 'error' in res:
return JsonResponse(res, status=500) return JsonResponse(res, status=500)
else:
return JsonResponse({'res': res})
return JsonResponse({'res': res}) return JsonResponse({'res': res})
def end_supplier_product_association(request):
""" This product is now unavailable from this supplier """
res = {}
data = json.loads(request.body.decode())
res = CagetteProduct.end_supplier_product_association(data)
if 'error' in res:
return JsonResponse(res, status=500)
else:
return JsonResponse({'res': res})
def create_orders(request): def create_orders(request):
""" Create products orders """ """ Create products orders """
res = { "created": [] } res = { "created": [] }
......
...@@ -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 = ''
......
...@@ -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, {});
}); });
} }
......
...@@ -9,8 +9,8 @@ ...@@ -9,8 +9,8 @@
}, },
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"lint": "eslint . --max-warnings 362 '**/*.js'", "lint": "eslint . --max-warnings 326 '**/*.js'",
"lint-fix": "eslint . --fix --max-warnings 362 '**/*.js'" "lint-fix": "eslint . --fix --max-warnings 326 '**/*.js'"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
......
...@@ -130,27 +130,91 @@ class CagetteProduct(models.Model): ...@@ -130,27 +130,91 @@ class CagetteProduct(models.Model):
@staticmethod @staticmethod
def associate_supplier_to_product(data): def associate_supplier_to_product(data):
api = OdooAPI() api = OdooAPI()
res = {}
product_tmpl_id = data["product_tmpl_id"] product_tmpl_id = data["product_tmpl_id"]
partner_id = data["supplier_id"] partner_id = data["supplier_id"]
package_qty = data["package_qty"] package_qty = data["package_qty"]
price = data["price"] price = data["price"]
# Look for existing association
f = ["id"]
c = [['product_tmpl_id', '=', product_tmpl_id], ['name', '=', partner_id]]
res_existing_supplierinfo = api.search_read('product.supplierinfo', c, f)
f = ["id", "standard_price", "purchase_ok"] if (len(res_existing_supplierinfo) > 0):
c = [['product_tmpl_id', '=', product_tmpl_id]] # A relation already exists, update it's start & end dates
res_products = api.search_read('product.product', c, f) psi_id = res_existing_supplierinfo[0]['id']
product = res_products[0]
f = { today = datetime.date.today().strftime("%Y-%m-%d")
'product_tmpl_id' : product_tmpl_id,
'product_id' : product["id"], f = {
'name' : partner_id, 'date_start': today,
'product_purchase_ok': product["purchase_ok"], 'date_end': False,
'price': price, 'price': price,
'base_price': price, 'base_price': price,
'package_qty': package_qty 'package_qty': package_qty,
} 'sequence': 1000 # lowest priority for the new suppliers
res = api.create('product.supplierinfo', f) }
try:
res["update"] = api.update('product.supplierinfo', psi_id, f)
except Exception as e:
res['error'] = str(e)
else:
# Relation doesn't exists, create one
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]
today = datetime.date.today().strftime("%Y-%m-%d")
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,
'date_start': today,
'sequence': 1000 # lowest priority for the new suppliers
}
try:
res['create'] = api.create('product.supplierinfo', f)
except Exception as e:
res['error'] = str(e)
return res
@staticmethod
def end_supplier_product_association(data):
api = OdooAPI()
res = {}
product_tmpl_id = data["product_tmpl_id"]
partner_id = data["supplier_id"]
f = ["id"]
c = [['product_tmpl_id', '=', product_tmpl_id], ['name', '=', partner_id], ['date_end', '=', False]]
res_supplierinfo = api.search_read('product.supplierinfo', c, f)
# End all active associations in case multiple are open (which shouldn't happen)
for psi in res_supplierinfo:
psi_id = psi['id']
today = datetime.date.today().strftime("%Y-%m-%d")
f = {
'date_end': today
}
try:
res["update"] = api.update('product.supplierinfo', psi_id, f)
except Exception as e:
res['error'] = str(e)
return res return res
...@@ -171,6 +235,23 @@ class CagetteProduct(models.Model): ...@@ -171,6 +235,23 @@ class CagetteProduct(models.Model):
return res return res
@staticmethod
def update_product_internal_ref(product_tmpl_id, default_code):
api = OdooAPI()
res = {}
f = {
'default_code': default_code
}
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."""
...@@ -455,31 +536,32 @@ class CagetteProducts(models.Model): ...@@ -455,31 +536,32 @@ class CagetteProducts(models.Model):
return res return res
@staticmethod @staticmethod
def get_products_for_order_helper(supplier_ids, pids = []): def get_products_for_order_helper(supplier_ids, pids = [], stats_from = None):
""" """
One of the two parameters must be not empty. supplier_ids: Get products by supplier if one or more supplier id is set. If set, pids is ignored.
Get products by supplier if one or more supplier_id is set. pids: If set & supplier_ids is None/empty, get products specified in pids. In this case, suppliers info won't be fetched.
If supplier_ids is empty, get products specified in pids. In this case, suppliers info won't be fetched. stats_from: date from which we should calculate sells stats.
""" """
api = OdooAPI() api = OdooAPI()
res = {} res = {}
# todo : try with no result
try: try:
today = datetime.date.today().strftime("%Y-%m-%d") today = datetime.date.today().strftime("%Y-%m-%d")
if supplier_ids is not None and len(supplier_ids) > 0: if supplier_ids is not None and len(supplier_ids) > 0:
# Get products/supplier relation # Get products/supplier relation
f = ["product_tmpl_id", 'date_start', 'date_end', 'package_qty', 'price', 'name'] f = ["product_tmpl_id", 'date_start', 'date_end', 'package_qty', 'price', 'name', 'product_code']
c = [['name', 'in', [ int(x) for x in supplier_ids]]] c = [['name', 'in', [ int(x) for x in supplier_ids]]]
psi = api.search_read('product.supplierinfo', c, f) psi = api.search_read('product.supplierinfo', c, f)
# Filter valid data # Filter valid data
ptids = [] ptids = []
valid_psi = []
for p in psi: for p in psi:
if (p["product_tmpl_id"] is not False 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_start"] is False or p["date_start"] 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)): and (p["date_end"] is False or p["date_end"] is not False and p["date_end"] > today)):
valid_psi.append(p)
ptids.append(p["product_tmpl_id"][0]) ptids.append(p["product_tmpl_id"][0])
else: else:
ptids = [ int(x) for x in pids ] ptids = [ int(x) for x in pids ]
...@@ -506,6 +588,10 @@ class CagetteProducts(models.Model): ...@@ -506,6 +588,10 @@ class CagetteProducts(models.Model):
# 'from': '2019-04-10', # 'from': '2019-04-10',
# 'to': '2019-08-10', # 'to': '2019-08-10',
} }
if stats_from is not None and stats_from != '':
sales_average_params['from'] = stats_from
sales = CagetteProducts.get_template_products_sales_average(sales_average_params) sales = CagetteProducts.get_template_products_sales_average(sales_average_params)
if 'list' in sales and len(sales['list']) > 0: if 'list' in sales and len(sales['list']) > 0:
...@@ -516,12 +602,16 @@ class CagetteProducts(models.Model): ...@@ -516,12 +602,16 @@ class CagetteProducts(models.Model):
# Add supplier data to product data # Add supplier data to product data
for i, fp in enumerate(filtered_products_t): for i, fp in enumerate(filtered_products_t):
if supplier_ids is not None and len(supplier_ids) > 0: 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"]) # Add all the product suppliersinfo (products from multiple suppliers into the suppliers list provided)
filtered_products_t[i]['suppliersinfo'] = [{ filtered_products_t[i]['suppliersinfo'] = []
'supplier_id': int(psi_item["name"][0]), for psi_item in valid_psi:
'package_qty': psi_item["package_qty"], if psi_item["product_tmpl_id"] is not False and psi_item ["product_tmpl_id"][0] == fp["id"]:
'price': psi_item["price"] filtered_products_t[i]['suppliersinfo'].append({
}] 'supplier_id': int(psi_item["name"][0]),
'package_qty': psi_item["package_qty"],
'price': psi_item["price"],
'product_code': psi_item["product_code"]
})
for s in sales: for s in sales:
if s["id"] == fp["id"]: if s["id"] == fp["id"]:
......
...@@ -10,6 +10,7 @@ urlpatterns = [ ...@@ -10,6 +10,7 @@ urlpatterns = [
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'^update_product_purchase_ok$', views.update_product_purchase_ok),
url(r'^update_product_internal_ref$', views.update_product_internal_ref),
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
......
...@@ -42,8 +42,10 @@ def get_simple_list(request): ...@@ -42,8 +42,10 @@ def get_simple_list(request):
def get_product_for_order_helper(request): def get_product_for_order_helper(request):
res = {} res = {}
try: try:
pids = json.loads(request.body.decode()) data = json.loads(request.body.decode())
res = CagetteProducts.get_products_for_order_helper(None, pids) pids = data['pids']
stats_from = data['stats_from']
res = CagetteProducts.get_products_for_order_helper(None, pids, stats_from)
except Exception as e: except Exception as e:
coop_logger.error("get_product_for_help_order_line : %s", str(e)) coop_logger.error("get_product_for_help_order_line : %s", str(e))
res['error'] = str(e) res['error'] = str(e)
...@@ -112,6 +114,17 @@ def update_product_purchase_ok(request): ...@@ -112,6 +114,17 @@ def update_product_purchase_ok(request):
else: else:
return JsonResponse({"res": res}) return JsonResponse({"res": res})
def update_product_internal_ref(request):
res = {}
data = json.loads(request.body.decode())
res = CagetteProduct.update_product_internal_ref(data["product_tmpl_id"], data["default_code"])
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."""
withCandidate = False withCandidate = False
......
...@@ -31,7 +31,7 @@ class CagetteReception(models.Model): ...@@ -31,7 +31,7 @@ class CagetteReception(models.Model):
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", 'create_uid']
# 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', 'valid_pending', 'br_valid']]] c = [['id', 'in', pids], ["x_reception_status", "in", [False, 'qty_valid', 'valid_pending', 'br_valid']]]
...@@ -48,6 +48,25 @@ class CagetteReception(models.Model): ...@@ -48,6 +48,25 @@ class CagetteReception(models.Model):
def get_order_lines_by_po(id_po, nullQty=False): def get_order_lines_by_po(id_po, nullQty=False):
"""Return all purchases order lines linked with purchase order id in Odoo.""" """Return all purchases order lines linked with purchase order id in Odoo."""
return Order(id_po).get_lines(withNullQty=nullQty) return Order(id_po).get_lines(withNullQty=nullQty)
def get_mail_create_po(id_po):
"""Return name et email from id_po of order"""
try:
api = OdooAPI()
f = ["create_uid"]
c = [['id', '=', id_po]]
res = api.search_read('purchase.order', c, f)
f = ["email", "display_name"]
c = [['user_ids', '=', int(res[0]['create_uid'][0])]]
res = api.search_read('res.partner', c, f)
except Exception as e:
print(str(e))
return res
def implies_scale_file_generation(self): def implies_scale_file_generation(self):
answer = False answer = False
...@@ -132,6 +151,7 @@ class CagetteReception(models.Model): ...@@ -132,6 +151,7 @@ class CagetteReception(models.Model):
self.o_api.update('stock.move.operation.link', [linked_move_op_id], {'qty': received_products[pack['product_id'][0]]}) self.o_api.update('stock.move.operation.link', [linked_move_op_id], {'qty': received_products[pack['product_id'][0]]})
pfields['product_uom_qty'] = received_qty pfields['product_uom_qty'] = received_qty
self.o_api.update('stock.move', [move_id], pfields) self.o_api.update('stock.move', [move_id], pfields)
del pfields['product_uom_qty'] # field not in stock.pack.operation
pfields['qty_done'] = pfields['product_qty'] = received_qty pfields['qty_done'] = pfields['product_qty'] = received_qty
self.o_api.update('stock.pack.operation', [int(pack['id'])], pfields) self.o_api.update('stock.pack.operation', [int(pack['id'])], pfields)
processed_lines += 1 processed_lines += 1
......
...@@ -103,18 +103,29 @@ function goto(id) { ...@@ -103,18 +103,29 @@ function goto(id) {
* @param {int} group_index index of group in groups array * @param {int} group_index index of group in groups array
*/ */
function group_goto(group_index) { function group_goto(group_index) {
let missing_orders = [];
// Make sure a couchdb document exists for all group's orders // Make sure a couchdb document exists for all group's orders
for (let i in order_groups.groups[group_index]) { for (let i in order_groups.groups[group_index]) {
let order_data = null; let order_data = null;
let order_id = order_groups.groups[group_index][i];
// Find order data // Find order data
for (let order of orders) { for (let order of orders) {
if (order.id == order_groups.groups[group_index][i]) { if (order.id == order_id) {
order_data = order; order_data = order;
} }
} }
create_order_doc(order_data); if (order_data != null) {
create_order_doc(order_data);
} else {
missing_orders.push(order_id);
}
}
if (missing_orders.length > 0) {
// TODO what to do when orders are missing from group?
} }
// go to first order // go to first order
...@@ -212,7 +223,7 @@ function validatePrices() { ...@@ -212,7 +223,7 @@ function validatePrices() {
var updates = { var updates = {
'group_amount_total' : order['amount_total'], 'group_amount_total' : order['amount_total'],
'update_type' : 'br_vaid', 'update_type' : 'br_valid',
'updated_products' : [], 'updated_products' : [],
'user_comments': "", 'user_comments': "",
'orders' : [order] 'orders' : [order]
...@@ -285,7 +296,10 @@ function group_action() { ...@@ -285,7 +296,10 @@ function group_action() {
* Remove the grouped orders from the order table to prevent grouping in multiple groups. * Remove the grouped orders from the order table to prevent grouping in multiple groups.
*/ */
function display_grouped_orders() { function display_grouped_orders() {
if (table_orders !== null) { if (table_orders !== null) {
var display_something = false;
$('#groups_items').empty(); $('#groups_items').empty();
let groups_display_content = "<ul>"; let groups_display_content = "<ul>";
...@@ -308,33 +322,38 @@ function display_grouped_orders() { ...@@ -308,33 +322,38 @@ function display_grouped_orders() {
} }
} }
// Display group if (group_orders.length > 0) {
document.getElementById("container_groups").hidden = false; // Display group
let group_row = `<li class="group_line"> Commandes de `; display_something = true;
document.getElementById("container_groups").hidden = false;
let group_row = `<li class="group_line"> Commandes de `;
for (let i in group_orders) {
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 + ", ";
}
}
for (let i in group_orders) { if (group_orders[0].reception_status == 'False') {
if (i == group_orders.length-1) { // last element of list group_row += "<button class='btn--primary' onClick='group_goto("
group_row += "<b>" + group_orders[i].partner + "</b> du " + group_orders[i].date_order + " : "; + group_index
+ ")'>Compter les produits</button>";
} else { } else {
group_row += "<b>" + group_orders[i].partner + "</b> du " + group_orders[i].date_order + ", "; group_row += "<button class='btn--success' onClick='group_goto("
+ group_index
+ ")'>Mettre à jour les prix</button>";
} }
}
if (group_orders[0].reception_status == 'False') { group_row += "</li>";
group_row += "<button class='btn--primary' onClick='group_goto(" groups_display_content += group_row;
+ group_index
+ ")'>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(); if (display_something === true) {
$('#groups_items').append(groups_display_content); $('#container_groups').show();
$('#groups_items').append(groups_display_content);
}
} }
} }
...@@ -346,7 +365,9 @@ function display_orders_table() { ...@@ -346,7 +365,9 @@ function display_orders_table() {
table_orders.clear().destroy(); table_orders.clear().destroy();
$('#orders').empty(); $('#orders').empty();
} }
for (let j in orders) {
console.log(orders[j].id);
}
table_orders = $('#orders').DataTable({ table_orders = $('#orders').DataTable({
data: orders, data: orders,
columns:[ columns:[
......
...@@ -84,10 +84,10 @@ function select_product_from_bc(barcode) { ...@@ -84,10 +84,10 @@ function select_product_from_bc(barcode) {
} }
}); });
if (found.data != null) { if (found.data == null) {
$.each(list_processed, function(i, e) { $.each(list_processed, function(i, e) {
if (e.product_id[0] == p.data[barcodes['keys']['id']]) { if (e.product_id[0] == p.data[barcodes['keys']['id']]) {
found.data = e; found.data = JSON.parse(JSON.stringify(e));
found.place = 'processed'; found.place = 'processed';
} }
}); });
...@@ -99,10 +99,6 @@ function select_product_from_bc(barcode) { ...@@ -99,10 +99,6 @@ function select_product_from_bc(barcode) {
let row = table_to_process.row($('#'+found.data.product_id[0])); let row = table_to_process.row($('#'+found.data.product_id[0]));
remove_from_toProcess(row, found.data); remove_from_toProcess(row, found.data);
} else {
let row = table_processed.row($('#'+found.data.product_id[0]));
remove_from_processed(row, found.data);
} }
} }
} }
...@@ -862,12 +858,20 @@ function editProductInfo (productToEdit, value = null, batch = false) { ...@@ -862,12 +858,20 @@ function editProductInfo (productToEdit, value = null, batch = false) {
var index = searchUpdatedProduct(); var index = searchUpdatedProduct();
var firstUpdate = false; var firstUpdate = false;
let newValue = value; let newValue = value;
var addition = false;
// If 'value' parameter not set, get value from edition input // If 'value' parameter not set, get value from edition input
if (value == null) { if (value == null) {
newValue = parseFloat(document.getElementById('edition_input').value.replace(',', '.')); newValue = parseFloat(document.getElementById('edition_input').value.replace(',', '.'));
} }
$.each(list_processed, function(i, e) {
if (e.product_id[0] == productToEdit.product_id[0]) {
addition = true;
productToEdit = e;
newValue = newValue + productToEdit.product_qty;
}
});
// If qty edition & Check if qty changed // If qty edition & Check if qty changed
if (reception_status == "False" && productToEdit.product_qty != newValue) { if (reception_status == "False" && productToEdit.product_qty != newValue) {
if (index == -1) { // First update if (index == -1) { // First update
...@@ -944,6 +948,12 @@ function editProductInfo (productToEdit, value = null, batch = false) { ...@@ -944,6 +948,12 @@ function editProductInfo (productToEdit, value = null, batch = false) {
update_distant_order(productToEdit.id_po); update_distant_order(productToEdit.id_po);
} }
if (addition) {
let row = table_processed.row($('#'+productToEdit.product_id[0]));
remove_from_processed(row, productToEdit);
}
add_to_processed(productToEdit); add_to_processed(productToEdit);
} catch (e) { } catch (e) {
err = {msg: e.name + ' : ' + e.message, ctx: 'edit product info'}; err = {msg: e.name + ' : ' + e.message, ctx: 'edit product info'};
...@@ -1206,6 +1216,39 @@ function send() { ...@@ -1206,6 +1216,39 @@ function send() {
error_report_data.orders.push(orders[i]); error_report_data.orders.push(orders[i]);
} }
//Create list of articl with no barcode
no_barcode_list = [];
for (var i = 0; i < list_processed.length; i++) {
if (list_processed[i].product_qty != 0 && (list_processed[i].barcode == false || list_processed[i].barcode == null || list_processed[i].barcode == "")) {
no_barcode_list.push([
list_processed[i]["product_id"][0],
list_processed[i]["product_id"][1]
]);
}
}
data_send_no_barcode={
"order" : orders[order_data['id_po']],
"no_barcode_list" : no_barcode_list
};
// Send of articl with no barcode to mail send
if (no_barcode_list.length > 0) {
$.ajax({
type: "POST",
url: "../send_mail_no_barcode",
dataType: "json",
traditional: true,
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data_send_no_barcode),
success: function() {},
error: function() {
alert('Erreur dans l\'envoi des produite sont barre code.');
}
});
}
// Send request for error report // Send request for error report
$.ajax({ $.ajax({
type: "POST", type: "POST",
...@@ -1248,7 +1291,7 @@ function send() { ...@@ -1248,7 +1291,7 @@ function send() {
document.getElementById("nothingToDo").hidden = true; document.getElementById("nothingToDo").hidden = true;
barcodes_to_print = true; barcodes_to_print = true;
} else if (list_processed[i].barcode == false || list_processed[i].barcode == null || list_processed[i].barcode == "") { } /* else if (list_processed[i].barcode == false || list_processed[i].barcode == null || list_processed[i].barcode == "") {
// Products with no barcode // Products with no barcode
var node = document.createElement('li'); var node = document.createElement('li');
let textNode = document.createTextNode(list_processed[i]["product_id"][1]); let textNode = document.createTextNode(list_processed[i]["product_id"][1]);
...@@ -1260,7 +1303,20 @@ function send() { ...@@ -1260,7 +1303,20 @@ function send() {
document.getElementById("barcodesEmpty").hidden = false; document.getElementById("barcodesEmpty").hidden = false;
document.getElementById("nothingToDo").hidden = true; document.getElementById("nothingToDo").hidden = true;
} }
} }*/
}
}
for (let i = 0; i < no_barcode_list.length; i++) {
var node = document.createElement('li');
let textNode = document.createTextNode(no_barcode_list[i]);
node.appendChild(textNode);
document.getElementById('barcodesEmpty_list').appendChild(node);
if (document.getElementById("barcodesEmpty").hidden) {
document.getElementById("barcodesEmpty").hidden = false;
document.getElementById("nothingToDo").hidden = true;
} }
} }
......
...@@ -16,5 +16,6 @@ urlpatterns = [ ...@@ -16,5 +16,6 @@ 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'^send_mail_no_barcode', views.send_mail_no_barcode)
] ]
...@@ -221,26 +221,6 @@ def update_orders(request): ...@@ -221,26 +221,6 @@ def update_orders(request):
m.update_order_status(order_id, data['update_type']) m.update_order_status(order_id, data['update_type'])
if data['update_type'] == 'br_valid': if data['update_type'] == 'br_valid':
answer_data[order_id]['finalyze_result'] = m.register_purchase_order_to_finalyze() answer_data[order_id]['finalyze_result'] = m.register_purchase_order_to_finalyze()
# Remove order's group
try:
# TODO remove from couchdb orders & group (here?)
if os.path.exists('temp/grouped_order.json'):
with open('temp/grouped_order.json', 'r') as json_file:
saved_groups = json.load(json_file)
for oi in order_ids:
for i, group in enumerate(saved_groups):
if oi in group:
saved_groups.pop(i)
break
with open('temp/grouped_order.json', 'w') as json_file:
json.dump(saved_groups, json_file)
except Exception as e:
# no saved groups
print(str(e))
# TODO else if first step, save first step data (here?)
else: else:
coop_logger.error("update_orders errors : %s", str(errors)) coop_logger.error("update_orders errors : %s", str(errors))
rep = JsonResponse(answer_data, safe=False) rep = JsonResponse(answer_data, safe=False)
...@@ -259,104 +239,112 @@ def save_error_report(request): ...@@ -259,104 +239,112 @@ def save_error_report(request):
save it to an excel file, save it to an excel file,
attach file to order in odoo attach file to order in odoo
""" """
if request.is_ajax(): if request.method == 'POST':
if request.method == 'POST': data = None
try:
myJson = request.body myJson = request.body
data = json.loads(myJson.decode()) data = json.loads(myJson.decode())
except Exception as e1:
coop_logger.error("Save reception report : Unable to load data %s (%s)", str(e1), str(myJson))
temp_files_name = [] if data and ('orders' in data):
orders_name = "" orders_name_elts = []
orders_partner = "" orders_partner = ""
group_ids = [] group_ids = []
for i, order in enumerate(data['orders']) :
# list of temp files: 1 report per order & group
data['orders'][i]['temp_file_name'] = "temp/" + order['name'] + "_rapport-reception_temp.xlsx"
group_ids.append(order['id']) try:
for i, order in enumerate(data['orders']) :
# list of temp files: 1 report per order & group
data['orders'][i]['temp_file_name'] = "temp/" + order['name'] + "_rapport-reception_temp.xlsx"
# Concatenate orders name group_ids.append(order['id'])
if orders_name != "" :
orders_name = orders_name + '-'
orders_name = orders_name + order['name']
# Concatenate orders partner orders_name_elts.append(order['name'])
if order['partner'] != orders_partner :
if orders_partner != "" :
orders_partner = orders_partner + ', '
orders_partner = orders_partner + order['partner'] + ' du ' + order['date_order']
# Concatenate orders partner
if order['partner'] != orders_partner:
if orders_partner != "":
orders_partner = orders_partner + ', '
orders_partner = orders_partner + order['partner'] + ' du ' + order['date_order']
# If group of orders
if len(data['orders']) > 1 :
temp_group_file_name = "temp/" + orders_name + "_rapport-reception_temp.xlsx"
# Add group in data['orders'] # If group of orders
group_order = { if len(data['orders']) > 1:
'name' : orders_name, orders_name = '-'.join(orders_name_elts)
'partner' : orders_partner, temp_group_file_name = "temp/" + orders_name + "_rapport-reception_temp.xlsx"
'date_order' : data['orders'][0]['date_order'],
'amount_total' : data['group_amount_total'],
'updated_products' : data['updated_products'],
'temp_file_name' : temp_group_file_name,
'group_ids' : group_ids
}
data['orders'].append(group_order) # Add group in data['orders']
group_order = {
'name': orders_name,
'partner': orders_partner,
'date_order': data['orders'][0]['date_order'],
'amount_total': data['group_amount_total'],
'updated_products': data['updated_products'],
'temp_file_name': temp_group_file_name,
'group_ids': group_ids
}
data['orders'].append(group_order)
else:
coop_logger.info("data['orders'] is a single PO (not inside group)")
except Exception as e2:
coop_logger.error("Save reception report : Error while create group_order %s", str(e2))
# Save qties & comments after step 1 # Save qties & comments after step 1
if data['update_type'] == 'qty_valid': if data['update_type'] == 'qty_valid':
for order in data['orders']: for order in data['orders']:
wb = Workbook()
ws = wb.active
ws.title = "Commande " + order['name']
ws.append(['type',
'nom_contenu',
'supplier_code',
'barcode',
'old_qty',
'product_qty',
'price_unit',
'supplier_shortage'])
# If in group add group name
if len(data['orders']) > 1 :
ws.append( ['group', orders_name] )
try: try:
if 'updated_products' in order: wb = Workbook()
for product in order['updated_products']: ws = wb.active
# Don't store products with same qties ws.title = "Commande " + order['name']
if product['old_qty'] != product['product_qty']:
if 'supplier_code' in product: ws.append(['type',
supplier_code = str(product['supplier_code']) 'nom_contenu',
else: 'supplier_code',
supplier_code = 'X' 'barcode',
'old_qty',
if 'supplier_shortage' in product: 'product_qty',
supplier_shortage = '/!\ Rupture fournisseur' 'price_unit',
else: 'supplier_shortage'])
supplier_shortage = ''
# If in group add group name
ws.append( ['produit', if len(data['orders']) > 1:
product['product_id'][1], ws.append(['group', orders_name])
supplier_code, try:
str(product['barcode']), if 'updated_products' in order:
product['old_qty'], for product in order['updated_products']:
product['product_qty'], # Don't store products with same qties
product['price_unit'], if product['old_qty'] != product['product_qty']:
supplier_shortage] ) if 'supplier_code' in product:
except Exception as exp: supplier_code = str(product['supplier_code'])
print("Error while updating products") else:
print(exp) supplier_code = 'X'
if ('user_comments' in data) and data['user_comments'] != "":
ws.append( ['commentaire', data['user_comments']] ) if 'supplier_shortage' in product:
else: supplier_shortage = '/!\ Rupture fournisseur'
ws.append( ['commentaire', 'Aucun commentaire.'] ) else:
supplier_shortage = ''
# Save file
wb.save(filename=order['temp_file_name']) ws.append(['produit',
product['product_id'][1],
supplier_code,
str(product['barcode']),
product['old_qty'],
product['product_qty'],
product['price_unit'],
supplier_shortage])
except Exception as exp:
coop_logger.error("Error while updating products in report : %s", str(exp))
if ('user_comments' in data) and data['user_comments'] != "":
ws.append( ['commentaire', data['user_comments']] )
else:
ws.append( ['commentaire', 'Aucun commentaire.'] )
# Save file
wb.save(filename=order['temp_file_name'])
except Exception as e3:
coop_logger.error("Save reception report : Error while create order Workbook %s", str(e3))
# Create report with data from steps 1 & 2 # Create report with data from steps 1 & 2
...@@ -385,13 +373,14 @@ def save_error_report(request): ...@@ -385,13 +373,14 @@ def save_error_report(request):
'supplier_shortage' : row[7] 'supplier_shortage' : row[7]
} }
elif row[0] == 'group': elif row[0] == 'group':
group_name = row[1] # group's orders name group_name = row[1] # group's orders name : Unused
elif row[0] == 'commentaire': elif row[0] == 'commentaire':
data_comment_s1 = row[1] # user comments data_comment_s1 = row[1] # user comments
# Clear step 1 temp file # Clear step 1 temp file
os.remove(order['temp_file_name']) os.remove(order['temp_file_name'])
except: except Exception as e4:
coop_logger.error("Save reception report : reloading first step xlsx file %s", str(e4))
data_comment_s1 = "Données de la première étape absentes !" data_comment_s1 = "Données de la première étape absentes !"
# Add data from step 2 # Add data from step 2
...@@ -450,140 +439,152 @@ def save_error_report(request): ...@@ -450,140 +439,152 @@ def save_error_report(request):
error_total_abs += abs(item['error_line']) error_total_abs += abs(item['error_line'])
data_full.append(item) data_full.append(item)
except Exception as exp: else:
coop_logger.info("Save reception error doc : no 'updated_products' in order (%s)", str(order))
except Exception as e5:
coop_logger.error("Save reception report : Error while updating products %s", str(e5))
# no updated products, do nothing # no updated products, do nothing
print("Error while updating products")
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 = { try:
'product_id': product['nom_contenu'], item = {
'product_supplier_code': product['supplier_code'], 'product_id': product['nom_contenu'],
'product_barcode': product['barcode'], 'product_supplier_code': product['supplier_code'],
'old_qty': float(product['old_qty']), 'product_barcode': product['barcode'],
'product_qty': float(product['product_qty']), 'old_qty': float(product['old_qty']),
'old_price_unit': float(product['price_unit']), 'product_qty': float(product['product_qty']),
'price_unit': '', 'old_price_unit': float(product['price_unit']),
'expected_amount':float(product['old_qty'])*float(product['price_unit']), 'price_unit': '',
'error_line': (float(product['old_qty'])-float(product['product_qty']))*float(product['price_unit']), 'expected_amount':float(product['old_qty'])*float(product['price_unit']),
'supplier_shortage': product['supplier_shortage'] 'error_line': (float(product['old_qty'])-float(product['product_qty']))*float(product['price_unit']),
} 'supplier_shortage': product['supplier_shortage']
}
error_total += item['error_line']
error_total_abs += abs(item['error_line']) error_total += item['error_line']
error_total_abs += abs(item['error_line'])
data_full.append(item)
data_full.append(item)
# Sort by error amount except Exception as e6:
def sortByError(e): coop_logger.error("Save reception report : Error while creating item from product %s (%s)", str(e6), str(e6))
return abs(e['error_line']) try:
data_full.sort(reverse=True, key=sortByError) # Sort by error amount
def sortByError(e):
# Create excel file return abs(e['error_line'])
wb = Workbook() data_full.sort(reverse=True, key=sortByError)
ws = wb.active
ws.title = "Commande " + order['name'] # Create excel file
# Group wb = Workbook()
if 'group_ids' in order : ws = wb.active
ws.append( ['Rapport de réception d\'un groupe de commandes'] ) ws.title = "Commande " + order['name']
ws.append( ['Fournisseur(s) : ', order['partner']] ) # Group
ws.append( ['Références des commandes du groupe : ', order['name']] ) if 'group_ids' in order :
else: ws.append( ['Rapport de réception d\'un groupe de commandes'] )
ws.append( ['Rapport de réception'] ) ws.append( ['Fournisseur(s) : ', order['partner']] )
ws.append( ['Fournisseur : ', order['partner']] ) ws.append( ['Références des commandes du groupe : ', order['name']] )
ws.append( ['Réf Commande : ', order['name']] ) else:
if len(data['orders']) > 1 : ws.append( ['Rapport de réception'] )
ws.append( ['Commande traitée en groupe. Groupe : ', orders_name] ) ws.append( ['Fournisseur : ', order['partner']] )
ws.append( ['Réf Commande : ', order['name']] )
ws.append( ['Date de la commande : ', order['date_order']] ) if len(data['orders']) > 1 :
ws.append( ['Date de la réception : ', time.strftime("%d/%m/%y")] ) ws.append( ['Commande traitée en groupe. Groupe : ', orders_name] )
ws.append( ['Montant total attendu (TTC) : ', str(round(order['amount_total'],2)) + ' €'] )
ws.append( [] ) ws.append( ['Date de la commande : ', order['date_order']] )
ws.append( ['Date de la réception : ', time.strftime("%d/%m/%y")] )
ws.append( ['Nom produit', ws.append( ['Montant total attendu (TTC) : ', str(round(order['amount_total'],2)) + ' €'] )
'Code Four.', ws.append( [] )
'Numéro de Code Barre',
'Qté commande', ws.append( ['Nom produit',
'Qté réception', 'Code Four.',
'Prix unit. initial', 'Numéro de Code Barre',
'Prix unit. MAJ', 'Qté commande',
'Prix total attendu', 'Qté réception',
"Montant erreur livraison (basé sur les différences de quantité)"] ) 'Prix unit. initial',
'Prix unit. MAJ',
if len(data_full) == 0: 'Prix total attendu',
ws.append( ['- Aucune modification -'] ) "Montant erreur livraison (basé sur les différences de quantité)"] )
else:
for product in data_full: if len(data_full) == 0:
ws.append( [product['product_id'], ws.append( ['- Aucune modification -'] )
product['product_supplier_code'], else:
str(product['product_barcode']), for product in data_full:
product['old_qty'], ws.append( [product['product_id'],
product['product_qty'], product['product_supplier_code'],
product['old_price_unit'], str(product['product_barcode']),
product['price_unit'], product['old_qty'],
round(product['expected_amount'], 2), product['product_qty'],
round(product['error_line'], 2), product['old_price_unit'],
product['supplier_shortage']] ) product['price_unit'],
ws.append( [] ) round(product['expected_amount'], 2),
ws.append( ['Montant total de l\'erreur :', '', '', '', '', '', '', '', round(error_total, 2)] ) round(product['error_line'], 2),
ws.append( ['Montant total en valeur absolue :', '', '', '', '', '', '', '', round(error_total_abs, 2)] ) product['supplier_shortage']] )
ws.append( [] )
ws.append( [] ) ws.append( ['Montant total de l\'erreur :', '', '', '', '', '', '', '', round(error_total, 2)] )
ws.append( [] ) ws.append( ['Montant total en valeur absolue :', '', '', '', '', '', '', '', round(error_total_abs, 2)] )
ws.append( ['Problèmes survenus durant le comptage :', data_comment_s1] ) ws.append( [] )
# Merge cells for comments ws.append( [] )
merge_begin = ws.max_row
merge_end = ws.max_row+3 ws.append( ['Problèmes survenus durant le comptage :', data_comment_s1] )
ws.append( [] ) # Merge cells for comments
ws.append( [] ) merge_begin = ws.max_row
ws.append( [] ) merge_end = ws.max_row+3
ws.merge_cells(start_row=merge_begin, start_column=1, end_row=merge_end, end_column=1) ws.append( [] )
ws.merge_cells(start_row=merge_begin, start_column=2, end_row=merge_end, end_column=7) ws.append( [] )
# Styling merged cells ws.append( [] )
top_left_cell = ws['A'+str(merge_begin)] ws.merge_cells(start_row=merge_begin, start_column=1, end_row=merge_end, end_column=1)
top_left_cell.alignment = Alignment(vertical="top") ws.merge_cells(start_row=merge_begin, start_column=2, end_row=merge_end, end_column=7)
top_left_cell = ws['B'+str(merge_begin)] # Styling merged cells
top_left_cell.alignment = Alignment(vertical="top") top_left_cell = ws['A'+str(merge_begin)]
top_left_cell.alignment = Alignment(vertical="top")
ws.append( ['Problèmes survenus durant la mise à jour des prix :', data_comment_s2] ) top_left_cell = ws['B'+str(merge_begin)]
merge_begin = ws.max_row top_left_cell.alignment = Alignment(vertical="top")
merge_end = ws.max_row+3
ws.append( [] ) ws.append( ['Problèmes survenus durant la mise à jour des prix :', data_comment_s2] )
ws.append( [] ) merge_begin = ws.max_row
ws.append( [] ) merge_end = ws.max_row+3
ws.merge_cells(start_row=merge_begin, start_column=1, end_row=merge_end, end_column=1) ws.append( [] )
ws.merge_cells(start_row=merge_begin, start_column=2, end_row=merge_end, end_column=7) ws.append( [] )
top_left_cell = ws['A'+str(merge_begin)] ws.append( [] )
top_left_cell.alignment = Alignment(vertical="top") ws.merge_cells(start_row=merge_begin, start_column=1, end_row=merge_end, end_column=1)
top_left_cell = ws['B'+str(merge_begin)] ws.merge_cells(start_row=merge_begin, start_column=2, end_row=merge_end, end_column=7)
top_left_cell.alignment = Alignment(vertical="top") top_left_cell = ws['A'+str(merge_begin)]
# "Auto fit" columns width to content top_left_cell.alignment = Alignment(vertical="top")
for column_cells in ws.columns: top_left_cell = ws['B'+str(merge_begin)]
length = max(len(as_text(cell.value)) for cell in column_cells) top_left_cell.alignment = Alignment(vertical="top")
# For other columns than the first, limit size # "Auto fit" columns width to content
if column_cells[3].column_letter != "A" and length > 20 : for column_cells in ws.columns:
length = 20 length = max(len(as_text(cell.value)) for cell in column_cells)
# For other columns than the first, limit size
ws.column_dimensions[column_cells[3].column_letter].width = length if column_cells[3].column_letter != "A" and length > 20 :
length = 20
ws.column_dimensions[column_cells[3].column_letter].width = length
except Exception as e7:
coop_logger.error("PO save report : error while creating final Workbook %s", str(e7))
# Save file # Save file
fileName = "temp/" + order['name'] + "_rapport-reception.xlsx" fileName = "temp/" + order['name'] + "_rapport-reception.xlsx"
try: try:
wb.save(filename=fileName) wb.save(filename=fileName)
except Exception as exp: #Attach file to order
print("Error while saving file %s"%fileName) if 'group_ids' in order: # group report
print(str(exp)) # Attach group report to each order
#Attach file to order for group_item_id in order['group_ids']:
if 'group_ids' in order : # group report m = CagetteReception(group_item_id)
# Attach group report to each order m.attach_file(fileName, False)
for group_item_id in order['group_ids'] : os.remove(fileName)
m = CagetteReception(group_item_id) else:
m.attach_file(fileName, False) m = CagetteReception(order['id'])
os.remove(fileName) m.attach_file(fileName)
else: coop_logger.info("%s attached to order id %s", fileName, str(order['id']))
m = CagetteReception(order['id']) except Exception as e8:
m.attach_file(fileName) coop_logger.error("PO save report Error while saving file %s (%s)", fileName, str(e8))
else:
coop_logger.error("Save reception error report : unknown state %s (%s) ", str(data['update_type']), str(data))
else:
coop_logger.error("Cant find 'orders' in data (%s)", str(data))
else:
coop_logger.info("Was waiting for a POST method (%s)", str(request.method))
return JsonResponse("ok", safe=False) return JsonResponse("ok", safe=False)
def reception_FAQ(request): def reception_FAQ(request):
...@@ -625,3 +626,39 @@ def reception_pricesValidated(request): ...@@ -625,3 +626,39 @@ def reception_pricesValidated(request):
def po_process_picking(request): def po_process_picking(request):
res = CagetteReception.process_enqueued_po_to_finalyze() res = CagetteReception.process_enqueued_po_to_finalyze()
return JsonResponse(res, safe=False) return JsonResponse(res, safe=False)
def send_mail_no_barcode(request):
"""
Receive json data with liste of product with no barcode
Send mail to order maker
"""
from django.core.mail import send_mail
if request.method == 'POST':
data = None
try:
myJson = request.body
data = json.loads(myJson.decode())
data_partner = CagetteReception.get_mail_create_po(int(data['order']['id']))
msg = settings.NO_BARCODE_MAIL_MSG
for barcode in data["no_barcode_list"]:
msg = msg + ' -' + str(barcode[0]) + '---' + str(barcode[1])+ '\n'
send_mail(settings.NO_BARCODE_MAIL_SUBJECT.format(data['order']['name']),
msg.format(data_partner[0]['display_name'], data['order']['name'],data['order']['date_order'], data['order']['partner']),
settings.DEFAULT_FROM_EMAIL,
[data_partner[0]['email']],
fail_silently=False,)
except Exception as e1:
coop_logger.error("Send_mail_no_barcode : Unable to load data %s (%s)", str(e1), str(myJson))
print(str(e1)+'\n'+ str(myJson))
return JsonResponse("ok", safe=False)
...@@ -162,7 +162,6 @@ function init_datatable() { ...@@ -162,7 +162,6 @@ function init_datatable() {
// Listener on inputs // Listener on inputs
$('#products_table tbody').on('change', '.stock_edit_input', function () { $('#products_table tbody').on('change', '.stock_edit_input', function () {
let qty = $(this).val(); let qty = $(this).val();
let row = products_table.row($(this).parents('tr')); let row = products_table.row($(this).parents('tr'));
let data = row.data(); let data = row.data();
...@@ -172,24 +171,7 @@ function init_datatable() { ...@@ -172,24 +171,7 @@ function init_datatable() {
data.qty = validated_data; data.qty = validated_data;
row.remove().draw(); row.remove().draw();
products_table.row.add(data).draw(); products_table.row.add(data).draw();
} else {
data.qty = null;
row.remove().draw();
products_table.row.add(data).draw();
if (validated_data == -2) {
$.notify("Ce produit est vendu à l'unité : la valeur doit être un nombre entier !", {
globalPosition:"top right",
className: "error"
});
} else if (validated_data == -1 || validated_data == -3) {
$.notify("Valeur invalide.", {
globalPosition:"top right",
className: "error"
});
}
} }
update_total_value(); update_total_value();
}); });
...@@ -441,20 +423,34 @@ var update_existing_product = function(product, added_qty, undo_option = false) ...@@ -441,20 +423,34 @@ var update_existing_product = function(product, added_qty, undo_option = false)
*/ */
function qty_validation(qty, uom_id) { function qty_validation(qty, uom_id) {
if (qty == null || qty == '') { if (qty == null || qty == '') {
$.notify("Il n'y a pas de quantité indiquée, ou ce n'est pas un nombre", {
globalPosition:"top right",
className: "error"});
return -1; return -1;
} }
if (uom_id == 1) { if (uom_id == 1) {
if (qty/parseInt(qty) != 1 && qty != 0) if (qty/parseInt(qty) != 1 && qty != 0) {
$.notify("Une quantité avec décimale est indiquée alors que c'est un article à l'unité", {
globalPosition:"top right",
className: "error"});
return -2; return -2;
}
qty = parseInt(qty); // product by unit qty = parseInt(qty); // product by unit
} else { } else {
qty = parseFloat(qty).toFixed(2); qty = parseFloat(qty).toFixed(2);
} }
if (isNaN(qty)) if (isNaN(qty)) {
$.notify("Une quantité n'est pas un nombre", {
globalPosition:"top right",
className: "error"});
return -3; return -3;
}
return qty; return qty;
} }
......
...@@ -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
...@@ -16,14 +16,20 @@ ...@@ -16,14 +16,20 @@
{% block content %} {% block content %}
<div class="page_body"> <div class="page_body">
<div id="select_order_content" class="page_content txtcenter"> <div id="select_order_content" class="page_content txtcenter">
<div class="login_area">
{% include "common/conn_admin.html" %}
</div>
<div id="new_order_area"> <div id="new_order_area">
<h2>Créer une nouvelle commande</h2> <h2>Créer une nouvelle commande</h2>
<form id="new_order_form"> <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..."> <input type="text" id="new_order_name" placeholder="Nom de la commande...">
<button type="submit" class="btn btn--primary">Valider</button> <button type="submit" class="btn btn--primary">C'est parti !</button>
</form> </form>
</div> </div>
<div id="existing_orders_area"> <div id="existing_orders_area" style="display:none;">
<h2>Ou, continuer une commande en cours de création</h2> <h2>Ou, continuer une commande en cours de création</h2>
<div id="existing_orders"></div> <div id="existing_orders"></div>
</div> </div>
...@@ -34,9 +40,30 @@ ...@@ -34,9 +40,30 @@
<button type="button" class="btn--danger" id="back_to_order_selection_from_main"> <button type="button" class="btn--danger" id="back_to_order_selection_from_main">
<i class="fas fa-arrow-left"></i>&nbsp; Retour <i class="fas fa-arrow-left"></i>&nbsp; Retour
</button> </button>
<button type="button" class='btn--primary' id="do_inventory" style="display:none;"> <div class="right_action_buttons">
Faire un inventaire
</button> <div id="actions_buttons_wrapper">
<button type="button" class='btn--primary' id="toggle_action_buttons">
<span class="button_content">
Actions
</span>
<span class="toggle_action_buttons_icon">
<i class="fas fa-chevron-down"></i>
</span>
</button>
<div id="actions_buttons_container">
<button type="button" class='btn--primary action_button' id="do_inventory" style="display:none;">
Faire un inventaire
</button>
<button type="button" class='btn--danger action_button' id="delete_order_button">
Supprimer la commande
</button>
</div>
</div>
<a class='btn--warning link_as_button' id="access_metabase" style="display:none;" href="{{metabase_url}}" target="_blank">
Stats Métabase
</a>
</div>
</div> </div>
<div class="header txtcenter"> <div class="header txtcenter">
...@@ -49,14 +76,23 @@ ...@@ -49,14 +76,23 @@
</div> </div>
<div class="txtcenter" id="order_forms_container"> <div class="txtcenter" id="order_forms_container">
<form action="javascript:;" id="coverage_form"> <form action="javascript:;" id="supplier_form" class="order_form_item">
<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"> <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> <button type="submit" class='btn--primary'>Ajouter le fournisseur</button>
</form> </form>
<form action="javascript:;" id="stats_date_from_form" class="order_form_item">
<label for="stats_date_period_select">Période de calcul de la conso moyenne </label>
<select name="stats_date_period_select" id="stats_date_period_select">
<option value="">Par défaut</option>
<option value="1week">1 semaine</option>
<option value="2weeks">2 semaines</option>
</select>
</form>
<form action="javascript:;" id="coverage_form" class="order_form_item">
<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>
</div> </div>
<div id="suppliers_container"></div> <div id="suppliers_container"></div>
...@@ -117,8 +153,14 @@ ...@@ -117,8 +153,14 @@
<span class="pill_supplier_name"></span> <span class="pill_supplier_name"></span>
<i class="fas fa-times remove_supplier_icon"></i> <i class="fas fa-times remove_supplier_icon"></i>
</div> </div>
<div class="supplier_total_value_container"> <div class="supplier_data">
Total: <span class="supplier_total_value">0</span> <div class="supplier_total_value_container">
Total : <span class="supplier_total_value">0</span>
</div>
&nbsp;&nbsp;|&nbsp;&nbsp;
<div class="supplier_total_packages_container">
Nb colis : <span class="supplier_total_packages">0</span>
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -126,6 +168,7 @@ ...@@ -126,6 +168,7 @@
<div id="order_pill_template"> <div id="order_pill_template">
<div class="pill order_pill btn btn--primary"> <div class="pill order_pill btn btn--primary">
<span class="pill_order_name"></span> <span class="pill_order_name"></span>
<i class="fas fa-times remove_order_icon"></i>
</div> </div>
</div> </div>
...@@ -136,7 +179,7 @@ ...@@ -136,7 +179,7 @@
<h4 class="new_order_date_planned"></h4> <h4 class="new_order_date_planned"></h4>
<div class='download_order_file'> <div class='download_order_file'>
<i class="fas fa-spinner fa-spin download_order_file_loading"></i> <i class="fas fa-spinner fa-spin download_order_file_loading"></i>
<a class='btn--success download_order_file_button' style="display:none;" href="#"> <a class='btn--success download_order_file_button link_as_button' style="display:none;" href="#">
Télécharger le fichier de commande Télécharger le fichier de commande
</a> </a>
</div> </div>
...@@ -156,6 +199,15 @@ ...@@ -156,6 +199,15 @@
<p>Voulez-vous quand même y accéder ?</p> <p>Voulez-vous quand même y accéder ?</p>
<hr/> <hr/>
</div> </div>
<div id="modal_remove_order">
<h3>Attention !</h3>
<p class="remove_order_modal_text">
Vous vous apprêtez à <b style="color: #d9534f;">supprimer</b> cette commande en cours : <span class="remove_order_name"></span>.<br/>
</p>
<p>Êtez-vous sûr ?</p>
<hr/>
</div>
<div id="modal_remove_supplier"> <div id="modal_remove_supplier">
<h3>Attention !</h3> <h3>Attention !</h3>
...@@ -193,6 +245,19 @@ ...@@ -193,6 +245,19 @@
<p>Êtez-vous sûr ?</p> <p>Êtez-vous sûr ?</p>
<hr/> <hr/>
</div> </div>
<div id="modal_end_supplier_product_association">
<h3>Attention !</h3>
<p>
Vous vous apprêtez à rendre le produit <span class="product_name"></span>
indisponible chez le fournisseur <span class="supplier_name"></span>.
</p>
<p>
L'association sera supprimée dès que vous aurez cliqué sur "Valider".<br/>
</p>
<p>Êtez-vous sûr ?</p>
<hr/>
</div>
<div id="modal_create_inventory"> <div id="modal_create_inventory">
<p> <p>
...@@ -244,6 +309,7 @@ ...@@ -244,6 +309,7 @@
var couchdb_dbname = '{{db}}'; var couchdb_dbname = '{{db}}';
var couchdb_server = '{{couchdb_server}}' + couchdb_dbname; var couchdb_server = '{{couchdb_server}}' + couchdb_dbname;
var odoo_server = '{{odoo_server}}'; var odoo_server = '{{odoo_server}}';
var metabase_url = '{{metabase_url}}';
</script> </script>
<script src="{% static "js/all_common.js" %}?v="></script> <script src="{% static "js/all_common.js" %}?v="></script>
<script type="text/javascript" src="{% static 'js/orders_helper.js' %}?v="></script> <script type="text/javascript" src="{% static 'js/orders_helper.js' %}?v="></script>
......
...@@ -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