Commit 97eaabd2 by Damien Moulard

Merge branch 'aide_a_la_commande' into dev_cooperatic

parents f4fd7818 aa99f68f
......@@ -109,3 +109,6 @@ RECEPTION_PB = "Ici, vous pouvez signaler toute anomalie lors d'une réception,
# display or not column "Autres" in reception process
DISPLAY_COL_AUTRES = False
# URL to the metabase dashboard for orders helper
ORDERS_HELPER_METABASE_URL = "https://metabase.lacagette-coop.fr/dashboard/16"
......@@ -258,6 +258,11 @@ class Order(models.Model):
}
for line in order_lines:
product_line_name = line["name"]
if "product_code" in line and line["product_code"] is not False:
product_code = str(line["product_code"])
product_line_name = "[" + product_code + "] " + product_line_name
order_data["order_line"].append(
[
0,
......@@ -267,7 +272,7 @@ class Order(models.Model):
"price_policy": "uom",
"indicative_package": True,
"product_id": line["product_variant_ids"][0],
"name": line["name"],
"name": product_line_name,
"date_planned": date_planned,
"account_analytic_id": False,
"product_qty_package":line["product_qty_package"],
......
......@@ -19,7 +19,7 @@
flex-direction: column;
justify-content: center;
align-items: center;
padding: 5px 30px 5px 30px;
padding: 7px 30px 7px 30px;
margin: 0 10px 5px 10px;
}
......@@ -31,6 +31,26 @@
background-color: #a1a2a3;
}
.link_as_button:hover {
text-decoration: none;
color: white;
}
.link_as_button:active {
text-decoration: none;
color: white;
}
.link_as_button:focus {
text-decoration: none;
color: white;
}
.remove_order_modal_text {
font-size: 2rem;
}
.remove_order_name {
font-weight: bold;
}
/* - Order selection screen */
#new_order_area {
margin-bottom: 40px;
......@@ -50,6 +70,25 @@
padding-top: 15px;
}
.order_pill {
flex-direction: row;
}
.pill_order_name {
flex: 3 0 auto;
}
.remove_order_icon {
flex: 0 1 auto;
color: #912930;
margin-left: 5px;
cursor: pointer;
z-index: 2;
transform: scale(1.2);
}
.remove_order_icon:hover {
color: #e62720;
}
.order_last_update {
font-weight: bold;
}
......@@ -82,6 +121,40 @@
justify-content: flex-start;
}
.right_action_buttons {
display: flex;
}
#actions_buttons_wrapper {
position: relative;
margin-right: 5px;
}
#toggle_action_buttons {
width: 250px;
position: relative;
}
.toggle_action_buttons_icon {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 15px;
}
#actions_buttons_container {
position: absolute;
display: flex;
flex-direction: column;
width: 250px;
display: none;
}
.action_button {
width: 100%;
min-height: 45px;
}
/* -- Order data */
#order_data_container {
font-size: 1.8rem;
......@@ -92,17 +165,27 @@
}
#order_forms_container {
margin-top: 30px;
margin-top: 20px;
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
}
.order_form_item {
margin-top: 10px;
}
#supplier_input {
width: 350px;
border-radius: 3px;
}
#date_planned_input, #coverage_days_input {
#stats_date_period_select {
margin-left: 5px;
min-width: 200px;
}
#date_planned_input, #coverage_days_input, #stats_date_period_select {
border-radius: 3px;
}
......@@ -144,18 +227,27 @@
width: 100px;
}
.product_ref_input {
padding: .5rem .5rem;
}
.supplier_package_qty {
font-style: italic;
font-size: 1.3rem;
}
.product_not_from_supplier {
background-color: #e7e9ed;
background-color: #e8ebf0;
cursor: pointer;
}
.product_not_from_supplier:hover {
background-color: #c7cace;
background-color: #d3d7db;
}
.product_ref_cell:hover {
background-color: #d3d7db;
cursor: pointer;
}
.product_name, .supplier_name, .product_npa {
......@@ -166,6 +258,10 @@
cursor: pointer;
}
.focused_line {
background-color: #76cf71 !important;
}
/* -- Footer */
#main_content_footer {
......@@ -185,19 +281,24 @@
align-items: center;
flex-wrap: wrap;
margin: 30px 0 20px 0;
position: -webkit-sticky;
position: sticky;
top: 20px;
z-index: 3;
}
.supplier_pill {
background-color: #e7e9edc5;
border: 1px solid black;
background-color: #a0daff;
border: 1px solid #6ea8cc;
}
.pill_supplier_name {
font-weight: bold;
}
.supplier_total_value_container {
.supplier_data {
font-size: 1.5rem;
display: flex;
}
.remove_supplier_icon {
......@@ -253,19 +354,6 @@
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;
}
......
......@@ -16,6 +16,7 @@ var dbc = null,
order_doc = {
_id: null,
coverage_days: null,
stats_date_period: '',
last_update: {
timestamp: null,
fingerprint: null
......@@ -42,6 +43,7 @@ function reset_data() {
order_doc = {
_id: null,
coverage_days: null,
stats_date_period: '',
last_update : {
timestamp: null,
fingerprint: null
......@@ -81,6 +83,39 @@ function dates_diff(date1, date2) {
return diff;
}
/**
* Compute the date from which to calculate stats of sells,
* depending on the selected parameter.
*
* @returns String value of the date, ISO format
*/
function _compute_stats_date_from() {
let val = '';
if (order_doc.stats_date_period !== '') {
let date = new Date();
switch (order_doc.stats_date_period) {
case '1week':
date.setDate(date.getDate() - 7);
break;
case '2weeks':
date.setDate(date.getDate() - 14);
break;
default:
break;
}
let day = ("0" + date.getDate()).slice(-2);
let month = ("0" + (date.getMonth() +1)).slice(-2);
let year = date.getFullYear();
val = `${year}-${month}-${day}`;
}
return val;
}
/* - PRODUCTS */
/**
......@@ -110,10 +145,15 @@ function add_product() {
return -1;
}
let data = {
pids: [product.tpl_id],
stats_from: _compute_stats_date_from()
};
$.ajax({
type: 'POST',
url: '/products/get_product_for_order_helper',
data: JSON.stringify([product.tpl_id]),
data: JSON.stringify(data),
dataType:"json",
traditional: true,
contentType: "application/json; charset=utf-8",
......@@ -199,7 +239,8 @@ function check_products_data() {
type: 'GET',
url: '/orders/get_supplier_products',
data: {
sids: suppliers_id
sids: suppliers_id,
stats_from: _compute_stats_date_from()
},
dataType:"json",
traditional: true,
......@@ -208,21 +249,24 @@ function check_products_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;
if (p_index === -1) {
// Add product if it wasn't fetched before (made available since last access to order)
products.push(product);
} else {
// Save old product suppliersinfo to keep user qty inputs
const old_suppliersinfo = [...products[p_index].suppliersinfo];
delete product.suppliersinfo;
products[p_index] = { ...products[p_index], ...product };
// Update product data
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);
// Re-set qties
for (let psi_index in products[p_index].suppliersinfo) {
const old_psi = old_suppliersinfo.find(psi => psi.supplier_id == products[p_index].suppliersinfo[psi_index].supplier_id);
if (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;
if (old_psi !== undefined && old_psi.qty !== undefined) {
products[p_index].suppliersinfo[psi_index].qty = old_psi.qty;
}
}
}
}
......@@ -248,6 +292,73 @@ function check_products_data() {
});
}
/**
* Update the product internal reference ('default_code')
*
* @param {HTMLElement} input_el
* @param {int} p_id
* @param {int} p_index
*/
function update_product_ref(input_el, p_id, p_index) {
const val = $(input_el).val();
const existing_val = products[p_index].default_code.replace("[input]", "");
products[p_index].default_code = val;
const row = $(input_el).closest('tr');
const new_row_data = prepare_datatable_data([p_id])[0];
products_table.row(row).data(new_row_data)
.draw();
$('#products_table')
.off('blur', 'tbody .product_ref_input')
.off('keypress', 'tbody .product_ref_input');
// Update in backend if value changed
if (existing_val !== val) {
const data = {
'product_tmpl_id': p_id,
'default_code': val
};
// Send request to create association
$.ajax({
type: "POST",
url: "/products/update_product_internal_ref",
dataType: "json",
traditional: true,
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
success: () => {
update_cdb_order();
$(".actions_buttons_area .right_action_buttons").notify(
"Référence sauvegardée !",
{
elementPosition:"bottom right",
className: "success",
arrowShow: false
}
);
},
error: function(data) {
let msg = "erreur serveur lors de la sauvegarde de la référence";
msg += ` (product_tmpl_id: ${product.id}`;
err = {msg: msg, ctx: 'update_product_ref'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
alert('Erreur lors de la sauvegarde de la référence dans Odoo. Veuillez recharger la page et ré-essayer plus tard.');
}
});
}
}
/* - SUPPLIERS */
......@@ -259,9 +370,9 @@ function check_products_data() {
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);
// Check if user input is a valid supplier
if (supplier === undefined) {
alert("Le fournisseur renseigné n'est pas valide.\n"
+ "Veuillez sélectionner un fournisseur dans la liste déroulante.");
......@@ -279,21 +390,22 @@ function add_supplier() {
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,
url: "/orders/get_supplier_products",
data: {
sids: [supplier.id],
stats_from: _compute_stats_date_from()
},
dataType:"json",
traditional: true,
contentType: "application/json; charset=utf-8",
success: function(data) {
supplier.total_value = 0;
supplier.total_packages = 0;
selected_suppliers.push(supplier);
save_supplier_products(supplier, data.res.products);
update_main_screen();
$("#supplier_input").val("");
......@@ -385,6 +497,7 @@ function save_supplier_product_association(product, supplier, cell) {
product.suppliersinfo.push({
supplier_id: supplier.id,
package_qty: package_qty,
product_code: false,
price: price
});
......@@ -403,8 +516,9 @@ function save_supplier_product_association(product, supplier, cell) {
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})`;
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') {
......@@ -421,6 +535,59 @@ function save_supplier_product_association(product, supplier, cell) {
}
/**
* Send to server the deletion of association product-supplier
*
* @param {object} product
* @param {object} supplier
*/
function end_supplier_product_association(product, supplier) {
openModal();
const data = {
product_tmpl_id: product.id,
supplier_id: supplier.id
};
// Send request to create association
$.ajax({
type: "POST",
url: "/orders/end_supplier_product_association",
dataType: "json",
traditional: true,
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
success: () => {
// Remove relation locally
let p_index = products.findIndex(p => p.id == product.id);
let psi_index = product.suppliersinfo.findIndex(psi => psi.supplier_id == supplier.id);
products[p_index].suppliersinfo.splice(psi_index, 1);
// Update table
display_products();
update_cdb_order();
closeModal();
},
error: function(data) {
let msg = "erreur serveur lors de la suppression de l'association product/supplier".
msg += ` (product_tmpl_id: ${product.id}; supplier_id: ${supplier.id})`;
err = {msg: msg, ctx: 'end_supplier_product_association'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
closeModal();
alert('Erreur lors de la suppression de l\'association. Veuillez ré-essayer plus tard.');
}
});
return 0;
}
/**
* When products are fetched, save them and the relation with the supplier.
* If product already saved, add the supplier to its suppliers list.
* Else, add product with supplier.
......@@ -435,11 +602,13 @@ function save_supplier_products(supplier, new_products) {
if (index === -1) {
products.push(np);
} else {
// Prevent adding ducplicate supplierinfo
// Prevent adding duplicate 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];
// Find the right supplierinfo in new product
let np_supplierinfo = np.suppliersinfo.find(psi => psi.supplier_id == supplier.id);
products[index].suppliersinfo.push(np_supplierinfo);
}
}
......@@ -484,15 +653,20 @@ function _compute_total_values_by_supplier() {
// Reinit
for (let s of selected_suppliers) {
s.total_value = 0;
s.total_packages = 0;
}
for (let p of products) {
for (let supinfo of p.suppliersinfo) {
let supplier_index = selected_suppliers.findIndex(s => s.id == supinfo.supplier_id);
// Value
let product_supplier_value = ('qty' in supinfo) ? supinfo.qty * supinfo.package_qty * supinfo.price : 0;
selected_suppliers[supplier_index].total_value += product_supplier_value;
// Packages
selected_suppliers[supplier_index].total_packages += ('qty' in supinfo) ? supinfo.qty : 0;
}
}
}
......@@ -526,11 +700,12 @@ function set_product_npa(p_id, npa) {
// Give time for modal to fade
setTimeout(function() {
$.notify(
$(".actions_buttons_area .right_action_buttons").notify(
"Produit passé en NPA !",
{
globalPosition:"top right",
className: "success"
elementPosition:"bottom right",
className: "success",
arrowShow: false
}
);
}, 500);
......@@ -596,7 +771,7 @@ function generate_inventory() {
modal_create_inventory.html(),
() => {
if (is_time_to('validate_generate_inventory')) {
$('#do_inventory').empty()
$('#toggle_action_buttons .button_content').empty()
.append(`<i class="fas fa-spinner fa-spin"></i>`);
$.ajax({
type: "POST",
......@@ -610,12 +785,12 @@ function generate_inventory() {
// Give time for modal to fade
setTimeout(function() {
$('#do_inventory').empty()
.append(`Faire un inventaire`);
$('#do_inventory').notify(
$('#toggle_action_buttons .button_content').empty()
.append(`Actions`);
$('#toggle_action_buttons').notify(
"Inventaire créé !",
{
globalPosition:"bottom center",
elementPosition:"bottom center",
className: "success"
}
);
......@@ -733,6 +908,8 @@ function create_cdb_order() {
/**
* Update order data of an existing order in couchdb
*
* @returns Promise resolved after update is complete
*/
function update_cdb_order() {
order_doc.products = products;
......@@ -761,6 +938,28 @@ function update_cdb_order() {
}
/**
* Delete an order in couchdb.
*
* @returns Promise resolved after delete is complete
*/
function delete_cdb_order() {
order_doc._deleted = true;
return new Promise((resolve, reject) => {
dbc.put(order_doc, function callback(err, result) {
if (!err && result !== undefined) {
resolve();
} else {
alert("Erreur lors de la suppression de la commande... Si l'erreur persiste contactez un administrateur svp.");
console.log(err);
reject(new Error("Error while deleting order"));
}
});
});
}
/**
* Create the Product Orders in Odoo
*/
function create_orders() {
......@@ -810,6 +1009,7 @@ function create_orders() {
// 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;
const product_code = p_supplierinfo.product_code;
orders_data.suppliers_data[supplier_id].lines.push({
'package_qty': p_supplierinfo.package_qty,
......@@ -820,7 +1020,8 @@ function create_orders() {
'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
'product_variant_ids': p.product_variant_ids,
'product_code': product_code
});
}
}
......@@ -866,13 +1067,14 @@ function create_orders() {
get_order_attachments();
// Clear data
order_doc._deleted = true;
update_cdb_order().then(() => {
update_order_selection_screen();
delete_cdb_order().finally(() => {
// Continue with workflow anyway
update_order_selection_screen().then(() => {
reset_data();
switch_screen('orders_created');
closeModal();
});
});
reset_data();
switch_screen('orders_created');
closeModal();
},
error: function(data) {
let msg = "erreur serveur lors de la création des product orders";
......@@ -1128,9 +1330,20 @@ function prepare_datatable_columns() {
{
data: "default_code",
title: "Ref",
width: "6%",
render: function (data) {
return (data === false) ? "" : data;
width: "8%",
render: function (data, type, full) {
if (data === false) {
return "";
} else if (data.includes("[input]")) {
let val = data.replace("[input]", "");
return `<div class="custom_cell_content">
<input type="text" class="product_ref_input" id="${full.id}_ref_input" value="${val}">
</div>`;
} else {
return data;
}
}
},
{
......@@ -1173,7 +1386,7 @@ function prepare_datatable_columns() {
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}>`;
<input type="number" class="product_qty_input" id="${base_id}_qty_input" min="-1" value=${data}>`;
if (full.package_qty === 'X') {
let product_data = products.find(p => p.id == full.id);
......@@ -1254,8 +1467,11 @@ function display_products(params) {
return -1;
}
// Empty datatable if it already exists
// If datatable already exists, empty & clear events
if (products_table) {
$(products_table.table().header()).off();
$('#products_table').off();
products_table.clear().destroy();
$('#products_table').empty();
}
......@@ -1276,7 +1492,6 @@ function display_products(params) {
sort_order_dir
]
],
stripeClasses: [], // Remove datatable cells coloring
orderClasses: false,
aLengthMenu: [
[
......@@ -1298,13 +1513,15 @@ function display_products(params) {
scrollX: true,
language: {url : '/static/js/datatables/french.json'},
createdRow: function(row) {
for (const cell_node of row.cells) {
for (var i = 0; i < row.cells.length; i++) {
const cell_node = row.cells[i];
const cell = $(cell_node);
if (cell.hasClass("supplier_input_cell")) {
if (cell.text() == "X") {
cell.addClass('product_not_from_supplier');
}
if (cell.hasClass("supplier_input_cell") && cell.text() === "X") {
cell.addClass('product_not_from_supplier');
} else if (i === 1) {
// Column at index 1 is product reference
cell.addClass('product_ref_cell');
}
}
}
......@@ -1314,35 +1531,86 @@ function display_products(params) {
$('#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();
// Color line on input focus
$('#products_table').on('focus', 'tbody td .product_qty_input', function () {
const row = $(this).closest('tr');
val = parseFloat(val);
row.addClass('focused_line');
});
// Manage data on inputs blur
$('#products_table').on('blur', 'tbody td .product_qty_input', function () {
// Remove line coloring on input blur
const row = $(this).closest('tr');
// 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];
row.removeClass('focused_line');
// Save value
save_product_supplier_qty(prod_id, supplier_id, val);
let val = ($(this).val() == '') ? 0 : $(this).val();
const id_split = $(this).attr('id')
.split('_');
const prod_id = id_split[1];
const supplier_id = id_split[3];
if (val == -1) {
let modal_end_supplier_product_association = $('#templates #modal_end_supplier_product_association');
// 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();
modal_end_supplier_product_association.find(".product_name").text(product.name);
const supplier = selected_suppliers.find(s => s.id == supplier_id);
update_cdb_order();
display_total_values();
modal_end_supplier_product_association.find(".supplier_name").text(supplier.display_name);
openModal(
modal_end_supplier_product_association.html(),
() => {
if (is_time_to('validate_end_supplier_product_association')) {
end_supplier_product_association(product, supplier);
}
},
'Valider',
false,
true,
() => {
// Reset value in input on cancel
const psi = product.suppliersinfo.find(psi_item => psi_item.supplier_id == supplier_id);
$(this).val(psi.qty);
}
);
} else {
$(this).val('');
val = parseFloat(val);
// If value is a number
if (!isNaN(val)) {
// Save value
save_product_supplier_qty(prod_id, supplier_id, val);
// Update row
const product = products.find(p => p.id == prod_id);
const new_row_data = prepare_datatable_data([product.id])[0];
products_table.row($(this).closest('tr')).data(new_row_data)
.draw();
update_cdb_order();
display_total_values();
} else {
$(this).val('');
}
}
});
})
.on('change', 'tbody td .product_qty_input', function () {
// Since data change is saved on blur, set focus on change in case of arrows pressed
$(this).focus();
})
.on('keypress', 'tbody td .product_qty_input', function(e) {
if (e.which == 13) {
// Validate on Enter pressed
$(this).blur();
}
});
// Associate product to supplier on click on 'X' in the table
$('#products_table').on('click', 'tbody .product_not_from_supplier', function () {
......@@ -1402,6 +1670,41 @@ function display_products(params) {
new_product_supplier_association.package_qty = $(this).val();
});
});
// Display input on click on product ref cell
$('#products_table').on('click', 'tbody .product_ref_cell', function () {
if ($(this).find('input').length === 0) {
const row = $(this).closest('tr');
const p_id = products_table.row(row).data().id;
const p_index = products.findIndex(p => p.id === p_id);
const existing_ref = products[p_index].default_code === false ? '' : products[p_index].default_code;
products[p_index].default_code = "[input]" + existing_ref;
const new_row_data = prepare_datatable_data([p_id])[0];
products_table.row(row).data(new_row_data)
.draw();
let ref_input = $(`#${p_id}_ref_input`);
ref_input.focus();
ref_input.select();
$('#products_table')
.on('blur', 'tbody .product_ref_input', function () {
update_product_ref(this, p_id, p_index);
})
.on('keypress', 'tbody .product_ref_input', function(e) {
// Validate on Enter pressed
if (e.which == 13) {
update_product_ref(this, p_id, p_index);
}
});
}
});
// Select row(s) on checkbox change
$(products_table.table().header()).on('click', 'th #select_all_products_cb', function () {
if (this.checked) {
......@@ -1474,6 +1777,8 @@ function display_products(params) {
* Unselect all rows from datatable.
*/
function unselect_all_rows() {
$("#select_all_products_cb").prop("checked", false);
products_table.rows().every(function() {
const node = $(this.node());
......@@ -1499,6 +1804,9 @@ function display_total_values() {
$(`#pill_supplier_${supplier.id}`).find('.supplier_total_value')
.text(parseFloat(supplier.total_value).toFixed(2));
order_total_value += supplier.total_value;
$(`#pill_supplier_${supplier.id}`).find('.supplier_total_packages')
.text(+parseFloat(supplier.total_packages).toFixed(2));
}
order_total_value = parseFloat(order_total_value).toFixed(2);
......@@ -1510,8 +1818,11 @@ function display_total_values() {
*/
function update_main_screen(params) {
// Remove listener before recreating them
$('#products_table').off('focus', 'tbody td .product_qty_input');
$('#products_table').off('blur', 'tbody td .product_qty_input');
$('#products_table').off('change', 'tbody td .product_qty_input');
$('#products_table').off('click', 'tbody .product_not_from_supplier');
$('#products_table').off('click', 'tbody .product_ref_cell');
$('#products_table').off('click', 'thead th #select_all_products_cb');
$('#products_table').off('click', 'tbody td .select_product_cb');
$(".remove_supplier_icon").off();
......@@ -1542,41 +1853,91 @@ function update_main_screen(params) {
} else {
$("#coverage_days_input").val('');
}
if (order_doc.stats_date_period !== undefined && order_doc.stats_date_period !== null) {
$("#stats_date_period_select").val(order_doc.stats_date_period);
} else {
$("#stats_date_period_select").val('');
}
}
/**
* Update DOM display on the order selection screen
*/
function update_order_selection_screen() {
dbc.allDocs({
include_docs: true
}).then(function (result) {
// Remove listener before recreating them
$(".order_pill").off();
return new Promise((resolve) => {
dbc.allDocs({
include_docs: true
})
.then(function (result) {
// Remove listener before recreating them
$(".order_pill").off();
let existing_orders_container = $("#existing_orders");
let existing_orders_container = $("#existing_orders");
existing_orders_container.empty();
$('#new_order_name').val('');
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");
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);
template.find(".pill_order_name").text(row.id);
existing_orders_container.append(template.html());
}
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);
});
$(".order_pill").on("click", order_pill_on_click);
$(".remove_order_icon").on("click", function(e) {
e.preventDefault();
e.stopImmediatePropagation();
order_name_container = $(this).prev()[0];
let order_id = $(order_name_container).text();
let modal_remove_order = $('#templates #modal_remove_order');
modal_remove_order.find(".remove_order_name").text(order_id);
openModal(
modal_remove_order.html(),
() => {
if (is_time_to('validate_remove_order')) {
dbc.get(order_id).then((doc) => {
order_doc = doc;
delete_cdb_order().then(() => {
update_order_selection_screen().then(() => {
reset_data();
setTimeout(function() {
$.notify(
"Commande supprimée !",
{
globalPosition:"top left",
className: "success"
}
);
}, 500);
});
})
.catch(() => {
console.log("error deleting order");
});
});
}
},
'Valider'
);
});
}
resolve();
})
.catch(function (err) {
alert('Erreur lors de la synchronisation des commandes. Vous pouvez créer une nouvelle commande.');
console.log(err);
});
});
}
/**
......@@ -1679,6 +2040,10 @@ $(document).ready(function() {
init_pouchdb_sync();
// Main screen
if (metabase_url !== '') {
$('#access_metabase').show();
}
$("#coverage_form").on("submit", function(e) {
e.preventDefault();
if (is_time_to('submit_coverage_form', 1000)) {
......@@ -1698,6 +2063,31 @@ $(document).ready(function() {
}
});
$("#toggle_action_buttons").on("click", function() {
if ($('#actions_buttons_container').is(":visible")) {
$('#actions_buttons_container').hide();
$('.toggle_action_buttons_icon').empty()
.append('<i class="fas fa-chevron-down"></i>');
} else {
$('#actions_buttons_container').show();
$('.toggle_action_buttons_icon').empty()
.append('<i class="fas fa-chevron-up"></i>');
}
});
// Close dropdown menu on click outside
$(document).click(function(event) {
let target = $(event.target);
if (
!target.closest('#actions_buttons_wrapper').length
&& $('#actions_buttons_container').is(":visible")
) {
$('#actions_buttons_container').hide();
$('.toggle_action_buttons_icon').empty()
.append('<i class="fas fa-chevron-down"></i>');
}
});
$("#supplier_form").on("submit", function(e) {
e.preventDefault();
......@@ -1713,12 +2103,64 @@ $(document).ready(function() {
}
});
$("#stats_date_period_select").on("change", function(e) {
e.preventDefault();
if (is_time_to('change_stats_date_period', 1000)) {
openModal();
order_doc.stats_date_period = $(this).val();
check_products_data()
.then(() => {
update_cdb_order();
update_main_screen();
closeModal();
});
}
});
$("#do_inventory").on("click", function() {
if (is_time_to('generate_inventory', 1000)) {
generate_inventory();
}
});
$("#delete_order_button").on("click", function() {
if (is_time_to('press_delete_order_button', 1000)) {
let modal_remove_order = $('#templates #modal_remove_order');
modal_remove_order.find(".remove_order_name").text(order_doc._id);
openModal(
modal_remove_order.html(),
() => {
if (is_time_to('validate_remove_order')) {
delete_cdb_order().then(() => {
update_order_selection_screen().then(() => {
reset_data();
switch_screen('order_selection');
setTimeout(function() {
$.notify(
"Commande supprimée !",
{
globalPosition:"top left",
className: "success"
}
);
}, 500);
});
})
.catch(() => {
console.log("error deleting order");
});
}
},
'Valider'
);
}
});
$('#back_to_order_selection_from_main').on('click', function() {
if (is_time_to('back_to_order_selection_from_main', 1000)) {
back();
......
......@@ -13,6 +13,7 @@ urlpatterns = [
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'^end_supplier_product_association$', views.end_supplier_product_association),
url(r'^create_orders$', views.create_orders),
url(r'^get_orders_attachment$', views.get_orders_attachment),
]
......@@ -19,7 +19,8 @@ def helper(request):
'title': 'Aide à la commande',
'couchdb_server': settings.COUCHDB['url'],
'db': settings.COUCHDB['dbs']['orders'],
'odoo_server': settings.ODOO['url']
'odoo_server': settings.ODOO['url'],
'metabase_url': getattr(settings, 'ORDERS_HELPER_METABASE_URL', '')
}
template = loader.get_template('orders/helper.html')
......@@ -42,7 +43,8 @@ def get_supplier_products(request):
""" Get supplier products """
suppliers_id = request.GET.getlist('sids', '')
res = CagetteProducts.get_products_for_order_helper(suppliers_id)
stats_from = request.GET.get('stats_from')
res = CagetteProducts.get_products_for_order_helper(suppliers_id, [], stats_from)
if 'error' in res:
return JsonResponse(res, status=500)
......@@ -52,15 +54,29 @@ def get_supplier_products(request):
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)
data = json.loads(request.body.decode())
res = CagetteProduct.associate_supplier_to_product(data)
if 'error' in res:
return JsonResponse(res, status=500)
else:
return JsonResponse({'res': res})
return JsonResponse({'res': res})
def end_supplier_product_association(request):
""" This product is now unavailable from this supplier """
res = {}
data = json.loads(request.body.decode())
res = CagetteProduct.end_supplier_product_association(data)
if 'error' in res:
return JsonResponse(res, status=500)
else:
return JsonResponse({'res': res})
def create_orders(request):
""" Create products orders """
res = { "created": [] }
......
......@@ -130,6 +130,7 @@ class CagetteProduct(models.Model):
@staticmethod
def associate_supplier_to_product(data):
api = OdooAPI()
res = {}
product_tmpl_id = data["product_tmpl_id"]
partner_id = data["supplier_id"]
......@@ -140,6 +141,8 @@ class CagetteProduct(models.Model):
c = [['product_tmpl_id', '=', product_tmpl_id]]
res_products = api.search_read('product.product', c, f)
product = res_products[0]
today = datetime.date.today().strftime("%Y-%m-%d")
f = {
'product_tmpl_id' : product_tmpl_id,
......@@ -149,9 +152,40 @@ class CagetteProduct(models.Model):
'price': price,
'base_price': price,
'package_qty': package_qty,
'date_start': today,
'sequence': 1000 # lowest priority for the new suppliers
}
res = api.create('product.supplierinfo', f)
try:
res['create'] = api.create('product.supplierinfo', f)
except Exception as e:
res['error'] = str(e)
return res
@staticmethod
def end_supplier_product_association(data):
api = OdooAPI()
res = {}
product_tmpl_id = data["product_tmpl_id"]
partner_id = data["supplier_id"]
f = ["id"]
c = [['product_tmpl_id', '=', product_tmpl_id], ['name', '=', partner_id], ['date_end', '=', False]]
res_supplierinfo = api.search_read('product.supplierinfo', c, f)
psi_id = res_supplierinfo[0]['id']
today = datetime.date.today().strftime("%Y-%m-%d")
f = {
'date_end': today
}
try:
res["update"] = api.update('product.supplierinfo', psi_id, f)
except Exception as e:
res['error'] = str(e)
return res
......@@ -172,6 +206,23 @@ class CagetteProduct(models.Model):
return res
@staticmethod
def update_product_internal_ref(product_tmpl_id, default_code):
api = OdooAPI()
res = {}
f = {
'default_code': default_code
}
try:
res["update"] = api.update('product.template', product_tmpl_id, f)
except Exception as e:
res["error"] = str(e)
print(str(e))
return res
class CagetteProducts(models.Model):
"""Initially used to make massive barcode update."""
......@@ -456,31 +507,32 @@ class CagetteProducts(models.Model):
return res
@staticmethod
def get_products_for_order_helper(supplier_ids, pids = []):
def get_products_for_order_helper(supplier_ids, pids = [], stats_from = None):
"""
One of the two parameters must be not empty.
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.
supplier_ids: Get products by supplier if one or more supplier id is set. If set, pids is ignored.
pids: If set & supplier_ids is None/empty, get products specified in pids. In this case, suppliers info won't be fetched.
stats_from: date from which we should calculate sells stats.
"""
api = OdooAPI()
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']
f = ["product_tmpl_id", 'date_start', 'date_end', 'package_qty', 'price', 'name', 'product_code']
c = [['name', 'in', [ int(x) for x in supplier_ids]]]
psi = api.search_read('product.supplierinfo', c, f)
# Filter valid data
ptids = []
valid_psi = []
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)):
and (p["date_start"] is False or p["date_start"] is not False and p["date_start"] <= today)
and (p["date_end"] is False or p["date_end"] is not False and p["date_end"] > today)):
valid_psi.append(p)
ptids.append(p["product_tmpl_id"][0])
else:
ptids = [ int(x) for x in pids ]
......@@ -507,6 +559,10 @@ class CagetteProducts(models.Model):
# 'from': '2019-04-10',
# 'to': '2019-08-10',
}
if stats_from is not None and stats_from != '':
sales_average_params['from'] = stats_from
sales = CagetteProducts.get_template_products_sales_average(sales_average_params)
if 'list' in sales and len(sales['list']) > 0:
......@@ -517,12 +573,16 @@ class CagetteProducts(models.Model):
# 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"]
}]
# Add all the product suppliersinfo (products from multiple suppliers into the suppliers list provided)
filtered_products_t[i]['suppliersinfo'] = []
for psi_item in valid_psi:
if psi_item["product_tmpl_id"] is not False and psi_item ["product_tmpl_id"][0] == fp["id"]:
filtered_products_t[i]['suppliersinfo'].append({
'supplier_id': int(psi_item["name"][0]),
'package_qty': psi_item["package_qty"],
'price': psi_item["price"],
'product_code': psi_item["product_code"]
})
for s in sales:
if s["id"] == fp["id"]:
......
......@@ -10,6 +10,7 @@ urlpatterns = [
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'^update_product_internal_ref$', views.update_product_internal_ref),
url(r'^labels_appli_csv(\/?[a-z]*)$', views.labels_appli_csv, name='labels_appli_csv'),
url(r'^label_print/([0-9]+)/?([0-9\.]*)/?([a-z]*)/?([0-9]*)$', views.label_print),
url(r'^shelf_labels$', views.shelf_labels), # massive print
......
......@@ -42,8 +42,10 @@ def get_simple_list(request):
def get_product_for_order_helper(request):
res = {}
try:
pids = json.loads(request.body.decode())
res = CagetteProducts.get_products_for_order_helper(None, pids)
data = json.loads(request.body.decode())
pids = data['pids']
stats_from = data['stats_from']
res = CagetteProducts.get_products_for_order_helper(None, pids, stats_from)
except Exception as e:
coop_logger.error("get_product_for_help_order_line : %s", str(e))
res['error'] = str(e)
......@@ -112,6 +114,17 @@ def update_product_purchase_ok(request):
else:
return JsonResponse({"res": res})
def update_product_internal_ref(request):
res = {}
data = json.loads(request.body.decode())
res = CagetteProduct.update_product_internal_ref(data["product_tmpl_id"], data["default_code"])
if ('error' in res):
return JsonResponse(res, status=500)
else:
return JsonResponse({"res": res})
def labels_appli_csv(request, params):
"""Generate files to put in DAV directory to be retrieved by scales app."""
withCandidate = False
......
......@@ -366,7 +366,7 @@ function display_orders_table() {
$('#orders').empty();
}
for (let j in orders) {
console.log(orders[j].id)
console.log(orders[j].id);
}
table_orders = $('#orders').DataTable({
data: orders,
......
......@@ -166,6 +166,7 @@ function init_datatable() {
let data = row.data();
let validated_data = qty_validation(qty, data.uom.id);
if (validated_data >= 0) {
data.qty = validated_data;
row.remove().draw();
......@@ -423,28 +424,33 @@ var update_existing_product = function(product, added_qty, undo_option = false)
function qty_validation(qty, uom_id) {
if (qty == null || qty == '') {
$.notify("Il n'y a pas de quantité indiquée, ou ce n'est pas un nombre", {
globalPosition:"top right",
className: "error"});
globalPosition:"top right",
className: "error"});
return -1;
}
if (uom_id == 1) {
if (qty/parseInt(qty) != 1 && qty != 0){
if (qty/parseInt(qty) != 1 && qty != 0) {
$.notify("Une quantité avec décimale est indiquée alors que c'est un article à l'unité", {
globalPosition:"top right",
className: "error"});
return -2;}
globalPosition:"top right",
className: "error"});
return -2;
}
qty = parseInt(qty); // product by unit
} else {
qty = parseFloat(qty).toFixed(2);
}
if (isNaN(qty)){
if (isNaN(qty)) {
$.notify("Une quantité n'est pas un nombre", {
globalPosition:"top right",
className: "error"});
return -3;}
globalPosition:"top right",
className: "error"});
return -3;
}
return qty;
}
......
......@@ -40,10 +40,29 @@
<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 class="right_action_buttons">
<div id="actions_buttons_wrapper">
<button type="button" class='btn--primary' id="toggle_action_buttons">
<span class="button_content">
Actions
</span>
<span class="toggle_action_buttons_icon">
<i class="fas fa-chevron-down"></i>
</span>
</button>
<div id="actions_buttons_container">
<button type="button" class='btn--primary action_button' id="do_inventory" style="display:none;">
Faire un inventaire
</button>
<button type="button" class='btn--danger action_button' id="delete_order_button">
Supprimer la commande
</button>
</div>
</div>
<a class='btn--warning link_as_button' id="access_metabase" style="display:none;" href="{{metabase_url}}" target="_blank">
Stats Métabase
</a>
</div>
</div>
......@@ -57,14 +76,23 @@
</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">
<form action="javascript:;" id="supplier_form" class="order_form_item">
<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>
<form action="javascript:;" id="stats_date_from_form" class="order_form_item">
<label for="stats_date_period_select">Période de calcul de la conso moyenne </label>
<select name="stats_date_period_select" id="stats_date_period_select">
<option value="">Par défaut</option>
<option value="1week">1 semaine</option>
<option value="2weeks">2 semaines</option>
</select>
</form>
<form action="javascript:;" id="coverage_form" class="order_form_item">
<input type="number" name="coverage_days" id="coverage_days_input" placeholder="Nb jours de couverture" min="1">
<button type="submit" class='btn--primary'>Calculer les besoins</button>
</form>
</div>
<div id="suppliers_container"></div>
......@@ -125,8 +153,14 @@
<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 class="supplier_data">
<div class="supplier_total_value_container">
Total : <span class="supplier_total_value">0</span>
</div>
&nbsp;&nbsp;|&nbsp;&nbsp;
<div class="supplier_total_packages_container">
Nb colis : <span class="supplier_total_packages">0</span>
</div>
</div>
</div>
</div>
......@@ -134,6 +168,7 @@
<div id="order_pill_template">
<div class="pill order_pill btn btn--primary">
<span class="pill_order_name"></span>
<i class="fas fa-times remove_order_icon"></i>
</div>
</div>
......@@ -144,7 +179,7 @@
<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="#">
<a class='btn--success download_order_file_button link_as_button' style="display:none;" href="#">
Télécharger le fichier de commande
</a>
</div>
......@@ -164,6 +199,15 @@
<p>Voulez-vous quand même y accéder ?</p>
<hr/>
</div>
<div id="modal_remove_order">
<h3>Attention !</h3>
<p class="remove_order_modal_text">
Vous vous apprêtez à <b style="color: #d9534f;">supprimer</b> cette commande en cours : <span class="remove_order_name"></span>.<br/>
</p>
<p>Êtez-vous sûr ?</p>
<hr/>
</div>
<div id="modal_remove_supplier">
<h3>Attention !</h3>
......@@ -201,6 +245,19 @@
<p>Êtez-vous sûr ?</p>
<hr/>
</div>
<div id="modal_end_supplier_product_association">
<h3>Attention !</h3>
<p>
Vous vous apprêtez à rendre le produit <span class="product_name"></span>
indisponible chez le fournisseur <span class="supplier_name"></span>.
</p>
<p>
L'association sera supprimée dès que vous aurez cliqué sur "Valider".<br/>
</p>
<p>Êtez-vous sûr ?</p>
<hr/>
</div>
<div id="modal_create_inventory">
<p>
......@@ -252,6 +309,7 @@
var couchdb_dbname = '{{db}}';
var couchdb_server = '{{couchdb_server}}' + couchdb_dbname;
var odoo_server = '{{odoo_server}}';
var metabase_url = '{{metabase_url}}';
</script>
<script src="{% static "js/all_common.js" %}?v="></script>
<script type="text/javascript" src="{% static 'js/orders_helper.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