Commit 39a5c17f by François C.

Merge branch 'dev_principale' into 'master'

Cooperatic and LGDS improvements

See merge request !24
parents a868d541 28dd803c
Pipeline #1027 passed with stage
in 21 seconds
COUCHDB_USER=admin
COUCHDB_PASSWORD=123abc
POSTGRES_USER=foodcoops
POSTGRES_PASSWORD=foodcoops
POSTGRES_DB=foodcoops
# Scripts
*.sh text eol=lf
\ No newline at end of file
......@@ -10,8 +10,10 @@ outils/settings_secret.py
outils/config.py
outils/texts/*
outils/js_errors.log
outils/scripts/scripts_settings.py
db.sqlite3
*/max_timeslot_carts.txt
.gitlab-ci.yml
shop/shop_admin_settings.json
shop/errors.log
.idea
This diff is collapsed. Click to expand it.
"""Company specific data values."""
MAG_NAME = 'Cleme'
OFFICE_NAME = ''
MAX_BEGIN_HOUR = '19:00'
COMPANY_NAME = 'La Cagette'
WELCOME_ENTRANCE_MSG = 'Bienvenue à La Cagette !'
WELCOME_MAIL_SUBJECT = 'Dernière étape de votre inscription à la Cagette.'
WELCOME_MAIL_TEMPLATE = 'members/bienvenue.html'
EMAIL_DOMAIN = 'lacagette-coop.fr'
OPEN_ON_SUNDAY = True
FLV_CSV_NB = 4
CAP_JOURNAL_ID = 9
CAP_APPELE_NON_VERSE_ACCOUNT_ID = 529
CAP_APPELE_VERSE_ACCOUNT_ID = 8
CAP_INVOICE_LINE_ACCOUNT_ID = 8
FUNDRAISING_CAT_ID = 1
UNITE_UOM_ID = 1
PARTS_A_PRODUCT_ID = 1008
PARTS_A_PRICE_UNIT = 10.0
COOP_BARCODE_RULE_ID = 11
CHECK_PAYMENT_ID = 8
CASH_PAYMENT_ID = 18
STOCK_LOC_ID = 12
STOCK_LOC_ID = 12
CATEG_FRUIT = 151
CATEG_LEGUME = 152
FLV_CSV_NB = 4
COEFF_MAG_ID = 1
......@@ -50,7 +55,7 @@ EM_URL = ''
RECEPTION_MERGE_ORDERS_PSWD = 'jpsrcp'
DAV_PATH = '/data/dav/cagette'
DAV_PATH = '/shared_dir/dav/'
TOOLS_SERVER = 'https://outils.lacagette-coop.fr'
......@@ -78,6 +83,8 @@ SHOP_CATEGORIES = {'epicerie': {'id': 75, 'label': 'Epicerie'},
'parfumerie': {'id': 133, 'label': 'Parfumerie'}}
SHOP_EXTRA_MENUS = ['shop/planning_livraison_pains.html', 'shop/combien_ca_pese.html']
SHOP_SURVEY_LINK = 'https://docs.google.com/forms/d/e/1FAIpQLSczl0mMRwx3s9LbUSPYwwFTiiRa6agx7YkQM9cL41eiQnXNUw/viewform'
SHOP_LIMIT_PRODUCTS = ['relatively_available', 'no_shelf']
EXCLUDE_SHOP_CATEGORIES = [108]
PROMOTE_SHELFS_IDS = [68]
DISCOUNT_SHELFS_IDS = [74]
......@@ -85,11 +92,18 @@ FL_SHELFS = [16, 17, 18]
VRAC_SHELFS = [20, 38]
SHIFT_EXCHANGE_DAYS_TO_HIDE = ''
SHIFT_INFO = """A la cagette, un service est une plage de trois heures un jour en particulier, par exemple : le mardi 25/09/2018 à 13h15.
<br />A l'inverse, un créneau est une plage de trois heures régulière, par exemple, tous les mardi de semaine A à 13h15."""
PB_INSTRUCTIONS = """Si j'ai un problème, que je suis désinscrit, que je veux changer de créneaux ou quoi que ce soit, merci de vous rendre dans la section \"J'ai un problème\" sur le site web de <a href=\"https://lacagette-coop.fr/?MonEspaceMembre\">La Cagette</a>"""
ENTRANCE_COME_FOR_SHOPING_MSG = "Hey coucou toi ! Cet été nous sommes plus de <strong>1000 acheteur·euses</strong> pour seulement <strong>300 coopérateur·rice·s</strong> en service. <br />Tu fais tes courses à La Cagette cet été ?<br/> Inscris-toi sur ton espace membre !"
# Members space / shifts
UNSUBSCRIBED_MSG = 'Vous êtes désincrit·e, merci de remplir <a href="https://docs.google.com/forms/d/e/1FAIpQLSfPiC2PkSem9x_B5M7LKpoFNLDIz0k0V5I2W3Mra9AnqnQunw/viewform">ce formulaire</a> pour vous réinscrire sur un créneau.<br />Vous pouvez également contacter le Bureau des Membres en remplissant <a href="https://docs.google.com/forms/d/e/1FAIpQLSeZP0m5-EXPVJxEKJk6EjwSyZJtnbiGdYDuAeFI3ENsHAOikg/viewform">ce formulaire</a>'
CONFIRME_PRESENT_BTN = 'Clique ici pour valider ta présence'
RECEPTION_PB = "Ici, vous pouvez signaler toute anomalie lors d'une réception, les produits non commandés, cassés ou pourris. \
Merci d'indiquer un maximum d'informations, le nom du produit et son code barre. \
Dans le cas de produits déteriorés, merci d'envoyer une photo avec votre téléphone à [Adresse_email]"
......@@ -91,4 +91,4 @@ BRINKS_MUST_IDENTIFY = True
ENTRANCE_FTOP_BUTTON_DISPLAY = False
CUSTOM_CSS_FILES = {'all': ['common_lgds.css'],
'members': ['inscription_lgds.css']}
'members': ['inscription_lgds.css','member_lgds.css']}
version: '3'
services:
app:
build:
context: .
dockerfile: dockerfiles/Dockerfile
env_file: .env
restart: always
ports:
- "8080:8080"
links:
- "couchdb:couchdb"
- "odoo:odoo"
volumes:
- "./:/home/app/"
couchdb:
build:
context: .
dockerfile: dockerfiles/Dockerfile.couchdb
env_file: .env
restart: always
ports:
- "5984:5984" # Expose port because it's used by the frontend
volumes:
- "couchdb-data:/opt/couchdb/data"
database:
image: "postgres:10"
env_file: .env
restart: always
volumes:
- "odoo-pg-data:/var/lib/postgresql/data"
odoo:
image: "registry.gitlab.com/lgds/foodcoops:9.0-cooperatic-2021-04-02"
env_file: .env
restart: always
ports:
- "8069:8069"
links:
- "database:database"
volumes:
- "odoo-shared-data:/home/app/.local/share/Odoo"
volumes:
odoo-shared-data:
odoo-pg-data:
couchdb-data:
FROM python:3
ENV PYTHONUNBUFFERED=1
# Add french locale
RUN apt update && \
apt install -y --no-install-recommends locales && \
rm -rf /var/lib/apt/lists/* && \
sed -i '/^#.* fr_FR.UTF-8 /s/^#//' /etc/locale.gen && \
locale-gen
WORKDIR /home/app
RUN pip install --upgrade pip
COPY requirements.txt /home/app/
RUN pip install -r requirements.txt
# Setup volume to be able to dev application locally
VOLUME /home/app
COPY . /home/app
# Run the application:
EXPOSE 8080
CMD ["./launch.sh", "0.0.0.0", "8080"]
FROM couchdb:3
COPY dockerfiles/local.ini /opt/couchdb/etc/
[couchdb]
single_node=true
users_db_security_editable = true
[httpd]
enable_cors = true
[cors]
credentials = true
origins = *
[replicator]
db = _replicator
[admins]
;admin = 123abc
admin = -pbkdf2-c5101d6618dfa702b4fb72b1ea0f335e1d1d84f9,506213b6a001da088296f40327cc2e86,10
......@@ -46,6 +46,7 @@ def archive_envelop(request):
"done": False,
"error": repr(e)
}
coop_logger.error("Payment error : %s \n %s", str(data), str(e))
if res['done']:
# Immediately save a token than this payment has been saved
......@@ -59,22 +60,19 @@ def archive_envelop(request):
try:
res['partner_name'] = envelop['envelop_content'][partner_id]['partner_name']
res['amount'] = envelop['envelop_content'][partner_id]['amount']
except:
except Exception as e:
res['error'] = "Wrong envelop structure"
coop_logger.error("Wrong envelop structure : %s", str(e))
res_payments.append(res)
try:
# Log the error
lf = open("/tmp/erreurs_django.log", "a")
lf.write(datetime.date.today().strftime("%Y-%m-%d") + " - Erreur lors de l'enregistrement du paiement de " + res['partner_name'] + "(odoo_id:" + partner_id + " )")
lf.write(res['error'] + "\n")
lf.close()
msg = 'Erreur lors de l\'enregistrement du paiement ' + envelop['type']
msg += ' ' + envelop['envelop_content'][partner_id]['amount'] + ' euros '
msg += ' ' + str(envelop['envelop_content'][partner_id]['amount']) + ' euros '
msg += ' (' + res['error'] + ')'
CagetteMember(int(partner_id)).attach_message(msg)
except:
pass
except Exception as e:
coop_logger.error("Cannot attach payment error message to member : %s", str(e))
try:
# Delete envelop from couchdb
......
#! /bin/sh
#!/usr/bin/env bash
port=34001
ip=127.0.0.1
if [ ! -z "$1" ]
then
if [ -n "$1" ]
then
ip=$1
fi
if [ ! -z "$2" ]
then
if [ -n "$2" ]
then
port=$2
fi
current_path=$(pwd)
export PYTHONPATH="$current_path:$current_path/lib:$PYTHONPATH"
echo yes | django-admin collectstatic --settings=outils.settings
django-admin runserver $ip:$port --settings=outils.settings
export DJANGO_SETTINGS_MODULE=outils.settings
# Collect static files
echo yes | django-admin collectstatic
# Make sure couchdb databases exist
python manage.py couchdb
# Run server
django-admin runserver "$ip:$port"
from django.core.management.base import BaseCommand, CommandError
from django.conf import settings
import couchdb
class Command(BaseCommand):
help = 'Initialize needed couchDB databases'
def handle(self, *args, **options):
if 'admin' not in settings.COUCHDB:
raise CommandError('''
Veuillez définir les accès d'admin à CouchDB dans votre fichier settings_secret.py
Vérifiez le fichier settings_secret_example.py pour un exemple.''')
url = settings.COUCHDB['admin']['url'] % (settings.COUCHDB['admin']['user'], settings.COUCHDB['admin']['password'])
dbnames = settings.COUCHDB['dbs']
couchserver = couchdb.Server(url)
for dbname in dbnames.values():
if dbname not in couchserver:
self.stdout.write(self.style.WARNING("Missing database %s" % dbname))
db = couchserver.create(dbname)
self.stdout.write(self.style.SUCCESS("✓ created db"))
if dbname == 'coops':
self.createCoopsViews(db)
elif dbname == 'envelop':
self.createEnvelopViews(db)
# db.security
self.createPublicAccess(db)
def createCoopsViews(self, dbConn):
byFpMapFunction = '''function(doc) {
emit(doc.fingerprint);
}'''
byCompletedMapFunction = '''function(doc) {
emit(doc.completed);
}'''
byOdooMapFunction = '''function(doc) {
emit(doc.odoo_id);
}'''
views = {
"by_fp": {
"map": byFpMapFunction
},
"by_completed": {
"map": byCompletedMapFunction
},
"by_odoo_id": {
"map": byOdooMapFunction
},
}
self.createView(dbConn, "index", views)
def createEnvelopViews(self, dbConn):
byTypeMapFunction = '''function(doc) {
emit(doc.type);
}'''
views = {
"by_type": {
"map": byTypeMapFunction
}
}
self.createView(dbConn, "index", views)
def createView(self, dbConn, designDoc, views):
self.stdout.write(self.style.SUCCESS("✓ created view %s" % designDoc))
data = {
"_id": "_design/%s" % designDoc,
"views": views,
"language": "javascript",
"options": {"partitioned": False }
}
dbConn.save(data)
def createPublicAccess(self, dbConn):
self.stdout.write(self.style.SUCCESS("✓ created security rule"))
security_doc = dbConn.resource.get_json("_security")[2]
dbConn.resource.put("_security", {})
......@@ -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):
......@@ -317,7 +319,7 @@ class CagetteMember(models.Model):
if 'street2' in post_data:
received_data['street2'] = post_data['street2']
if 'phone' in post_data:
received_data['phone'] = post_data['phone']
received_data['phone'] = format_phone_number(post_data['phone'])
r = c_db.updateDoc(received_data, 'odoo_id')
if r:
if ('odoo_id' in r):
......@@ -394,7 +396,7 @@ class CagetteMember(models.Model):
'street': post_data['address'],
'zip': post_data['zip'],
'city': post_data['city'],
'phone': post_data['mobile'], # Because list view default show Phone and people mainly gives mobile
'phone': format_phone_number(post_data['mobile']), # Because list view default show Phone and people mainly gives mobile
'barcode_rule_id': settings.COOP_BARCODE_RULE_ID
}
if ('_id' in post_data):
......@@ -407,10 +409,10 @@ class CagetteMember(models.Model):
f['street2'] = post_data['street2']
if ('phone' in post_data) and len(post_data['phone']) > 0:
if len(f['phone']) == 0:
f['phone'] = post_data['phone']
f['phone'] = format_phone_number(post_data['phone'])
else:
f['mobile'] = f['phone']
f['phone'] = post_data['phone']
f['phone'] = format_phone_number(post_data['phone'])
# Create coop
if not ('odoo_id' in post_data):
......
......@@ -21,6 +21,16 @@ var to_fill_box = $('#to_fill'),
problem_delete = $('#problem_delete'),
vform = $('#coop_validation_form');
// date validation
Date.prototype.isValid = function () {
// If the date object is invalid it
// will return 'NaN' on getTime()
// and NaN is never equal to itself.
return this.getTime() === this.getTime();
};
sync.on('change', function (info) {
// handle change
if (info.direction == 'pull') {
......@@ -248,17 +258,30 @@ function save_current_coop(callback) {
sex_error = false;
if (/([0-9]{2})\/([0-9]{2})\/([0-9]{4})/.exec(birthdate)) {
try{
var jj = RegExp.$1,
mm = RegExp.$2,
aaaa = RegExp.$3;
if (jj > 31 || mm > 12 || aaaa < 1900 || aaaa > 2018) {
let tmp_date = aaaa + "-" + mm + "-" + jj;
// try to create a date object
date_test = new Date(tmp_date);
// if date is invalid a correction is apply in date object. Check it
// january start at 0, so we add + 1 for the month
if ((date_test.getDate() !== parseInt(jj)) || ((date_test.getMonth()+1) !== parseInt(mm)) || (date_test.getFullYear() !== parseInt(aaaa)) || !date_test.isValid())
{
birthdate_error = true;
}
}catch(Exception){
birthdate_error = true;
}
} else {
birthdate_error = true;
}
let street2_input = form.find('[name="street2"]'),
phone_input = form.find('[name="phone"]');
......
......@@ -48,5 +48,4 @@ urlpatterns = [
url(r'^add_pts_to_everybody/([0-9]+)/([a-zA-Z0-9_ ]+)$', admin.add_pts_to_everybody),
# conso / groupe recherche / socio
url(r'^panel_get_purchases$', views.panel_get_purchases),
]
......@@ -23,7 +23,8 @@ def index(request):
'WELCOME_ENTRANCE_MSG': getattr(settings, 'WELCOME_ENTRANCE_MSG', 'Bienvenue !'),
'WELCOME_SUBTITLE_ENTRANCE_MSG': getattr(settings, 'WELCOME_SUBTITLE_ENTRANCE_MSG', ''),
'ENTRANCE_SHOPPING_BTN': getattr(settings, 'ENTRANCE_SHOPPING_BTN', 'Je viens faire mes courses'),
'ENTRANCE_SERVICE_BTN': getattr(settings, 'ENTRANCE_SERVICE_BTN', 'Je viens faire mon service')
'ENTRANCE_SERVICE_BTN': getattr(settings, 'ENTRANCE_SERVICE_BTN', 'Je viens faire mon service'),
'CONFIRME_PRESENT_BTN' : getattr(settings, 'CONFIRME_PRESENT_BTN', 'Présent.e')
}
for_shoping_msg = getattr(settings, 'ENTRANCE_COME_FOR_SHOPING_MSG', '')
......@@ -102,6 +103,7 @@ def prepa_odoo(request):
'office_place_string': settings.OFFICE_NAME,
'max_begin_hour': settings.MAX_BEGIN_HOUR,
'payment_meanings': settings.SUBSCRIPTION_PAYMENT_MEANINGS,
'input_phone_pattern': getattr(settings, 'INPUT_PHONE_PATTERN', default_input_phone_pattern),
'input_barcode': getattr(settings, 'SUBSCRIPTION_INPUT_BARCODE', False),
'ask_for_sex': getattr(settings, 'SUBSCRIPTION_ASK_FOR_SEX', False),
'ask_for_street2': getattr(settings, 'SUBSCRIPTION_ADD_STREET2', False),
......@@ -135,6 +137,7 @@ def validation_inscription(request, email):
'office_place_string': settings.OFFICE_NAME,
'max_begin_hour': settings.MAX_BEGIN_HOUR,
'payment_meanings': settings.SUBSCRIPTION_PAYMENT_MEANINGS,
'input_phone_pattern': getattr(settings, 'INPUT_PHONE_PATTERN', default_input_phone_pattern),
'ask_for_sex': getattr(settings, 'SUBSCRIPTION_ASK_FOR_SEX', False),
'ask_for_street2': getattr(settings, 'SUBSCRIPTION_ADD_STREET2', False),
'ask_for_second_phone': getattr(settings, 'SUBSCRIPTION_ADD_SECOND_PHONE', False),
......
......@@ -77,6 +77,9 @@ class OdooAPI:
class CouchDB:
"""Class to handle interactions with CouchDB"""
if 'private_url' in settings.COUCHDB:
url = settings.COUCHDB['private_url']
else:
url = settings.COUCHDB['url']
dbs = settings.COUCHDB['dbs']
db = None
......
# -*- coding: utf-8 -*-
"""commons apps functions ."""
from django.conf import settings
default_input_phone_pattern = "^((\+33(-| )\d{1})|\d{2})(\.| )\d{2}(\.| )\d{2}(\.| )\d{2}(\.| )\d{2}$"
def format_phone_number(phone_string):
"""Format phone number for DB insertion (french format)"""
try:
import re
# keep only figures
figures = re.sub(r'[^0-9]', '', phone_string)
international_prefix = ''
if len(figures) > 10:
international_prefix = figures[:len(figures) -9]
figures = figures[-9:]
# for the moment, international prefix is omitted, since only french format is processed
if len(figures) == 9:
figures = '0' + figures
if len(figures) == 10:
number_pairs = [figures[:2]]
for i in range(1,5):
idx = i*2
number_pairs.append(figures[idx:idx + 2])
phone_pairs_separator = getattr(settings, 'PHONE_PAIRS_SEPARATOR', ' ')
output_phone_number = phone_pairs_separator.join(number_pairs)
else:
output_phone_number = phone_string
except:
output_phone_number = phone_string
return output_phone_number
......@@ -79,3 +79,4 @@ LOGGING = {
}
"""
coop_logger = logging.getLogger("coop.framework")
......@@ -106,6 +106,14 @@
Maximum accepted checks numbers
- INPUT_PHONE_PATTERN = "^(0\d{9})$"
Regexp pattern which is used to validate values input in phone fields
Default is "^((\+33(-| )\d{1})|\d{2})(\.| )\d{2}(\.| )\d{2}(\.| )\d{2}(\.| )\d{2}$"
- PHONE_PAIRS_SEPARATOR = "."
Character which by used to separate every 2 phone figures (04.67.23.89.21 for example)
Default is " "
### Scales and labels files generation
- DAV_PATH = '/data/dav/cagette'
......
......@@ -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],
......
# coding: utf-8
import sys, getopt, os
sys.path.append(os.path.abspath('../..'))
from outils.common import OdooAPI
from outils.config import COOP_BARCODE_RULE_ID
from openpyxl import Workbook
from openpyxl import load_workbook
from openpyxl.styles import Alignment
import datetime
# from django.conf import settings
from openpyxl.utils.exceptions import InvalidFileException
def main():
""" Pass file as script arg """
if len(sys.argv) < 2:
print("Il faut renseigner le chemin du fichier contenant les données des rattachés (format Excel).")
exit(2)
api = OdooAPI()
data_file = sys.argv[1]
try:
wb = load_workbook(data_file)
except FileNotFoundError:
print("Fichier introuvable.")
exit(2)
except InvalidFileException:
print("Le fichier fourni est invalide, il doit être au format Excel (.xlsx,.xlsm,.xltx,.xltm)")
exit(2)
while True:
try:
worksheet_number = input(f'Numéro de la feuille contenant les données (entre 0 et {len(wb.worksheets)-1}) : ')
worksheet_number = int(worksheet_number)
ws = wb.worksheets[worksheet_number]
except ValueError:
print("Veuillez rentrer un numéro entier.")
continue
except IndexError:
print("Cette feuille n'existe pas.")
continue
else:
break
columns_valid = input("""
Les colonnes doivent être les suivantes :
A: active*
B: barode_base*
C: name*
D: (ignorée)
E: date_inclusion
F: email
G: birthdate
H: mobile
I: phone
J: street
K: street2
L: zip
M: city
N: sex
O: (ignorée)
P: parent_member_id*
(* ces champs doivent être renseignés)
Vous confirmez ? (O/n) """)
while True:
if (columns_valid == 'n' or columns_valid == 'N'):
print("Veuillez formatter correctement le fichier avant de continuer !")
exit()
elif (columns_valid == 'o' or columns_valid == 'O' or columns_valid== ''):
break
else:
columns_valid = input("Vous confirmez ? (O/n) ")
continue
users = []
has_error = False
for row in ws.iter_rows(min_row=2, values_only=True):
# active
if row[0] is None or row[0] is False or row[0] == "=FALSE()" or row[0] == "=false":
active = False
else:
active = True
# If line is not empty (mandatory field check)
if row[2] is not None:
user = {
"is_member": False,
"is_associated_people": True,
"active": active,
"barcode_rule_id": COOP_BARCODE_RULE_ID,
"name": row[2],
"parent_id": int(row[15]), # for development, override with local existing member id
}
if row[1] is not None and row[1] != "NON":
try:
user["barcode_base"] = int(row[1])
except Exception:
print(f"[Mauvais format du champ 'barcode_base' pour l'utilisateur '{row[2]}' (Attendu : nombre entier)")
has_error = True
if row[4] is not None:
user["comment"] = f"Date d'inclusion : {row[4].date().strftime('%d/%m/%Y')}"
if row[5] is not None:
user["email"] = row[5]
if row[6] is not None:
user["birthdate"] = str(row[6].date())
if row[7] is not None:
try:
mobile = str(row[7]).replace(" ", "")
if mobile[0] != '+' and mobile[0] != '0':
mobile = '0' + mobile
user["mobile"] = mobile
except Exception:
print(f"[Mauvais format du champ 'mobile' pour l'utilisateur '{row[2]}'")
has_error = True
if row[8] is not None:
try:
phone = str(row[8]).replace(" ", "")
if phone[0] != '+' and phone[0] != '0':
phone = '0' + phone
user["phone"] = phone
except Exception:
print(f"[Mauvais format du champ 'phone' pour l'utilisateur '{row[2]}'")
has_error = True
if row[9] is not None:
user["street"] = row[9]
if row[10] is not None:
user["street2"] = row[10]
if row[11] is not None:
try:
zipcode = str(int(row[11]))
if len(zipcode) == 4:
zipcode = '0' + zipcode
user["zip"] = zipcode
except Exception:
print(f"[Mauvais format du champ 'zipcode' pour l'utilisateur '{row[2]}'")
has_error = True
if row[12] is not None:
user["city"] = row[12]
if row[13] == "Femme":
user["sex"] = "f"
elif row[13] == "Homme":
user["sex"] = "m"
users.append(user)
if has_error:
print("L'import a été interrompu, veuillez régler les erreurs.")
exit(2)
res = None
marshal_none_error = 'cannot marshal None unless allow_none is enabled'
for user in users:
try:
res = api.create('res.partner', user)
if res:
print(f"Rattaché.e importé.e avec succès : {user['name']} (id : {res})")
except Exception as e:
if not (marshal_none_error in str(e)):
print(f"Erreur lors de l'insertion de {user['name']}, vérifiez ses données dans le tableau ({str(e)})")
else:
pass
if __name__ == "__main__":
main()
\ No newline at end of file
SECRET_KEY = 'Mettre_plein_de_caracteres_aleatoires_iezezezeezezci'
ODOO = {
'url': 'http://127.0.0.1:8069'
'user': 'api',
'passwd': 'xxxxxxxxxxxx',
'db': 'bd_test',
}
COUCHDB = {
'url': 'http://127.0.0.1:5984',
'dbs': {
'member': 'coops',
'inventory': 'inventory',
'envelops': 'envelop',
'shop': 'shopping_carts'
}
}
......@@ -14,7 +14,6 @@ https://docs.djangoproject.com/en/1.8/ref/settings/
import os
from .settings_secret import *
from .texts.cagette import *
from .config import *
from .customized_errors_filter import *
......@@ -53,6 +52,7 @@ INSTALLED_APPS = (
'orders',
'shop',
'shelfs',
'sales',
# 'tests'
)
......@@ -222,3 +222,5 @@ LOGGING = {
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
CORS_ORIGIN_ALLOW_ALL = True # Needed to make dev test with different IP and ports
ADMIN_IDS = [1]
"""Secret data for DB connexion ."""
ODOO = {
'url': 'http://127.0.0.1:8069'
'url': 'http://odoo:8069',
'user': 'api',
'passwd': 'xxxxxxxxxxxx',
'db': 'bd_test',
'passwd': 'foodcoops',
'db': 'foodcoops',
}
COUCHDB = {
'private_url': 'http://couchdb:5984',
'url': 'http://127.0.0.1:5984',
'admin': {
'url': 'http://%s:%s@couchdb:5984',
'user': 'admin',
'password': '123abc',
},
'dbs': {
'member': 'coops',
'inventory': 'inventory',
......@@ -17,7 +23,7 @@ COUCHDB = {
}
}
""" To ignore """
SQL_OFF = {
'db': 'open_food_facts',
'user': 'off_user',
......
......@@ -44,6 +44,7 @@ urlpatterns = [
url(r'^website/', include('website.urls')),
url(r'^shop/', include('shop.urls')),
url(r'^shelfs/', include('shelfs.urls')),
url(r'^sales/', include('sales.urls')),
]
try:
......
......@@ -5,7 +5,7 @@
"main": "index.js",
"dependencies": {},
"devDependencies": {
"eslint": "^7.16.0"
"eslint": "^7.24.0"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
......
......@@ -108,6 +108,16 @@ class CagetteProduct(models.Model):
def register_start_supplier_shortage(product_id, partner_id, date_start):
"""Start a supplier shortage for a product"""
api = OdooAPI()
c = [['product_id', '=', product_id],
['partner_id', '=', partner_id],
['date_start', '=', date_start]]
existing = api.search_read('product.supplier.shortage', c)
if existing:
res = "already on shortage"
return res
f = {
'product_id' : product_id,
'partner_id' : partner_id,
......
......@@ -32,6 +32,21 @@ IFCBarcodes = {
display_last_error: function() {
alert(this.errors[this.errors.length - 1]);
},
get_quantity_eq_to_encoded_price: function (value, list_price, currency) {
let qty = 0;
try {
let price = parseFloat(value);
if (currency == 'FF')
price = price / 6.55957;
qty = parseFloat(price / list_price).toFixed(3);
} catch (error) {
console.log(error);
}
return qty;
},
get_corresponding_odoo_product: function(bc) {
//console.log('To analyze :' + bc)
var index = 0,
......@@ -49,31 +64,45 @@ IFCBarcodes = {
if (bc.indexOf(significant_prefix) === 0) {
/*
Submitted barcode-code matches a pattern rule
For example,
bc = 0493213018809
pattern = 0493...{NNDDD}
*/
//console.log(pattern)
// console.log(bc)
odoo_bc = '';
pattern_found = true;
pattern_type = this.patterns[index].type;
pattern = pattern.replace(/[^0-9.ND]/, '');
bc = bc.slice(0, -1); // remove original check figure
// Read pattern character by character
/*
Read pattern character by character
to find out Odoo article barcode
and encoded_value (weight, price, units, if exists)
*/
for (var i = 0; i < pattern.length; i++) {
if (/[0-9]/.exec(pattern[i])) {
// it's a figure, nothing to do but to add it to string
odoo_bc += pattern[i];
} else if (pattern[i].indexOf('.') === 0) {
/*
it's a substitution character,
so add the submitted barcode figure which is in this position
*/
odoo_bc += bc[i];
} else if (/[ND]/.exec(pattern[i])) {
/*
A figure which encoding a value is in this position
(corresponding to a 0 in Odoo article barcode)
*/
odoo_bc += '0';
/* let's add a decimal sepator if D is read for the first time */
if (pattern[i] === 'D' && encoded_value.indexOf('.') < 0)
encoded_value += '.';
encoded_value += bc[i];
}
}
// Add check digit at the end of odoo_bc to find out "normalized" code
bc = odoo_bc + eanCheckDigit(odoo_bc);
}
......@@ -101,16 +130,35 @@ IFCBarcodes = {
if (product_data !== null) {
p_uom = (this.uoms)[product_data[this.keys.uom_id]];
let qty = 1;
if (encoded_value.length > 0 && !isNaN(encoded_value)) {
qty = 0; //if no rule is found it will advise user that there is a problem
/*
Warning :
Tests are dependant on La Cagette / Cooperatic uom system and barcode rules
TODO : Defines them outside of this part of code
*/
if (p_uom == 'Unit(s)' || p_uom == 'unité') {
encoded_value = parseInt(encoded_value, 10);
qty = encoded_value;
} else {
encoded_value = parseFloat(encoded_value);
if (pattern_type == 'weight' || pattern_type == 'FF_price_to_weight' || pattern_type == 'price_to_weight') {
if (pattern_type == 'weight') {
qty = encoded_value;
} else {
let list_price = product_data[this.keys.list_price];
let currency = null;
if (pattern_type == 'FF_price_to_weight') currency = 'FF'
qty = parseFloat(this.get_quantity_eq_to_encoded_price(encoded_value, list_price, currency));
}
}
odoo_product = {barcode: bc, data: product_data, rule: pattern_type, value: encoded_value};
}
}
odoo_product = {barcode: bc, data: product_data, rule: pattern_type, value: encoded_value, qty: qty};
}
return odoo_product;
......
......@@ -18,13 +18,13 @@ var orders = {},
is_group = false,
group_ids = [];
var reception_status,
var reception_status = null,
list_to_process = [],
list_processed = [],
table_to_process,
table_processed,
table_to_process = null,
table_processed = null,
editing_product = null, // Store the product currently being edited
editing_origin, // Keep track of where editing_product comes from
editing_origin = null, // Keep track of where editing_product comes from
processed_row_counter = 0, // Order in which products were added in processed list
user_comments = "",
updatedProducts = [], // Keep record of updated products
......@@ -108,6 +108,8 @@ function select_product_from_bc(barcode) {
console.error(err);
report_JS_error(err, 'reception');
}
return 0;
}
/* INIT */
......@@ -231,7 +233,7 @@ function initLists() {
data:"product_id.1",
title:"Produit",
width: "45%",
render: function (data, type, full, meta) {
render: function (data, type, full) {
// Add tooltip with barcode over product name
let display_barcode = "Aucun";
......@@ -300,7 +302,7 @@ function initLists() {
data:"product_id.1",
title:"Produit",
width: "55%",
render: function (data, type, full, meta) {
render: function (data, type, full) {
// Add tooltip with barcode over product name
let display_barcode = "Aucun";
......@@ -344,7 +346,7 @@ function initLists() {
title:"Autres",
className:"dt-body-center",
orderable: false,
render: function (data, type, full, meta) {
render: function (data, type, full) {
let disabled = (full.supplier_shortage) ? "disabled" : '';
return "<select class='select_product_action'>"
......@@ -889,7 +891,7 @@ function editProductInfo (productToEdit, value = null) {
}
// Validate product edition
function validateEdition(form) {
function validateEdition(form = null) {
if (editing_product != null) {
if (editProductInfo(editing_product)) {
clearLineEdition();
......@@ -900,10 +902,12 @@ function validateEdition(form) {
// Set the quantity to 0 for all the products in to_process
function setAllQties() {
// Iterate over all rows in to_process
table_to_process.rows().every(function (rowIdx, tableLoop, rowLoop) {
table_to_process.rows().every(function () {
var data = this.data();
editProductInfo(data, 0);
return true;
});
list_to_process = [];
table_to_process.rows().remove()
......@@ -1128,7 +1132,9 @@ function send() {
traditional: true,
contentType: "application/json; charset=utf-8",
data: JSON.stringify(update_data),
success: function(data) {
success: function() {
closeModal();
try {
// If step 1 (counting), open pop-up with procedure explanation
if (reception_status == "False") {
......@@ -1213,11 +1219,11 @@ function send() {
}
// Go back to to_process list if modal closed
$('#modal_closebtn_top').on('click', function (e) {
$('#modal_closebtn_top').on('click', function () {
document.location.href = "/reception";
});
$('#modal_closebtn_bottom').on('click', function (e) {
$('#modal_closebtn_bottom').on('click', function () {
document.location.href = "/reception";
});
......@@ -1303,7 +1309,7 @@ function confirmPricesAllValid() {
function confirm_all_left_is_good() {
// all products left are to be considered as well filled
// Iterate over all rows in to_process
table_to_process.rows().every(function (rowIdx, tableLoop, rowLoop) {
table_to_process.rows().every(function () {
let data = this.data();
var value = null;
......@@ -1313,6 +1319,8 @@ function confirm_all_left_is_good() {
value = data.price_unit;
}
editProductInfo(data, value);
return true;
});
list_to_process = [];
table_to_process.rows().remove()
......@@ -1328,9 +1336,18 @@ function openFAQ() {
function openErrorReport() {
openModal($('#templates #modal_error_report').html(), saveErrorReport, 'Confirmer');
// listener for error report textarea
// this is necessary because default behavior is overwritten by the listener defined in jquery.pos.js;
$("#error_report").keypress(function(e) {
var key = e.keyCode;
if (key === 13) {
this.value += "\n";
}
});
var textarea = document.getElementById("error_report");
textarea.value = user_comments;
textarea.value = (user_comments != undefined) ? user_comments : "";
textarea.focus();
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
}
......@@ -1473,7 +1490,7 @@ $(document).ready(function() {
reception_status = orders[Object.keys(orders)[0]].reception_status;
// Load user comments from local storage, get it from first order
user_comments = orders[Object.keys(orders)[0]].user_comments;
user_comments = orders[Object.keys(orders)[0]].user_comments || "";
}
// Fetch orders data
......@@ -1560,12 +1577,12 @@ $(document).ready(function() {
container_edition.addEventListener('animationend', onAnimationEnd);
container_edition.addEventListener('webkitAnimationEnd', onAnimationEnd);
function onAnimationEnd(e) {
function onAnimationEnd() {
container_edition.classList.remove('blink_me');
}
// Disable mousewheel on an input number field when in focus
$('#edition_input').on('focus', function (e) {
$('#edition_input').on('focus', function () {
$(this).on('wheel.disableScroll', function (e) {
e.preventDefault();
/*
......@@ -1580,7 +1597,7 @@ $(document).ready(function() {
*/
});
})
.on('blur', function (e) {
.on('blur', function () {
$(this).off('wheel.disableScroll');
});
......@@ -1635,6 +1652,13 @@ $(document).ready(function() {
}
});
$("#edition_input").keypress(function(event) {
// Force validation when enter pressed in edition
if (event.keyCode == 13 || event.which == 13) {
validateEdition();
}
});
// Barcode reader
$(document).pos();
$(document).on('scan.pos.barcode', function(event) {
......
......@@ -16,5 +16,6 @@ urlpatterns = [
url(r'^reception_qtiesValidated', views.reception_qtiesValidated),
url(r'^reception_pricesValidated', views.reception_pricesValidated),
# url(r'^update_order_status/([0-9]+)$', views.tmp_update_order_status),
url(r'^po_process_picking$', views.po_process_picking)
url(r'^po_process_picking$', views.po_process_picking),
url(r'^save_order_group$', views.save_order_group)
]
from django.contrib import admin
# Register your models here.
from django.apps import AppConfig
class SalesConfig(AppConfig):
name = 'sales'
from django.db import models
from outils.common_imports import *
from outils.common import OdooAPI
class CagetteSales(models.Model):
"""Class to manage operations on envelops"""
def __init__(self):
"""Init with odoo id."""
self.o_api = OdooAPI()
def get_sales(self, date_from, date_to):
res = []
# Get pos sessions
cond = [['stop_at', '>=', date_from], ['stop_at', '<=', date_to], ['state', '=', "closed"]]
fields = []
sessions = self.o_api.search_read('pos.session', cond, fields)
# Get bank statements of these sessions
statements = []
for s in sessions:
statements = statements + s["statement_ids"]
# Get payment lines
cond = [['statement_id', 'in', statements]]
fields = ["partner_id", "amount", "journal_id", "create_date", "date"]
payments = self.o_api.search_read('account.bank.statement.line', cond, fields, order="create_date ASC", limit=50000)
item = None
try:
for payment in payments:
# POS session can contain payments from another day (closing session on next morning, ...)
if payment["date"] >= date_from and payment["date"] <= date_to:
# If the consecutive payment in the results is from the same partner on the same day, we consider it's the same basket
if item is not None and item["partner_id"][0] == payment["partner_id"][0] and item["date"] == payment["date"]:
res[len(res)-1]["total_amount"] += round(float(payment["amount"]), 2)
res[len(res)-1]["payments"].append({
"amount": round(float(payment["amount"]), 2),
"journal_id": payment["journal_id"]
})
else:
item = {
"partner_id": payment["partner_id"],
"create_date": payment["create_date"],
"date": payment["date"],
"total_amount": round(float(payment["amount"]), 2),
"payments": [
{
"amount": round(float(payment["amount"]), 2),
"journal_id": payment["journal_id"]
}
]
}
res.append(item)
except Exception as e:
pass
return res
.select_sales_dates {
margin-top: 25px;
}
.sales_date {
display: inline-block;
margin-right: 10px;
}
.main {
margin-top: 25px;
}
.table_area {
margin: auto;
width: 90%;
padding: 10px;
}
.select_sales_date_input {
border-radius:5px;
}
.btn_export_movements {
margin-top: 10px;
}
\ No newline at end of file
var dateFormat = "yy-mm-dd",
from_datepicker = null,
to_datepicker = null,
orders_table = null;
// Return a date from a string if valid, else return null
function getDate(element) {
var date = null;
try {
date = $.datepicker.parseDate(dateFormat, element);
} catch (error) {
date = null;
}
return date;
}
// Enable validation button if all fields are valid
function enable_validation() {
if (getDate(from_datepicker.val()) &&
getDate(to_datepicker.val())) {
$('#dates_selection_button').prop('disabled', false);
} else {
$('#dates_selection_button').prop('disabled', true);
}
}
function display_orders(orders) {
// Empty datatable if already exists
if (orders_table) {
orders_table.destroy();
}
orders_table = $('#orders_table').DataTable({
data: orders,
columns:[
{
data:"create_date",
title:"Date de vente",
width: "10%"
},
{
data:"partner_id",
title:"Membre",
width: "50%",
render: function (data) {
return data[1];
}
},
{
data:"total_amount",
title: "Montant du panier",
className:"dt-body-center",
render: function (data) {
return parseFloat(data).toFixed(2) + ' €';
}
},
{
data:"payments",
title:"Paiements",
orderable: false,
render: function (data) {
let res = '<ul>';
for (p of data) {
res += `<li>${p.journal_id[1]} : ${p.amount} €</li>`
}
res += "</ul>"
return res;
}
}
],
order: [
[
0,
"asc"
]
],
buttons: [
{
extend: 'excelHtml5',
text: 'Export en Excel',
className: 'btn--primary btn_export_movements'
}
],
dom: '<lr<t>ip><"clear"><B>',
iDisplayLength: 100,
language: {url : '/static/js/datatables/french.json'}
});
$('.main').show();
}
function get_sales() {
openModal();
var url = "/sales/get_sales";
url += '?from=' + encodeURIComponent(from_datepicker.val());
url += '&to=' + encodeURIComponent(to_datepicker.val());
$.ajax({
type: 'GET',
url: url,
dataType:"json",
traditional: true,
contentType: "application/json; charset=utf-8",
success: function(data) {
display_orders(data.res);
closeModal();
},
error: function(data) {
err = {msg: "erreur serveur lors de la sélection des mouvements de stock", ctx: 'get_movements'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'stock');
closeModal();
alert('Erreur lors de la récupération, réessayez plus tard');
}
});
}
$(document).ready(function() {
$.ajaxSetup({ headers: { "X-CSRFToken": getCookie('csrftoken') } });
// Set datepicker
$.datepicker.regional['fr'] = {
monthNames: [
'Janvier',
'Fevrier',
'Mars',
'Avril',
'Mai',
'Juin',
'Juillet',
'Aout',
'Septembre',
'Octobre',
'Novembre',
'Decembre'
],
dayNamesMin: [
'Di',
'Lu',
'Ma',
'Me',
'Je',
'Ve',
'Sa'
],
dateFormat: dateFormat,
firstDay: 1,
minDate: 1,
maxDate: '+12M +0D'
};
$.datepicker.setDefaults($.datepicker.regional['fr']);
from_datepicker = $("#from")
.datepicker({
defaultDate: "-1d",
changeMonth: true,
changeYear: true,
yearRange: "-03:+00",
minDate: new Date(2007, 1 - 1, 1),
maxDate: new Date()
})
.on("change", function() {
to_datepicker.datepicker("option", "minDate", getDate(this.value));
});
to_datepicker = $("#to")
.datepicker({
defaultDate: "-1d",
changeMonth: true,
changeYear: true,
yearRange: "-03:+00",
minDate: new Date(2007, 1 - 1, 1),
maxDate: new Date()
})
.on("change", function() {
from_datepicker.datepicker("option", "maxDate", getDate(this.value));
});
$('.select_sales_date_input').change(function() {
enable_validation();
});
$( "#sales_form" ).submit(function( event ) {
event.preventDefault();
get_sales();
});
});
"""."""
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.index),
url(r'^get_sales$', views.get_sales),
]
from outils.common_imports import *
from outils.for_view_imports import *
from sales.models import CagetteSales
def index(request):
"""Display sales export screen"""
context = {'title': 'Export de ventes'}
template = loader.get_template('sales/index.html')
# m = CagetteSales()
# sales = m.get_sales()
# print(sales)
return HttpResponse(template.render(context, request))
def get_sales(request):
res = {}
date_from = request.GET.get('from', '')
date_to = request.GET.get('to', '')
m = CagetteSales()
res = m.get_sales(date_from, date_to)
if 'errors' in res and res['errors']:
return JsonResponse(res, status=500)
else:
return JsonResponse({'res': res})
\ No newline at end of file
......@@ -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() {
......
......@@ -13,44 +13,23 @@ def shop_index(request):
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:
......
......@@ -256,10 +256,10 @@ function init_confirmation_datatable() {
});
}
function without_consent_update_product(p) {
function without_consent_update_product(p, added_qty) {
let undo_option = true;
update_existing_product(p, undo_option);
update_existing_product(p, added_qty, undo_option);
}
function get_stored_product_with_bc(barcode) {
......@@ -307,19 +307,17 @@ function fetch_product_from_bc(barcode) {
'uom': barcodes['uoms'][p.data[barcodes['keys']['uom_id']]],
'standard_price' : p.data[barcodes['keys']['standard_price']], // cost
'list_price': p.data[barcodes['keys']['list_price']] // public price
'list_price': p.data[barcodes['keys']['list_price']], // public price
'qty': p.qty
};
product['uom']['id'] = p.data[barcodes['keys']['uom_id']];
product['value'] = parseFloat(p.value) || 1;
product['rule'] = p.rule;
p_existing = get_stored_product_with_bc(p.barcode);
if (p_existing !== null) {
product.qty = p_existing.qty;
without_consent_update_product(product);
without_consent_update_product(p_existing, product.qty);
return 0;
} else {
add_product(product);
......@@ -335,16 +333,6 @@ function fetch_product_from_bc(barcode) {
var add_product = function(product) {
try {
// Add to list
product.qty = 1;
if (typeof product.value == "number" || (product.value.length > 0 && !isNaN(product.value))) {
//encoded value will be translated in quantity
if (product.rule == "FF_price_to_weight") {
product.qty = get_quantity_eq_to_franc_price(product);
} else {
product.qty = parseFloat(product.value);
}
}
products.push(product);
......@@ -385,26 +373,11 @@ var update_in_products = function(product) {
else console.log("Le produit n'a pas pu être trouvé dans la variable products !");
};
var get_quantity_eq_to_franc_price = function(product) {
let value = 0;
try {
let price = parseFloat(product.value / 6.55957);
value = parseFloat(price / product.list_price).toFixed(3);
} catch (error) {
console.log(error);
}
return value;
};
/*
* Update a line in the table: update quantity
*/
var update_existing_product = function(product, undo_option = false) {
// By default added qty is 1 unit
let added_qty = 1;
var update_existing_product = function(product, added_qty, undo_option = false) {
let op = "augmentée";
let notify_options = {
......@@ -413,35 +386,8 @@ var update_existing_product = function(product, undo_option = false) {
clickToHide: false
};
// type product qty value
if (product.rule == 'weight' || product.rule == 'FF_price_to_weight' && product.value) {
// Quantities are kg or price
product.qty = parseFloat(product.qty) || 0;
if (product.rule == 'weight') {
added_qty = parseFloat(product.value);
} else {
if (product.value < 0) added_qty = parseFloat(product.value); // value is already a qty
else added_qty = parseFloat(get_quantity_eq_to_franc_price(product));
}
} else {
//Quantity is by "defaut" considered as to be in "unit"
product.qty = parseInt(product.qty, 10);
if (product.rule == "" && product.value) {
added_qty = product.value;
}
}
product.qty += added_qty;
/* Surprisingly, this assignment by addition (0 + value)
always correctly "typing" the value
whereas "product.qty = added_qty"
is sometimes typed as "string" !!
*/
// always set to empty to avoid next operation on same product to be misprocessed
product.value = "";
// Find index of row which match product id in the first column
var indexes = products_table.rows().eq(0)
......@@ -470,10 +416,7 @@ var update_existing_product = function(product, undo_option = false) {
notify_options.autoHide = false;
// notify_options.autoHideDelay = 10000;
notify_options.style = 'cancelable';
// msg = $('<span>').text(msg)
// .attr('data-barcode', product.barcode)
// .attr('data-addedqty', added_qty);
// msg = msg.html()
msg = '<span class="msg" data-barcode="' + product.barcode + '" data-added_qty="' + added_qty + '">'
+ "<b>" + product.name + "</b><br/>" + msg
+ '</span>';
......@@ -919,8 +862,7 @@ $(document).ready(function() {
let product = get_stored_product_with_bc(bc);
if (product !== null) {
product.value = - added_qty;
update_existing_product(product);
update_existing_product(product, - added_qty);
} else {
alert("Le produit n'a pas été retrouvé dans la mémoire de travail.");
}
......
......@@ -4,9 +4,9 @@
* Ecran de rechreche d'un article sur le nom et sur le code barre */
var table_article;
var table_article = null;
var dataSet =[];
var csrftoken;
var csrftoken = '';
// lance la recherche sur le nom des l'article
function search_table_article() {
......@@ -30,7 +30,7 @@ $(document).ready(function() {
{
data:"image_small",
"title":"Photo",
"render": function (data, type, full, meta) {
"render": function (data, type, full) {
debut = '<button id="page1" type="button" data-toggle="modal" data-target=".modal" data-remote=' + full.id + ' class="btn btn-primary">';
fin = "</button>";
......@@ -41,14 +41,14 @@ $(document).ready(function() {
{data:"name", "title":"Article", "width": "50%"},
{data:"qty_available", "title":"En Stock", "width": "10%"},
{data:"uom_id",
"render":function (data, type, row) {
"render":function (data) {
return data[1];
},
"title":"Unité", "width":"5%"},
{data:"reception_status",
"title":"Rupture", "className":"dt-body-center",
"render": function (data, type, full, meta) {
"render": function (data, type, full) {
if (full.qty_available > 0) {
return "<div><button id='bt_change' href='#'>Rupture</button></div>";
} else {
......@@ -135,7 +135,7 @@ $(document).ready(function() {
});
// Lancement de la rupture sur l'article choisie
function ruptureArticle(test) {
function ruptureArticle() {
var jIdArcticle = { 'idArticle': selArctileData.id, 'uom_id' : selArctileData.uom_id[0] };
......@@ -148,7 +148,7 @@ function ruptureArticle(test) {
contentType: "application/json; charset=utf-8",
data: JSON.stringify(jIdArcticle),
success: function(data) {
success: function() {
document.location.href = "/stock/listArticleBreaking";
},
......@@ -159,7 +159,7 @@ function ruptureArticle(test) {
});
}
var selArctileData;
var selArctileData = null;
// Fenetre de validation sur l'article
......
......@@ -64,7 +64,7 @@ $(document).on('click', '#dp_Search', function() {
search_table_article();
});
var csrftoken;
var csrftoken = '';
$(document).ready(function() {
csrftoken = getCookie('csrftoken');
......@@ -98,7 +98,7 @@ function actionButton (vUrl, jIdArcticle, followPage) {
traditional: true,
contentType: "application/json; charset=utf-8",
data: JSON.stringify(jIdArcticle),
success: function(data) {
success: function() {
document.location.href = followPage;
},
error: function(resultat, statut, erreur) {
......@@ -108,7 +108,7 @@ function actionButton (vUrl, jIdArcticle, followPage) {
});
}
var selArctileData;
var selArctileData = null;
// Fenetre de validation sur l'article
......
......@@ -15,7 +15,7 @@ $(document).ready(function() {
{data:"name", "title":"Article", "width": "50%"},
{data:"maxdate",
"render":function (data, type, row) {
"render":function (data) {
my = new Date(data);
return my.toLocaleDateString() +" " + my.toLocaleTimeString();
......@@ -23,7 +23,7 @@ $(document).ready(function() {
"title":"Date", "width":"15%"},
{data:"maxdate",
"render":function (data, type, row) {
"render":function (data) {
my = new Date(data);
var today = new Date();
......@@ -37,7 +37,7 @@ $(document).ready(function() {
{data:"purchase_ok", "width":"5%",
"title":"Achetable", "className":"dt-body-center",
"render": function (data, type, full, meta) {
"render": function (data) {
if (data == true) {
return '<div><input type="checkbox" id="bt_dontPurchase" checked><div>';
......@@ -48,14 +48,14 @@ $(document).ready(function() {
},
{data:"reception_status", "width":"5%",
"title":"Rupture", "className":"dt-body-center",
"render": function (data, type, full, meta) {
"render": function () {
return "<div><button id='bt_change' href='#'>Stock à 0</button></div>";
}
},
{data:"reception_status", "width":"5%",
"title":"Archive", "className":"dt-body-center",
"render": function (data, type, full, meta) {
"render": function () {
return "<div><button id='bt_archive' href='#'>Archive</button></div>";
}
}
......@@ -103,7 +103,7 @@ $(document).on('click', '#dp_Search', function() {
search_table_article();
});
var csrftoken;
var csrftoken ='';
$(document).ready(function() {
csrftoken = getCookie('csrftoken');
......@@ -137,7 +137,7 @@ function actionButton (vUrl, jIdArcticle, followPage) {
traditional: true,
contentType: "application/json; charset=utf-8",
data: JSON.stringify(jIdArcticle),
success: function(data) {
success: function() {
document.location.href = followPage;
},
error: function(resultat, statut, erreur) {
......@@ -147,7 +147,7 @@ function actionButton (vUrl, jIdArcticle, followPage) {
});
}
var selArctileData;
var selArctileData = null;
// Fenetre de validation sur l'article
......
......@@ -161,7 +161,7 @@
<div class="validation_wrapper col-6">
<section id="service_validation" class="col-6 grid-6 has-gutter">
<div class="col-2"></div>
<a class="col-2 btn present">Présent.e</a>
<a class="col-2 btn present">{{CONFIRME_PRESENT_BTN|safe}}</a>
<span class="loading2"><img width="75" src="/static/img/Pedro_luis_romani_ruiz.gif" alt="Chargement en cours...." /></span>
<div class="col-2"></div>
</section>
......
......@@ -21,9 +21,9 @@
<input name="city" placeholder="Ville" class="b_green" autocomplete="address-level4"/>
<input name="country" placeholder="Pays" class="b_yellow" autocomplete="address-level4"/>
<span class="phone-wrapper{% if ask_for_second_phone %}-2{% endif %}">
<input type="tel" name="mobile" placeholder="Tél. mobile" class="b_green" pattern="^(\+\d{1,3}(-| ))?\d{1,2}(\.| )\d{1,2}(\.| )\d{1,2}(\.| )\d{1,2}(\.| )\d{0,2}?$" autocomplete="address-level4"/>
<input type="tel" name="mobile" placeholder="Tél. mobile" class="b_green" pattern="{{input_phone_pattern}}" autocomplete="address-level4"/>
{% if ask_for_second_phone %}
<input type="tel" name="phone" placeholder="Tél. fixe" class="b_green" pattern="^(\+\d{1,3}(-| ))?\d{1,2}(\.| )\d{1,2}(\.| )\d{1,2}(\.| )\d{1,2}(\.| )\d{0,2}?$" autocomplete="address-level4"/>
<input type="tel" name="phone" placeholder="Tél. fixe" class="b_green" pattern="{{input_phone_pattern}}" autocomplete="address-level4"/>
{% endif %}
</span>
</p>
......
......@@ -54,6 +54,7 @@
<script src="{% static "js/all_common.js" %}?v="></script>
<script type="text/javascript">
var merge_orders_pswd = '{{merge_orders_pswd}}';
var server_stored_groups = {{server_stored_groups}};
</script>
<script src="{% static "js/common.js" %}?v="></script>
{% endblock %}
......@@ -120,9 +120,7 @@
<hr />
<p class="txtleft">
Ici, vous pouvez signaler toute anomalie lors d'une réception, les produits non commandés, cassés ou pourris.
Merci d'indiquer un maximum d'informations, le nom du produit et son code barre.
Dans le cas de produits déteriorés, merci d'envoyer une photo avec votre téléphone à <strong>{{RECEPTION_PB_EMAIL}}</strong>
{{RECEPTION_PB}}
</p>
<br>
<textarea id="error_report"></textarea>
......
{% 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/sales.css" %}?v=">
{% 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/datatables/dataTables.plugins.js' %}"></script>
<script type="text/javascript" src="{% static 'js/datatables/datatables.buttons.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/datatables/buttons.html5.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/datatables/jszip.min.js' %}"></script>
{% endblock %}
{% block content %}
<div class="page_body">
<div id="content_main" class="page_content">
<div class="header txtcenter">
<h1>Extraction de ventes</h1>
</div>
<div class="select_sales_dates txtcenter">
<form id="sales_form" action="javascript:;">
<div class="sales_date">
<label for="from">Entre :</label>
<input type="text" name="from" id="from" class="select_sales_date_input" required>
</div>
<div class="sales_date">
<label for="to">et :</label>
<input type="text" name="to" id="to" class="select_sales_date_input" required>
</div>
<button type="submit" class="btn--primary" id="dates_selection_button" disabled>C'est parti !</button>
</form>
</div>
<div class="main" style="display:none;">
<div class="table_area">
<table id="orders_table" class="display" cellspacing="0" width="100%"></table>
</div>
</div>
</div>
<div id="templates" style="display:none;">
</div>
</div>
<script src="{% static "js/all_common.js" %}?v="></script>
<script src="{% static "js/sales.js" %}?v="></script>
{% endblock %}
......@@ -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