Commit 857ba3b1 by Administrator

Merge branch 'aide_a_la_commande' into 'dev_cooperatic'

Aide a la commande

See merge request cooperatic-foodcoops/third-party!35
parents ae3dbb0f e7f80fe7
...@@ -52,7 +52,7 @@ class CagetteInventory(models.Model): ...@@ -52,7 +52,7 @@ class CagetteInventory(models.Model):
file_data = json.load(json_file) file_data = json.load(json_file)
date_time = datetime.fromtimestamp(int(filename)) date_time = datetime.fromtimestamp(int(filename))
d = date_time.strftime("%m/%d/%Y, %H:%M") d = date_time.strftime("%d/%m/%Y, %H:%M")
file_data['id'] = int(filename) file_data['id'] = int(filename)
file_data['datetime_created'] = d file_data['datetime_created'] = d
...@@ -113,7 +113,7 @@ class CagetteInventory(models.Model): ...@@ -113,7 +113,7 @@ class CagetteInventory(models.Model):
return file_data['inventory_status'] return file_data['inventory_status']
@staticmethod @staticmethod
def create_custom_inv_file(line_ids, line_type): def create_custom_inv_file(line_ids, line_type, default_partners_id=[]):
res = {} res = {}
try: try:
...@@ -127,7 +127,22 @@ class CagetteInventory(models.Model): ...@@ -127,7 +127,22 @@ class CagetteInventory(models.Model):
api = OdooAPI() api = OdooAPI()
ids = [] ids = []
order = ['', ''] order = ['', '']
user = partner = '' 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:
fields = ['create_uid', 'product_id', 'partner_id'] fields = ['create_uid', 'product_id', 'partner_id']
cond = [['id', 'in', line_ids]] cond = [['id', 'in', line_ids]]
if (line_type == 'cpo'): if (line_type == 'cpo'):
...@@ -139,24 +154,27 @@ class CagetteInventory(models.Model): ...@@ -139,24 +154,27 @@ class CagetteInventory(models.Model):
lines = api.search_read(model, cond, fields) lines = api.search_read(model, cond, fields)
if len(lines) == len(line_ids): if len(lines) == len(line_ids):
for l in lines: for l in lines:
if line_type == 'product_templates':
ids.append(l['id'])
else:
ids.append(l['product_id'][0]) ids.append(l['product_id'][0])
user = l['create_uid'][1] user = l['create_uid'][1]
if (line_type == 'cpo'): if (line_type == 'cpo'):
order = l['computed_purchase_order_id'] order = l['computed_purchase_order_id']
else: else:
order = l['order_id'] order = l['order_id']
partner = l['partner_id'][1] partners.append(l['partner_id'][1])
if (line_type == 'cpo'): if (line_type == 'cpo'):
# partner_id isn't defined # partner_id isn't defined
f = ['partner_id'] f = ['partner_id']
c = [['id', '=', int(order[0])]] c = [['id', '=', int(order[0])]]
cpo = api.search_read('computed.purchase.order', c, f) cpo = api.search_read('computed.purchase.order', c, f)
if len(cpo) > 0: if len(cpo) > 0:
partner = cpo[0]['partner_id'][1] partners.append(cpo[0]['partner_id'][1])
file_data = { file_data = {
'order': order[1], 'order': order[1],
'user': user, 'user': user,
'partner': partner, 'partners': partners,
'inventory_status': '', 'inventory_status': '',
'products': ids 'products': ids
} }
......
...@@ -4,6 +4,13 @@ var shelfs_table = null, ...@@ -4,6 +4,13 @@ var shelfs_table = null,
function init_datatable() { function init_datatable() {
// For a smooth migration...
for (const i in lists) {
if (('partners' in lists[i]) === false) {
lists[i]['partners'] = [lists[i]['partner']]
}
}
return $('#lists').DataTable({ return $('#lists').DataTable({
data: lists, // data passed at page loading data: lists, // data passed at page loading
rowId: 'id', rowId: 'id',
...@@ -17,8 +24,15 @@ function init_datatable() { ...@@ -17,8 +24,15 @@ function init_datatable() {
} }
}, },
{ {
data:"partner", data:"partners",
title:"Fournisseur" title:"Fournisseur(s)",
render: function (data) {
res = "";
for (const i in data) {
res += `${data[i]}<br/>`;
}
return res;
}
}, },
{ {
data:"order", data:"order",
......
...@@ -43,7 +43,6 @@ def custom_list_inventory(request, id): ...@@ -43,7 +43,6 @@ def custom_list_inventory(request, id):
products = CagetteInventory.get_custom_list_products(id) products = CagetteInventory.get_custom_list_products(id)
if 'error' in products: if 'error' in products:
print(products)
products['data'] = [] products['data'] = []
context = {'title': 'Inventaire', context = {'title': 'Inventaire',
...@@ -112,10 +111,25 @@ def do_custom_list_inventory(request): ...@@ -112,10 +111,25 @@ def do_custom_list_inventory(request):
def generate_inventory_list(request): def generate_inventory_list(request):
"""Responding to Odoo ajax call (no csrf).""" """Responding to Odoo ajax call (no csrf)."""
res = {} res = {}
default_partners_id = []
try: try:
lines = json.loads(request.POST.get('lines')) lines = json.loads(request.POST.get('lines'))
ltype = request.POST.get('type') ltype = request.POST.get('type')
res = CagetteInventory.create_custom_inv_file(lines, ltype) except Exception as e:
try:
# POST.get() returns None when request from django
data = json.loads(request.body.decode())
lines = data["lines"]
ltype = data["type"]
if "partners_id" in data:
default_partners_id = data["partners_id"]
except Exception as ee:
res['error'] = str(ee)
coop_looger.error("generate_inventory_list : %s", str(e))
return JsonResponse(res, status=500)
try:
res = CagetteInventory.create_custom_inv_file(lines, ltype, default_partners_id)
except Exception as e: except Exception as e:
res['error'] = str(e) res['error'] = str(e)
coop_looger.error("generate_inventory_list : %s", str(e)) coop_looger.error("generate_inventory_list : %s", str(e))
......
...@@ -268,3 +268,15 @@ class Orders(models.Model): ...@@ -268,3 +268,15 @@ class Orders(models.Model):
coop_logger.error('Orders get_custom_barcode_labels_to_print(oids) : %s', str(e)) coop_logger.error('Orders get_custom_barcode_labels_to_print(oids) : %s', str(e))
return labels_data return labels_data
class CagetteSuppliers(models.Model):
@staticmethod
def get_suppliers():
api = OdooAPI()
f = ['id', 'name', 'display_name']
c = [['supplier', '=', 1], ['parent_id', '=', False]]
res = api.search_read('res.partner', c, f)
return res
.page_body{
position: relative;
}
.page_content {
position: absolute;
top: 0;
left: 0;
right: 0;
}
/* - Common */
.pill {
border-radius: 30px;
min-width: 200px;
min-height: 35px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 8px 15px 8px 15px;
margin: 0 10px 5px 0;
}
.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;
}
#actions_buttons_area {
position: absolute;
top: 0;
right: 0;
}
/* -- Supplier form */
#supplier_form_container {
margin-top: 30px;
}
#supplier_input {
width: 500px;
margin-right: 10px;
border-radius: 5px;
}
/* -- Table */
#products_table_filter{
text-align: right !important;
}
#products_table_filter input{
height: 40px;
}
#table_header_select_all{
display: flex;
align-content: center;
justify-content: center;
flex-wrap: wrap;
}
#table_header_select_all input{
margin-left: 5px;
}
.product_qty_input {
width: 100px;
}
.product_not_from_supplier {
background-color: #e7e9ed;
cursor: pointer;
}
.product_name, .supplier_name {
font-weight: bold;
}
.select_product_cb {
cursor: pointer;
}
/* -- Suppliers list */
#suppliers_container {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
margin: 20px 0 20px 0;
}
.supplier_pill {
background-color: #e7e9ed;
}
.remove_supplier_icon {
color: red;
margin-left: 5px;
cursor: pointer;
}
\ No newline at end of file
var suppliers_list = [],
products_table = null,
products = [],
selected_suppliers = [],
selected_rows = [],
dbc = null,
sync = null,
order_doc = {
_id: null,
last_update : {
timestamp: null,
fingerprint: null,
},
products: [],
selected_suppliers: [],
selected_rows: []
},
fingerprint = null;
/* - UTILS */
/**
* Reset data that changes between screens
*/
function reset_data() {
products = [],
selected_suppliers = [],
selected_rows = [],
order_doc = {
_id: null,
last_update : {
timestamp: null,
fingerprint: null,
},
products: [],
selected_suppliers: []
};
}
/**
* 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;
}
/* - 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
const 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();
selected_suppliers.push(supplier);
let url = "/orders/get_supplier_products";
url += "?sid=" + 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_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].suppliers = products[i].suppliers.filter(supplier => supplier.id != supplier_id);
}
// Remove products only associated to this product
products = products.filter(product => product.suppliers.length > 0);
update_main_screen();
update_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();
const data = {
product_tmpl_id: product.id,
supplier_id: supplier.id
};
// Fetch supplier products
$.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 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();
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.');
}
});
}
/**
* 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) {
np.suppliers = [{ ...supplier }];
products.push(np);
} else {
products[index].suppliers.push({ ...supplier });
}
}
}
/**
* 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].suppliers) {
if (products[i].suppliers[j].id == supplier_id) {
products[i].suppliers[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.suppliers.find(s => s.id === supplier.id) !== undefined;
}
/* - 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 supplier of product.suppliers) {
if (data.partners_id.indexOf(supplier.id) === -1) {
data.partners_id.push(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(),
() => {
$.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() {
$.notify(
"Inventaire créé !",
{
globalPosition:"top left",
className: "success"
}
);
}, 500);
},
error: function(data) {
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'
);
}
}
}
/* - DISPLAY */
/**
* Create a string to represent a supplier column in product data
* @returns String
*/
function supplier_column_name(supplier) {
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 (supplier of selected_suppliers) {
let template = $("#templates #supplier_pill_template");
template.find(".pill_supplier_name").text(supplier.display_name);
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];
let modal_remove_supplier = $('#templates #modal_remove_supplier');
modal_remove_supplier.find(".supplier_name").text(supplier.display_name);
openModal(
modal_remove_supplier.html(),
() => {
remove_supplier(supplier_id);
},
'Valider'
);
});
}
/**
* @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) {
let item = {
id: product.id,
name: product.name,
default_code: product.default_code,
incoming_qty: +parseFloat(product.incoming_qty).toFixed(3), // + sign removes unecessary zeroes at the end
qty_available: +parseFloat(product.qty_available).toFixed(3),
uom: product.uom_id[1]
};
// If product not related to supplier : false ; else null (qty to be set) or qty
for (product_supplier of product.suppliers) {
item[supplier_column_name(product_supplier)] = ("qty" in product_supplier) ? product_supplier.qty : null;
}
for (supplier of selected_suppliers) {
if (!is_product_related_to_supplier(product, supplier)) {
item[supplier_column_name(supplier)] = false;
}
}
data.push(item);
}
return data;
}
/**
* @returns Array of formatted data for datatable columns setup
*/
function prepare_datatable_columns() {
columns = [
{
data: "id",
title:` <div id="table_header_select_all">
Tout
<input type="checkbox" class="select_product_cb" id="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: "Référence Produit",
width: "8%",
render: function (data) {
return (data === false) ? "" : data;
}
},
{
data: "name",
title: "Produit"
},
{
data: "qty_available",
title: "Stock",
width: "4%"
},
{
data: "incoming_qty",
title: "Quantité entrante",
width: "4%"
}
];
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="cell_content">X</div>`;
} else {
return `<div id="${base_id}_cell_content" class="cell_content">
<input type="number" class="product_qty_input" id="${base_id}_qty_input" value=${data}>
</div>`;
}
}
});
}
columns.push({
data: "uom",
title: "UDM",
width: "4%"
});
return columns;
}
/**
* Display the Datatable containing the products
*/
function display_products() {
if (products.length == 0) {
$('.main').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();
products_table = $('#products_table').DataTable({
data: data,
columns: columns,
order: [
[
5, // Order by default by first supplier
"asc"
]
],
stripeClasses: [], // Remove datatable cells coloring
orderClasses: false,
iDisplayLength: 100,
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();
// Save value on inputs change
$('#products_table').on('input', 'tbody td .product_qty_input', function () {
let val = parseFloat($(this).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_product_supplier_qty(prod_id, supplier_id, val);
update_order();
}
});
// 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(),
() => {
save_supplier_product_association(product, supplier, this);
},
'Valider',
false
);
});
// 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
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);
}
});
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 = [];
}
/**
* Update DOM display on main screen
*/
function update_main_screen() {
// Remove listener before recreating them
$('#products_table').off('input', '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();
// 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;
});
}
}
/**
* Update DOM display on the order selection screen
*/
function update_order_selection_screen() {
// Remove listener before recreating them
$(".order_pill").off();
let existing_orders_container = $("#existing_orders");
existing_orders_container.empty();
dbc.allDocs({
include_docs: true
}).then(function (result) {
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 screen between order selection & main screens
* @param {String} direction target screen
*/
function switch_screen(direction = 'main_screen') {
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 {
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);
}
/* - ORDER */
function goto_main_screen(doc) {
order_doc = doc;
products = order_doc.products;
selected_suppliers = order_doc.selected_suppliers;
update_order()
update_main_screen();
switch_screen();
}
function back() {
reset_data();
update_order_selection_screen();
switch_screen('order_selection');
}
/**
* Event fct: on click on an order button
*/
function order_pill_on_click() {
let order_name_container = $(this).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(),
() => {
goto_main_screen(doc)
},
'Valider'
);
} else {
goto_main_screen(doc)
}
})
.catch(function (err) {
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_order() {
const order_name = $("#new_order_name").val();
order_doc._id = order_name;
dbc.put(order_doc, function callback(err, result) {
if (!err) {
order_doc._rev = result.rev;
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_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,
};
dbc.put(order_doc, function callback(err, result) {
if (!err && result !== undefined) {
order_doc._rev = result.rev;
} else {
alert("Erreur lors de la sauvegarde de la commande... Si l'erreur persiste contactez un administrateur svp.");
console.log(err);
}
});
}
$(document).ready(function() {
fingerprint = new Fingerprint({canvas: true}).get();
$.ajaxSetup({ headers: { "X-CSRFToken": getCookie('csrftoken') } });
openModal();
// 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) {
// If current order was modified somewhere else
if (order_doc._id === doc._id && order_doc._rev !== doc._rev) {
$.notify(
"Un autre navigateur est en train de modifier cette commande !",
{
globalPosition:"top right",
className: "error"
}
);
back();
break;
}
}
}
}).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);
});
// Main screen listeners
$("#supplier_form").on("submit", function(e) {
e.preventDefault();
add_supplier();
});
$("#do_inventory").on("click", function() {
generate_inventory();
});
$('#back_to_order_selection').on('click', function() {
back();
});
// Order selection screen
update_order_selection_screen();
$("#new_order_form").on("submit", function(e) {
e.preventDefault();
create_order();
});
// 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)
});
closeModal();
},
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');
}
});
});
...@@ -8,5 +8,9 @@ urlpatterns = [ ...@@ -8,5 +8,9 @@ urlpatterns = [
url(r'^export/([0-9]+)', views.export_one), url(r'^export/([0-9]+)', views.export_one),
url(r'^export/([a-z]+)', views.export_regex), url(r'^export/([a-z]+)', views.export_regex),
url(r'^get_pdf_labels$', views.get_pdf_labels), url(r'^get_pdf_labels$', views.get_pdf_labels),
url(r'^print_product_labels$', views.print_product_labels) url(r'^print_product_labels$', views.print_product_labels),
url(r'^helper$', views.helper),
url(r'^get_suppliers$', views.get_suppliers),
url(r'^get_supplier_products$', views.get_supplier_products),
url(r'^associate_supplier_to_product$', views.associate_supplier_to_product),
] ]
from outils.common_imports import * from outils.common_imports import *
from outils.for_view_imports import * from outils.for_view_imports import *
from orders.models import Order, Orders from orders.models import Order, Orders, CagetteSuppliers
from products.models import CagetteProduct from products.models import CagetteProduct, CagetteProducts
from openpyxl import Workbook from openpyxl import Workbook
from openpyxl.writer.excel import save_virtual_workbook from openpyxl.writer.excel import save_virtual_workbook
...@@ -13,6 +13,52 @@ def as_text(value): return str(value) if value is not None else "" ...@@ -13,6 +13,52 @@ def as_text(value): return str(value) if value is not None else ""
def index(request): def index(request):
return HttpResponse('Orders') return HttpResponse('Orders')
def helper(request):
context = {
'title': 'Aide à la commande',
'couchdb_server': settings.COUCHDB['url'],
'db': settings.COUCHDB['dbs']['orders']
}
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 """
sid = request.GET.get('sid', '')
res = CagetteProducts.get_products_by_supplier(sid)
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["product_tmpl_id"], data["supplier_id"])
except Exception as e:
res["error"] = str(e)
return JsonResponse(res, status=500)
return JsonResponse({'res': res})
def export_one(request, oid): def export_one(request, oid):
msg = '' msg = ''
try: try:
......
...@@ -99,6 +99,7 @@ STATICFILES_DIRS = ( ...@@ -99,6 +99,7 @@ STATICFILES_DIRS = (
"website/static", "website/static",
"shop/static", "shop/static",
"shelfs/static", "shelfs/static",
"orders/static",
# "tests/static" # "tests/static"
) )
......
...@@ -19,7 +19,8 @@ COUCHDB = { ...@@ -19,7 +19,8 @@ COUCHDB = {
'member': 'coops', 'member': 'coops',
'inventory': 'inventory', 'inventory': 'inventory',
'envelops': 'envelop', 'envelops': 'envelop',
'shop': 'shopping_carts' 'shop': 'shopping_carts',
'orders': 'orders_test'
} }
} }
......
...@@ -6,6 +6,7 @@ from outils.common import OdooAPI ...@@ -6,6 +6,7 @@ from outils.common import OdooAPI
import csv import csv
import tempfile import tempfile
import pymysql.cursors import pymysql.cursors
import datetime
vcats = [] vcats = []
...@@ -126,6 +127,27 @@ class CagetteProduct(models.Model): ...@@ -126,6 +127,27 @@ class CagetteProduct(models.Model):
res = api.create('product.supplier.shortage', f) res = api.create('product.supplier.shortage', f)
return res return res
@staticmethod
def associate_supplier_to_product(product_tmpl_id, partner_id):
api = OdooAPI()
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': product["standard_price"], # By default, use product price
'base_price': product["standard_price"],
}
res = api.create('product.supplierinfo', f)
return res
class CagetteProducts(models.Model): class CagetteProducts(models.Model):
"""Initially used to make massive barcode update.""" """Initially used to make massive barcode update."""
...@@ -387,6 +409,43 @@ class CagetteProducts(models.Model): ...@@ -387,6 +409,43 @@ class CagetteProducts(models.Model):
bc_map[bc] = bc bc_map[bc] = bc
return bc_map return bc_map
@staticmethod
def get_products_by_supplier(supplier_id):
api = OdooAPI()
res = {}
# todo : try with no result
try:
today = datetime.date.today().strftime("%Y-%m-%d")
# Get products/supplier relation
f = ["product_tmpl_id", 'date_start', 'date_end']
c = [['name', '=', int(supplier_id)]]
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])
# Get products templates
f = ["id", "state", "name", "default_code", "qty_available", "incoming_qty", "uom_id"]
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"]
# Note: if product.product is needed, get "product_variant_ids" from product template
res["products"] = filtered_products_t
except Exception as e:
print(str(e))
res["error"] = str(e)
return res
class OFF(models.Model): class OFF(models.Model):
"""OpenFoodFact restricted DB queries.""" """OpenFoodFact restricted DB queries."""
......
...@@ -10,10 +10,6 @@ def index(request): ...@@ -10,10 +10,6 @@ def index(request):
context = {'title': 'Export de ventes'} context = {'title': 'Export de ventes'}
template = loader.get_template('sales/index.html') template = loader.get_template('sales/index.html')
# m = CagetteSales()
# sales = m.get_sales()
# print(sales)
return HttpResponse(template.render(context, request)) return HttpResponse(template.render(context, request))
def get_sales(request): def get_sales(request):
......
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
lists = {{lists|safe}} var lists = {{lists|safe}}
</script> </script>
<script src="{% static "js/all_common.js" %}?v="></script> <script src="{% static "js/all_common.js" %}?v="></script>
<script src="{% static "js/common.js" %}?v="></script> <script src="{% static "js/common.js" %}?v="></script>
......
{% 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 id="new_order_area">
<h2>Créer une nouvelle commande</h2>
<form id="new_order_form">
<input type="text" id="new_order_name" placeholder="Nom de la commande...">
<button type="submit" class="btn btn--primary">Valider</button>
</form>
</div>
<div id="existing_orders_area">
<h2>Ou, continuer une commande existante</h2>
<div id="existing_orders"></div>
</div>
</div>
<div id="main_content" class="page_content" style="display:none;">
<div id="back_to_order_selection">
<button type="button" class="btn--danger"><i class="fas fa-arrow-left"></i>&nbsp; Retour</button>
</div>
<div id="actions_buttons_area">
<button type="button" class='btn--primary' id="do_inventory">Faire un inventaire</button>
</div>
<div class="header txtcenter">
<h1>Aide à la commande</h1>
<i>Commande : <span class="order_name_container"></span></i>
</div>
<div class="txtcenter" id="supplier_form_container">
<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 class="txtcenter" 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>
<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>
</div>
<div id="order_pill_template">
<div class="pill order_pill btn btn--primary">
<span class="pill_order_name"></span>
</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/>
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>
</div>
<script src="{% static "js/pouchdb.min.js" %}"></script>
<script type="text/javascript">
var couchdb_dbname = '{{db}}';
var couchdb_server = '{{couchdb_server}}' + couchdb_dbname;
</script>
<script src="{% static "js/all_common.js" %}?v="></script>
<script type="text/javascript" src="{% static 'js/orders_helper.js' %}?v="></script>
{% endblock %}
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