Commit ad361176 by François C.

Merge branch 'dev_cooperatic' into 'dev_principale'

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

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