Commit 1ddbc7fd by François C.

Merge branch 'boutique_plus_parametrable' into 'dev_cooperatic'

Shop and delivery improvements (contacts can order, more configurable variables)

See merge request cooperatic-foodcoops/third-party!10
parents 7359d92d 8a03b3b5
......@@ -93,7 +93,9 @@ class CagetteMember(models.Model):
fp = request.POST.get('fp') # fingerprint (prevent using stolen cookies)
if login and password:
api = OdooAPI()
cond = [['email', '=', login], ['is_member', '=', True]]
cond = [['email', '=', login]]
if getattr(settings, 'ALLOW_NON_MEMBER_TO_CONNECT', False) is False:
cond.append(['is_member', '=', True])
fields = ['name', 'email', 'birthdate', 'create_date', 'cooperative_state']
res = api.search_read('res.partner', cond, fields)
if (res and len(res) >= 1):
......
......@@ -22,7 +22,7 @@ class CagetteMail:
html_message=html_msg)
@staticmethod
def sendCartValidation(email, cart):
def sendCartValidation(email, cart, mode="shop"):
"""Used by Shop"""
from django.core.mail import send_mail
from django.utils.html import strip_tags
......@@ -43,7 +43,10 @@ class CagetteMail:
html_msg = render_to_string(mail_template, ctx)
msg = strip_tags(html_msg)
send_mail("Votre commande en ligne à " + settings.COMPANY_NAME,
subject_prefix = "Votre commande en ligne à "
if mode == "delivery":
subject_prefix = "Votre demande de livraison à "
send_mail(subject_prefix + settings.COMPANY_NAME,
msg,
settings.DEFAULT_FROM_EMAIL,
[email],
......
......@@ -47,7 +47,11 @@ def get_all_children(branch):
children += get_all_children(c)
return children
def get_all_children_ids(branch):
ids = []
for c in get_all_children(branch):
ids.append(c['id'])
return ids
class CagetteShop(models.Model):
"""Class to handle cagette Shop."""
......@@ -62,7 +66,7 @@ class CagetteShop(models.Model):
def filter_products_according_settings(pdts):
res = pdts
try:
conditions = settings.SHOP_LIMIT_PRODUCTS
conditions = getattr(settings, 'SHOP_LIMIT_PRODUCTS', [])
filtered = []
for p in pdts:
keep_it = True
......@@ -124,13 +128,14 @@ class CagetteShop(models.Model):
fields = ['parent_id', 'name']
res = api.search_read('product.category', [], fields)
tree = build_tree_from_categories(res)
except:
pass
except Exception as e:
coop_logger.error('get_product_categories : %s', str(e))
return tree
@staticmethod
def get_cat_children_ids(categ_id):
cat_ids = [categ_id]
tree = CagetteShop.get_product_categories()
branch = None
for cats in tree:
......@@ -159,15 +164,23 @@ class CagetteShop(models.Model):
return children
@staticmethod
def get_categories_nb_of_products():
"""Needs lacagette_categories Odoo module to be activated"""
res = {}
try:
api = OdooAPI()
res = api.execute('lacagette.categories', 'get_all_with_products_count', {})
except Exception as e:
coop_logger.error('get_categories_nb_of_products %s', str(e))
res['error'] = str(e)
return res
@staticmethod
def get_category_products(categ_id):
res = {}
try:
pdts = []
limit_conditions = []
try:
limit_conditions = settings.SHOP_LIMIT_PRODUCTS
except:
pass
limit_conditions = getattr(settings, 'SHOP_LIMIT_PRODUCTS', [])
api = OdooAPI()
cat_ids = CagetteShop.get_cat_children_ids(categ_id)
# removed ['qty_available', '>', 0]
......@@ -183,6 +196,7 @@ class CagetteShop(models.Model):
res['pdts'] = CagetteShop.filter_products_according_settings(pdts)
except Exception as e:
coop_logger.error('get_category_products %s %s', categ_id, str(e))
res['error'] = str(e)
return res
......@@ -239,7 +253,7 @@ class CagetteShop(models.Model):
@staticmethod
def registrerCart(cart, partner_id):
def registrerCart(cart, partner_id, mode="shop"):
result = {}
try:
cart['submitted_time'] = time.time()
......@@ -260,7 +274,7 @@ class CagetteShop(models.Model):
if result:
try:
from outils.mail import CagetteMail
CagetteMail.sendCartValidation(partner['email'], cart)
CagetteMail.sendCartValidation(partner['email'], cart, mode)
except Exception as e:
coop_logger.error("Shop, registrerCart : %s, %s", str(e), str(cart))
except Exception as e:
......
......@@ -252,7 +252,10 @@ li.tab { border-right: 1px solid white; }
#my-orders-sumup .date {text-align: left;padding: 0 4px;}
#my-orders-sumup tbody tr:hover {background-color: #b3b7c4;}
td.actions .fa-trash, td.actions .fa-paper-plane, td.date .fa-edit {cursor: pointer;}
td.actions .fa-trash,
td.actions .fa-eye,
td.actions .fa-paper-plane,
td.date .fa-edit {cursor: pointer;}
.no-action-available-msg {margin-top: 15px;}
#survey_link {text-decoration: none;}
......
......@@ -26,6 +26,7 @@ var main_content = $('#main-content'),
dragSrcEl = null,
forbidden_slots = [],
closing_dates = [],
my_sent_orders = [],
right_column = $('#right-column'),
visit_mode = false,
timer = null;
......@@ -717,7 +718,7 @@ var addProductToCart = function() {
var msg = "";
var too_much = "Vous avez pris plus de produit que le stock indicatif.\nVous n'aurez peut-être pas toute la quantité.";
if (parseFloat(qty) > available_qty) {
if (parseFloat(qty) > available_qty && stock_warning == true) {
msg = too_much;
}
var u = p_div.find('.unit').text()
......@@ -757,7 +758,7 @@ var addProductToCart = function() {
}
}
if (typeof answer.warning !== "undefined") {
if (answer.warning == "max_qty")
if (answer.warning == "max_qty" && stock_warning == true)
msg = too_much;
}
});
......@@ -1059,11 +1060,28 @@ var loadAllAvailableBoughtProducts = function() {
}
};
var shouldCategoryBeShown = function (cat_id) {
let answer = true;
if (excluded_cat.indexOf(cat_id) > -1) {
answer = false;
}
if (typeof cat_nb_pdts != "undefined") {
let list = cat_nb_pdts.list;
let cat_ids = Object.keys(list).map(x => parseInt(x,10));
//cat_ids is now an array of category ids which have product
if (cat_ids.indexOf(cat_id) < 0) {
// cat_id is corresponding to a category which have no product
answer = false;
}
}
return answer;
}
var appendChildrenCatToMenu = function (catdiv, children) {
var ul = catdiv.find('ul');
$.each(children, function(i, e) {
if (excluded_cat.indexOf(e.id) < 0) {
if (shouldCategoryBeShown(e.id)) {
var li = $('<li>').addClass("nav-item");
// Remove TVA in cat name
......@@ -1092,7 +1110,6 @@ var getCategChildren = function() {
if (typeof category_elts[cat_id] == "undefined") {
try {
$.ajax({
//url :'/shop/get_categ_products',
url : '/shop/get_cat_children',
data: {id: cat_id},
dataType: 'json'
......@@ -1236,12 +1253,13 @@ var displaySentOrders = function() {
}
} else if (typeof rData.res.data.orders != "undefined") {
if (rData.res.data.orders.length > 0) {
my_sent_orders = rData.res.data.orders;
var eye = '<i class="fas fa-eye fl"></i>';
var delete_icon = '<i class="fas fa-trash fr"></i>';
var edit = '<i class="fas fa-edit"></i>';
var show_no_action_available_msg = false;
$.each(rData.res.data.orders, function(i, o) {
$.each(my_sent_orders, function(i, o) {
var bdate_content = "<span>" + o.best_date + "</span>";
if (o.state == "init" || o.state == "validating") bdate_content += " " + edit;
......@@ -1267,7 +1285,8 @@ var displaySentOrders = function() {
.text(o.products.length);
var td4 = $('<td>').addClass('amount')
.text(parseFloat(o.total).toFixed(2));
//var td5 = $('<td>').addClass('actions').html(eye + ' ' + delete_icon)
actions_content = eye + ' ' + actions_content;
var td5 = $('<td>').addClass('actions')
.html(actions_content);
......@@ -1374,6 +1393,36 @@ var changeBestDate = function() {
};
var showSentCart = function() {
let clicked = $(this),
clicked_tr = clicked.closest('tr'),
id = clicked_tr.data('id'),
content = $('<div>'),
table = $('<table>');
let header = $('<tr><th>Article</th><th>Qté</th><th>Prix Total (T.T.C)</th></tr>');
let bottom_msg = $('<p>').html("<em>Contenu non modifiable.</em>")
table.append(header);
$.each(my_sent_orders, function(i,e) {
if (e._id == id) {
$.each(e.products, function (j,p) {
let tr = $('<tr>'),
name = $('<td>').text(p.name),
qty = $('<td>').text(p.qty),
total = $('<td>').text(p.total)
tr.append(name);
tr.append(qty);
tr.append(total);
table.append(tr);
})
}
})
content.append(table);
content.append(bottom_msg);
displayMsg(content.html());
}
var destroySentCart = function() {
var clicked = $(this);
var clicked_tr = clicked.closest('tr'),
......@@ -1561,15 +1610,19 @@ valid_cart.click(function() {
$('#get_my_bought_products').click(loadAllAvailableBoughtProducts);
$(document).on('change', '[name^="bday"]', filterHourOptions);
$(document).on('change', '[name="bhour"]', adaptTimeGivenForValidationMsg);
$(document).on('click', '#alim_categ > div, #non_alim_categ > div', getCategChildren);
$(document).on('click', '#alim_categ ul li span, #non_alim_categ ul li span', getCategProducts);
$(document).on('click', '.product button', addProductToCart);
$(document).on('click', '.forbidden-slots .fs-close', closeForbiddenList);
$(document).on('click', 'td.date .fa-edit', changeBestDate);
$(document).on('click', 'td.actions .fa-eye', showSentCart);
$(document).on('click', 'td.actions .fa-trash', destroySentCart);
if (shop_mode == 'shop')
$(document).on('change', '[name^="bday"]', filterHourOptions);
$(document).on(
'click', '.new-order',
function() {
......
......@@ -8,49 +8,28 @@ from shop.models import CagetteShop
@never_cache
def shop_index(request):
return index(request, mode='shop')
@never_cache
def delivery_index(request):
return index(request, mode='delivery')
def index(request, mode="shop"):
template = loader.get_template('shop/index.html')
credentials = CagetteMember.get_credentials(request)
shop_settings = CagetteShop.get_shop_settings()
def _get_index_context(credentials, shop_settings, mode):
context = {'title': 'Commande / Réservation',
'mode': mode,
'COMPANY_NAME': settings.COMPANY_NAME,
'SHOP_CATEGORIES': settings.SHOP_CATEGORIES,
'EXCLUDE_SHOP_CATEGORIES': settings.EXCLUDE_SHOP_CATEGORIES,
'MIN_DELAY_FOR_SLOT': settings.MIN_DELAY_FOR_SLOT,
'HOURS_FOR_VALIDATION': settings.HOURS_FOR_VALIDATION_SHOP}
'header_img': getattr(settings, 'SHOP_HEADER_IMG', '/static/img/header.jpg')
}
if 'capital_message' in shop_settings:
context['capital_message'] = shop_settings['capital_message']
allowed_states = ["up_to_date", "alert", "delay"]
# Uncomment if 'coop_state' in credentials .... etc
# to prevent other states people to use the shop
allowed = True
if ('failure' in credentials):
# Visitor has not been identified
template = loader.get_template('website/connect.html')
context['msg'] = ''
if 'msg' in credentials:
context['msg'] = credentials['msg']
context['password_placeholder'] = 'Mot de passe'
context['password_notice'] = "Par défaut, la date de naissance (jjmmaaaa)"
context['with_shop_header'] = True
try:
context['header_img'] = settings.SHOP_HEADER_IMG
except:
context['header_img'] = '/static/img/header.jpg'
else:
if hasattr(settings, 'SHOP_OPENING'):
context['SHOP_OPENING'] = settings.SHOP_OPENING
if hasattr(settings, 'SHOP_SLOT_SIZE'):
context['SHOP_SLOT_SIZE'] = settings.SHOP_SLOT_SIZE
if hasattr(settings, 'SHOP_OPENING_START_DATE'):
context['SHOP_OPENING_START_DATE'] = settings.SHOP_OPENING_START_DATE
if mode == 'shop' and hasattr(settings, 'SHOP_CAN_BUY'):
context['SHOP_CAN_BUY'] = settings.SHOP_CAN_BUY
context['DELIVERY_CAN_BUY'] = False
......@@ -58,22 +37,53 @@ def index(request, mode="shop"):
context['SHOP_CAN_BUY'] = False
context['DELIVERY_CAN_BUY'] = settings.DELIVERY_CAN_BUY
context['SHOP_CATEGORIES'] = getattr(settings, 'SHOP_CATEGORIES', [])
context['EXCLUDE_SHOP_CATEGORIES'] = getattr(settings, 'EXCLUDE_SHOP_CATEGORIES', [])
context['MIN_DELAY_FOR_SLOT'] = getattr(settings, 'MIN_DELAY_FOR_SLOT', 30)
context['HOURS_FOR_VALIDATION'] = getattr(settings, 'HOURS_FOR_VALIDATION_SHOP', 2)
context['SHOP_OPENING'] = getattr(settings, 'SHOP_OPENING', {})
context['SHOP_SLOT_SIZE'] = getattr(settings, 'SHOP_SLOT_SIZE', 15)
context['SHOP_OPENING_START_DATE'] = getattr(settings, 'SHOP_OPENING_START_DATE', None)
context['survey_link'] = getattr(settings, 'SHOP_SURVEY_LINK', '')
context['extra_menus'] = getattr(settings, 'SHOP_EXTRA_MENUS', None)
context['SHOW_SUBSTITUTION_OPTION'] = getattr(settings, 'SHOW_SUBSTITUTION_OPTION', False)
context['CART_VALIDATION_BOTTOM_MSG'] = getattr(settings, 'CART_VALIDATION_BOTTOM_MSG', "")
context['SHOP_BOTTOM_VALIDATION_MSG'] = getattr(settings, 'SHOP_BOTTOM_VALIDATION_MSG',\
"Si vous arrivez avec un retard de plus d'une heure, la commande pourrait ne plus être disponible.")
stock_warning = getattr(settings, 'SHOP_STOCK_WARNING', True)
if stock_warning is True:
context['SHOP_STOCK_WARNING'] = 'true'
else:
context['SHOP_STOCK_WARNING'] = 'false'
return context
def index(request, mode="shop"):
template = loader.get_template('shop/index.html')
credentials = CagetteMember.get_credentials(request)
shop_settings = CagetteShop.get_shop_settings()
allowed_states = ["up_to_date", "alert", "delay"]
# Uncomment if 'coop_state' in credentials .... etc
# to prevent other states people to use the shop
allowed = True
context = _get_index_context(credentials, shop_settings, mode)
if ('failure' in credentials):
# Visitor has not been identified
template = loader.get_template('website/connect.html')
else:
d_p_pdts = CagetteShop.get_promoted_and_discounted_products()
context['discounted_pdts'] = d_p_pdts['discounted']
context['promoted_pdts'] = d_p_pdts['promoted']
context['survey_link'] = ''
if hasattr(settings, 'SHOP_EXTRA_MENUS'):
context['extra_menus'] = settings.SHOP_EXTRA_MENUS
if hasattr(settings, 'SHOP_SURVEY_LINK'):
context['survey_link'] = settings.SHOP_SURVEY_LINK
context['SHOW_SUBSTITUTION_OPTION'] = True
if hasattr(settings, 'SHOW_SUBSTITUTION_OPTION'):
if settings.SHOW_SUBSTITUTION_OPTION is False:
del context['SHOW_SUBSTITUTION_OPTION']
if hasattr(settings, 'CART_VALIDATION_BOTTOM_MSG'):
context['CART_VALIDATION_BOTTOM_MSG'] = settings.CART_VALIDATION_BOTTOM_MSG
cat_nb_pdts = CagetteShop.get_categories_nb_of_products()
if 'error' in cat_nb_pdts:
context['cat_nb_pdts'] = None
else:
context['cat_nb_pdts'] = cat_nb_pdts
# if 'coop_state' in credentials and not (credentials['coop_state'] in allowed_states):
# allowed = False
......@@ -119,6 +129,7 @@ def get_categ_products(request):
result['error'] = 'Authentification non valide'
return JsonResponse({'res': result})
def search_product(request):
result = {}
credentials = CagetteMember.get_credentials(request)
......@@ -177,7 +188,10 @@ def cart(request):
credentials = CagetteMember.get_credentials(request)
if 'success' in credentials:
try:
result['cart'] = CagetteShop.registrerCart(cart, request.COOKIES['id'])
mode = "shop"
if 'type' in cart:
mode = cart['type']
result['cart'] = CagetteShop.registrerCart(cart, request.COOKIES['id'], mode)
except Exception as e:
result['error'] = str(e)
else:
......@@ -308,4 +322,4 @@ def remove_unused_orders(request):
res['deleted'] = CagetteShop.remove_unused_orders()
except Exception as e:
res['error'] = str(e)
return JsonResponse(res)
\ No newline at end of file
return JsonResponse(res)
......@@ -391,7 +391,7 @@
<option value="23:00">23:00</option>
</select>
{% endif %}
<div class="slots-constraints" style="display: none;">
<span class="delay24h"><strong>avec un délai de {{MIN_DELAY_FOR_SLOT}}h minimum</strong></span>
<!--<input type="text" placeholder="Exemple: mardi 24 mars à 10h30" name="best_date" style="width: 350px;"/>--><br/>
......@@ -405,7 +405,7 @@
{%if HOURS_FOR_VALIDATION > 0 %}
<div class="tv-msg" style="display: none;">
<strong>
Vous aurez <span class="time-given-for-validation">{{HOURS_FOR_VALIDATION}} heure(s)</span> pour valider votre commande.<br/>
Vous aurez <span class="time-given-for-validation">{{HOURS_FOR_VALIDATION}} heure(s)</span> pour valider votre commande.<br/>
Passé ce temps, la {% if mode == 'shop' %}réservation du créneau horaire{% elif mode == 'delivery' %}commande{% endif %} sera annulée.<br/>
Vous devrez rechoisir un horaire (le panier sera mémorisé).
</strong>
......@@ -436,7 +436,7 @@
</p>
<p>
{% if mode == 'shop' %}
<strong><i>Si vous arrivez avec un retard de plus d'une heure, la commande pourrait ne plus être disponible.</i></strong>
<strong><i>{{SHOP_BOTTOM_VALIDATION_MSG}}</i></strong>
{%endif%}
</p>
<p>
......@@ -500,6 +500,10 @@
{%if SHOP_OPENING_START_DATE%}
const opening_start_date = new Date('{{SHOP_OPENING_START_DATE}}')
{%endif%}
{%if cat_nb_pdts%}
const cat_nb_pdts = {{cat_nb_pdts|safe}}
{%endif%}
const stock_warning = {{SHOP_STOCK_WARNING|safe}}
</script>
<script src="{% static 'js/all_common.js' %}?v="></script>
<script type="text/javascript" src="{% static 'js/shop.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