Commit 26bae4b3 by Damien Moulard

Merge branch 'story_2824' into 'dev_cooperatic'

Add change product shelf function

See merge request !175
parents bc6ab168 e42f5e6f
Pipeline #2196 failed with stage
in 1 minute 28 seconds
...@@ -47,6 +47,7 @@ def custom_list_inventory(request, id): ...@@ -47,6 +47,7 @@ def custom_list_inventory(request, id):
context = {'title': 'Inventaire', context = {'title': 'Inventaire',
'products' : json.dumps(products['data']), 'products' : json.dumps(products['data']),
'ahead_shelfs_ids': json.dumps(getattr(settings, 'SHELFS_TO_BE_AHEAD_IN_SELECT_LIST', []))
} }
# Reuse shelf inventory template: same process # Reuse shelf inventory template: same process
......
...@@ -386,6 +386,12 @@ ...@@ -386,6 +386,12 @@
- MEALS_PICKING_TYPE_ID = 10 - MEALS_PICKING_TYPE_ID = 10
### Inventory
- SHELFS_TO_BE_AHEAD_IN_SELECT_LIST = [90,74]
These shelfs (odoo ids) will be shown first in select list
### New members space ### New members space
- USE_NEW_MEMBERS_SPACE = True - USE_NEW_MEMBERS_SPACE = True
......
...@@ -528,11 +528,14 @@ class Shelf(models.Model): ...@@ -528,11 +528,14 @@ class Shelf(models.Model):
class Shelfs(models.Model): class Shelfs(models.Model):
def get_all(): def get_all(precision='full'):
res = [] res = []
try: try:
api = OdooAPI() api = OdooAPI()
res = api.execute('product.shelfs', 'get', {}) if precision == 'simple':
res = api.search_read('product.shelfs', [], ['name', 'sort_order'], order='sort_order asc')
else:
res = api.execute('product.shelfs', 'get', {})
except Exception as e: except Exception as e:
coop_logger.error("Rayons, get_all : %s", str(e)) coop_logger.error("Rayons, get_all : %s", str(e))
return res return res
...@@ -552,3 +555,28 @@ class Shelfs(models.Model): ...@@ -552,3 +555,28 @@ class Shelfs(models.Model):
except Exception as e: except Exception as e:
coop_logger.error("Rayons, get_shelfs_sortorder : %s", str(e)) coop_logger.error("Rayons, get_shelfs_sortorder : %s", str(e))
return res return res
@staticmethod
def make_products_shelf_links(data):
"""Set shelf_id for each product found in data."""
res = {}
try:
api = OdooAPI()
res['done'] = []
# First of all, group product by shelf_id to save api server calls
products_shelf = {}
for elt in data:
if elt['shelf_id'] not in products_shelf:
products_shelf[elt['shelf_id']] = []
products_shelf[elt['shelf_id']].append(int(elt['product_id']))
# iterate on each shelf element to record changes
for shelf_id, product_ids in products_shelf.items():
f = {'shelf_id': shelf_id}
if api.update('product.product', product_ids , f):
res['done'] += product_ids
except Exception as e:
res['error'] = str(e)
coop_logger.error("Rayons, make_products_shelf_links : %s", str(e))
return res
\ No newline at end of file
...@@ -147,6 +147,13 @@ table.dataTable { ...@@ -147,6 +147,13 @@ table.dataTable {
padding: 5px; padding: 5px;
} }
#header_container_left {
float: left;
}
#change_shelf_btn {
float: right;
}
div#container_edition { div#container_edition {
padding: 8px; padding: 8px;
background-color: #e7e9ed; background-color: #e7e9ed;
...@@ -296,3 +303,8 @@ hr { ...@@ -296,3 +303,8 @@ hr {
opacity: 1; opacity: 1;
} }
/* Change shelf modal */
.shelf_selection {
max-width: 240px;
}
...@@ -12,7 +12,9 @@ var validation_msg = $('#validation_msg'), ...@@ -12,7 +12,9 @@ var validation_msg = $('#validation_msg'),
process_all_items_msg = $('#process_all_items_msg'), process_all_items_msg = $('#process_all_items_msg'),
faq_content = $("#FAQ_modal_content"), faq_content = $("#FAQ_modal_content"),
issues_reporting = $("#issues_reporting"), issues_reporting = $("#issues_reporting"),
add_product_form = $("#add_product_form"); add_product_form = $("#add_product_form"),
change_shelf_form = $("#change_shelf_form"),
change_shelf_btn = $('#change_shelf_btn');
var shelf = null, var shelf = null,
parent_location = '/shelfs', parent_location = '/shelfs',
...@@ -28,7 +30,9 @@ var shelf = null, ...@@ -28,7 +30,9 @@ var shelf = null,
adding_product = false, // True if modal to add a product is open. adding_product = false, // True if modal to add a product is open.
barcodes = null, // Barcodes stored locally barcodes = null, // Barcodes stored locally
// datetime for which shelf's ongoing_inv_start_datetime is considered null // datetime for which shelf's ongoing_inv_start_datetime is considered null
default_inventory_start_datetime = "0001-01-01 00:00:00"; default_inventory_start_datetime = "0001-01-01 00:00:00",
selected_products_for_shelf_change = [],
all_shelfs = null; // Use get_all_shelfs to access it's value
/* UTILS */ /* UTILS */
...@@ -297,6 +301,25 @@ function validateEdition() { ...@@ -297,6 +301,25 @@ function validateEdition() {
} }
} }
/**
* Unselect all rows from datatable
* Only for table_to_process
*/
function unselect_all_rows() {
$("#select_all_products_cb").prop("checked", false);
table_to_process.rows().every(function() {
const node = $(this.node());
node.removeClass('selected');
node.find(".select_product_cb").first()
.prop("checked", false);
return 0;
});
selected_rows = [];
}
/* /*
* Update a product info and add it to processed items. * Update a product info and add it to processed items.
* If 'value' is set, use it as new value. * If 'value' is set, use it as new value.
...@@ -323,6 +346,136 @@ function editProductInfo (productToEdit, value = null) { ...@@ -323,6 +346,136 @@ function editProductInfo (productToEdit, value = null) {
return true; return true;
} }
/* Change shelf process */
/*
data should be an array of {product_id: xx, shelf_id: yy}
*/
function record_products_shelf_on_server(data) {
return new Promise(resolve => {
$.ajax({
type: "POST",
url: "/shelfs/change_products_shelfs",
dataType: "json",
data: JSON.stringify(data),
traditional: true,
contentType: "application/json; charset=utf-8",
success: function(data) {
if (typeof data.res !== "undefined" && typeof data.res.done !== "undefined")
resolve(data.res.done);
else
resolve(null);
},
error: function() {
alert("Impossible de mettre à jour les données");
resolve(null);
}
});
});
}
// call on change_shelf_btn click action
async function open_change_shelf_modal() {
selected_products_for_shelf_change = [];
$('.select_product_cb:checked').each(function(idx,elt){
const row = $(elt).closest('tr');
selected_products_for_shelf_change.push(table_to_process.row(row).data())
});
if (selected_products_for_shelf_change.length > 0) {
/*
As button is not shown if no product is selected, should be always true
But, with CSS changes, it could happen that length == 0
*/
let shelfs = await get_all_shelfs();
if (shelfs !== null) {
let modal_content = $('#templates #change_shelf_form').clone(),
shelf_selector = $('<select>').addClass('shelf_selection'),
table = modal_content.find('table tbody').empty();
/* construct shelfs selector */
// first of all, sort by name
shelfs.sort((a,b) => (a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0));
// if ahead_shelfs_ids is not empty, put them ahead
if (ahead_shelfs_ids.length > 0) {
let to_move = {},
idx = 0;
// find index of shelfs to move
shelfs.forEach((shelf) => {
if (ahead_shelfs_ids.indexOf(shelf.id) > -1) {
to_move[shelf.id] = idx;
}
idx += 1;
});
// Respecting ahead_shelfs_ids order, move shelf ahead
// splice can not be used, since more than 1 elt could be involved
let ahead_elts = [];
ahead_shelfs_ids.forEach((shelf_id) => {
let shelf = shelfs[to_move[shelf_id]];
ahead_elts.push(shelf);
});
//remove ahead elts
shelfs = shelfs.filter((item) => {return !ahead_elts.includes(item.id)});
// put them ahead by concatenation
shelfs = ahead_elts.concat(shelfs);
}
shelfs.forEach(
(shelf) => {
let option = $('<option>')
.val(shelf.id)
.text(shelf.name + ' (' + shelf.sort_order + ')');
shelf_selector.append(option);
});
/* add product rows */
selected_products_for_shelf_change.forEach(
(product) => {
let tr = $('<tr>').attr('data-id',product.id)
.append($('<td>').text(product.name))
.append($('<td>').append(shelf_selector.clone()));
table.append(tr);
});
openModal(
modal_content.html(),
() => {
if (is_time_to('change_product_shelf', 500)) {
make_change = async () => {
// Prepare data to be transmitted to server to be recorded
let data = [];
$('.overlay-content table tbody tr').each(function(idx,e){
data.push({
product_id : $(e).data('id'),
shelf_id : $(e).find('select').val()
});
});
const update_result = await record_products_shelf_on_server(data);
if (update_result !== null) {
update_result.forEach(
(product_id) => {
remove_from_toProcess(table_to_process.row($('tr#'+product_id)));
});
let message = "L'opération a bien réussi.";
if (update_result.length !== data.length) {
message = "L'opération a partiellement réussi.\n";
message += (data.length - update_result.length) + " produit(s) non déplacé(s).";
//TODO: display which products changes were in error
}
alert(message);
}
}
make_change();
}
},
'Changer les produits de rayons'
);
} else {
alert("Les informations des autres rayons n'ont pas pu être récupérées.")
}
}
}
/* LISTS HANDLING */ /* LISTS HANDLING */
...@@ -342,8 +495,24 @@ function initLists() { ...@@ -342,8 +495,24 @@ function initLists() {
} }
// Init table for items to process // Init table for items to process
var columns_to_process = [
{data:"id", title: "id", visible: false}, var columns_to_process = [];
if (shelf.inventory_status !== "") {
columns_to_process.push({data:"id", title: "id", visible: false});
} else {
columns_to_process.push({
title: `<div id="table_header_select_all" class="txtcenter">
<input type="checkbox" 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" />`;
},
width: "4%"});
}
columns_to_process = columns_to_process.concat([
{data:"name", title:"Produit", width: "60%"}, {data:"name", title:"Produit", width: "60%"},
{data:"uom_id.1", title:"Unité de mesure", className:"dt-body-center"}, {data:"uom_id.1", title:"Unité de mesure", className:"dt-body-center"},
{ {
...@@ -351,7 +520,7 @@ function initLists() { ...@@ -351,7 +520,7 @@ function initLists() {
defaultContent: "<a class='btn' id='process_item' href='#'><i class='far fa-edit'></i></a>", "className":"dt-body-center", defaultContent: "<a class='btn' id='process_item' href='#'><i class='far fa-edit'></i></a>", "className":"dt-body-center",
orderable: false orderable: false
} }
]; ]);
if (originView == 'custom_list') { if (originView == 'custom_list') {
columns_to_process.splice(1, 0, {data:"shelf_sortorder", title:"Rayon", className:"dt-body-center"}); columns_to_process.splice(1, 0, {data:"shelf_sortorder", title:"Rayon", className:"dt-body-center"});
...@@ -451,6 +620,41 @@ function initLists() { ...@@ -451,6 +620,41 @@ function initLists() {
clearLineEdition(); clearLineEdition();
} }
}); });
// Select row(s) on checkbox change (copied from orders_helper.js -only table_to_process changed-)
$(table_to_process.table().header()).on('click', 'th #select_all_products_cb', function () {
if (this.checked) {
selected_rows = [];
table_to_process.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;
});
change_shelf_btn.show();
} else {
unselect_all_rows();
change_shelf_btn.hide();
}
});
$(table_to_process.table().body()).on('click', '.select_product_cb', function () {
if (this.checked) {
change_shelf_btn.show();
} else {
// must hide change_shelf_btn only if no other product is selected
if ($('.select_product_cb:checked').length === 0) {
change_shelf_btn.hide();
}
}
});
change_shelf_btn.click(open_change_shelf_modal);
} }
// Add a line to the 'items to process' list // Add a line to the 'items to process' list
...@@ -766,6 +970,37 @@ function saveIssuesReport() { ...@@ -766,6 +970,37 @@ function saveIssuesReport() {
/* INIT */ /* INIT */
// (for shelf change)
function get_all_shelfs() {
return new Promise(resolve => {
if (all_shelfs !== null) {
resolve(all_shelfs);
} else {
$.ajax({
type: 'GET',
url: "/shelfs/all/simple",
dataType:"json",
traditional: true,
contentType: "application/json; charset=utf-8",
success: function(data) {
shelfs = null;
if (typeof data.res !== "undefined" && data.res.length > 0)
shelfs = data.res;
resolve(shelfs);
},
error: function(data) {
err = {msg: "erreur serveur lors de la récupération des rayons", ctx: 'get_all_shelfs'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'shelf_inventory');
resolve(null);
}
});
}
});
}
// Get shelf data from server if not in local storage // Get shelf data from server if not in local storage
function get_shelf_data() { function get_shelf_data() {
var url = (originView == 'shelf') ? '../' + shelf.id : '../get_custom_list_data?id=' + shelf.id; var url = (originView == 'shelf') ? '../' + shelf.id : '../get_custom_list_data?id=' + shelf.id;
......
...@@ -9,8 +9,9 @@ urlpatterns = [ ...@@ -9,8 +9,9 @@ urlpatterns = [
url(r'^shelf_view/([0-9]+)$', views.shelf_view), url(r'^shelf_view/([0-9]+)$', views.shelf_view),
url(r'^shelf_inventory/([0-9]+)$', views.shelf_inventory), url(r'^shelf_inventory/([0-9]+)$', views.shelf_inventory),
url(r'^inventory_process_state/([0-9]+)$', views.inventory_process_state), url(r'^inventory_process_state/([0-9]+)$', views.inventory_process_state),
url(r'^all$', views.all), url(r'^all/?([a-z]*)$', views.all),
url(r'^get_shelves_extra_data$', views.get_shelves_extra_data), url(r'^get_shelves_extra_data$', views.get_shelves_extra_data),
url(r'^change_products_shelfs$', views.change_products_shelfs),
url(r'^(?P<shelf_id>\d+)$', views.shelf_data), url(r'^(?P<shelf_id>\d+)$', views.shelf_data),
url(r'^(?P<shelf_id>\d+)/products$', views.products), url(r'^(?P<shelf_id>\d+)/products$', views.products),
url(r'^(?P<shelf_id>\d+)/add_product$', views.add_product), url(r'^(?P<shelf_id>\d+)/add_product$', views.add_product),
......
...@@ -47,7 +47,9 @@ def shelf_inventory(request, id): ...@@ -47,7 +47,9 @@ def shelf_inventory(request, id):
shelf_products = Shelf(id).get_products() shelf_products = Shelf(id).get_products()
context = {'title': 'Inventaire du rayon', context = {'title': 'Inventaire du rayon',
'products': json.dumps(shelf_products['data'])} 'products': json.dumps(shelf_products['data']),
'ahead_shelfs_ids': json.dumps(getattr(settings, 'SHELFS_TO_BE_AHEAD_IN_SELECT_LIST', []))
}
template = loader.get_template('shelfs/shelf_inventory.html') template = loader.get_template('shelfs/shelf_inventory.html')
return HttpResponse(template.render(context, request)) return HttpResponse(template.render(context, request))
...@@ -79,9 +81,9 @@ def delete_ongoing_inv_data(request, shelf_id): ...@@ -79,9 +81,9 @@ def delete_ongoing_inv_data(request, shelf_id):
else: else:
return JsonResponse({'res': res}) return JsonResponse({'res': res})
def all(request): def all(request, precision):
"""Get all shelves data""" """Get all shelves data"""
return JsonResponse({'res': Shelfs.get_all()}) return JsonResponse({'res': Shelfs.get_all(precision)})
def get_shelves_extra_data(request): def get_shelves_extra_data(request):
"""Get data that need calculation, so long execution time""" """Get data that need calculation, so long execution time"""
...@@ -138,6 +140,19 @@ def inventory_process_state(request, shelf_id): ...@@ -138,6 +140,19 @@ def inventory_process_state(request, shelf_id):
else: else:
return JsonResponse({'res': res}) return JsonResponse({'res': res})
def change_products_shelfs(request):
res = {}
try:
data = json.loads(request.body.decode())
res = Shelfs.make_products_shelf_links(data)
except Exception as e:
res['error'] = str(e)
coop_logger.error("change_products_shelfs : %s", str(e))
if 'error' in res:
return JsonResponse(res, status=500)
else:
return JsonResponse({'res': res})
def do_shelf_inventory(request): def do_shelf_inventory(request):
"""Process shelf inventory""" """Process shelf inventory"""
""" """
......
...@@ -91,6 +91,9 @@ ...@@ -91,6 +91,9 @@
<div class="container_products" id="container_left"> <div class="container_products" id="container_left">
<h4 id="header_container_left">Produits à compter</h4> <h4 id="header_container_left">Produits à compter</h4>
<button style="display:none;" id="change_shelf_btn" class="btn btn--primary">
Changer de rayon
</button>
<table id="table_to_process" class="display" cellspacing="0"></table> <table id="table_to_process" class="display" cellspacing="0"></table>
</div> </div>
<div class="container_products" id="container_right"> <div class="container_products" id="container_right">
...@@ -167,10 +170,24 @@ ...@@ -167,10 +170,24 @@
<input autocomplete="off" type="text" placeholder="Code barre du produit" class="add_product_input"> <input autocomplete="off" type="text" placeholder="Code barre du produit" class="add_product_input">
<hr /> <hr />
</div> </div>
<div id="change_shelf_form">
<h3>Changement de rayons</h3>
<hr />
<table>
<thead>
<tr>
<th>Produit</th>
<th>Rayon</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div> </div>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
var products = {{products|safe}} const ahead_shelfs_ids = {{ahead_shelfs_ids|safe}};
var products = {{products|safe}};
</script> </script>
<script src="{% static "js/all_common.js" %}?v="></script> <script src="{% static "js/all_common.js" %}?v="></script>
<script src="{% static "js/common.js" %}?v="></script> <script src="{% static "js/common.js" %}?v="></script>
......
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