Commit b90c7869 by Yvon

Merge branch 'dev_cooperatic' into supercafoutch-prod-20221003

parents 263c35e2 043e2a4b
Pipeline #2832 failed with stage
in 1 minute 6 seconds
......@@ -19,3 +19,4 @@ shop/errors.log
.idea
members/settings.json
.DS_Store
exec.*.log
......@@ -146,3 +146,6 @@ AMNISTIE_DATE= "2021-11-24 00:00:00"
# BDM Admin
BDM_SHOW_FTOP_BUTTON = True
# Entree
MINIMUM_SECONDS_BETWEEN_TWO_COMITEE_VALIDATION = 0
\ No newline at end of file
......@@ -93,5 +93,9 @@ VRAC_SHELFS = []
# Should block service exchange if old service is happening in less than 24h
BLOCK_SERVICE_EXCHANGE_24H_BEFORE = False
ALLOW_FOUR_DIGITS_IN_RECEPTION_PRICE = True
REMOVE_15_MINUTES_AT_SHIFT_END = False
# Fix incompatibility between meal voucher module and an old version of michel bibikoff product label printer software used in supercafoutch
DO_NOT_SHOW_MEAL_VOUCHER_OK_LINE_IN_PRODUCT_INFO_FOR_LABEL = True
......@@ -51,7 +51,8 @@ class CagetteInventory(models.Model):
with open(os.path.join(r, file)) as json_file:
file_data = json.load(json_file)
date_time = datetime.fromtimestamp(int(filename))
local_tz = pytz.timezone('Europe/Paris')
date_time = datetime.fromtimestamp(int(filename), local_tz)
d = date_time.strftime("%d/%m/%Y, %H:%M")
file_data['id'] = int(filename)
......
......@@ -18,6 +18,7 @@ function init_datatable() {
{data: "id", title:"id", "visible": false},
{
data:"datetime_created",
orderData:[0],
title:"Liste",
render: function (data) {
return "Liste du " + data;
......
......@@ -5,10 +5,11 @@ from outils.common import OdooAPI
from members.models import CagetteUser
from members.models import CagetteMembers
from members.models import CagetteMember
from members.models import CagetteServices
from shifts.models import CagetteServices
from shifts.models import CagetteShift
from members_space.models import CagetteMembersSpace
from outils.common import MConfig
from datetime import datetime
from datetime import datetime, date
default_msettings = {'msg_accueil': {'title': 'Message borne accueil',
'type': 'textarea',
......@@ -120,7 +121,7 @@ default_msettings = {'msg_accueil': {'title': 'Message borne accueil',
'sort_order': 16
},
'member_cant_have_delay_form_link': {
'title': 'Lien vers le formulaire pour les membres n\'ayant pas rattrapé leur service après 6 mois',
'title': 'Lien vers le formulaire pour les membres n\'ayant pas rattrapé leur service après la durée de l\'extension',
'type': 'text',
'value': '',
'class': 'link',
......@@ -320,8 +321,11 @@ def admin(request):
def manage_makeups(request):
""" Administration des membres """
template = loader.get_template('members/admin/manage_makeups.html')
m = CagetteMembersSpace()
context = {'title': 'BDM - Rattrapages',
'module': 'Membres'}
'module': 'Membres',
'extension_duration': m.get_extension_duration()
}
return HttpResponse(template.render(context, request))
def manage_shift_registrations(request):
......@@ -342,6 +346,8 @@ def manage_regular_shifts(request):
""" Administration des créneaux des membres """
template = loader.get_template('members/admin/manage_regular_shifts.html')
committees_shift_id = CagetteServices.get_committees_shift_id()
committees_shift_name = getattr(settings, 'COMMITTEES_SHIFT_NAME', "service des Comités")
exemptions_shift_id = CagetteServices.get_exemptions_shift_id()
context = {
'title': 'BDM - Créneaux',
'module': 'Membres',
......@@ -353,7 +359,9 @@ def manage_regular_shifts(request):
'show_ftop_button': getattr(settings, 'BDM_SHOW_FTOP_BUTTON', True),
'has_committe_shift': committees_shift_id is not None,
'committees_shift_id': committees_shift_id,
'ASSOCIATE_MEMBER_SHIFT' : getattr(settings, 'ASSOCIATE_MEMBER_SHIFT', '')
'committees_shift_name': committees_shift_name,
'ASSOCIATE_MEMBER_SHIFT' : getattr(settings, 'ASSOCIATE_MEMBER_SHIFT', ''),
'exemptions_shift_id': exemptions_shift_id,
}
return HttpResponse(template.render(context, request))
......@@ -400,7 +408,7 @@ def update_members_makeups(request):
points_update = points_diff
data = {
'name': "Modif manuelle des rattrapages depuis l'admin BDM",
'name': "Admin BDM - " + member_data["description"],
'shift_id': False,
'type': member_data["member_shift_type"],
'partner_id': int(member_data["member_id"]),
......@@ -408,13 +416,78 @@ def update_members_makeups(request):
}
cm.update_member_points(data)
# Better to call run_process_target_status now, otherwise partner remains
# in alert until routine is called (every 5 minutes). It is a bit weird for users and
# allocation of rattrapages before the routine is executed will not trigger change to delay state !
# (the parner would have to go back to espace membre and click on "j'affecte mes rattrapage"
# even though it shows 'J'ai 0 rattrapages à effecter' for the delay state change to be eventually triggered)
api = OdooAPI()
api.execute('res.partner', 'run_process_target_status', [])
response = JsonResponse(res)
else:
res["message"] = "Unauthorized"
response = JsonResponse(res, status=403)
return response
def regenerate_member_delay(request):
""" From BDM admin, close existing extension if exists & recreate for 6 months """
res = {}
is_connected_user = CagetteUser.are_credentials_ok(request)
if is_connected_user is True:
raw_data = json.loads(request.body.decode())
# Close extension if has one
member_id = int(raw_data["member_id"])
cm = CagetteMember(member_id)
cm.close_extension()
# Recreate starting now
cs = CagetteShift()
data = {
"idPartner": member_id,
"start_date": date.today().isoformat()
}
duration = raw_data["duration"]
ext_name = "Délai étendue depuis l'admin BDM"
res["create_delay"] = cs.create_delay(data=data, duration=duration, ext_name=ext_name)
if (res["create_delay"]):
try:
# Add 0 pt to counter so odoo updates member status
data = {
'name': "Forcer l'entrée en délai",
'shift_id': False,
'type': "standard",
'partner_id': member_id,
'point_qty': 0
}
cm.update_member_points(data)
data = {
'name': "Forcer l'entrée en délai",
'shift_id': False,
'type': "ftop",
'partner_id': member_id,
'point_qty': 0
}
cm.update_member_points(data)
res["force_entry_delay"] = True
except Exception as e:
print(str(e))
else:
coop_logger.error("regenerate_member_delay: %s, %s", str(res["create_delay"]), str(data))
return HttpResponseServerError()
res["member_data"] = CagetteMembers.get_makeups_members([member_id])[0]
response = JsonResponse(res, safe=False)
else:
res["message"] = "Unauthorized"
response = JsonResponse(res, status=403)
return response
# --- Gestion des créneaux
......@@ -427,15 +500,16 @@ def delete_shift_registration(request):
member_id = int(data["member_id"])
shift_registration_id = int(data["shift_registration_id"])
shift_is_makeup = data["shift_is_makeup"]
cancellation_description = data["cancellation_description"]
# Note: 'upcoming_registration_count' in res.partner won't change because the _compute method
# in odoo counts canceled shift registrations.
m = CagetteShift()
res["cancel_shift"] = m.cancel_shift([shift_registration_id], origin='bdm')
res["cancel_shift"] = m.cancel_shift([shift_registration_id], origin='bdm', description=cancellation_description)
if shift_is_makeup is True:
fields = {
'name': "Admin BDM - Suppression d'un rattrapage",
'name': "Admin BDM (annulation de rattrapage par une annulation de présence) - " + cancellation_description,
'shift_id': False,
'type': data["member_shift_type"],
'partner_id': member_id,
......@@ -508,15 +582,18 @@ def shift_subscription(request):
if shift_type == 1:
# 1 = standard
shift_template_id = int(data["shift_template_id"])
else:
elif shift_type == 2:
# 2 = ftop
# First try to get committees shift
shift_template_id = CagetteServices.get_committees_shift_id()
# If None, no committees shift, get the first ftop shift
if shift_template_id is None:
shift_template_id = CagetteServices.get_first_ftop_shift_id()
else:
# 3 = exempté
# Get exemptions shift
shift_template_id = CagetteServices.get_exemptions_shift_id()
m = CagetteMember(partner_id)
......@@ -535,9 +612,10 @@ def shift_subscription(request):
)
res["unsubscribe_member"] = m.unsubscribe_member(changing_shift = True)
m.create_coop_shift_subscription(shift_template_id, shift_type)
reg_id = m.create_coop_shift_subscription(shift_template_id, shift_type)
# Return necessary data
if reg_id is not None:
api = OdooAPI()
c = [['id', '=', shift_template_id]]
f = ['id', 'name']
......@@ -546,9 +624,11 @@ def shift_subscription(request):
c = [['id', '=', partner_id]]
f = ['cooperative_state']
res["cooperative_state"] = api.search_read('res.partner', c, f)[0]['cooperative_state']
coop_logger.info("Resultat shift_subscription : %s (données reçues = %s)", str(res), str(data))
response = JsonResponse(res)
else:
response = JsonResponse({"message": "Subscription failed"}, status=500)
else:
response = JsonResponse({"message": "Unauthorized"}, status=403)
return response
......
......@@ -317,7 +317,7 @@ class CagetteMember(models.Model):
[invoice_id])
return [invoice_id, invoice_line_id]
def create_coop_shift_subscription(self, shift_t_id, stype):
def create_coop_shift_subscription(self, shift_t_id, stype, call_nb=1):
"""Store coop shift subscription."""
# Get shift template ticket corresponding to given shift temp. id
sti = None
......@@ -345,7 +345,11 @@ class CagetteMember(models.Model):
}
st_r_id = self.o_api.create('shift.template.registration',
st_r_fields)
except:
except Exception as ex:
if 'seules les inscriptions ABCD sont possibles' in str(ex) and call_nb < 2:
return self.create_coop_shift_subscription(shift_t_id, 1, call_nb + 1)
else:
coop_logger.error("Error while creating shift.template.registration : %s, (fields =%s)", str(ex), str(st_r_fields))
st_r_id = None
return st_r_id
......@@ -724,6 +728,10 @@ class CagetteMember(models.Model):
fr_state = 'A jour'
elif coop_state == 'exempted':
fr_state = 'Exempté(e)'
elif coop_state == 'associated':
fr_state = 'En binôme'
elif coop_state == 'gone':
fr_state = 'Parti(e)'
else:
fr_state = 'Inconnu'
return fr_state
......@@ -793,8 +801,9 @@ class CagetteMember(models.Model):
cond.append(['is_associated_people', '=', True])
else:
cond.append(['is_associated_people', '=', False])
cond.append(['cooperative_state', '!=', 'associated'])
# cond.append(['cooperative_state', '!=', 'unsubscribed'])
if search_type == "full" or search_type == 'members':
if search_type == "full" or search_type == 'members' or search_type == "manage_shift_registrations":
fields = CagetteMember.m_default_fields
if not shift_id is None:
CagetteMember.m_default_fields.append('tmpl_reg_line_ids')
......@@ -840,7 +849,7 @@ class CagetteMember(models.Model):
elif search_type == "shift_template_data":
fields = CagetteMember.m_short_default_fields
fields = fields + ['id', 'makeups_to_do', 'cooperative_state']
fields = fields + ['id', 'makeups_to_do', 'cooperative_state','parent_name']
res = api.search_read('res.partner', cond, fields)
if res:
......@@ -853,6 +862,9 @@ class CagetteMember(models.Model):
partner['shift_template_id'] = shift_template_reg[0]['shift_template_id']
else:
partner['shift_template_id'] = None
if not partner['parent_name'] is False:
partner['name'] += ' (suppléant.e de son binôme ' + partner['parent_name'] + ')'
del partner['parent_name']
return res
else:
......@@ -1004,14 +1016,7 @@ class CagetteMember(models.Model):
if changing_shift is False:
# Close extensions if just unsubscribing, else keep it
c = [['partner_id', '=', self.id], ['date_start', '<=', now], ['date_stop', '>=', now]]
f = ['id']
res_ids = self.o_api.search_read("shift.extension", c, f)
ids = [d['id'] for d in res_ids]
if ids:
f = {'date_stop': now}
res["close_extensions"] = self.o_api.update('shift.extension', ids, f)
res["close_extensions"] = self.close_extension()
return res
......@@ -1032,6 +1037,20 @@ class CagetteMember(models.Model):
return res
def close_extension(self):
now = datetime.datetime.now().isoformat()
c = [['partner_id', '=', self.id], ['date_start', '<=', now], ['date_stop', '>=', now]]
f = ['id']
res_ids = self.o_api.search_read("shift.extension", c, f)
ids = [d['id'] for d in res_ids]
if ids:
f = {'date_stop': now}
return self.o_api.update('shift.extension', ids, f)
else:
return False
class CagetteMembers(models.Model):
"""Class to manage operations on all members or part of them."""
......@@ -1254,10 +1273,14 @@ class CagetteMembers(models.Model):
return res
@staticmethod
def get_makeups_members():
def get_makeups_members(ids=[]):
api = OdooAPI()
cond = [['makeups_to_do','>', 0]]
fields = ['id', 'name', 'display_std_points', 'display_ftop_points', 'shift_type', 'makeups_to_do']
if len(ids) > 0:
cond.append(['id','in', ids])
fields = ['id', 'name', 'display_std_points', 'display_ftop_points', 'shift_type', 'makeups_to_do', 'date_delay_stop']
res = api.search_read('res.partner', cond, fields)
return res
......@@ -1269,528 +1292,6 @@ class CagetteMembers(models.Model):
res = api.search_read('res.partner', cond, fields)
return res
class CagetteServices(models.Model):
"""Class to handle cagette Odoo services."""
@staticmethod
def get_all_shift_templates():
"""Return all recorded shift templates recorded in Odoo database."""
creneaux = {}
try:
api = OdooAPI()
f = ['name', 'week_number', 'start_datetime_tz', 'end_datetime_tz',
'seats_reserved', 'shift_type_id', 'seats_max',
'seats_available','registration_qty']
c = [['active', '=', True]]
shift_templates = api.search_read('shift.template', c, f)
# Get count of active registrations for each shift template
# shift_templates_active_count = api.execute('lacagette_shifts', 'get_active_shifts', [])
# With LGDS tests, seats_reserved reflects better what's shown in Odoo ...
title = re.compile(r"^(\w{1})(\w{3})\. - (\d{2}:\d{2}) ?-? ?(\w*)")
for l in shift_templates:
# nb_reserved = 0
# for stac in shift_templates_active_count:
# if stac['shift_template_id'] == l['id']:
# nb_reserved = stac['seats_active_registration']
line = {}
end = time.strptime(l['end_datetime_tz'], "%Y-%m-%d %H:%M:%S")
end_min = str(end.tm_min)
if end_min == '0':
end_min = '00'
line['end'] = str(end.tm_hour) + ':' + end_min
line['max'] = l['seats_max']
# line['reserved'] = nb_reserved
#line['reserved'] = l['seats_reserved']
line['reserved'] = l['registration_qty']
line['week'] = l['week_number']
line['id'] = l['id']
line['type'] = l['shift_type_id'][0]
t_elts = title.search(l['name'])
if t_elts:
line['day'] = t_elts.group(2)
line['begin'] = t_elts.group(3)
line['place'] = t_elts.group(4)
creneaux[str(l['id'])] = {'data': line}
except Exception as e:
coop_logger.error(str(e))
return creneaux
@staticmethod
def get_shift_templates_next_shift(id):
"""Retrieve next shift template shift."""
api = OdooAPI()
c = [['shift_template_id.id', '=', id],
['date_begin', '>=', datetime.datetime.now().isoformat()]]
f = ['date_begin']
# c = [['id','=',2149]]
shift = {}
res = api.search_read('shift.shift', c, f, 1, 0, 'date_begin ASC')
if (res and res[0]):
locale.setlocale(locale.LC_ALL, 'fr_FR.utf8')
local_tz = pytz.timezone('Europe/Paris')
date, t = res[0]['date_begin'].split(' ')
year, month, day = date.split('-')
start = datetime.datetime(int(year), int(month), int(day),
0, 0, 0, tzinfo=pytz.utc)
start_date = start.astimezone(local_tz)
shift['date_begin'] = start_date.strftime("%A %d %B %Y")
return shift
@staticmethod
def get_services_at_time(time, tz_offset, with_members=True):
"""Retrieve present services with members linked."""
default_acceptable_minutes_after_shift_begins = getattr(settings, 'ACCEPTABLE_ENTRANCE_MINUTES_AFTER_SHIFT_BEGINS', 15)
minutes_before_shift_starts_delay = getattr(settings, 'ACCEPTABLE_ENTRANCE_MINUTES_BEFORE_SHIFT', 15)
minutes_after_shift_starts_delay = default_acceptable_minutes_after_shift_begins
late_mode = getattr(settings, 'ENTRANCE_WITH_LATE_MODE', False)
max_duration = getattr(settings, 'MAX_DURATION', 180)
if late_mode is True:
minutes_after_shift_starts_delay = getattr(settings, 'ENTRANCE_VALIDATION_GRACE_DELAY', 60)
api = OdooAPI()
now = dateutil.parser.parse(time) - datetime.timedelta(minutes=tz_offset)
start1 = now + datetime.timedelta(minutes=minutes_before_shift_starts_delay)
start2 = now - datetime.timedelta(minutes=minutes_after_shift_starts_delay)
end = start1 + datetime.timedelta(minutes=max_duration)
cond = [['date_end_tz', '<=', end.isoformat()]]
cond.append('|')
cond.append(['date_begin_tz', '>=', start1.isoformat()])
cond.append(['date_begin_tz', '>=', start2.isoformat()])
fields = ['name', 'week_number', 'registration_ids',
'standard_registration_ids',
'shift_template_id', 'shift_ticket_ids',
'date_begin_tz', 'date_end_tz', 'state']
services = api.search_read('shift.shift', cond, fields,order ="date_begin_tz ASC")
for s in services:
if (len(s['registration_ids']) > 0):
if late_mode is True:
s['late'] = (
now.replace(tzinfo=None)
-
dateutil.parser.parse(s['date_begin_tz']).replace(tzinfo=None)
).total_seconds() / 60 > default_acceptable_minutes_after_shift_begins
if with_members is True:
cond = [['id', 'in', s['registration_ids']], ['state', 'not in', ['cancel', 'waiting', 'draft']]]
fields = ['partner_id', 'shift_type', 'state', 'is_late', 'associate_registered']
members = api.search_read('shift.registration', cond, fields)
s['members'] = sorted(members, key=lambda x: x['partner_id'][0])
if len(s['members']) > 0:
# search for associated people linked to these members
mids = []
for m in s['members']:
mids.append(m['partner_id'][0])
cond = [['parent_id', 'in', mids]]
fields = ['id', 'parent_id', 'name','barcode_base']
associated = api.search_read('res.partner', cond, fields)
if len(associated) > 0:
for m in s['members']:
for a in associated:
if int(a['parent_id'][0]) == int(m['partner_id'][0]):
m['partner_name'] = m['partner_id'][1]
m['partner_id'][1] += ' en binôme avec ' + a['name']
m['associate_name'] = str(a['barcode_base']) + ' - ' + a['name']
return services
@staticmethod
def registration_done(registration_id, overrided_date="", typeAction=""):
"""Equivalent to click present in presence form."""
api = OdooAPI()
f = {'state': 'done'}
if(typeAction != "normal" and typeAction != ""):
f['associate_registered'] = typeAction
if typeAction == "both":
f['should_increment_extra_shift_done'] = True
else:
f['should_increment_extra_shift_done'] = False
late_mode = getattr(settings, 'ENTRANCE_WITH_LATE_MODE', False)
if late_mode is True:
# services = CagetteServices.get_services_at_time('14:28',0, with_members=False)
if len(overrided_date) > 0 and getattr(settings, 'APP_ENV', "prod") != "prod":
now = overrided_date
else:
local_tz = pytz.timezone('Europe/Paris')
now = datetime.datetime.utcnow().replace(tzinfo=pytz.utc).astimezone(local_tz).strftime("%H:%MZ")
# coop_logger.info("Maintenant = %s (overrided %s) %s", now, overrided_date)
services = CagetteServices.get_services_at_time(now, 0, with_members=False)
if len(services) > 0:
# Notice : Despite is_late is defined as boolean in Odoo, 0 or 1 is needed for api call
is_late = 0
if services[0]['late'] is True:
is_late = 1
f['is_late'] = is_late
else:
return False
return api.update('shift.registration', [int(registration_id)], f)
@staticmethod
def reopen_registration(registration_id, overrided_date=""):
api = OdooAPI()
f = {'state': 'open'}
return api.update('shift.registration', [int(registration_id)], f)
@staticmethod
def record_rattrapage(mid, sid, stid, typeAction):
"""Add a shift registration for member mid.
(shift sid, shift ticket stid)
Once created, shift presence is confirmed.
"""
api = OdooAPI()
fields = {
"partner_id": mid,
"shift_id": sid,
"shift_ticket_id": stid,
"shift_type": "standard", # ou ftop -> voir condition
"related_shift_state": 'confirm',
"state": 'open'}
reg_id = api.create('shift.registration', fields)
f = {'state': 'done'}
if(typeAction != "normal" and typeAction != ""):
f['associate_registered'] = typeAction
if typeAction == "both":
f['should_increment_extra_shift_done'] = True
else:
f['should_increment_extra_shift_done'] = False
return api.update('shift.registration', [int(reg_id)], f)
@staticmethod
def record_absences(date):
"""Called by cron script."""
import dateutil.parser
if len(date) > 0:
now = dateutil.parser.parse(date)
else:
now = datetime.datetime.now()
# now = dateutil.parser.parse('2020-09-15T15:00:00Z')
date_24h_before = now - datetime.timedelta(hours=24)
# let authorized people time to set presence for those who came in late
end_date = now - datetime.timedelta(hours=2)
api = OdooAPI()
# Let's start by adding an extra shift to associated member who came together
cond = [['date_begin', '>=', date_24h_before.isoformat()],
['date_begin', '<=', end_date.isoformat()],
['state', '=', 'done'],
['associate_registered', '=', 'both'],
['should_increment_extra_shift_done', '=', True]]
fields = ['id', 'state', 'partner_id', 'date_begin']
res = api.search_read('shift.registration', cond, fields)
extra_shift_done_incremented_srids = [] # shift registration ids
for r in res:
cond = [['id', '=', r['partner_id'][0]]]
fields = ['id','extra_shift_done']
res_partner = api.search_read('res.partner', cond, fields)
f = {'extra_shift_done': res_partner[0]['extra_shift_done'] + 1 }
api.update('res.partner', [r['partner_id'][0]], f)
extra_shift_done_incremented_srids.append(int(r['id']))
# Make sure the counter isn't incremented twice
f = {'should_increment_extra_shift_done': False}
api.update('shift.registration', extra_shift_done_incremented_srids, f)
absence_status = 'excused'
res_c = api.search_read('ir.config_parameter',
[['key', '=', 'lacagette_membership.absence_status']],
['value'])
if len(res_c) == 1:
absence_status = res_c[0]['value']
cond = [['date_begin', '>=', date_24h_before.isoformat()],
['date_begin', '<=', end_date.isoformat()],
['state', '=', 'open']]
fields = ['state', 'partner_id', 'date_begin', 'shift_id']
res = api.search_read('shift.registration', cond, fields)
ids = []
partner_ids = []
excluded_partner = []
canceled_reg_ids = [] # for exempted people
shift_ids = []
for r in res:
partner_ids.append(int(r['partner_id'][0]))
shift_id = int(r['shift_id'][0])
if shift_id not in shift_ids:
shift_ids.append(shift_id)
cond = [['id', 'in', partner_ids],
['cooperative_state', 'in', ['exempted']]]
fields = ['id']
res_exempted = api.search_read('res.partner', cond, fields)
for r in res_exempted:
excluded_partner.append(int(r['id']))
for r in res:
if not (int(r['partner_id'][0]) in excluded_partner):
d_begin = r['date_begin']
(d, h) = d_begin.split(' ')
(_h, _m, _s) = h.split(':')
if int(_h) < 21:
ids.append(int(r['id']))
else:
canceled_reg_ids.append(int(r['id']))
# coop_logger.info("Traitement absences shift_registration ids %s", ids)
f = {'state': absence_status, 'date_closed': now.isoformat()}
update_shift_reg_result = {'update': api.update('shift.registration', ids, f), 'reg_shift': res, 'errors': []}
if update_shift_reg_result['update'] is True:
update_shift_reg_result['process_status_res'] = api.execute('res.partner','run_process_target_status', [])
# change shift state by triggering button_done method for all related shifts
if len(canceled_reg_ids) > 0:
f = {'state': 'cancel', 'date_closed': now.isoformat()}
api.update('shift.registration', canceled_reg_ids, f)
for sid in shift_ids:
try:
api.execute('shift.shift', 'button_done', sid)
except Exception as e:
marshal_none_error = 'cannot marshal None unless allow_none is enabled'
if not (marshal_none_error in str(e)):
update_shift_reg_result['errors'].append({'shift_id': sid, 'msg' :str(e)})
return update_shift_reg_result
@staticmethod
def close_ftop_service():
"""Called by cron script"""
# Retrieve the latest past FTOP service
import dateutil.parser
now = datetime.datetime.now()
# now = dateutil.parser.parse('2019-10-20T00:00:00Z')
cond = [['shift_type_id','=', 2],['date_end', '<=', now.isoformat()],['state','=', 'draft'], ['active', '=', True]]
fields = ['name']
api = OdooAPI()
res = api.search_read('shift.shift', cond, fields,order ="date_end ASC", limit=1)
# return res[0]['id']
result = {}
if res and len(res) > 0:
result['service_found'] = True
# Exceptions are due to the fact API returns None whereas the action is really done !...
marshal_none_error = 'cannot marshal None unless allow_none is enabled'
actual_errors = 0
try:
api.execute('shift.shift', 'button_confirm', [res[0]['id']])
except Exception as e:
if not (marshal_none_error in str(e)):
result['exeption_confirm'] = str(e)
actual_errors += 1
try:
api.execute('shift.shift', 'button_makeupok', [res[0]['id']])
except Exception as e:
if not (marshal_none_error in str(e)):
result['exeption_makeupok'] = str(e)
actual_errors += 1
try:
api.execute('shift.shift', 'button_done', [res[0]['id']])
except Exception as e:
if not (marshal_none_error in str(e)):
result['exeption_done'] = str(e)
actual_errors += 1
if actual_errors == 0:
result['done'] = True
else:
result['done'] = False
result['actual_errors'] = actual_errors
else:
result['service_found'] = False
return result
@staticmethod
def get_committees_shift_id():
shift_id = None
try:
api = OdooAPI()
res = api.search_read('ir.config_parameter',
[['key','=', 'lacagette_membership.committees_shift_id']],
['value'])
if len(res) > 0:
try:
shift_id = int(res[0]['value'])
except:
pass
except:
pass
return shift_id
@staticmethod
def get_first_ftop_shift_id():
shift_id = None
try:
api = OdooAPI()
res = api.search_read('shift.template',
[['shift_type_id','=', 2]],
['id', 'registration_qty'])
# Get the ftop shift template with the max registrations: most likely the one in use
ftop_shift = {'id': None, 'registration_qty': 0}
for shift_reg in res:
if shift_reg["registration_qty"] > ftop_shift["registration_qty"]:
ftop_shift = shift_reg
try:
shift_id = int(ftop_shift['id'])
except:
pass
except:
pass
return shift_id
@staticmethod
def easy_validate_shift_presence(coop_id):
"""Add a presence point if the request is valid."""
res = {}
try:
committees_shift_id = CagetteServices.get_committees_shift_id()
api = OdooAPI()
# let verify coop_id is corresponding to a ftop subscriber
cond = [['id', '=', coop_id]]
fields = ['tmpl_reg_line_ids']
coop = api.search_read('res.partner', cond, fields)
if coop:
if len(coop[0]['tmpl_reg_line_ids']) > 0 :
cond = [['id', '=', coop[0]['tmpl_reg_line_ids'][0]]]
fields = ['shift_template_id']
shift_templ_res = api.search_read('shift.template.registration.line', cond, fields)
if (len(shift_templ_res) > 0
and
shift_templ_res[0]['shift_template_id'][0] == committees_shift_id):
evt_name = getattr(settings, 'ENTRANCE_ADD_PT_EVENT_NAME', 'Validation service comité')
c = [['partner_id', '=', coop_id], ['name', '=', evt_name]]
f = ['create_date']
last_point_mvts = api.search_read('shift.counter.event', c, f,
order ="create_date DESC", limit=1)
ok_for_adding_pt = False
if len(last_point_mvts):
now = datetime.datetime.now()
past = datetime.datetime. strptime(last_point_mvts[0]['create_date'],
'%Y-%m-%d %H:%M:%S')
if (now - past).total_seconds() >= 3600 * 24:
ok_for_adding_pt = True
else:
ok_for_adding_pt = True
if ok_for_adding_pt is True:
res['evt_id'] = CagetteMember(coop_id).add_pts('ftop', 1, evt_name)
else:
res['error'] = "One point has been added less then 24 hours ago"
else:
res['error'] = "Unallowed coop"
else:
res['error'] = "Unregistred coop"
else:
res['error'] = "Invalid coop id"
except Exception as e:
coop_logger.error("easy_validate_shift_presence : %s %s", str(coop_id), str(e))
return res
class CagetteService(models.Model):
"""Class to handle cagette Odoo service."""
def __init__(self, id):
"""Init with odoo id."""
self.id = int(id)
self.o_api = OdooAPI()
def _process_associated_people_extra_shift_done(self):
cond = [['shift_id', '=', self.id],
['state', '=', 'done'],
['associate_registered', '=', 'both'],
['should_increment_extra_shift_done', '=', True]]
fields = ['id', 'state', 'partner_id', 'date_begin']
res = self.o_api.search_read('shift.registration', cond, fields)
extra_shift_done_incremented_srids = [] # shift registration ids
for r in res:
cond = [['id', '=', r['partner_id'][0]]]
fields = ['id','extra_shift_done']
res_partner = self.o_api.search_read('res.partner', cond, fields)
f = {'extra_shift_done': res_partner[0]['extra_shift_done'] + 1 }
self.o_api.update('res.partner', [r['partner_id'][0]], f)
extra_shift_done_incremented_srids.append(int(r['id']))
# Make sure the counter isn't incremented twice
f = {'should_increment_extra_shift_done': False}
self.o_api.update('shift.registration', extra_shift_done_incremented_srids, f)
def _process_related_shift_registrations(self):
now = datetime.datetime.now()
absence_status = 'excused'
res_c = self.o_api.search_read('ir.config_parameter',
[['key', '=', 'lacagette_membership.absence_status']],
['value'])
if len(res_c) == 1:
absence_status = res_c[0]['value']
cond = [['shift_id', '=', self.id],
['state', '=', 'open']]
fields = ['state', 'partner_id', 'date_begin']
res = self.o_api.search_read('shift.registration', cond, fields)
ids = []
partner_ids = []
excluded_partner = []
canceled_reg_ids = [] # for exempted people
for r in res:
partner_ids.append(int(r['partner_id'][0]))
cond = [['id', 'in', partner_ids],
['cooperative_state', 'in', ['exempted']]]
fields = ['id']
res_exempted = self.o_api.search_read('res.partner', cond, fields)
for r in res_exempted:
excluded_partner.append(int(r['id']))
for r in res:
if not (int(r['partner_id'][0]) in excluded_partner):
d_begin = r['date_begin']
(d, h) = d_begin.split(' ')
(_h, _m, _s) = h.split(':')
if int(_h) < 21:
ids.append(int(r['id']))
else:
canceled_reg_ids.append(int(r['id']))
# coop_logger.info("Traitement absences shift_registration ids %s", ids)
f = {'state': absence_status, 'date_closed': now.isoformat()}
update_shift_reg_result = {'update': self.o_api.update('shift.registration', ids, f), 'reg_shift': res, 'errors': []}
if update_shift_reg_result['update'] is True:
update_shift_reg_result['process_status_res'] = self.o_api.execute('res.partner','run_process_target_status', [])
# change shift state by triggering button_done method for all related shifts
if len(canceled_reg_ids) > 0:
f = {'state': 'cancel', 'date_closed': now.isoformat()}
self.o_api.update('shift.registration', canceled_reg_ids, f)
try:
self.o_api.execute('shift.shift', 'button_done', self.id)
except Exception as e:
marshal_none_error = 'cannot marshal None unless allow_none is enabled'
if not (marshal_none_error in str(e)):
update_shift_reg_result['errors'].append({'shift_id': self.id, 'msg' :str(e)})
return update_shift_reg_result
def record_absences(self, request):
"""Can only been executed if an Odoo user is beeing connected."""
res = {}
try:
if CagetteUser.are_credentials_ok(request) is True:
self._process_associated_people_extra_shift_done()
res = self._process_related_shift_registrations()
else:
res['error'] = 'Forbidden'
except Exception as e:
coop_logger.error("CagetteService.record_absences : %s %s", str(self.id), str(e))
res['error'] = str(e)
return res
class CagetteUser(models.Model):
@staticmethod
......
......@@ -68,3 +68,12 @@
.btn_possible_member {
margin: 0 1rem;
}
.member_name {
font-weight: bold;
}
.title_center {
display: flex;
justify-content: center;
}
\ No newline at end of file
......@@ -29,7 +29,7 @@ video {max-width:none;}
#webcam_button {min-width:115px; max-width:115px;}
#multi_results_preview button {margin:2px;}
#member_slide {grid-gap:0;padding:5px;display:none;}
#member_slide .coop-info {background: #fbfbd5;}
#member_slide .coop-info {background: #449d44;}
#image_medium {width:128px;float:left;}
#image_medium:hover {cursor: url(/static/img/ip-camera.png), url(/static/img/ip-camera.svg) 5 5, pointer;}
#barcode {height:128px;}
......
......@@ -284,6 +284,7 @@ function create_pair(payload) {
data.responseJSON.errors.map(function(error) {
message += ('\n' + error);
return null;
});
}
......
......@@ -56,7 +56,7 @@ function display_makeups_members() {
columns: [
{
data: "id",
title: '',
title: "",
className: "dt-body-center",
orderable: false,
render: function (data) {
......@@ -69,11 +69,30 @@ function display_makeups_members() {
title: "Nom"
},
{
data: "shift_type",
title: "Nb de points",
data: "id",
title: "",
className: "dt-body-center",
orderable: false,
width: "10%",
render: function (data, type, row) {
return `<button class="btn--primary extend_delay_button" data-member-id="${row.id}">Augmenter le délai</button>`;
}
},
{
data: "date_delay_stop",
title: "<div class='title_center'>Limite du délai</div>",
className: "dt-body-center",
width: "10%",
render: function (data) {
return (data === false) ? "Pas de délai en cours" : new Date(data).toLocaleDateString();
}
},
{
data: "shift_type",
title: "<div class='title_center'>Nb de points</div>",
className: "dt-body-center",
width: "5%",
render: function (data, type, row) {
if (data == 'ftop') {
return row.display_ftop_points;
} else if (data == 'standard') {
......@@ -85,7 +104,7 @@ function display_makeups_members() {
},
{
data: "makeups_to_do",
title: "Nb rattrapages",
title: "<div class='title_center'>Nb rattrapages</div>",
className: "dt-body-center",
width: "10%",
render: function (data, type, full) {
......@@ -150,10 +169,18 @@ function display_makeups_members() {
const member = makeups_members.find(m => m.id == member_id);
let modal_template = $("#modal_decr_makeup_counter");
modal_template.find(".member_name").text(member.name);
openModal(
`Enlever un rattrapage à ${member.name} ?`,
modal_template.html(),
() => {
update_members_makeups([member_id], "decrement");
update_members_makeups(
[member_id],
"decrement",
($("#decr-signature")[0].value || "auteur inconnu") + ' : ' + ($("#decr-explanation")[0].value || "pas d'explication")
);
},
"Confirmer",
false
......@@ -167,10 +194,18 @@ function display_makeups_members() {
const member = makeups_members.find(m => m.id == member_id);
let modal_template = $("#modal_incr_makeup_counter");
modal_template.find(".member_name").text(member.name);
openModal(
`Ajouter un rattrapage à ${member.name} ?`,
modal_template.html(),
() => {
update_members_makeups([member_id], "increment");
update_members_makeups(
[member_id],
"increment",
($("#incr-signature")[0].value || "auteur inconnu") + ' : ' + ($("#incr-explanation")[0].value || "pas d'explication")
);
},
"Confirmer",
false
......@@ -198,9 +233,13 @@ function display_makeups_members() {
if (first_select) {
$("#decrement_selected_members_makeups").on("click", () => {
openModal(
`Enlever un rattrapage aux membres sélectionnés ?`,
$("#modal_decr_selected_makeup_counter").html(),
() => {
update_members_makeups(selected_rows, "decrement");
update_members_makeups(
selected_rows,
"decrement",
($("#decr-signature-selected")[0].value || "auteur inconnu") + ' : ' + ($("#decr-explanation-selected")[0].value || "pas d'explication")
);
},
"Confirmer",
false
......@@ -212,6 +251,24 @@ function display_makeups_members() {
.hide();
}
});
$('#makeups_members_table').on('click', 'tbody td .extend_delay_button', function () {
const member_id = this.dataset.memberId;
const member = makeups_members.find(m => m.id == member_id);
let modal = $("#modal_extend_delay_template");
modal.find(".member_name").text(member.name);
openModal(
modal.html(),
() => {
extend_member_delay(member);
},
"Confirmer",
false
);
});
}
/**
......@@ -219,8 +276,9 @@ function display_makeups_members() {
*
* @param {Array} member_ids
* @param {String} action increment | decrement
* @param description
*/
function update_members_makeups(member_ids, action) {
function update_members_makeups(member_ids, action, description) {
openModal();
data = [];
......@@ -255,7 +313,8 @@ function update_members_makeups(member_ids, action) {
target_makeups_nb: makeups_members[member_index].makeups_to_do,
member_shift_type: makeups_members[member_index].shift_type,
display_ftop_points: makeups_members[member_index].display_ftop_points,
display_std_points: makeups_members[member_index].display_std_points
display_std_points: makeups_members[member_index].display_std_points,
description: description,
});
}
......@@ -276,10 +335,56 @@ function update_members_makeups(member_ids, action) {
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'members_admin');
report_JS_error(err, 'members_admin-manage_makeups');
closeModal();
alert('Erreur serveur pour décrémenter les rattrapages. Veuillez contacer le service informatique.');
}
});
}
/**
* Send request to extend the member's delay, or create one if none open.
* @param {Object} member
*/
function extend_member_delay(member) {
openModal();
let today = new Date();
let today_plus_extension_duration = new Date();
today_plus_extension_duration.setMonth(today_plus_extension_duration.getMonth()+ extension_duration);
const diff_time = Math.abs(today_plus_extension_duration - today);
const diff_days = Math.ceil(diff_time / (1000 * 60 * 60 * 24));
let data = {
member_id: member.id,
duration: diff_days
};
$.ajax({
type: 'POST',
url: "/members/admin/regenerate_member_delay",
data: JSON.stringify(data),
dataType:"json",
traditional: true,
contentType: "application/json; charset=utf-8",
success: function(result) {
let i = makeups_members.findIndex(m => m.id == result.member_data.id);
makeups_members[i].date_delay_stop = result.member_data.date_delay_stop;
display_makeups_members();
closeModal();
},
error: function(data) {
err = {msg: "erreur serveur pour créer un délai", ctx: 'extend_member_delay'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'members_admin-manage_makeups');
closeModal();
alert('Erreur serveur pour décrémenter les rattrapages. Ré-essayez plus tard.');
alert('Erreur serveur pour créer un délai. Veuillez contacer le service informatique.');
}
});
}
......@@ -331,10 +436,18 @@ function display_possible_members() {
display_ftop_points: member.display_ftop_points
});
let modal_template = $("#modal_incr_makeup_counter");
modal_template.find(".member_name").text(member.name);
openModal(
`Ajouter un rattrapage à ${member.name} ?`,
modal_template.html(),
() => {
update_members_makeups([member.id], "increment");
update_members_makeups(
[member.id],
"increment",
($("#incr-signature")[0].value || "auteur inconnu") + ' : ' + ($("#incr-explanation")[0].value || "pas d'explication")
);
members_search_results = [];
$('#search_member_input').val('');
$('.search_member_results_area').hide();
......
......@@ -37,7 +37,12 @@ function remove_from_shift_template() {
display_member_info();
closeModal();
},
error: function() {
error: function(err_data) {
if (err_data.status == 403) {
enqueue_message_for_next_loading("Problème d'autorisation. Merci de vous réidentifier.");
location.reload();
} else {
err = {
msg: "erreur serveur lors de la suppression du membre du créneau",
ctx: 'members.admin.manage_regular_shifts.remove_from_shift_template'
......@@ -50,6 +55,8 @@ function remove_from_shift_template() {
className: "error"
});
}
}
});
}
......@@ -57,7 +64,7 @@ function remove_from_shift_template() {
* Send the request to register a member to a shift template.
* Ask to unsuscribe first if the member was subscribed.
*
* @param {int} shift_type 1 === standard ; 2 === ftop
* @param {int} shift_type 1 === standard ; 2 === ftop ; 3 === exempté
* @param {int} shift_template_id null for ftop shift type
* @param {String} shift_template_name selected shift template name
*/
......@@ -115,6 +122,10 @@ function shift_subscrition(shift_type, shift_template_id = null, shift_template_
true,
false
);
} else if (err_data.status == 403) {
enqueue_message_for_next_loading("Problème d'autorisation. Merci de vous réidentifier.");
location.reload();
} else {
err = {
msg: "erreur serveur lors de l'inscription du membre au créneau",
......@@ -185,18 +196,15 @@ function display_member_info() {
* Set calendar and associated listeners.
*/
function set_subscription_area() {
retrieve_and_draw_shift_tempates();
retrieve_and_draw_shift_tempates({shift_listener: false});
$("#shifts_calendar_area").show();
// Wait for listeners to be set in common.js
// TODO use "signals" to avoid waiting an arbitrary time
setTimeout(() => {
// Cancel listeners from subscription page & set custom listeners
$("#shifts_calendar_area button[data-select='Volant']").off("click");
$("#shifts_calendar_area button[data-select='Volant']").on("click", function() {
$(document).off("click", "#shifts_calendar_area button[data-select='Volant']");
$(document).on("click", "#shifts_calendar_area button[data-select='Volant']", function() {
// Subscribe to comitee/ftop shift
msg = (has_committe_shift === "True")
? `Inscrire ${selected_member.name} au service des Comités ?`
? `Inscrire ${selected_member.name} au ${committees_shift_name} ?`
: `Inscrire ${selected_member.name} en Volant ?`;
openModal(
......@@ -209,8 +217,23 @@ function set_subscription_area() {
);
});
$(".shift").off("click");
$(".shift").on("click", function() {
$(document).off("click", "#shifts_calendar_area button[data-select='Exemption']");
$(document).on("click", "#shifts_calendar_area button[data-select='Exemption']", function() {
// Subscribe to comitee/ftop shift
msg = `Inscrire ${selected_member.name} en Éxempté ?`;
openModal(
msg,
() => {
shift_subscrition(3);
},
"Confirmer",
false
);
});
$(document).off("click", ".shift");
$(document).on("click", ".shift", function() {
// Subscribe to shift template
let shift_template_id = select_shift_among_compact(null, this, false); // method from common.js
let shift_template_data = shift_templates[shift_template_id].data;// shift_templates: var from common.js
......@@ -225,7 +248,7 @@ function set_subscription_area() {
false
);
});
}, 1000);
}
/**
......@@ -280,12 +303,12 @@ function display_possible_members() {
$(document).ready(function() {
if (coop_is_connected()) {
$.ajaxSetup({ headers: { "X-CSRFToken": getCookie('csrftoken') } });
dbc = new PouchDB(couchdb_dbname);
dbc = new PouchDB(couchdb_server);
$(".page_content").show();
if (has_committe_shift === "True") {
$("#shifts_calendar_area button[data-select='Volant']").text("Comités");
$("#shifts_calendar_area button[data-select='Volant']").text(committees_shift_name);
}
// Set action to search for the member
......
......@@ -102,16 +102,21 @@ function display_member_shifts() {
const shift_reg_id = row_data.id;
const shift_is_makeup = row_data.is_makeup;
let msg = `<p>Enlever la présence de <b>${member.name}</b> au service du <b>${row_data.shift_id[1]}</b> ?</p>`;
let modal_template = $("#modal_delete_shift_registration");
modal_template.find(".member_name").text(member.name);
modal_template.find(".service_name").text(row_data.shift_id[1]);
if (shift_is_makeup === true) {
msg += `<p><i class="fas fa-exclamation-triangle"></i> Ce service est un rattrapage. Le supprimer ajoutera un point au compteur de ce.tte membre.</p>`;
modal_template.find("#makeup_case_explanation").show();
}
openModal(
msg,
$("#modal_delete_shift_registration").html(),
() => {
delete_shift_registration(shift_reg_id, shift_is_makeup);
delete_shift_registration(
shift_reg_id,
shift_is_makeup,
($("#cancellation-explanation")[0].value || "pas d'explication") + ' : ' + ($("#cancellation-signature")[0].value || "auteur inconnu"),
);
},
"Confirmer",
false
......@@ -123,15 +128,17 @@ function display_member_shifts() {
* Send request to delete shift registration
* @param {Int} shift_reg_id Id of the shift_registration to delete
* @param {Boolean} shift_is_makeup Is the shift a makeup?
* @param {String} description explanation and signature for the cancellation from bdm
*/
function delete_shift_registration(shift_reg_id, shift_is_makeup) {
function delete_shift_registration(shift_reg_id, shift_is_makeup, description) {
openModal();
data = {
member_id: selected_member.id,
member_shift_type: selected_member.shift_type,
shift_registration_id: shift_reg_id,
shift_is_makeup: shift_is_makeup
shift_is_makeup: shift_is_makeup,
cancellation_description: description
};
$.ajax({
......@@ -231,7 +238,7 @@ $(document).ready(function() {
let search_str = $('#search_member_input').val();
$.ajax({
url: '/members/search/' + search_str,
url: `/members/search/${search_str}?search_type=manage_shift_registrations`,
dataType : 'json',
success: function(data) {
members_search_results = [];
......
......@@ -38,7 +38,7 @@ sync.on('change', function (info) {
});
if (need_reload == true) {
//On recharge depuis Odoo et on traite les enregistrements depuis CouchDB
retrieve_and_draw_shift_tempates('without_modal');
retrieve_and_draw_shift_tempates({without_modal: true, shift_listener: true});
}
}
......@@ -92,12 +92,12 @@ function new_coop_validation() {
coop_registration_details.find('.shift_template').text(st);
process_state.html(current_coop.firstname + ' ' +current_coop.lastname);
coop_registration_details.find("#parentName").text("")
coop_registration_details.find("#parent").attr("hidden", true)
coop_registration_details.find("#parentName").text("");
coop_registration_details.find("#parent").attr("hidden", true);
if (current_coop.parent_name !== undefined) {
coop_registration_details.find("#parentName").text(current_coop.parent_name)
coop_registration_details.find("#parent").removeAttr("hidden")
coop_registration_details.find("#parentName").text(current_coop.parent_name);
coop_registration_details.find("#parent").removeAttr("hidden");
}
if (current_coop.shift_template.data && current_coop.shift_template.data.id != ASSOCIATE_MEMBER_SHIFT) {
......@@ -126,7 +126,8 @@ function create_new_coop() {
$('.chosen_associate').html("");
$('.chosen_associate_area').hide();
$('.member_choice').removeClass('choice_active');
$(".remove_binome_icon").on("click", hide_chosen_associate)
$(".remove_binome_icon").on("click", hide_chosen_associate);
$('input[name="binome"]').prop('checked', false);
local_in_process = getLocalInProcess();
if (getLocalInProcess().length > 0) {
empty_waiting_local_processes();
......@@ -268,15 +269,35 @@ function store_new_coop(event) {
}
}
if ($('#associate_area').is(':visible')) {
// If user choose yes for binome, a type of association must be selected
let associated_data_selected = false;
if (
($(active_asso_area[0]).attr('id') === "new_member_choice")
||
($(active_asso_area[0]).attr('id') === "existing_member_choice")
) {
associated_data_selected = true;
}
if (associated_data_selected === false) errors.push("Un des deux choix concernant la mise en binôme doit être sélectionné");
}
if (active_asso_area.length > 0) {
// If user click as if a "binôme" is beeing created, data about parent member must exist
let associated_data_ok = false;
if (
($(active_asso_area[0]).attr('id') === "new_member_choice" && $('#new_member_input').val().trim().length > 0)
($(active_asso_area[0]).attr('id') === "new_member_choice" && $('#new_member_input').val()
.trim().length > 0)
||
($(active_asso_area[0]).attr('id') === "existing_member_choice" && $('#existing_member_choice_action .chosen_associate div.member').length > 0)
) {
associated_data_ok = true;
} else if ($(active_asso_area[0]).attr('id') === "") {
associated_data_ok = false;
errors.push("Un des deux choix doit être sélectionné");
}
if (associated_data_ok === false) errors.push("Le membre 'titulaire' du binôme n'est pas défini");
}
......@@ -361,6 +382,7 @@ function modify_current_coop() {
$('#new_member_choice_action').hide();
$('#existing_member_choice').addClass('choice_active');
var member_button = '<div>' + current_coop.parent_name + '</div>';
$('.chosen_associate').html(member_button);
$('.chosen_associate_area').show();
associated_old_choice = 'existing_member_choice';
......@@ -608,8 +630,7 @@ $('#coop_create').submit(store_new_coop);
$('#generate_email').click(generate_email);
$('#odoo_user_connect').click();
$('#add_binome').click(function() {
if ($('#associate_area').is(':visible')) {
$('#no_binome').click(function() {
$('#associate_area').hide();
$('#new_member_input').val('');
$('#associate_area .choice_active').removeClass("choice_active");
......@@ -620,13 +641,15 @@ $('#add_binome').click(function() {
delete current_coop.is_associated_people;
delete current_coop.shift_template;
}
} else {
});
$('#add_binome').click(function() {
$('#associate_area').show();
$('.member_choice').removeClass('choice_active');
$('#existing_member_choice_action').hide();
$('#new_member_choice_action').hide();
associated_old_choice = null;
}
});
$('.member_choice').on('click', function() {
......
......@@ -75,7 +75,7 @@ var reset_shift_process_actions_zone = function() {
current_shift_process_data_actions.off('click', 'a');
current_shift_process_data_actions.hide();
current_shift_process_data_actions.empty();
}
};
function fill_member_slide(member) {
no_pict_msg.hide();
......@@ -101,18 +101,22 @@ function fill_member_slide(member) {
}
html_elts.image_medium.html('<img src="'+img_src+'" width="128" />');
html_elts.cooperative_state.html(member.cooperative_state);
if (member.cooperative_state == 'Rattrapage') {
if (member.cooperative_state == 'Suspendu(e)') {
var explanation = "Tu as dû manquer un service! Pour pouvoir faire tes courses aujourd'hui, tu dois d'abord sélectionner un rattrapage sur ton espace membre.";
html_elts.status_explanation.html(explanation);
}
if (member.cooperative_state == 'Désinscrit(e)') coop_info.addClass('b_red');
else if (member.cooperative_state == 'En alerte' || member.cooperative_state == 'Délai accordé' || member.cooperative_state == 'Rattrapage') coop_info.addClass('b_orange');
if (member.cooperative_state == 'Désinscrit(e)' || member.cooperative_state == 'Parti(e)' || member.cooperative_state == 'Rattrapage' || member.cooperative_state == 'Suspendu(e)') {
coop_info.addClass('b_red');
}
else if (member.cooperative_state == 'En alerte' || member.cooperative_state == 'Délai accordé') {
coop_info.addClass('b_yellow')
}
if (member.shifts.length > 0) {
html_elts.next_shifts.append('Prochains services : ');
var slist = $('<ul>');
for (i in member.shifts) {
var s = $('<li>').text(member.shifts[i].start);
......@@ -330,12 +334,13 @@ function fill_service_entry(s) {
if (s.state == 'draft' || s.state == 'confirm') {
let btn = $('<a>').addClass('btn btn--primary txtcenter')
.text('Enregistrer les absences / présences')
.attr('id','record_shift_absences');
.attr('id', 'record_shift_absences');
current_shift_process_data_actions.append(btn);
current_shift_process_data_actions.on('click', '#record_shift_absences', function(){
current_shift_process_data_actions.on('click', '#record_shift_absences', function() {
msg = "<p>Lancer le traitement des présences et absences de ce service</p>";
openModal(msg, function() {
btn.attr('disabled', 'true')
btn.attr('disabled', 'true');
try {
$.ajax({
url: '/members/record_shift_absences/' + s.id,
......
......@@ -100,13 +100,11 @@ function display_current_coop_form() {
$('#checks').show();
for (var i = 1; i <= current_coop.checks_nb; i++) {
$(check_details).append('<p>Chèque #' + i +' : <input type="text" name="check_' + i + '" class="b_green check_item" required/> € </p>');
$(check_details).append('<p>Chèque #' + i + ' : <input type="text" name="check_' + i + '" class="b_green check_item" required/> € </p>');
}
}
}
var show_change_shift = false;
if (current_coop.shift_template) {
var st = current_coop.shift_template.data;
......@@ -121,25 +119,20 @@ function display_current_coop_form() {
place = 'Bureau';
}
form.find('[name="place"]').val(place);
if (current_coop.coop_msg) {
show_change_shift = true;
}
} else {
show_change_shift = true;
}
let show_change_shift = current_coop.validation_state === 'waiting_validation_employee';
if (show_change_shift == true) {
chgt_shift_btn.show();
chgt_shift_btn.on('click', open_shift_choice);
}
if (typeof(coop_page) != "undefined") {
if (typeof (coop_page) != "undefined") {
coop_page.show();
}
}
$('#payment_meaning').change(function() {
$('#payment_meaning').change(function () {
if ($(this).val() == 'ch') {
show_checks_nb();
} else {
......
......@@ -66,6 +66,7 @@ urlpatterns = [
url(r'^delete_shift_template_registration$', admin.delete_shift_template_registration),
url(r'^shift_subscription$', admin.shift_subscription),
url(r'^admin/manage_attached$', admin.manage_attached),
url(r'^admin/regenerate_member_delay$', admin.regenerate_member_delay),
url(r'^admin/manage_attached/create_pair$', admin.create_pair),
url(r'^admin/manage_attached/delete_pair$', admin.delete_pair),
url(r'^get_makeups_members$', admin.get_makeups_members),
......
......@@ -5,7 +5,7 @@ from outils.for_view_imports import *
from members.models import CagetteMember
from members.models import CagetteUser
from members.models import CagetteMembers
from members.models import CagetteServices, CagetteService
from shifts.models import CagetteServices, CagetteService
from outils.forms import GenericExportMonthForm
import datetime
......@@ -48,6 +48,15 @@ def index(request):
return HttpResponse("Le créneau des comités n'est pas configuré dans Odoo !")
else:
context['committees_shift_id'] = committees_shift_id
if getattr(settings, 'USE_EXEMPTIONS_SHIFT_TEMPLATE', False) is True:
exemptions_shift_id = CagetteServices.get_exemptions_shift_id()
if exemptions_shift_id is None:
return HttpResponse("Le créneau des exemptions n'est pas configuré dans Odoo !")
else:
context['exemptions_shift_id'] = exemptions_shift_id
else:
context['exemptions_shift_id'] = 0
if 'no_picture_member_advice' in msettings:
if len(msettings['no_picture_member_advice']['value']) > 0:
......@@ -88,6 +97,10 @@ def inscriptions(request, type=1):
template = loader.get_template('members/inscriptions.html')
committees_shift_id = CagetteServices.get_committees_shift_id()
if getattr(settings, 'USE_EXEMPTIONS_SHIFT_TEMPLATE', False) is True:
exemptions_shift_id = CagetteServices.get_exemptions_shift_id()
else:
exemptions_shift_id = 0
context = {
'type': type, 'title': 'Inscriptions',
'couchdb_server': settings.COUCHDB['url'],
......@@ -108,6 +121,7 @@ def inscriptions(request, type=1):
'can_create_binome': getattr(settings, 'CAN_CREATE_BINOME', True),
'prepa_odoo_url' : getattr(settings, 'PREPA_ODOO_URL', '/members/prepa-odoo'),
'committees_shift_id': committees_shift_id,
'exemptions_shift_id': exemptions_shift_id,
}
response = HttpResponse(template.render(context, request))
......@@ -124,6 +138,12 @@ def get_shift_templates_next_shift(request, id):
def prepa_odoo(request):
"""Generate coop subscription form, to be fill by BDM."""
template = loader.get_template('members/prepa_odoo.html')
committees_shift_id = CagetteServices.get_committees_shift_id()
if getattr(settings, 'USE_EXEMPTIONS_SHIFT_TEMPLATE', False) is True:
exemptions_shift_id = CagetteServices.get_exemptions_shift_id()
else:
exemptions_shift_id = 0
context = {'title': 'Préparation Odoo Inscriptions',
'warning_placeholder': 'Par exemple, il manque un chèque',
'couchdb_server': settings.COUCHDB['url'],
......@@ -137,7 +157,10 @@ def prepa_odoo(request):
'ask_for_street2': getattr(settings, 'SUBSCRIPTION_ADD_STREET2', False),
'ask_for_second_phone': getattr(settings, 'SUBSCRIPTION_ADD_SECOND_PHONE', False),
'show_ftop_button': getattr(settings, 'SHOW_FTOP_BUTTON', True),
'db': settings.COUCHDB['dbs']['member']}
'db': settings.COUCHDB['dbs']['member'],
'committees_shift_id': committees_shift_id,
'exemptions_shift_id': exemptions_shift_id,
}
# with_addr_complement
# with_second_phone
......@@ -290,7 +313,8 @@ def record_service_presence(request):
stid = int(request.POST.get("stid", 0)) # shift_ticket_id
cancel = request.POST.get("cancel") == 'true'
typeAction = str(request.POST.get("type"))
coop_logger.info("Enregistrement presence : mid = %s, rid = %s, sid = %s, stid = %s, cancel = %s, typeAction = %s",
str(mid), str(rid), str(sid), str(stid), str(cancel), typeAction)
app_env = getattr(settings, 'APP_ENV', "prod")
if (rid > -1 and mid > 0):
overrided_date = ""
......@@ -312,6 +336,7 @@ def record_service_presence(request):
res['update'] = 'ok'
else:
res['update'] = 'ko'
coop_logger.info("Résultat update record_service_presence : %s", res['update'])
if res['update'] == 'ok':
members = CagetteMember.search('id', mid)
m = members[0]
......@@ -327,6 +352,7 @@ def record_service_presence(request):
except Exception as e:
res['error'] = str(e)
coop_logger.error("Erreur record_service_presence : %s", str(e))
return JsonResponse({'res': res})
def easy_validate_shift_presence(request):
......
from django.db import models
from outils.common_imports import *
from members.models import CagetteServices
from shifts.models import CagetteServices
from outils.common import OdooAPI
......@@ -27,6 +27,13 @@ class CagetteMembersSpace(models.Model):
answer = True
return answer
def get_extension_duration(self):
"""Return nb of months"""
# TODO : add a unit parameter and convert if not month
extension_duration = OdooAPI().get_system_param('lacagette_membership.extension_duration')
nb, unit = extension_duration.split(' ')
return nb
def get_shifts_history(self, partner_id, limit, offset, date_from):
""" Get partner shifts history """
res = []
......
......@@ -87,6 +87,10 @@ body {
}
}
#comite_my_shifs_message {
min-width: 20em;
}
#home_incoming_services {
min-height: 80px;
display: flex;
......@@ -350,3 +354,11 @@ body {
.attached-unblocked {
display: none;
}
.block_service_exchange {
display: none;
}
.free_service_exchange {
display: none;
}
\ No newline at end of file
......@@ -18,6 +18,7 @@ function init_faq() {
$(".ask_bdm_form_link").prop("href", request_form_link);
display_messages_for_attached_people();
display_messages_for_service_exchange_24h_before();
}
$(document).on('click', "#shift_exchange_btn", () => {
......@@ -46,3 +47,5 @@ function display_messages_for_attached_people() {
$(".attached-blocked").show();
}
}
......@@ -4,8 +4,24 @@ function init_my_shifts_tile() {
if (incoming_shifts.length === 0) {
$("#home_tile_my_services #home_incoming_services").text("Aucun service à venir...");
} else {
$("#home_tile_my_services #home_incoming_services").empty();
if (partner_data.comite === "True") {
let message = $('#comite_my_shifs_message').clone()
message.find('[data-type="nb_of_shifs_state"] [data-type="shifts_nb"]').text(partner_data.final_ftop_point)
if (Math.abs(partner_data.final_ftop_point) > 1) {
message.find('[data-type="nb_of_shifs_state"] [data-type="service_txt"]').text("services")
}
// let's get next ftop shift (incoming_shifts is ordered)
if (incoming_shifts.length > 0) {
const next_shift = incoming_shifts[0]
let ns_date = new Date(next_shift.date_begin)
const date_options = {dateStyle: "short"}
message.find('[data-type="next_ftop_shift_date"]').text(ns_date.toLocaleDateString('fr-FR', date_options))
}
$("#home_tile_my_services .tile_content").html(message)
} else {
$("#home_tile_my_services #home_incoming_services").empty();
let cpt = 0;
for (shift of incoming_shifts) {
......@@ -20,12 +36,13 @@ function init_my_shifts_tile() {
}
}
}
}
}
function process_asked_shift_template_change(shift_t_id) {
var s_data = shift_templates[shift_t_id].data;
var shift_name = get_shift_name(s_data);
let msg = 'Inscription au créneau ' + shift_name
let msg = 'Inscription au créneau ' + shift_name;
openModal(
msg,
......@@ -94,8 +111,8 @@ function process_asked_shift_template_change(shift_t_id) {
);
}
function edit_shift_template_registration(){
let external = true;
function edit_shift_template_registration() {
const calendar_params = {external: true, without_modal: true, shift_listener: true};
if (calendar == null) calendar = $('#modal-calendar-choice').clone();
if ($('#modal-calendar-choice').html().length > 0) {
$('#modal-calendar-choice').empty();
......@@ -103,8 +120,11 @@ function edit_shift_template_registration(){
}
calendar.find('.oddeven_selector').empty();
displayMsg(calendar.html());
$('#week_types').find('input').change(()=>{filter_weeks(external)});
retrieve_and_draw_shift_tempates(external);
$('#week_types').find('input')
.change(() => {
filter_weeks(calendar_params);
});
retrieve_and_draw_shift_tempates(calendar_params);
}
......@@ -127,7 +147,8 @@ function init_home() {
});
$(".member_shift_name_area").on("click", ".fa-edit", (e) => {
$('#week_types').find('input').change(filter_weeks);
$('#week_types').find('input')
.change(filter_weeks);
e.preventDefault();
edit_shift_template_registration();
});
......@@ -153,4 +174,6 @@ function init_home() {
load_partner_shifts(partner_data.concerned_partner_id)
.then(init_my_shifts_tile);
}
display_messages_for_service_exchange_24h_before();
}
\ No newline at end of file
......@@ -177,7 +177,22 @@ function init_history() {
function init_incoming_shifts() {
$(".loading-incoming-shifts").hide();
$("#incoming_shifts").show();
if (partner_data.comite === "True") {
let message = $('#comite_my_shifs_message').clone()
message.find('[data-type="nb_of_shifs_state"] [data-type="shifts_nb"]').text(partner_data.final_ftop_point)
if (Math.abs(partner_data.final_ftop_point) > 1) {
message.find('[data-type="nb_of_shifs_state"] [data-type="service_txt"]').text("services")
}
// let's get next ftop shift (incoming_shifts is ordered)
if (incoming_shifts.length > 0) {
const next_shift = incoming_shifts[0]
let ns_date = new Date(next_shift.date_begin)
const date_options = {dateStyle: "short"}
message.find('[data-type="next_ftop_shift_date"]').text(ns_date.toLocaleDateString('fr-FR', date_options))
}
$("#incoming_shifts_area").html(message)
} else {
if (incoming_shifts.length === 0) {
$("#incoming_shifts").text("Aucun service à venir...");
} else {
......@@ -211,6 +226,7 @@ function init_incoming_shifts() {
$("#incoming_shifts").append(shift_line_template.html());
}
}
}
}
function init_my_shifts() {
......
......@@ -119,12 +119,16 @@ function add_or_change_shift(new_shift_id) {
error: function(error) {
closeModal();
selected_shift = null;
if (error.status === 400 && 'msg' in error.responseJSON && error.responseJSON.msg === "Old service in less than 24hours.") {
alert(`Désolé ! Le service que tu souhaites échanger démarre dans moins de 24h. ` +
`Afin de faciliter la logistique des services, il n'est plus possible de l'échanger. ` +
`Si tu ne peux vraiment pas venir, tu seras noté.e absent.e à ton service. ` +
`Tu devras alors sélectionner un service de rattrapage sur ton espace membre.`);
} else if (error.status === 400 && 'msg' in error.responseJSON && error.responseJSON.msg === "Not allowed to change shift") {
alert(`Désolé ! Le service que tu souhaites échanger démarre dans trop peu de temps. ` +
`Afin de faciliter la logistique des services, il n'est plus possible de l'échanger. ` +
`Si tu ne peux vraiment pas venir, tu seras noté.e absent.e à ton service. ` +
`Tu devras alors sélectionner un service de rattrapage sur ton espace membre.`);
} else if (error.status === 500 && 'msg' in error.responseJSON && error.responseJSON.msg === "Fail to create shift") {
// TODO differentiate error cases!
alert(`Une erreur est survenue. ` +
......@@ -459,7 +463,7 @@ function init_shifts_list() {
*/
function init_calendar_page() {
let template_explanations = $("#calendar_explaination_template");
let event_src = '/shifts/get_list_shift_calendar/' + partner_data.concerned_partner_id;
if (vw <= 992) {
$(".loading-calendar").show();
......@@ -551,7 +555,7 @@ function init_calendar_page() {
contentHeight: "auto",
eventDisplay: "block",
hiddenDays: hidden_days,
events: '/shifts/get_list_shift_calendar/' + partner_data.concerned_partner_id,
events: event_src,
eventClick: function(info) {
if (!$(info.el).hasClass("shift_booked") && !$(info.el).hasClass("shift_booked_makeup")) {
const new_shift_id = info.event.id;
......@@ -594,6 +598,7 @@ function init_calendar_page() {
} else {
// Display modal
let modal_template = $("#modal_add_shift_template");
modal_template.find(".date_new_shift").text(new_shift_date);
modal_template.find(".time_new_shift").text(new_shift_time);
openModal(
......@@ -607,12 +612,12 @@ function init_calendar_page() {
} else if (should_select_makeup()) {
/* choose a makeup service */
// Check if selected new shift is in less than 6 months
// Check if selected new shift is in less than extension end
if (partner_data.date_delay_stop !== 'False') {
date_partner_delay_stop = new Date(partner_data.date_delay_stop);
if (datetime_new_shift > date_partner_delay_stop) {
let msg = `Vous avez jusqu'au ${date_partner_delay_stop.toLocaleDateString("fr-fr", date_options)} ` +
`pour sélectionner un rattrapage (soit une période de 6 mois depuis votre absence).`;
`pour sélectionner un rattrapage (soit une période de ${extension_duration} mois depuis votre absence).`;
alert(msg);
......@@ -650,7 +655,7 @@ function init_calendar_page() {
calendar.render();
}
function init_read_only_calendar_page() {
async function init_read_only_calendar_page() {
let template_explanations = $("#calendar_explaination_template");
if (vw <= 992) {
......@@ -674,8 +679,8 @@ function init_read_only_calendar_page() {
if (incoming_shifts !== null) {
init_shifts_list();
} else {
load_partner_shifts(partner_data.concerned_partner_id)
.then(init_shifts_list);
await load_partner_shifts(partner_data.concerned_partner_id)
init_shifts_list();
}
if (should_select_makeup()) {
......@@ -712,7 +717,16 @@ function init_read_only_calendar_page() {
const hidden_days = days_to_hide.length > 0 ? $.map(days_to_hide.split(", "), Number) : [];
const calendarEl = document.getElementById('read_only_calendar');
let event_src = '/shifts/get_list_shift_calendar/' + partner_data.concerned_partner_id;
if (partner_data.comite === "True") {
let next_evts = []
if (incoming_shifts.length > 0) {
incoming_shifts.forEach((s) => {
next_evts.push({id: s.id, title: 'Prélèvement 1 point', allDay: true, start: s.date_begin})
});
}
event_src = next_evts
}
calendar = new FullCalendar.Calendar(calendarEl, {
locale: 'fr',
initialView: default_initial_view,
......@@ -728,7 +742,7 @@ function init_read_only_calendar_page() {
contentHeight: "auto",
eventDisplay: "block",
hiddenDays: hidden_days,
events: '/shifts/get_list_shift_calendar/' + partner_data.concerned_partner_id,
events: event_src,
eventDidMount: function() {
// Calendar is hidden at first on mobile to hide header change when data is loaded
$(".loading-calendar").hide();
......@@ -752,7 +766,8 @@ function init_delete_registration_buttons() {
if (partner_data.extra_shift_done > 0) {
$(".delete_registration_button").on("click", function() {
let shift_name = $(this).closest("div")
.parent().parent()
.parent()
.parent()
.find(".shift_line_date")
.text()
.trim();
......
......@@ -139,10 +139,10 @@ function request_delay() {
const delay_start = today.getFullYear()+'-'+(today.getMonth()+1)+'-'+today.getDate();
let today_plus_six_month = new Date();
let today_plus_extension_duration = new Date();
today_plus_six_month.setMonth(today_plus_six_month.getMonth()+6);
const diff_time = Math.abs(today_plus_six_month - today);
today_plus_extension_duration.setMonth(today_plus_extension_duration.getMonth()+ extension_duration);
const diff_time = Math.abs(today_plus_extension_duration - today);
const diff_days = Math.ceil(diff_time / (1000 * 60 * 60 * 24));
$.ajax({
......@@ -157,7 +157,7 @@ function request_delay() {
},
success: function() {
partner_data.cooperative_state = 'delay';
partner_data.date_delay_stop = today_plus_six_month.getFullYear()+'-'+(today_plus_six_month.getMonth()+1)+'-'+today_plus_six_month.getDate();
partner_data.date_delay_stop = today_plus_extension_duration.getFullYear()+'-'+(today_plus_extension_duration.getMonth()+1)+'-'+today_plus_extension_duration.getDate();
resolve();
},
......@@ -314,6 +314,7 @@ $(document).ready(function() {
// For associated people, their parent name is attached in their display name
let partner_name_split = partner_data.name.split(', ');
partner_data.name = partner_name_split[partner_name_split.length - 1];
base_location = (app_env === 'dev') ? '/members_space/' : '/';
......@@ -354,3 +355,11 @@ $(document).ready(function() {
};
})(jQuery, 'smartresize');
function display_messages_for_service_exchange_24h_before() {
if (block_service_exchange_24h_before === "False") {
$(".free_service_exchange").show();
} else {
$(".block_service_exchange").show();
}
}
......@@ -29,7 +29,9 @@ def index(request, exception=None):
context = {
'title': 'Espace Membre',
'COMPANY_LOGO': getattr(settings, 'COMPANY_LOGO', None),
'block_actions_for_attached_people' : getattr(settings, 'BLOCK_ACTIONS_FOR_ATTACHED_PEOPLE', True)
'block_actions_for_attached_people' : getattr(settings, 'BLOCK_ACTIONS_FOR_ATTACHED_PEOPLE', True),
'permanent_message': getattr(settings, 'PERMANENT_MESSAGE_BELOW_CONNECTION_FIELDS', None),
'block_service_exchange_24h_before' : getattr(settings, 'BLOCK_SERVICE_EXCHANGE_24H_BEFORE', True),
}
template = loader.get_template('members_space/index.html')
......@@ -119,6 +121,7 @@ def index(request, exception=None):
partnerData["associated_partner_name"] = str(associated_partner["barcode_base"]) + ' - ' + partnerData["associated_partner_name"]
m = CagetteMembersSpace()
context['extension_duration'] = m.get_extension_duration()
context['show_faq'] = getattr(settings, 'MEMBERS_SPACE_FAQ_TEMPLATE', 'members_space/faq.html')
context['show_abcd_calendar'] = getattr(settings, 'SHOW_ABCD_CALENDAR_TAB', True)
partnerData["comite"] = m.is_comite(partner_id)
......@@ -159,6 +162,14 @@ def index(request, exception=None):
context['helper_unsubscribe_form_link'] = msettings['helper_unsubscribe_form_link']['value'] if 'helper_unsubscribe_form_link' in msettings else ''
context['covid_form_link'] = msettings['covid_form_link']['value'] if 'covid_form_link' in msettings else ''
context['covid_end_form_link'] = msettings['covid_end_form_link']['value'] if 'covid_end_form_link' in msettings else ''
if getattr(settings, 'USE_EXEMPTIONS_SHIFT_TEMPLATE', False) is True:
exemptions_shift_id = CagetteServices.get_exemptions_shift_id()
if exemptions_shift_id is None:
return HttpResponse("Le créneau des exemptions n'est pas configuré dans Odoo !")
else:
context['exemptions_shift_id'] = exemptions_shift_id
else:
context['exemptions_shift_id'] = 0
else:
# may arrive when switching database without cleaning cookie
return redirect('/website/deconnect')
......@@ -182,6 +193,9 @@ def home(request):
partnerData = cs.get_data_partner(partner_id)
if partnerData['cooperative_state'] == "unsubscribed":
coop_can_change_shift_template = False
if getattr(settings, 'ASSOCIATE_PEOPLE_CAN_CHANGE_SHIFT_TEMPLE_REGISTRATION', False) is False:
if partnerData['is_associated_people'] is True:
coop_can_change_shift_template = False
context = {
'title': 'Espace Membres',
'coop_can_change_shift_template': coop_can_change_shift_template,
......@@ -216,9 +230,11 @@ def my_shifts(request):
def shifts_exchange(request):
""" Endpoint the front-end will call to load the "Shifts exchange" page. """
template = loader.get_template('members_space/shifts_exchange.html')
m = CagetteMembersSpace()
context = {
'title': 'Échange de Services',
'canAddShift': getattr(settings, 'CAN_ADD_SHIFT', False)
'canAddShift': getattr(settings, 'CAN_ADD_SHIFT', False),
'extension_duration': m.get_extension_duration()
}
return HttpResponse(template.render(context, request))
......
......@@ -48,7 +48,9 @@ class Order(models.Model):
return result
def get_lines(self, forExport=False, withNullQty=False):
lines_data = {'lines': None, 'used_coeffs': None}
f = ['id', 'product_id', 'package_qty', 'product_qty_package', 'product_qty', 'product_uom', 'price_unit', 'partner_id']
if forExport is True:
f += ['discount', 'price_subtotal', 'price_tax', 'taxes_id']
......@@ -67,12 +69,15 @@ class Order(models.Model):
f = ['barcode', 'product_tmpl_id', 'shelf_id']
if forExport is False: # i.e for reception
f += ['taxes_id', 'standard_price']
coeff = self.get_coop_main_coeff()
# add the 9 product coeffs
for i in range(1,10):
f.append('coeff' + str(i) + '_id')
c = [['id', 'in', pids]]
res_bc = self.o_api.search_read('product.product', c, f)
tmpl_ids = []
if res_bc:
taxes = {}
taxes = {} # Needed to add tax coeff for each product
res_tax = self.get_taxes_data_for_lines(res_bc)
if res_tax:
for tax in res_tax:
......@@ -92,21 +97,26 @@ class Order(models.Model):
if len(shelf_ids) > 0:
shelfs_sortorder = Shelfs.get_shelfs_sortorder(shelf_ids)
found_coeffs_ids = [] # Extract from all products, to make a unique query after loop
for l in res_bc:
for p in res:
if p['product_id'][0] == l['id']:
# coop_logger.info(str(l))
p['shelf_sortorder'] = 'X'
p['barcode'] = l['barcode']
p['product_tmpl_id'] = l['product_tmpl_id'][0]
if ('standard_price' in l):
p['p_price'] = l['standard_price']
p_coeff = None
try:
tax_coeff = (1 + (float(taxes[str(l['taxes_id'][0])]))/100)
p_coeff = coeff * tax_coeff
except Exception as e:
coop_logger.warning('order get_lines : %s', str(e))
p['p_coeff'] = p_coeff
# Let's add product coeff order and id (needed to compute sale price in further operations : new_shelf_price for ex.)
for i in range(1,10):
if l['coeff' + str(i) + '_id'] is not False:
coeff_id = l['coeff' + str(i) + '_id'][0]
p['coeff' + str(i) + '_id'] = coeff_id
if coeff_id not in found_coeffs_ids:
found_coeffs_ids.append(coeff_id)
p['tax_coeff'] = (1 + (float(taxes[str(l['taxes_id'][0])]))/100)
if l['shelf_id'] is not False:
for s in shelfs_sortorder:
......@@ -116,6 +126,7 @@ class Order(models.Model):
p['shelf_sortorder'] = 'X'
tmpl_ids.append(l['product_tmpl_id'][0])
used_coeffs = self.o_api.search_read('product.coefficient', [['id', 'in', found_coeffs_ids]], ['operation_type', 'value'])
# Adding indicative_package for every product
f = ['indicative_package','product_tmpl_id','product_code']
......@@ -133,8 +144,9 @@ class Order(models.Model):
except Exception as e:
# if product is not active, it is not included in res_bc result
p['active'] = False
return res
lines_data['lines'] = res
lines_data['used_coeffs'] = used_coeffs
return lines_data
def get_taxes_data_for_lines(self, lines):
taxes_id = []
......@@ -156,7 +168,8 @@ class Order(models.Model):
c = [['id', '=', self.id]]
order = self.o_api.search_read('purchase.order', c, f)
if order:
lines = self.get_lines(forExport=True)
lines_data = self.get_lines(forExport=True)
lines = lines_data['lines']
res['taxes'] = self.get_taxes_data_for_lines(lines)
res['order'] = order[0]
res['lines'] = lines
......@@ -214,7 +227,8 @@ class Order(models.Model):
import re
fixed_prefix = getattr(settings, 'FIXED_BARCODE_PREFIX', '0490')
labels_data = {'total': 0, 'details': []}
lines = self.get_lines()
lines_data = self.get_lines()
lines = lines_data['lines']
bc_pattern = re.compile('^' + fixed_prefix)
for l in lines:
if ('barcode' in l) and not (bc_pattern.match(str(l['barcode'])) is None):
......@@ -341,7 +355,8 @@ class Orders(models.Model):
try:
fixed_prefix = getattr(settings, 'FIXED_BARCODE_PREFIX', '0490')
bc_pattern = re.compile('^' + fixed_prefix)
for l in Orders.get_lines(oids):
lines_data = Orders.get_lines(oids)
for l in lines_data['lines']:
if not (bc_pattern.match(str(l['barcode'])) is None):
if not (l['product_tmpl_id'] in labels_data):
labels_data[l['product_tmpl_id']] = 0
......@@ -351,6 +366,22 @@ class Orders(models.Model):
return labels_data
@staticmethod
def get_orders_between_dates(date_from, date_to):
"""@depends on Odoo module lacagette_purchase"""
o_api = OdooAPI()
params = {'date_from': date_from, 'date_to': date_to}
res = {}
try:
res = o_api.execute('purchase.order', 'get_received_orders_between_dates', [], params)
except Exception as e:
res["error"] = str(e)
coop_logger.error('get_orders_between_dates : %s', str(e))
return res
class CagetteSuppliers(models.Model):
@staticmethod
......
......@@ -138,7 +138,7 @@ function debounceFunction(func, delay = 1000) {
*/
function handle_unauthorize() {
alert("La session a expiré. Vous allez devoir vous reconnecter.");
$( "#logout" ).trigger( "click" );
$("#logout").trigger("click");
}
/* - PRODUCTS */
......@@ -284,9 +284,13 @@ function compute_purchase_qty_for_coverage(product, coeff, stock, incoming_qty,
purchase_package_qty_for_coverage = 1;
} else {
purchase_qty_for_coverage = days * daily_conso - stock - incoming_qty + product.minimal_stock;
purchase_qty_for_coverage = days * daily_conso - stock - incoming_qty;
purchase_qty_for_coverage = (purchase_qty_for_coverage < 0) ? 0 : purchase_qty_for_coverage;
if (purchase_qty_for_coverage + stock + incoming_qty < product.minimal_stock) {
purchase_qty_for_coverage = product.minimal_stock - stock - incoming_qty;
}
// Reduce to nb of packages to purchase
purchase_package_qty_for_coverage = purchase_qty_for_coverage / product.suppliersinfo[0].package_qty;
......@@ -314,6 +318,7 @@ function compute_and_affect_product_supplier_quantities(coeff, days) {
// Set qty to purchase for supplier with higher priority
let target_supplierinfo_index = 0;
let min_sequence = Number.POSITIVE_INFINITY; // min sequence = higher priority
for (let i in products[key].suppliersinfo) {
let suppliersinfo_sequence = products[key].suppliersinfo[i].sequence;
......@@ -343,7 +348,9 @@ function compute_products_coverage_qties() {
order_doc.coeff = coeff;
if (order_doc.coverage_days != null) {
compute_and_affect_product_supplier_quantities(coeff, order_doc.coverage_days);
} else if (order_doc.targeted_amount != null) {
}
if (order_doc.targeted_amount != null) {
const small_step = 0.1,
max_iter = 182; // Assume that no more than 1/2 year coverage is far enough
let go_on = true,
......
......@@ -20,7 +20,7 @@ def helper(request):
'title': 'Aide à la commande',
'couchdb_server': settings.COUCHDB['url'],
'db': settings.COUCHDB['dbs']['orders'],
'odoo_server': settings.ODOO['url'],
'odoo_server': getattr(settings, 'ODOO_PUBLIC_URL', settings.ODOO['url']),
'metabase_url': getattr(settings, 'ORDERS_HELPER_METABASE_URL', ''),
'nb_past_days_to_compute_sales_average': OdooAPI().get_system_param('lacagette_products.nb_past_days_to_compute_sales_average'),
'nb_of_consecutive_non_sale_days_considered_as_break': OdooAPI().get_system_param('lacagette_products.nb_of_consecutive_non_sale_days_considered_as_break')
......
......@@ -133,7 +133,7 @@
- CAN_CREATE_BINOME = True (by default)
If set to False, in new member creation form, a member can be selected to be associated with.
If set to True, in new member creation form, a member can be selected to be associated with.
- ASSOCIATE_MEMBER_SHIFT = ''
......@@ -346,6 +346,17 @@
By default, False. Set if coop can or not add shifts in their memberspace calendar
- ASSOCIATE_PEOPLE_CAN_CHANGE_SHIFT_TEMPLE_REGISTRATION = True
By default, False.
- EXTENSION_TYPE_ID = 6
By default 1. Extension type id (from shift_extension_type table) used to create extension
- COMMITTEES_SHIFT_NAME = 'Coolisses'
By default "service des Comités"
### Reception
- RECEPTION_ADD_ADMIN_MODE = True
......@@ -442,6 +453,17 @@
True by default
- PERMANENT_MESSAGE_BELOW_CONNECTION_FIELDS = "Si vous avez des difficultés à vous connecter, ...."
Default is None
- STANDARD_BLOCK_SERVICE_EXCHANGE_DELAY = 36 (default = 24)
Define duration, in hours, before shift starts within exchange is not more available, for standard shift_type member
- REMOVE_15_MINUTES_AT_SHIFT_END = False
True by default. Remove 15 minutes to Odoo shift end (https://redmine.cooperatic.fr/issues/1680)
### BDM Admin
- BDM_SHOW_FTOP_BUTTON = True (by default)
......
.green {color:#60B000;}
.b_green, .b_more_than_50pc {background:#d9ebd2}
.b_green, .b_more_than_50pc {background:#d9ebd2 !important;}
.orange {color:#FFA500;}
.b_orange, .b_less_than_50pc {background:#F0B000 !important;}
.yellow {color:#F0B000;}
.b_yellow {background: #fcf3cc;}
.b_yellow {background: #fcf3cc !important;}
.red {color:#FF0000;}
.b_red, .b_less_than_25pc {background:#ff3333 !important;}
.loading {background-image: url("/static/img/ajax-loader.gif"); background-repeat:no-repeat; background-position: center; background-color: #efefef;}
......
......@@ -21,6 +21,7 @@ var volant = null;
function get_displayed_weeks() {
displayed_weeks = [];
$('#week_types').find('.selected_weeks :checked').each(function() {
......@@ -181,8 +182,11 @@ function draw_table(begin_hours, callback) {
}
function draw_shift_templates(external) {
if (typeof external !== "undefined" && external == true) shift_table = $('#shift_choice table');
function draw_shift_templates(params) {
if (params && typeof params.external !== "undefined" && params.external == true){
// Simplified calendar loaded in modal (members_space for ex.)
shift_table = $('#shift_choice table');
}
var existing_shifts = shift_table.find('.shift');
existing_shifts.off("click", single_shift_click);
......@@ -224,6 +228,8 @@ function draw_shift_templates(external) {
});
draw_table(begin_hours, function() {
const is_inscription_page = $('body').hasClass('inscriptions');
$.each(shift_templates, function(i, e) {
if (e.data) {
var keep_it = false;
......@@ -291,8 +297,10 @@ function draw_shift_templates(external) {
});
if (type == 1) {
if (!params || (typeof params.shift_listener !== "undefined" && params.shift_listener == true) || is_inscription_page == true){
shift_table.find('.shift').on("click", single_shift_click);
}
}
if (type == 2) {
for (k in boxes) {
var k_elts = k.split("_");
......@@ -315,13 +323,15 @@ function draw_shift_templates(external) {
}
}
if (!params || (typeof params.shift_listener !== "undefined" && params.shift_listener == true) || is_inscription_page == true){
shift_table.find('.shift').on("click", select_shift_among_compact);
}
}
sc_lat.find('.info').html(dispo + ' places disponibles<br />(/'+max+')');
if (typeof external == "undefined") {
if (!params || typeof params.without_modal === "undefined" || (typeof params.without_modal !== "undefined" && params.without_modal == false)) {
closeModal();
}
});
......@@ -329,13 +339,12 @@ function draw_shift_templates(external) {
}
function retrieve_and_draw_shift_tempates(external) {
function retrieve_and_draw_shift_tempates(params) {
if (shift_table.length == 0) shift_table = $('#shift_choice table');
if (!external) {
if (!params || typeof params.without_modal === "undefined" || (typeof params.without_modal !== "undefined" && params.without_modal == false)) {
openModal();
}
shift_table.find('.shift').remove();
$.ajax({url : st_url,
dataType :'json'
......@@ -374,10 +383,10 @@ function retrieve_and_draw_shift_tempates(external) {
}
});
draw_shift_templates(external);
draw_shift_templates(params);
});
} else {
draw_shift_templates(external);
draw_shift_templates(params);
}
......@@ -385,7 +394,7 @@ function retrieve_and_draw_shift_tempates(external) {
});
}
function filter_weeks(external) {
function filter_weeks(params) {
var clicked = $(this);
var week_types = $('#week_types');
var parent_div = clicked.closest('div');
......@@ -430,7 +439,7 @@ function filter_weeks(external) {
if (!w2.is(':checked') || !w4.is(':checked')) {
$('#odd_weeks').prop('checked', false);
}
draw_shift_templates(external);
draw_shift_templates(params);
}
function shift_loc_selection() {
......@@ -438,14 +447,21 @@ function shift_loc_selection() {
st_loc_buttons.removeClass('highlighted');
clicked.addClass('highlighted');
if (clicked.data('select') != 'Volant') {
if (clicked.data('select') !== 'Volant' && clicked.data('select') !== 'Exemption') {
retrieve_and_draw_shift_tempates();
} else {
} else if (clicked.data('select') === 'Volant') {
//shift_templates[volant] is not always set (when call from bdm interface)
if (typeof volant !== "undefined" && typeof shift_templates[volant] !== "undefined") {
subscribe_shift(volant);
}
} else if (clicked.data('select') === 'Exemption') {
subscribe_shift(exemptions_shift_id);
}
}
st_loc_buttons.click(shift_loc_selection);
week_types.find('input').change(filter_weeks);
week_types.find('input').change(() => {
filter_weeks({shift_listener: true});
});
......@@ -60,6 +60,11 @@
.replace( /ã/g, 'a' )
.replace( /õ/g, 'o' )
.replace( /ç/g, 'c' )
.replace( /â/g, 'a' )
.replace( /à/g, 'a' )
.replace( /ù/g, 'u' )
.replace( /ï/g, 'i' )
.replace( /œ/g, 'oe' )
.replace( /ì/g, 'i' );
}
......
......@@ -20,6 +20,7 @@ from . import monitor
from .views import FieldsView
from .views import ExportCompta
from .views import ExportPOS
from .views import ExportOrders
urlpatterns = [
......@@ -31,6 +32,7 @@ urlpatterns = [
url(r'^entity/example$', views.entity_example, name='entity example'),
url(r'^export_compta$', ExportCompta.as_view(), name='export_compta'),
url(r'^export_pos$', ExportPOS.as_view(), name='Export POS'),
url(r'^export_orders$', ExportOrders.as_view(), name='export_orders'),
url(r'^monitor/$', monitor.index),
url(r'^monitor/js_errors$', monitor.js_errors),
url(r'^members/', include('members.urls')),
......
......@@ -15,6 +15,7 @@ from .forms import ExportComptaForm
from outils.lib.compta import *
from openpyxl import Workbook
from openpyxl.writer.excel import save_virtual_workbook
from orders.models import Orders
def test_compta(request):
......@@ -194,9 +195,11 @@ class ExportPOS(View):
'CHQ': 0,
'CB_DEJ': 0,
'CHQ_DEJ': 0,
'MonA': 0,
'TOTAL': 0}
sub_total = 0
cb = chq = csh = cbd = chqd = 0
cb = chq = csh = cbd = chqd = mona = 0
coop_logger.info("payments : %s", s['payments'])
for p in s['payments']:
# p['name'] is a sequence generated string
# Test order is important as CHEQDEJ contains CHEQ for ex.
......@@ -212,28 +215,32 @@ class ExportPOS(View):
cbd = sub_amount
elif 'CB' in p['name']:
cb = sub_amount
elif 'MonA' in p['name'] or 'MonA' in p['journal']:
mona = sub_amount
sub_total += sub_amount
totals[key]['CB'] += cb
totals[key]['CSH'] += csh
totals[key]['CHQ'] += chq
totals[key]['CB_DEJ'] += cbd
totals[key]['CHQ_DEJ'] += chqd
totals[key]['MonA'] += mona
totals[key]['TOTAL'] += round(sub_total, 2)
details_lines.append([mois, s['mm_dates']['min'], s['mm_dates']['min'], s['caisse'], s['name'],
cb, csh, chq, cbd, chqd, sub_total])
details_lines.append([mois, s['mm_dates']['min'], s['mm_dates']['max'], s['caisse'], s['name'],
cb, csh, chq, cbd, chqd, mona, sub_total])
wb = Workbook()
ws1 = wb.create_sheet("Totaux " + mois, 0)
ws2 = wb.create_sheet("Détails " + mois, 1)
ws1.append(['date', 'CB', 'CSH', 'CHQ', 'CB_DEJ', 'CHQ_DEJ', 'Total'])
ws1.append(['date', 'CB', 'CSH', 'CHQ', 'CB_DEJ', 'CHQ_DEJ', 'MonA', 'Total'])
for day in sorted(totals):
cb = totals[day]['CB']
csh = totals[day]['CSH']
chq = totals[day]['CHQ']
cbd = totals[day]['CB_DEJ']
chqd = totals[day]['CHQ_DEJ']
mona = totals[day]['MonA']
total = totals[day]['TOTAL']
ws1.append([day, cb, csh, chq, cbd, chqd, total])
ws2.append(['mois', 'min_date', 'max_date', 'Caisse', 'session', 'CB', 'CSH','CHQ', 'CB_DEJ', 'CHQ_DEJ', 'total'])
ws1.append([day, cb, csh, chq, cbd, chqd, mona, total])
ws2.append(['mois', 'min_date', 'max_date', 'Caisse', 'session', 'CB', 'CSH','CHQ', 'CB_DEJ', 'CHQ_DEJ', 'MonA', 'total'])
for row in details_lines:
ws2.append(row)
wb_name = 'export_sessions__' + mois + '.xlsx'
......@@ -375,3 +382,56 @@ class ExportPOS(View):
if not (month is None):
response = self.__ca_sessions_ng(month)
return response
class ExportOrders(View):
def get(self, request, *args, **kwargs):
u"""Display form"""
template = loader.get_template('outils/export_orders.html')
context = {'title': 'Export Commandes Réceptionnées'}
return HttpResponse(template.render(context, request))
def post(self, request, *args, **kwargs):
u"""Generate orders export between two dates"""
date_from = request.POST.get('from')
date_to = request.POST.get('to')
orders = Orders.get_orders_between_dates(date_from, date_to)
if "error" in orders:
error = "Une erreur est survenue, merci de contacter le service informatique."
return JsonResponse({'erreur': error, 'details': orders["error"]})
try:
wb = Workbook()
ws1 = wb.active
ws1.title = "Commandes réceptionnées"
ws1.append(['Fournisseur', 'Réf commande', 'Statut', 'Montant HT', 'Montant Total', 'Date réception'])
for order in orders["data"]:
supplier_name = order['supplier_name']
id_po = order['id_po']
amount_untaxed = order['amount_untaxed']
amount_total = order['amount_total']
if order['state'] == "purchase":
state = "Commande fournisseur"
elif order['state'] == "done":
state = "Terminé"
else:
state = order['state']
date_done_obj = datetime.datetime.strptime(order['date_done'], '%Y-%m-%d %H:%M:%S')
date_done = date_done_obj.strftime("%d/%m/%Y")
ws1.append([supplier_name, id_po, state, amount_untaxed, amount_total, date_done])
wb_name = 'export_orders_' + date_from + '_' + date_to + '.xlsx'
response = HttpResponse(content=save_virtual_workbook(wb),
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
response['Content-Disposition'] = 'attachment; filename=' + wb_name
return response
except Exception as e:
error = "Une erreur est survenue, merci de contacter le service informatique."
coop_logger.error("Erreur export_orders : %s", str(e))
return JsonResponse({'erreur': error, 'details': str(e)})
{
"name": "third-party",
"version": "1.0.0",
"lockfileVersion": 1,
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "third-party",
"version": "1.0.0",
"devDependencies": {
"eslint": "^7.24.0"
}
},
"node_modules/@babel/code-frame": {
"version": "7.12.11",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz",
"integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==",
"dev": true,
"dependencies": {
"@babel/highlight": "^7.10.4"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.12.11",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
"dev": true
},
"node_modules/@babel/highlight": {
"version": "7.13.10",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz",
"integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.12.11",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
}
},
"node_modules/@babel/highlight/node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@eslint/eslintrc": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.0.tgz",
"integrity": "sha512-2ZPCc+uNbjV5ERJr+aKSPRwZgKd2z11x0EgLvb1PURmUrn9QNRXFqje0Ldq454PfAVyaJYyrDvvIKSFP4NnBog==",
"dev": true,
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.1.1",
"espree": "^7.3.0",
"globals": "^12.1.0",
"ignore": "^4.0.6",
"import-fresh": "^3.2.1",
"js-yaml": "^3.13.1",
"minimatch": "^3.0.4",
"strip-json-comments": "^3.1.1"
},
"engines": {
"node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/@eslint/eslintrc/node_modules/globals": {
"version": "12.4.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz",
"integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==",
"dev": true,
"dependencies": {
"type-fest": "^0.8.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-jsx": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz",
"integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
"dev": true,
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ansi-colors": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
"integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/astral-regex": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chalk/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/chalk/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/chalk/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/chalk/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/chalk/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dev": true,
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"dev": true,
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/deep-is": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
"dev": true
},
"node_modules/doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
"dev": true,
"dependencies": {
"esutils": "^2.0.2"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
"node_modules/enquirer": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
"integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==",
"dev": true,
"dependencies": {
"ansi-colors": "^4.1.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"dev": true,
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/eslint": {
"version": "7.24.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.24.0.tgz",
"integrity": "sha512-k9gaHeHiFmGCDQ2rEfvULlSLruz6tgfA8DEn+rY9/oYPFFTlz55mM/Q/Rij1b2Y42jwZiK3lXvNTw6w6TXzcKQ==",
"dev": true,
"dependencies": {
"@babel/code-frame": "7.12.11",
"@eslint/eslintrc": "^0.4.0",
"ajv": "^6.10.0",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
"debug": "^4.0.1",
"doctrine": "^3.0.0",
"enquirer": "^2.3.5",
"eslint-scope": "^5.1.1",
"eslint-utils": "^2.1.0",
"eslint-visitor-keys": "^2.0.0",
"espree": "^7.3.1",
"esquery": "^1.4.0",
"esutils": "^2.0.2",
"file-entry-cache": "^6.0.1",
"functional-red-black-tree": "^1.0.1",
"glob-parent": "^5.0.0",
"globals": "^13.6.0",
"ignore": "^4.0.6",
"import-fresh": "^3.0.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"js-yaml": "^3.13.1",
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
"lodash": "^4.17.21",
"minimatch": "^3.0.4",
"natural-compare": "^1.4.0",
"optionator": "^0.9.1",
"progress": "^2.0.0",
"regexpp": "^3.1.0",
"semver": "^7.2.1",
"strip-ansi": "^6.0.0",
"strip-json-comments": "^3.1.0",
"table": "^6.0.4",
"text-table": "^0.2.0",
"v8-compile-cache": "^2.0.3"
},
"bin": {
"eslint": "bin/eslint.js"
},
"engines": {
"node": "^10.12.0 || >=12.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-scope": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
"dev": true,
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^4.1.1"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/eslint-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
"integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
"dev": true,
"dependencies": {
"eslint-visitor-keys": "^1.1.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/mysticatea"
}
},
"node_modules/eslint-utils/node_modules/eslint-visitor-keys": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
"integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/eslint-visitor-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz",
"integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/espree": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
"integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==",
"dev": true,
"dependencies": {
"acorn": "^7.4.0",
"acorn-jsx": "^5.3.1",
"eslint-visitor-keys": "^1.3.0"
},
"engines": {
"node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/espree/node_modules/eslint-visitor-keys": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
"integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true,
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/esquery": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
"integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
"dev": true,
"dependencies": {
"estraverse": "^5.1.0"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/esquery/node_modules/estraverse": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
"integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
"dev": true,
"engines": {
"node": ">=4.0"
}
},
"node_modules/esrecurse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
"dependencies": {
"estraverse": "^5.2.0"
},
"engines": {
"node": ">=4.0"
}
},
"node_modules/esrecurse/node_modules/estraverse": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
"integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
"dev": true,
"engines": {
"node": ">=4.0"
}
},
"node_modules/estraverse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"dev": true,
"engines": {
"node": ">=4.0"
}
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true
},
"node_modules/fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
"dev": true
},
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
"integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
"dev": true,
"dependencies": {
"flat-cache": "^3.0.4"
},
"engines": {
"node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/flat-cache": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
"integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
"dev": true,
"dependencies": {
"flatted": "^3.1.0",
"rimraf": "^3.0.2"
},
"engines": {
"node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/flatted": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz",
"integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==",
"dev": true
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
},
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
"node_modules/functional-red-black-tree": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
"dev": true
},
"node_modules/get-intrinsic": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/globals": {
"version": "13.8.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.8.0.tgz",
"integrity": "sha512-rHtdA6+PDBIjeEvA91rpqzEvk/k3/i7EeNQiryiWuJH0Hw9cpyJMAt2jtbAwUaRdhD+573X4vWw6IcjKPasi9Q==",
"dev": true,
"dependencies": {
"type-fest": "^0.20.2"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/globals/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/has-symbols": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/ignore": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
"dev": true,
"engines": {
"node": ">= 4"
}
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
"dev": true,
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
"dev": true,
"engines": {
"node": ">=0.8.19"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"node_modules/is-boolean-object": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz",
"integrity": "sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/is-glob": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
"dev": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number-object": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz",
"integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-string": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz",
"integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
"dev": true
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
},
"node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"dev": true,
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
"dev": true
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
"dev": true,
"dependencies": {
"prelude-ls": "^1.2.1",
"type-check": "~0.4.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
"dev": true
},
"node_modules/lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=",
"dev": true
},
"node_modules/lodash.truncate": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
"integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=",
"dev": true
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
"dev": true
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"dependencies": {
"wrappy": "1"
}
},
"node_modules/optionator": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
"integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
"dev": true,
"dependencies": {
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
"levn": "^0.4.1",
"prelude-ls": "^1.2.1",
"type-check": "^0.4.0",
"word-wrap": "^1.2.3"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"dependencies": {
"callsites": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true,
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/regexpp": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz",
"integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==",
"dev": true,
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/mysticatea"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dev": true,
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/slice-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
"integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
"is-fullwidth-code-point": "^3.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/slice-ansi/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/slice-ansi/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/slice-ansi/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
"dev": true
},
"node_modules/string-width": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
"integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true,
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/table": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/table/-/table-6.1.0.tgz",
"integrity": "sha512-T4G5KMmqIk6X87gLKWyU5exPpTjLjY5KyrFWaIjv3SvgaIUGXV7UEzGEnZJdTA38/yUS6f9PlKezQ0bYXG3iIQ==",
"dev": true,
"dependencies": {
"ajv": "^8.0.1",
"is-boolean-object": "^1.1.0",
"is-number-object": "^1.0.4",
"is-string": "^1.0.5",
"lodash.clonedeep": "^4.5.0",
"lodash.flatten": "^4.4.0",
"lodash.truncate": "^4.4.2",
"slice-ansi": "^4.0.0",
"string-width": "^4.2.0"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/table/node_modules/ajv": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.1.0.tgz",
"integrity": "sha512-B/Sk2Ix7A36fs/ZkuGLIR86EdjbgR6fsAcbx9lOP/QBSXujDNbVmIS/U4Itz5k8fPFDeVZl/zQ/gJW4Jrq6XjQ==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/table/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
"dev": true
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
"dependencies": {
"prelude-ls": "^1.2.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/type-fest": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
"dependencies": {
"punycode": "^2.1.0"
}
},
"node_modules/v8-compile-cache": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
"dev": true
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
}
},
"dependencies": {
"@babel/code-frame": {
"version": "7.12.11",
......@@ -81,7 +1443,8 @@
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz",
"integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
"dev": true
"dev": true,
"requires": {}
},
"ajv": {
"version": "6.12.6",
......
......@@ -93,6 +93,8 @@ class CagetteProduct(models.Model):
v = round(float(price) / float(product['weight_net']), 2)
if k == 'price_volume' and len(v) > 0 and len(price) > 0 and float(price) > 0:
v = round(float(price) / float(product['volume']), 2)
if directory != "/product_labels/" or (directory == "/product_labels/" and k != "meal_voucher_ok"):
# add parameter to text unless it's for a product label and parameter is meal_voucher_ok
txt += k + '=' + str(v).strip() + "\r\n"
if not (nb is None) and len(nb) > 0:
txt += 'nb_impression=' + str(nb) + "\r\n"
......
......@@ -74,9 +74,9 @@ class CagetteReception(models.Model):
def implies_scale_file_generation(self):
answer = False
lines = Order(self.id).get_lines()
bc_pattern = re.compile('^0493|0499')
for l in lines:
lines_data = Order(self.id).get_lines()
bc_pattern = re.compile('^0493|0499') # TODO : Adjust for other pattern (such as Supercoop)
for l in lines_data['lines']:
if not (bc_pattern.match(str(l['barcode'])) is None):
answer = True
# print ('answer=' + str(answer))
......@@ -131,7 +131,8 @@ class CagetteReception(models.Model):
"""
import json
processed_lines = 0
order_lines = CagetteReception.get_order_lines_by_po(self.id, nullQty=True)
order_lines_data = CagetteReception.get_order_lines_by_po(self.id, nullQty=True)
order_lines = order_lines_data['lines']
received_products = {}
for p in order_lines:
received_products[p['product_id'][0]] = p['product_qty']
......@@ -176,7 +177,8 @@ class CagetteReception(models.Model):
def update_products_price(self):
processed = 0
errors = []
order_lines = CagetteReception.get_order_lines_by_po(self.id)
order_lines_data = CagetteReception.get_order_lines_by_po(self.id)
order_lines = order_lines_data['lines']
if order_lines and len(order_lines) > 0:
# Exceptions are due to the fact API returns None whereas the action is really done !...
marshal_none_error = 'cannot marshal None unless allow_none is enabled'
......@@ -194,7 +196,32 @@ class CagetteReception(models.Model):
success = True
else:
success = False
return {'errors': errors, 'processed': processed, 'success': success}
return {'errors': errors, 'processed': processed, 'success': success, 'lines': order_lines}
def print_shelf_labels_for_updated_prices(self, lines):
import requests
# don't print barcode which begin with these codes
noprint_list = ["0493", "0492", "0499"]
pids = []
for l in lines:
pids.append(l['product_id'][0])
products_to_print = self.o_api.search_read('product.product', [['id','in', pids]], ['product_tmpl_id', 'barcode', 'to_print'])
if products_to_print:
to_reset = []
for p_to_print in products_to_print:
coop_logger.info('candidate to print %s', str(p_to_print))
if p_to_print['to_print'] is True and p_to_print['barcode'][:4] not in noprint_list:
try:
tools_url = settings.TOOLS_SERVER + '/products/label_print/'
tools_url += str(p_to_print['product_tmpl_id'][0])
requests.get(tools_url)
to_reset.append(p_to_print['id'])
except Exception as e:
coop_logger.error("Shelf label printing : %s",str(e))
if len(to_reset) > 0:
self.o_api.update('product.product', to_reset, {'to_print': 0})
def finalyze_picking(self):
......@@ -234,6 +261,8 @@ class CagetteReception(models.Model):
new_x_reception_status += '/error_uprice'
if new_x_reception_status == '':
new_x_reception_status = 'done'
self.print_shelf_labels_for_updated_prices(price_update['lines'])
if result != 'already done':
self.o_api.update('purchase.order', [self.id], {'x_reception_status': new_x_reception_status})
......@@ -275,6 +304,7 @@ class CagetteReception(models.Model):
print_label = m.implies_scale_file_generation()
if fp == 'processed' or fp == 'already done':
os.remove(p['file'])
processed.append({p['id']: fp})
os.remove('data/po_in_process_' + str(p['id']))
if print_label is True:
......
......@@ -221,11 +221,19 @@ div#container_edition {
}
tr.even td.row_product_no_qty {
background-color: #f0ad4e; /*#ec971f*/
background-color: #F2B969 !important;
}
tr.odd td.row_product_no_qty {
background-color: rgb(236, 182, 106); /*#ec971f*/
background-color: #f0ad4e !important;
}
tr.even td.row_product_qty_changed {
background-color: #ffc0b7 !important;
}
tr.odd td.row_product_qty_changed {
background-color: #FFB7AD !important;
}
.add_products_button_container {
......
......@@ -15,7 +15,8 @@ Sémantiquement, ici :
* If 1 element: single order
*/
var orders = {},
group_ids = [];
group_ids = [],
product_coeffs = [];
var reception_status = null,
list_to_process = [],
......@@ -267,6 +268,13 @@ function resetPopUpButtons() {
/* FETCH SERVER DATA */
function store_received_product_coeffs(coeffs) {
for (let i=0; i<coeffs.length; i++) {
if (product_coeffs.indexOf(coeffs[i]) == -1)
product_coeffs.push(coeffs[i]);
}
}
/**
* Get order(s) data from server
* @param {Array} po_ids if set, fetch data for these po only
......@@ -285,6 +293,7 @@ function fetch_data(po_ids = null) {
success: function(data) {
// for each order
for (order_data of data.orders) {
store_received_product_coeffs(order_data.used_coeffs);
// for each product in order
for (i in order_data.po) {
// If in step 2, find old qty in previous step data
......@@ -695,6 +704,17 @@ function initLists() {
$(cell_node).addClass('row_product_no_qty');
}
} else if (
row_data !== undefined
&& row_data.product_qty !== 0
&& 'old_qty' in row_data
&& row_data.old_qty != row_data.product_qty
) {
for (var j = 0; j < row.cells.length; j++) {
const cell_node = row.cells[j];
$(cell_node).addClass('row_product_qty_changed');
}
}
}
});
......@@ -726,6 +746,17 @@ function initLists() {
$(cell_node).addClass('row_product_no_qty');
}
} else if (
row_data !== undefined
&& row_data.product_qty !== 0
&& 'old_qty' in row_data
&& row_data.old_qty != row_data.product_qty
) {
for (var j = 0; j < row.cells.length; j++) {
const cell_node = row.cells[j];
$(cell_node).addClass('row_product_qty_changed');
}
}
}
});
......@@ -1182,7 +1213,7 @@ function setLineEdition(product) {
} else {
document.getElementById('product_uom').innerHTML = ' / unité';
$('#edition_input').attr('type', 'number')
.attr('step', 0.01)
.attr('step', (allow_four_digits_in_reception_price == "True" ? 0.0001 : 0.01))
.attr('max', 9999);
}
} else if (editing_product.product_uom[0] == 21) { // kg
......@@ -1194,7 +1225,7 @@ function setLineEdition(product) {
} else {
document.getElementById('product_uom').innerHTML = ' / kg';
$('#edition_input').attr('type', 'number')
.attr('step', 0.01)
.attr('step', (allow_four_digits_in_reception_price == "True" ? 0.0001 : 0.01))
.attr('max', 9999);
}
}
......@@ -1345,20 +1376,37 @@ function editProductInfo (productToEdit, value = null, batch = false) {
if (reception_status == "qty_valid" && productToEdit.price_unit != newValue) {
if (index == -1) { // First update
productToEdit.old_price_unit = productToEdit.price_unit;
productToEdit.new_shelf_price = null;
if (! isNaN(productToEdit.p_coeff)) {
productToEdit.new_shelf_price = parseFloat(newValue);
try {
new_shelf_price = parseFloat(newValue * productToEdit.p_coeff);
old_shelf_price = parseFloat(productToEdit.p_price * productToEdit.p_coeff);
if (Math.abs(new_shelf_price - old_shelf_price) > 0.001)
productToEdit.new_shelf_price = new_shelf_price.toFixed(2);
// Let's compute product final price, using coeffs.
let computing_shelf_price_details = {base_value: productToEdit.new_shelf_price, intermediate_values: []};
for (let k = 1; k <10; k++) {
if (typeof productToEdit['coeff' + k + '_id'] !== "undefined") {
product_coeffs.forEach((coeff) => {
if (coeff.id == productToEdit['coeff' + k + '_id']) {
if (coeff.operation_type == "fixed") {
productToEdit.new_shelf_price += coeff.value;
computing_shelf_price_details.intermediate_values.push({msg: "Found fixed coeff " + coeff.value, new_value: productToEdit.new_shelf_price});
} else if (coeff.operation_type == "multiplier") {
productToEdit.new_shelf_price *= (1 + coeff.value);
computing_shelf_price_details.intermediate_values.push({msg: "Found multiplier coeff " + coeff.value, new_value: productToEdit.new_shelf_price});
}
}
});
}
}
productToEdit.new_shelf_price *= productToEdit.tax_coeff;
computing_shelf_price_details.intermediate_values.push({msg: "Applying tax coeff " + productToEdit.tax_coeff, new_value: productToEdit.new_shelf_price});
productToEdit.new_shelf_price = productToEdit.new_shelf_price.toFixed(2);
computing_shelf_price_details.final_value = productToEdit.new_shelf_price;
productToEdit.computing_shelf_price_details = computing_shelf_price_details;
} catch (e) {
productToEdit.new_shelf_price = null;
err = {msg: e.name + ' : ' + e.message, ctx: 'computing new_shelf_price'};
console.error(err);
report_JS_error(err, 'reception');
}
}
firstUpdate = true;
} else if (productToEdit.old_price_unit == newValue) {
......@@ -1484,8 +1532,20 @@ function print_product_labels() {
try {
if (is_time_to('print_pdt_labels', 10000)) {
$.ajax("../../orders/print_product_labels?oids=" + group_ids.join(','))
.done(function() {
alert('Impression des étiquettes à coller sur les articles lancée.');
.done(function(data) {
let success = false;
if (typeof data.res !== "undefined") {
if (typeof data.res.error === "undefined") {
success = true;
}
}
if (success == true) {
alert("l' impression des étiquettes à coller sur les articles vient d'être lancée.");
$('#barcodesToPrint').hide();
} else {
alert("Une erreur est survenue.");
}
});
} else {
alert("Vous avez cliqué il y a moins de 10s... Patience, la demande est en cours de traitement.");
......@@ -1980,10 +2040,12 @@ function add_products_action() {
for (let qty_input of qty_inputs) {
if ($(qty_input).val() === "") {
has_empty_qty_input = true;
$(qty_input).closest(".product_qty").find(".product_qty_input_alert")
$(qty_input).closest(".product_qty")
.find(".product_qty_input_alert")
.show();
} else {
$(qty_input).closest(".product_qty").find(".product_qty_input_alert")
$(qty_input).closest(".product_qty")
.find(".product_qty_input_alert")
.hide();
}
}
......@@ -2025,8 +2087,10 @@ function create_orders() {
for (let i = 0; i < add_products_lines.length; i++) {
let line = add_products_lines[i];
if ($(line).find(".product_name").text() === p.name) {
product_uom = $(line).find(".product_uom").text();
if ($(line).find(".product_name")
.text() === p.name) {
product_uom = $(line).find(".product_uom")
.text();
if (product_uom.includes("kg")) {
product_qty = parseFloat($(line).find(".product_qty_input")
......@@ -2046,15 +2110,16 @@ function create_orders() {
// If package qty is > than input value, package qty will be used while creating order
let package_qty = p_supplierinfo.package_qty;
if (product_qty < package_qty) {
package_qty = product_qty;
}
// Round differently for unit & kg products
if (product_uom.includes("kg") ) {
if (product_uom.includes("kg")) {
item_qty_package = Math.round((product_qty / package_qty) * 1e2) / 1e2;
} else {
item_qty_package = Math.round(product_qty / package_qty)
item_qty_package = Math.round(product_qty / package_qty);
}
orders_data.suppliers_data[supplier_id].lines.push({
......
......@@ -33,7 +33,7 @@ def home(request):
'merge_orders_pswd': getattr(settings, 'RECEPTION_MERGE_ORDERS_PSWD', 'makeastop'),
'couchdb_server': settings.COUCHDB['url'],
'db': settings.COUCHDB['dbs']['reception'],
'POUCHDB_VERSION': getattr(settings, 'POUCHDB_VERSION', '')
'POUCHDB_VERSION': getattr(settings, 'POUCHDB_VERSION', ''),
}
template = loader.get_template('reception/index.html')
......@@ -73,8 +73,9 @@ def get_list_orders(request):
}
if get_order_lines is True:
order_lines = CagetteReception.get_order_lines_by_po(int(order["id"]), nullQty = True)
ligne["po"] = order_lines
order_lines_data = CagetteReception.get_order_lines_by_po(int(order["id"]), nullQty = True)
ligne["po"] = order_lines_data['lines']
ligne["used_coeffs"] = order_lines_data['used_coeffs']
orders.append(ligne)
......@@ -93,7 +94,8 @@ def produits(request, id):
"ADD_ALL_LEFT_IS_GOOD_QTIES": False,
"ADD_ALL_LEFT_IS_GOOD_PRICES": False,
'add_products_pswd': getattr(settings, 'RECEPTION_ADD_PRODUCTS_PSWD', 'makeastop'),
'update_qty_pswd': getattr(settings, 'RECEPTION_UPDATE_QTY_PSWD', 'makeastop')
'update_qty_pswd': getattr(settings, 'RECEPTION_UPDATE_QTY_PSWD', 'makeastop'),
'allow_four_digits_in_reception_price': getattr(settings, 'ALLOW_FOUR_DIGITS_IN_RECEPTION_PRICE', False),
}
fixed_barcode_prefix = '0490'
......@@ -123,17 +125,17 @@ def produits(request, id):
def get_order_lines(request, id_po):
"""Send content of an order"""
order_lines = CagetteReception.get_order_lines_by_po(int(id_po))
order_lines_data = CagetteReception.get_order_lines_by_po(int(id_po))
return JsonResponse({'id_po': id_po, 'po': order_lines})
return JsonResponse({'id_po': id_po, 'po': order_lines_data['lines'], 'used_coeffs': order_lines_data['used_coeffs']})
def get_orders_lines(request):
"""Send content of multiple orders"""
data = json.loads(request.body.decode())
orders = []
for id_po in data['po_ids']:
order_lines = CagetteReception.get_order_lines_by_po(int(id_po), nullQty = True)
orders.append({'id_po': id_po, 'po': order_lines})
order_lines_data = CagetteReception.get_order_lines_by_po(int(id_po), nullQty = True)
orders.append({'id_po': id_po, 'po': order_lines_data['lines'], 'used_coeffs': order_lines_data['used_coeffs']})
return JsonResponse({'orders': orders})
......@@ -219,14 +221,15 @@ def update_orders(request):
errors.append('error registering shortage on p'+order_line['id']+':'+str(e))
# Print etiquette with new price if update if successful and barcode is authorized
if (print_labels is True) and (update is True) and (data['update_type'] == 'br_valid') and order_line['new_shelf_price'] and order_line['barcode'][:4] not in noprint_list:
try:
tools_url = settings.TOOLS_SERVER + '/products/label_print/'
tools_url += str(order_line['product_tmpl_id']) + '/'
tools_url += str(order_line['new_shelf_price'])
requests.get(tools_url)
except Exception as e:
coop_logger.error("Shelf label printing : %s",str(e))
# Printing at that point is inhibited because of random ununderstandable wrong new_shelf_price
# if (print_labels is True) and (update is True) and (data['update_type'] == 'br_valid') and order_line['new_shelf_price'] and order_line['barcode'][:4] not in noprint_list:
# try:
# tools_url = settings.TOOLS_SERVER + '/products/label_print/'
# tools_url += str(order_line['product_tmpl_id']) + '/'
# tools_url += str(order_line['new_shelf_price'])
# requests.get(tools_url)
# except Exception as e:
# coop_logger.error("Shelf label printing : %s",str(e))
except KeyError:
coop_logger.info("No line to update.")
......@@ -301,8 +304,8 @@ def save_error_report(request):
}
data['orders'].append(group_order) # group "order" has to be last in orders list
else:
coop_logger.info("data['orders'] is a single PO (not inside group)")
# else:
# coop_logger.info("data['orders'] is a single PO (not inside group)")
except Exception as e2:
coop_logger.error("Save reception report : Error while create group_order %s", str(e2))
......@@ -329,6 +332,7 @@ def save_error_report(request):
}
else:
# Read step 1 data from couch db document
order_id = 'order_' + str(order['id'])
order_doc = c_db.getDocById(order_id)
......
......@@ -29,7 +29,7 @@ class CagetteSales(models.Model):
statements_orders[s] = o["name"]
# Get payment lines
cond = [['id', 'in', statements]]
fields = ["amount", "journal_id", "create_date"]
fields = ["amount", "journal_id", "create_date", "meal_voucher_issuer"]
payments = self.o_api.search_read('account.bank.statement.line', cond, fields, order="create_date ASC", limit=50000)
try:
for payment in payments:
......@@ -41,7 +41,8 @@ class CagetteSales(models.Model):
"payments": [
{
"amount": round(float(payment["amount"]), 2),
"journal_id": payment["journal_id"]
"journal_id": payment["journal_id"],
"meal_voucher_issuer": payment["meal_voucher_issuer"]
}
]
})
......
......@@ -49,13 +49,11 @@ function display_orders(orders) {
{
data:"partner",
title:"Membre",
width: "40%"
title:"Membre"
},
{
data:"total_amount",
title: "Montant dû",
className:"dt-body-center",
render: function (data) {
return parseFloat(data).toFixed(2) + ' €';
}
......@@ -74,7 +72,14 @@ function display_orders(orders) {
return res;
}
},
{
data:"payments[0].meal_voucher_issuer",
title: "Émetteur CB Déj",
render: function (data) {
return data === 'false' || data == false ? '' : data;
}
},
],
order: [
[
......
......@@ -1230,52 +1230,6 @@ function init() {
$(this).off('wheel.disableScroll');
});
// client-side validation of numeric inputs, optionally replacing separator sign(s).
$("input.number").on("keydown", function (e) {
// allow function keys and decimal separators
if (
// backspace, delete, tab, escape, enter, comma and .
$.inArray(e.keyCode, [
46,
8,
9,
27,
13,
110,
188,
190
]) !== -1 ||
// Ctrl/cmd+A, Ctrl/cmd+C, Ctrl/cmd+X
($.inArray(e.keyCode, [
65,
67,
88
]) !== -1 && (e.ctrlKey === true || e.metaKey === true)) ||
// home, end, left, right
(e.keyCode >= 35 && e.keyCode <= 39)) {
/*
// optional: replace commas with dots in real-time (for en-US locals)
if (e.keyCode === 188) {
e.preventDefault();
$(this).val($(this).val() + ".");
}
// optional: replace decimal points (num pad) and dots with commas in real-time (for EU locals)
if (e.keyCode === 110 || e.keyCode === 190) {
e.preventDefault();
$(this).val($(this).val() + ",");
}
*/
return;
}
// block any non-number
if ((e.shiftKey || (e.keyCode < 48 || e.keyCode > 57)) && (e.keyCode < 96 || e.keyCode > 105)) {
e.preventDefault();
}
});
// Manual and textual input
$('#search_input').keypress(function(e) {
if (e.which >= 48 && e.which <= 57) { // figures [0-9]
......
......@@ -2,12 +2,14 @@ from django.db import models
from outils.common_imports import *
from outils.common import OdooAPI
from members.models import CagetteMember
from pytz import timezone
import locale
import re
import dateutil.parser
class CagetteShift(models.Model):
"""Class to handle cagette Odoo Shift."""
......@@ -17,6 +19,87 @@ class CagetteShift(models.Model):
self.tz = pytz.timezone("Europe/Paris")
self.o_api = OdooAPI()
def get_cycle_week_data(self, date=None):
result = {}
try:
res_param = self.o_api.search_read('ir.config_parameter',
[['key', '=', 'coop_shift.week_a_date']],
['value'])
if res_param:
import math
WEEKS = ['A', 'B', 'C', 'D']
start_A = tz.localize(datetime.datetime.strptime(res_param[0]['value'], '%Y-%m-%d'))
result['start'] = start_A
now = datetime.datetime.now(tz) # + datetime.timedelta(hours=72)
if date is not None:
now = tz.localize(datetime.datetime.strptime(date, '%Y-%m-%d'))
diff = now - start_A
weeks_diff = diff.total_seconds() / 3600 / 24 / 7
week_index = math.floor(weeks_diff % 4)
result['week_name'] = WEEKS[week_index]
result['start_date'] = start_A + datetime.timedelta(weeks=math.floor(weeks_diff))
except Exception as e:
coop_logger.error("get_current_cycle_week_data %s", str(e))
result['error'] = str(e)
return result
def is_matching_ftop_rules(self, partner_id, idNewShift, idOldShift=0):
answer = True
rules = getattr(settings, 'FTOP_SERVICES_RULES', {})
if ("successive_shifts_allowed" in rules
or
"max_shifts_per_cycle" in rules
):
try:
now = datetime.datetime.now(tz)
# Have to retrive shifts (from now to a cycle period forward to check rules respect)
[shift_registrations, is_ftop] = self.get_shift_partner(partner_id, now + datetime.timedelta(weeks=4))
new_shift = self.get_shift(idNewShift) # WARNING : use date_begin_tz while shift_registrations use date_begin (UTC)
if "successive_shifts_allowed" in rules:
min_duration = getattr(settings, 'MIN_SHIFT_DURATION', 2)
for sr in shift_registrations:
if int(sr['shift_id'][0]) != int(idOldShift):
diff = (datetime.datetime.strptime(sr['date_begin'], '%Y-%m-%d %H:%M:%S').astimezone(tz)
- tz.localize(datetime.datetime.strptime(new_shift['date_begin_tz'], '%Y-%m-%d %H:%M:%S')))
if abs(diff.total_seconds() / 3600) < (min_duration * 2) * (int(rules['successive_shifts_allowed']) + 1):
answer = False
# coop_logger.info(sr['date_begin'] + ' - ' + new_shift['date_begin_tz'])
# coop_logger.info(str(diff.total_seconds()/3600) + 'h')
if "max_shifts_per_cycle" in rules:
[ymd, hms] = new_shift['date_begin_tz'].split(" ")
cw = self.get_cycle_week_data(ymd)
if 'start_date' in cw:
sd = cw['start_date']
ed = cw['start_date'] + datetime.timedelta(weeks=4)
[cycle_shift_regs, is_ftop] = self.get_shift_partner(partner_id, start_date=sd, end_date=ed)
if len(cycle_shift_regs) >= int(rules['max_shifts_per_cycle']):
answer = False
coop_logger.info("services max par cycle atteint pour partner_id %s", str(partner_id))
except Exception as e:
coop_logger.error("is_shift_exchange_allowed %s %s", str(e), str(new_shift))
return answer
def is_shift_exchange_allowed(self, idOldShift, idNewShift, shift_type, partner_id):
answer = True
min_delay = getattr(settings, 'STANDARD_BLOCK_SERVICE_EXCHANGE_DELAY', 0)
if shift_type == "ftop":
min_delay = getattr(settings, 'FTOP_BLOCK_SERVICE_EXCHANGE_DELAY', 0)
if min_delay > 0:
now = datetime.datetime.now(tz)
old_shift = self.get_shift(idOldShift)
day_before_old_shift_date_start = \
tz.localize(datetime.datetime.strptime(old_shift['date_begin_tz'], '%Y-%m-%d %H:%M:%S')
- datetime.timedelta(hours=min_delay))
if now > day_before_old_shift_date_start:
answer = False
elif shift_type == "ftop":
answer = self.is_matching_ftop_rules(partner_id, idNewShift, idOldShift)
return answer
def get_shift(self, id):
"""Get one shift by id"""
cond = [['id', '=', id]]
......@@ -40,7 +123,6 @@ class CagetteShift(models.Model):
'street', 'street2', 'zip', 'city', 'mobile', 'phone', 'email',
'is_associated_people', 'parent_id', 'extra_shift_done']
partnerData = self.o_api.search_read('res.partner', cond, fields, 1)
if partnerData:
partnerData = partnerData[0]
if partnerData['is_associated_people']:
......@@ -210,12 +292,12 @@ class CagetteShift(models.Model):
coop_logger.error("Model affect shift nobody found : %s", str(cond))
return response
def cancel_shift(self, idsRegisteur, origin='memberspace'):
def cancel_shift(self, idsRegisteur, origin='memberspace', description=''):
"""Annule un shift"""
fieldsDatas = { "related_shift_state": 'cancel',
"origin": origin,
"state": 'cancel'}
"state": 'cancel',
"cancellation_description": description}
return self.o_api.update('shift.registration', idsRegisteur, fieldsDatas)
def reopen_shift(self, data):
......@@ -236,7 +318,7 @@ class CagetteShift(models.Model):
coop_logger.error("Reopen shift : %s", str(e))
return response
def create_delay(self, data, duration=28):
def create_delay(self, data, duration=28, ext_name="Extension créée depuis l'espace membre"):
"""
Create a delay for a member.
If no duration is specified, a delay is by default 28 days from the given start_date.
......@@ -245,6 +327,7 @@ class CagetteShift(models.Model):
Else, create a 28 days delay.
Args:
data
idPartner: int
start_date: string date at iso format (eg. "2019-11-19")
Date from which the delay end date is calculated
......@@ -253,6 +336,7 @@ class CagetteShift(models.Model):
Should be inferior than start_date.
(at creation only: odoo ignores delays if today's not inside)
duration: nb of days
ext_name: will be displayed in odoo extensions list
"""
action = 'create'
......@@ -261,7 +345,6 @@ class CagetteShift(models.Model):
fields = ['extension_ids']
partner_extensions = self.o_api.search_read('res.partner', cond, fields)
response = False
# If has extensions
if 'extension_ids' in partner_extensions[0]:
# Look for current extension: started before today and ends after
......@@ -294,7 +377,7 @@ class CagetteShift(models.Model):
else:
# Get the 'Extension' type id
extension_types = self.o_api.search_read('shift.extension.type')
ext_type_id = 1 # Default
ext_type_id = getattr(settings, 'EXTENSION_TYPE_ID', 1)
for val in extension_types:
if val['name'] == 'Extension':
ext_type_id = val['id']
......@@ -310,7 +393,7 @@ class CagetteShift(models.Model):
"type_id": ext_type_id,
"date_start": starting_date.isoformat(),
"date_stop": ending_date.isoformat(),
"name": "Extension créée depuis l'espace membre"
"name": ext_name
}
response = self.o_api.create('shift.extension', fields)
......@@ -383,3 +466,558 @@ class CagetteShift(models.Model):
def update_counter_event(self, fields):
""" Add/remove points """
return self.o_api.create('shift.counter.event', fields)
class CagetteServices(models.Model):
"""Class to handle cagette Odoo services."""
@staticmethod
def get_all_shift_templates():
"""Return all recorded shift templates recorded in Odoo database."""
creneaux = {}
try:
api = OdooAPI()
f = ['name', 'week_number', 'start_datetime_tz', 'end_datetime_tz',
'seats_reserved', 'shift_type_id', 'seats_max',
'seats_available','registration_qty']
c = [['active', '=', True]]
shift_templates = api.search_read('shift.template', c, f)
# Get count of active registrations for each shift template
# shift_templates_active_count = api.execute('lacagette_shifts', 'get_active_shifts', [])
# With LGDS tests, seats_reserved reflects better what's shown in Odoo ...
title = re.compile(r"^(\w{1})(\w{3})\. - (\d{2}:\d{2}) ?-? ?(\w*)")
for l in shift_templates:
# nb_reserved = 0
# for stac in shift_templates_active_count:
# if stac['shift_template_id'] == l['id']:
# nb_reserved = stac['seats_active_registration']
line = {}
end = time.strptime(l['end_datetime_tz'], "%Y-%m-%d %H:%M:%S")
end_min = str(end.tm_min)
if end_min == '0':
end_min = '00'
line['end'] = str(end.tm_hour) + ':' + end_min
line['max'] = l['seats_max']
# line['reserved'] = nb_reserved
#line['reserved'] = l['seats_reserved']
line['reserved'] = l['registration_qty']
line['week'] = l['week_number']
line['id'] = l['id']
line['type'] = l['shift_type_id'][0]
t_elts = title.search(l['name'])
if t_elts:
line['day'] = t_elts.group(2)
line['begin'] = t_elts.group(3)
line['place'] = t_elts.group(4)
creneaux[str(l['id'])] = {'data': line}
except Exception as e:
coop_logger.error(str(e))
return creneaux
@staticmethod
def get_shift_templates_next_shift(id):
"""Retrieve next shift template shift."""
api = OdooAPI()
c = [['shift_template_id.id', '=', id],
['date_begin', '>=', datetime.datetime.now().isoformat()]]
f = ['date_begin']
# c = [['id','=',2149]]
shift = {}
res = api.search_read('shift.shift', c, f, 1, 0, 'date_begin ASC')
if (res and res[0]):
locale.setlocale(locale.LC_ALL, 'fr_FR.utf8')
local_tz = pytz.timezone('Europe/Paris')
date, t = res[0]['date_begin'].split(' ')
year, month, day = date.split('-')
start = datetime.datetime(int(year), int(month), int(day),
0, 0, 0, tzinfo=pytz.utc)
start_date = start.astimezone(local_tz)
shift['date_begin'] = start_date.strftime("%A %d %B %Y")
return shift
@staticmethod
def get_services_at_time(time, tz_offset, with_members=True):
"""Retrieve present services with members linked."""
default_acceptable_minutes_after_shift_begins = getattr(settings, 'ACCEPTABLE_ENTRANCE_MINUTES_AFTER_SHIFT_BEGINS', 15)
minutes_before_shift_starts_delay = getattr(settings, 'ACCEPTABLE_ENTRANCE_MINUTES_BEFORE_SHIFT', 15)
minutes_after_shift_starts_delay = default_acceptable_minutes_after_shift_begins
late_mode = getattr(settings, 'ENTRANCE_WITH_LATE_MODE', False)
max_duration = getattr(settings, 'MAX_DURATION', 180)
if late_mode is True:
minutes_after_shift_starts_delay = getattr(settings, 'ENTRANCE_VALIDATION_GRACE_DELAY', 60)
api = OdooAPI()
now = dateutil.parser.parse(time) - datetime.timedelta(minutes=tz_offset)
start1 = now + datetime.timedelta(minutes=minutes_before_shift_starts_delay)
start2 = now - datetime.timedelta(minutes=minutes_after_shift_starts_delay)
end = start1 + datetime.timedelta(minutes=max_duration)
cond = [['date_end_tz', '<=', end.isoformat()]]
cond.append('|')
cond.append(['date_begin_tz', '>=', start1.isoformat()])
cond.append(['date_begin_tz', '>=', start2.isoformat()])
fields = ['name', 'week_number', 'registration_ids',
'standard_registration_ids',
'shift_template_id', 'shift_ticket_ids',
'date_begin_tz', 'date_end_tz', 'state']
services = api.search_read('shift.shift', cond, fields,order ="date_begin_tz ASC")
for s in services:
if (len(s['registration_ids']) > 0):
if late_mode is True:
s['late'] = (
now.replace(tzinfo=None)
-
dateutil.parser.parse(s['date_begin_tz']).replace(tzinfo=None)
).total_seconds() / 60 > default_acceptable_minutes_after_shift_begins
if with_members is True:
cond = [['id', 'in', s['registration_ids']], ['state', 'not in', ['cancel', 'waiting', 'draft']]]
fields = ['partner_id', 'shift_type', 'state', 'is_late', 'associate_registered']
members = api.search_read('shift.registration', cond, fields)
s['members'] = sorted(members, key=lambda x: x['partner_id'][0])
if len(s['members']) > 0:
# search for associated people linked to these members
mids = []
for m in s['members']:
mids.append(m['partner_id'][0])
cond = [['parent_id', 'in', mids]]
fields = ['id', 'parent_id', 'name','barcode_base']
associated = api.search_read('res.partner', cond, fields)
if len(associated) > 0:
for m in s['members']:
for a in associated:
if int(a['parent_id'][0]) == int(m['partner_id'][0]):
m['partner_name'] = m['partner_id'][1]
m['partner_id'][1] += ' en binôme avec ' + a['name']
m['associate_name'] = str(a['barcode_base']) + ' - ' + a['name']
return services
@staticmethod
def registration_done(registration_id, overrided_date="", typeAction=""):
"""Equivalent to click present in presence form."""
api = OdooAPI()
f = {'state': 'done'}
if(typeAction != "normal" and typeAction != ""):
f['associate_registered'] = typeAction
if typeAction == "both":
f['should_increment_extra_shift_done'] = True
else:
f['should_increment_extra_shift_done'] = False
late_mode = getattr(settings, 'ENTRANCE_WITH_LATE_MODE', False)
if late_mode is True:
# services = CagetteServices.get_services_at_time('14:28',0, with_members=False)
if len(overrided_date) > 0 and getattr(settings, 'APP_ENV', "prod") != "prod":
now = overrided_date
else:
local_tz = pytz.timezone('Europe/Paris')
now = datetime.datetime.utcnow().replace(tzinfo=pytz.utc).astimezone(local_tz).strftime("%H:%MZ")
# coop_logger.info("Maintenant = %s (overrided %s) %s", now, overrided_date)
services = CagetteServices.get_services_at_time(now, 0, with_members=False)
if len(services) > 0:
# Notice : Despite is_late is defined as boolean in Odoo, 0 or 1 is needed for api call
is_late = 0
if services[0]['late'] is True:
is_late = 1
f['is_late'] = is_late
else:
return False
return api.update('shift.registration', [int(registration_id)], f)
@staticmethod
def reopen_registration(registration_id, overrided_date=""):
api = OdooAPI()
f = {'state': 'open'}
try:
cond = [['id', '=', int(registration_id)]]
fields = ['partner_id']
res = api.search_read('shift.registration', cond, fields)
coop_logger.info("On invalide la présence de %s ", res[0]['partner_id'][1])
except Exception as e:
coop_logger.error("On invalide shift_registration %s (erreur : %s)", str(registration_id), str(e))
return api.update('shift.registration', [int(registration_id)], f)
@staticmethod
def record_rattrapage(mid, sid, stid, typeAction):
"""Add a shift registration for member mid.
(shift sid, shift ticket stid)
Once created, shift presence is confirmed.
"""
api = OdooAPI()
fields = {
"partner_id": mid,
"shift_id": sid,
"shift_ticket_id": stid,
"shift_type": "standard", # ou ftop -> voir condition
"related_shift_state": 'confirm',
"state": 'open'}
reg_id = api.create('shift.registration', fields)
f = {'state': 'done'}
if(typeAction != "normal" and typeAction != ""):
f['associate_registered'] = typeAction
if typeAction == "both":
f['should_increment_extra_shift_done'] = True
else:
f['should_increment_extra_shift_done'] = False
return api.update('shift.registration', [int(reg_id)], f)
@staticmethod
def record_absences(date):
"""Called by cron script."""
import dateutil.parser
if len(date) > 0:
now = dateutil.parser.parse(date)
else:
now = datetime.datetime.now()
# now = dateutil.parser.parse('2020-09-15T15:00:00Z')
date_24h_before = now - datetime.timedelta(hours=24)
# let authorized people time to set presence for those who came in late
end_date = now - datetime.timedelta(hours=2)
api = OdooAPI()
# Let's start by adding an extra shift to associated member who came together
cond = [['date_begin', '>=', date_24h_before.isoformat()],
['date_begin', '<=', end_date.isoformat()],
['state', '=', 'done'],
['associate_registered', '=', 'both'],
['should_increment_extra_shift_done', '=', True]]
fields = ['id', 'state', 'partner_id', 'date_begin']
res = api.search_read('shift.registration', cond, fields)
extra_shift_done_incremented_srids = [] # shift registration ids
for r in res:
cond = [['id', '=', r['partner_id'][0]]]
fields = ['id','extra_shift_done']
res_partner = api.search_read('res.partner', cond, fields)
f = {'extra_shift_done': res_partner[0]['extra_shift_done'] + 1 }
api.update('res.partner', [r['partner_id'][0]], f)
extra_shift_done_incremented_srids.append(int(r['id']))
# Make sure the counter isn't incremented twice
f = {'should_increment_extra_shift_done': False}
api.update('shift.registration', extra_shift_done_incremented_srids, f)
absence_status = 'excused'
res_c = api.search_read('ir.config_parameter',
[['key', '=', 'lacagette_membership.absence_status']],
['value'])
if len(res_c) == 1:
absence_status = res_c[0]['value']
cond = [['date_begin', '>=', date_24h_before.isoformat()],
['date_begin', '<=', end_date.isoformat()],
['state', '=', 'open']]
fields = ['state', 'partner_id', 'date_begin', 'shift_id']
res = api.search_read('shift.registration', cond, fields)
ids = []
partner_ids = []
excluded_partner = []
canceled_reg_ids = [] # for exempted people
shift_ids = []
for r in res:
partner_ids.append(int(r['partner_id'][0]))
shift_id = int(r['shift_id'][0])
if shift_id not in shift_ids:
shift_ids.append(shift_id)
cond = [['id', 'in', partner_ids],
['cooperative_state', 'in', ['exempted']]]
fields = ['id']
res_exempted = api.search_read('res.partner', cond, fields)
for r in res_exempted:
excluded_partner.append(int(r['id']))
for r in res:
if not (int(r['partner_id'][0]) in excluded_partner):
d_begin = r['date_begin']
(d, h) = d_begin.split(' ')
(_h, _m, _s) = h.split(':')
if int(_h) < 21:
ids.append(int(r['id']))
else:
canceled_reg_ids.append(int(r['id']))
# coop_logger.info("Traitement absences shift_registration ids %s", ids)
f = {'state': absence_status, 'date_closed': now.isoformat()}
update_shift_reg_result = {'update': api.update('shift.registration', ids, f), 'reg_shift': res, 'errors': []}
if update_shift_reg_result['update'] is True:
update_shift_reg_result['process_status_res'] = api.execute('res.partner','run_process_target_status', [])
# change shift state by triggering button_done method for all related shifts
if len(canceled_reg_ids) > 0:
f = {'state': 'cancel', 'date_closed': now.isoformat()}
api.update('shift.registration', canceled_reg_ids, f)
for sid in shift_ids:
try:
api.execute('shift.shift', 'button_done', sid)
except Exception as e:
marshal_none_error = 'cannot marshal None unless allow_none is enabled'
if not (marshal_none_error in str(e)):
update_shift_reg_result['errors'].append({'shift_id': sid, 'msg' :str(e)})
return update_shift_reg_result
@staticmethod
def close_ftop_service():
"""Called by cron script"""
# Retrieve the latest past FTOP service
import dateutil.parser
now = datetime.datetime.now()
# now = dateutil.parser.parse('2019-10-20T00:00:00Z')
cond = [['shift_type_id','=', 2],['date_end', '<=', now.isoformat()],['state','=', 'draft'], ['active', '=', True]]
fields = ['name']
api = OdooAPI()
res = api.search_read('shift.shift', cond, fields,order ="date_end ASC", limit=1)
# return res[0]['id']
result = {}
if res and len(res) > 0:
result['service_found'] = True
# Exceptions are due to the fact API returns None whereas the action is really done !...
marshal_none_error = 'cannot marshal None unless allow_none is enabled'
actual_errors = 0
try:
api.execute('shift.shift', 'button_confirm', [res[0]['id']])
except Exception as e:
if not (marshal_none_error in str(e)):
result['exeption_confirm'] = str(e)
actual_errors += 1
try:
api.execute('shift.shift', 'button_makeupok', [res[0]['id']])
except Exception as e:
if not (marshal_none_error in str(e)):
result['exeption_makeupok'] = str(e)
actual_errors += 1
try:
api.execute('shift.shift', 'button_done', [res[0]['id']])
except Exception as e:
if not (marshal_none_error in str(e)):
result['exeption_done'] = str(e)
actual_errors += 1
if actual_errors == 0:
result['done'] = True
else:
result['done'] = False
result['actual_errors'] = actual_errors
else:
result['service_found'] = False
return result
@staticmethod
def get_committees_shift_id():
shift_id = None
try:
api = OdooAPI()
res = api.search_read('ir.config_parameter',
[['key','=', 'lacagette_membership.committees_shift_id']],
['value'])
if len(res) > 0:
try:
shift_id = int(res[0]['value'])
except:
pass
except:
pass
return shift_id
@staticmethod
def get_exemptions_shift_id():
shift_id = None
try:
api = OdooAPI()
res = api.search_read('ir.config_parameter',
[['key','=', 'lacagette_exemptions.exemptions_shift_id']],
['value'])
if len(res) > 0:
try:
shift_id = int(res[0]['value'])
except:
pass
except:
pass
return shift_id
@staticmethod
def get_first_ftop_shift_id():
shift_id = None
try:
api = OdooAPI()
res = api.search_read('shift.template',
[['shift_type_id','=', 2]],
['id', 'registration_qty'])
# Get the ftop shift template with the max registrations: most likely the one in use
ftop_shift = {'id': None, 'registration_qty': 0}
for shift_reg in res:
if shift_reg["registration_qty"] > ftop_shift["registration_qty"]:
ftop_shift = shift_reg
try:
shift_id = int(ftop_shift['id'])
except:
pass
except:
pass
return shift_id
@staticmethod
def easy_validate_shift_presence(coop_id):
"""Add a presence point if the request is valid."""
res = {}
try:
committees_shift_id = CagetteServices.get_committees_shift_id()
api = OdooAPI()
# let verify coop_id is corresponding to a ftop subscriber
cond = [['id', '=', coop_id]]
fields = ['tmpl_reg_line_ids']
coop = api.search_read('res.partner', cond, fields)
if coop:
if len(coop[0]['tmpl_reg_line_ids']) > 0 :
cond = [['id', '=', coop[0]['tmpl_reg_line_ids'][0]]]
fields = ['shift_template_id']
shift_templ_res = api.search_read('shift.template.registration.line', cond, fields)
if (len(shift_templ_res) > 0
and
shift_templ_res[0]['shift_template_id'][0] == committees_shift_id):
ok_for_adding_pt = False
mininum_seconds_interval = getattr(settings, 'MINIMUM_SECONDS_BETWEEN_TWO_COMITEE_VALIDATION', 3600 * 24)
evt_name = getattr(settings, 'ENTRANCE_ADD_PT_EVENT_NAME', 'Validation service comité')
if mininum_seconds_interval > 0:
# A constraint has been set to prevent from adding more than 1 point during a time period
# Let's find out when was the last time a "special point" has been addes
c = [['partner_id', '=', coop_id], ['name', '=', evt_name]]
f = ['create_date']
last_point_mvts = api.search_read('shift.counter.event', c, f,
order ="create_date DESC", limit=1)
if len(last_point_mvts):
now = datetime.datetime.now()
past = datetime.datetime. strptime(last_point_mvts[0]['create_date'],
'%Y-%m-%d %H:%M:%S')
if (now - past).total_seconds() >= mininum_seconds_interval:
ok_for_adding_pt = True
else:
ok_for_adding_pt = True
else:
# mininum_seconds_interval is 0 : Point can we added without any condition
ok_for_adding_pt = True
if ok_for_adding_pt is True:
res['evt_id'] = CagetteMember(coop_id).add_pts('ftop', 1, evt_name)
else:
res['error'] = "Un point a déjà été ajouté trop récemment."
else:
res['error'] = "Vous n'avez pas le droit d'ajouter un point."
else:
res['error'] = "Unregistred coop"
else:
res['error'] = "Invalid coop id"
except Exception as e:
coop_logger.error("easy_validate_shift_presence : %s %s", str(coop_id), str(e))
return res
class CagetteService(models.Model):
"""Class to handle cagette Odoo service."""
def __init__(self, id):
"""Init with odoo id."""
self.id = int(id)
self.o_api = OdooAPI()
def _process_associated_people_extra_shift_done(self):
cond = [['shift_id', '=', self.id],
['state', '=', 'done'],
['associate_registered', '=', 'both'],
['should_increment_extra_shift_done', '=', True]]
fields = ['id', 'state', 'partner_id', 'date_begin']
res = self.o_api.search_read('shift.registration', cond, fields)
extra_shift_done_incremented_srids = [] # shift registration ids
for r in res:
cond = [['id', '=', r['partner_id'][0]]]
fields = ['id','extra_shift_done']
res_partner = self.o_api.search_read('res.partner', cond, fields)
f = {'extra_shift_done': res_partner[0]['extra_shift_done'] + 1 }
self.o_api.update('res.partner', [r['partner_id'][0]], f)
extra_shift_done_incremented_srids.append(int(r['id']))
# Make sure the counter isn't incremented twice
f = {'should_increment_extra_shift_done': False}
self.o_api.update('shift.registration', extra_shift_done_incremented_srids, f)
def _process_related_shift_registrations(self):
now = datetime.datetime.now()
absence_status = 'excused'
res_c = self.o_api.search_read('ir.config_parameter',
[['key', '=', 'lacagette_membership.absence_status']],
['value'])
if len(res_c) == 1:
absence_status = res_c[0]['value']
cond = [['shift_id', '=', self.id],
['state', '=', 'open']]
fields = ['state', 'partner_id', 'date_begin']
res = self.o_api.search_read('shift.registration', cond, fields)
ids = []
partner_ids = []
excluded_partner = []
canceled_reg_ids = [] # for exempted people
for r in res:
partner_ids.append(int(r['partner_id'][0]))
cond = [['id', 'in', partner_ids],
['cooperative_state', 'in', ['exempted']]]
fields = ['id']
res_exempted = self.o_api.search_read('res.partner', cond, fields)
for r in res_exempted:
excluded_partner.append(int(r['id']))
for r in res:
if not (int(r['partner_id'][0]) in excluded_partner):
d_begin = r['date_begin']
(d, h) = d_begin.split(' ')
(_h, _m, _s) = h.split(':')
if int(_h) < 21:
ids.append(int(r['id']))
else:
canceled_reg_ids.append(int(r['id']))
# coop_logger.info("Traitement absences shift_registration ids %s", ids)
f = {'state': absence_status, 'date_closed': now.isoformat()}
update_shift_reg_result = {'update': self.o_api.update('shift.registration', ids, f), 'reg_shift': res, 'errors': []}
if update_shift_reg_result['update'] is True:
update_shift_reg_result['process_status_res'] = self.o_api.execute('res.partner','run_process_target_status', [])
# change shift state by triggering button_done method for all related shifts
if len(canceled_reg_ids) > 0:
f = {'state': 'cancel', 'date_closed': now.isoformat()}
self.o_api.update('shift.registration', canceled_reg_ids, f)
try:
self.o_api.execute('shift.shift', 'button_done', self.id)
except Exception as e:
marshal_none_error = 'cannot marshal None unless allow_none is enabled'
if not (marshal_none_error in str(e)):
update_shift_reg_result['errors'].append({'shift_id': self.id, 'msg' :str(e)})
return update_shift_reg_result
def record_absences(self, request):
"""Can only been executed if an Odoo user is beeing connected."""
res = {}
try:
if CagetteUser.are_credentials_ok(request) is True:
self._process_associated_people_extra_shift_done()
res = self._process_related_shift_registrations()
else:
res['error'] = 'Forbidden'
except Exception as e:
coop_logger.error("CagetteService.record_absences : %s %s", str(self.id), str(e))
res['error'] = str(e)
return res
......@@ -99,7 +99,7 @@ def get_list_shift_calendar(request, partner_id):
registerPartner = cs.get_shift_partner(partner_id)
use_new_members_space = getattr(settings, 'USE_NEW_MEMBERS_SPACE', False)
remove_15_minutes_at_shift_end = getattr(settings, 'REMOVE_15_MINUTES_AT_SHIFT_END', True)
listRegisterPartner = []
listMakeUpShift = []
for v in registerPartner:
......@@ -113,8 +113,8 @@ def get_list_shift_calendar(request, partner_id):
events = []
for value in listService:
events.append(value)
if value['shift_type_id'][0] == 1 or getattr(settings, 'USE_STANDARD_SHIFT', True) is False:
# Standard ou volant si on n'utilise pas les services standards (config)
l = set(value['registration_ids']) & set(listRegisterPartner)
# if (int(value['seats_reserved']) == int(value['seats_max']) and len(l) > 0 ) or (int(value['seats_reserved']) < int(value['seats_max'])):
if (int(value['seats_available']) > 0 or len(l) > 0 ):
......@@ -134,7 +134,9 @@ def get_list_shift_calendar(request, partner_id):
event["start"] = dateIsoUTC(value['date_begin_tz'])
datetime_object = datetime.datetime.strptime(value['date_end_tz'], "%Y-%m-%d %H:%M:%S") - datetime.timedelta(minutes=15)
datetime_object = datetime.datetime.strptime(value['date_end_tz'], "%Y-%m-%d %H:%M:%S")
if remove_15_minutes_at_shift_end is True:
datetime_object -= datetime.timedelta(minutes=15)
event["end"] = dateIsoUTC(datetime_object.strftime("%Y-%m-%d %H:%M:%S"))
if len(l) > 0:
......@@ -177,14 +179,11 @@ def get_list_shift_calendar(request, partner_id):
def get_list_shift_partner(request, partner_id):
cs = CagetteShift()
shiftData = cs.get_shift_partner(partner_id)
empty_data = False
for value in shiftData:
value['date_begin'] = value['date_begin'] + "Z"
value['date_end'] = value['date_end'] + "Z"
if "Services des comités" in value['shift_id'][1]:
empty_data = True
if empty_data is True:
shiftData = []
return JsonResponse(shiftData, safe=False)
def change_shift(request):
......@@ -215,6 +214,11 @@ def change_shift(request):
response = {'msg': "Old service in less than 24hours."}
return JsonResponse(response, status=400)
if cs.is_shift_exchange_allowed(idOldShift, data["idShift"], data["shift_type"], data["idPartner"]) is False:
response = {'msg': "Not allowed to change shift"}
return JsonResponse(response, status=400)
st_r_id = False
#Insertion du nouveau shift
try:
......@@ -329,10 +333,9 @@ def cancel_shift(request):
listRegister = [int(request.POST['idRegister'])]
try:
response = cs.cancel_shift(listRegister)
# decrement extra_shift_done if param exists
if 'extra_shift_done' in request.POST:
response = cs.cancel_shift(listRegister, origin='memberspace extra shift done')
target = int(request.POST["extra_shift_done"]) - 1
# extra security
......@@ -341,6 +344,8 @@ def cancel_shift(request):
cm = CagetteMember(partner_id)
cm.update_extra_shift_done(target)
else:
response = cs.cancel_shift(listRegister)
return JsonResponse({"res" : 'response'})
except Exception as e:
......
......@@ -373,6 +373,8 @@ class CagetteStock(models.Model):
cond = [['qty_available','>', 0], ['active', '=', True]]
fields = ["barcode", "display_name", "qty_available", "standard_price"]
articles = api.search_read('product.product', cond, fields, 1000000)
for a in articles:
a['total'] = a['qty_available'] * a['standard_price']
except Exception as e:
coop_logger.error("Erreur get_valuable_stock : %s", str(e))
return articles
......
......@@ -6,14 +6,29 @@ $(document).ready(function() {
"data": ""
},
"columns":[
{data:"barcode", "title":"Code-barre", "width": "50%"},
{data:"barcode", "title":"Code-barre", "width": "10em"},
{data:"display_name", "title":"Article", "width": "50%"},
{data:"qty_available", "title":"Stock", "width":"15%"
{data:"qty_available", "title":"Stock", "width":"5em",
render: function(data) {
if (data == parseInt(data,10)) {
return data
} else {
return data.toFixed(3)
}
}
},
{data:"standard_price", "title":"Prix achat", "width":"15%"
{data:"standard_price", "title":"Prix Achat", "width":"4em",
render: function(data) {
return data.toFixed(2)
}
},
{data: "total", "title": "Total",
render: function(data, type, full) {
return data.toFixed(2)
}
}
],
......@@ -24,7 +39,7 @@ $(document).ready(function() {
"desc"
]
],
"iDisplayLength": 50,
"paging": true,
"language": {
"emptyTable": "Pas de donnée",
"info": "Affiché : lignes _START_ à _END_ sur _TOTAL_",
......@@ -55,6 +70,6 @@ $(document).ready(function() {
className: 'btn--primary btn_export'
},
],
dom: '<lr<t>ip><"clear"><B>',
dom: '<lr<t>ip><"clear"><B>'
});
});
......@@ -36,7 +36,7 @@
<script src="{% static "js/modernizr-custom.min.js" %}"></script>
{% block additionnal_scripts %}{% endblock %}
</head>
<body>
<body class="{% block body_class %}nospecific{% endblock %}">
{% block menu %}{% endblock %}
<!-- The overlay -->
<div id="modal" class="overlay">
......
......@@ -56,9 +56,35 @@
</div>
<div id="templates" style="display:none;"></div>
<div id="templates" style="display:none;">
<div id="modal_extend_delay_template">
<p>
Vous vous apprêtez à rallonger le délai de
<span class="member_name"></span>
pour une durée de {{extension_duration}} mois à partir d'aujourd'hui.
</p>
</div>
<div id="modal_incr_makeup_counter">
<p>Ajouter un rattrapage à <span class="member_name"></span> ?</p>
<br><label for="incr-explanation">Explication : </label><input class="" type="text" id="incr-explanation">
<br><br><label for="incr-signature">Signature : </label><input class="" type="text" id="incr-signature">
</div>
<div id="modal_decr_makeup_counter">
<p>Enlever un rattrapage à <span class="member_name"></span> ?</p>
<br><label for="decr-explanation">Explication : </label><input class="" type="text" id="decr-explanation">
<br><br><label for="decr-signature">Signature : </label><input class="" type="text" id="decr-signature">
</div>
<div id="modal_decr_selected_makeup_counter">
<p>Enlever un rattrapage aux membres sélectionnés ?</p>
<br><label for="decr-explanation-selected">Explication : </label><input class="" type="text" id="decr-explanation-selected">
<br><br><label for="decr-signature-selected">Signature : </label><input class="" type="text" id="decr-signature-selected">
</div>
</div>
</div>
<script src='{% static "js/all_common.js" %}?v=1651853225'></script>
<script src='{% static "js/admin/manage_makeups.js" %}?v=1651853225'></script>
<script>
const extension_duration = {{extension_duration}};
</script>
{% endblock %}
{% extends "base.html" %}
{% load static %}
{% block body_class %}manage_regular_shifts{% endblock %}
{% block additionnal_css %}
<link rel="stylesheet" href="{% static 'css/datatables/datatables.min.css' %}">
<link rel="stylesheet" href="{% static 'css/admin/manage_regular_shifts.css' %}">
......@@ -96,6 +96,8 @@
var couchdb_dbname = '{{db}}';
var couchdb_server = '{{couchdb_server}}' + couchdb_dbname;
var committees_shift_id = '{{committees_shift_id}}';
var exemptions_shift_id = '{{exemptions_shift_id}}';
const committees_shift_name = '{{committees_shift_name}}';
var ASSOCIATE_MEMBER_SHIFT = '{{ASSOCIATE_MEMBER_SHIFT}}';
</script>
<script src='{% static "js/common.js" %}?v=1651853225'></script>
......
......@@ -51,7 +51,16 @@
</div>
</div>
<div id="templates" style="display:none;"></div>
<div id="templates" style="display:none;">
<div id="modal_delete_shift_registration">
<p>Enlever la présence de <b><span class="member_name"></span></b> au service du <b><span class="service_name"></span></b> ?</p>
<div id="makeup_case_explanation" style="display:none">
<p><i class="fas fa-exclamation-triangle"></i> Ce service est un rattrapage. Le supprimer ajoutera un point au compteur de ce.tte membre.</p>
</div>
<br><label for="cancellation-explanation">Explication : </label><input class="" type="text" id="cancellation-explanation">
<br><br><label for="cancellation-signature">Signature : </label><input class="" type="text" id="cancellation-signature">
</div>
</div>
</div>
<script src='{% static "js/all_common.js" %}?v=1651853225'></script>
......
{% extends "members/base.html" %}
{% block body_class %}inscriptions{% endblock %}
{% load static %}
{% block additionnal_css %}
<link rel="stylesheet" href="{% static "css/inscriptions.css" %}?v=1651853225">
......@@ -80,15 +81,25 @@
{% endif %}
{% if can_create_binome %}
<p id="add_binome" >+ Binomes (facultatif)</p>
<div>Voulez-vous inscrire la personne en tant que deuxième personne d'un binôme ?</div>
<label>
<input type="radio" name="binome" id="no_binome" required>
Non
</label><br>
<label>
<input type="radio" name="binome" id="add_binome" required>
Oui
</label><br>
<div id="associate_area" style="display:none;">
<div class="choice_button_area d-flex" >
<div id="existing_member_choice" class="member_choice">
A mettre en binome avec un.e membre existant.e
A mettre en binôme avec un.e membre existant.e
</div>
{% if ASSOCIATE_MEMBER_SHIFT %}
<div id="new_member_choice" class="member_choice">
A mettre en binome avec un.e nouveau membre
A mettre en binôme avec un nouveau membre
</div>
{% endif %}
</div>
......@@ -186,6 +197,7 @@
var max_begin_hour = '{{max_begin_hour}}';
var email_domain = '{{email_domain}}';
var committees_shift_id = '{{committees_shift_id}}';
var exemptions_shift_id = '{{exemptions_shift_id}}';
</script>
<script src="{% static "js/all_common.js" %}?v=1651853225"></script>
......
......@@ -106,6 +106,8 @@
var mag_place_string = '{{mag_place_string}}';
var office_place_string = '{{office_place_string}}'
var max_begin_hour = '{{max_begin_hour}}'
var committees_shift_id = '{{committees_shift_id}}';
var exemptions_shift_id = '{{exemptions_shift_id}}';
</script>
<script src="{% static "js/all_common.js" %}"?v=1651853225></script>
<script src="{% static "js/common.js" %}"?v=1651853225></script>
......
......@@ -7,6 +7,11 @@
ou ...
<br />
<button class="flex-container--column" data-select="Volant">Volant</button>
{% if exemptions_shift_id != 0 %}
ou ...
<br />
<button class="flex-container--column" data-select="Exemption">Exempté</button>
{% endif %}
<div class="info"></div>
</div>
<div class="col-5 main_content">
......
......@@ -9,6 +9,11 @@
<br />
<button class="flex-container--column" data-select="Volant">Volant</button>
{% endif %}
{% if exemptions_shift_id != 0 %}
ou ...
<br />
<button class="flex-container--column" data-select="Exemption">Exempté</button>
{% endif %}
<div class="info"></div>
</div>
<div class="col-5 main_content">
......
......@@ -488,18 +488,21 @@
</div>
</div>
<button type="button" class="accordion btn_faq">
<span class="full_width" >009 Faire une autre demande au BDM: seulement si les autres formulaires ne correspondent pas à ma demande </span>
<span class="full_width" >009 Faire une autre demande au BDM : seulement si les autres formulaires ne correspondent pas à ma demande </span>
</button>
<div class="input-container panel">
<div class="grp_text">
<br>
<div>
Nous avons créé des formulaires spécifiques pour la plupart des problèmes rencontrés par les membres. Changer de créneau, créer un binôme, ajouter un produit à la gamme, partir en vacances... <br />
Cela dit, nous en découvrons de nouveaux tous les jours.<br />
Si tu n'as pas su quel formulaire remplir, tu es au bon endroit. <br />
Vas-y dit nous tout !<br /><br />
Attention : si tu souhaites contacter le BDM pour prévenir que tu seras absent-e à ton service cela ne sert à rien! Il te faut déplacer ton service via ton espace membre. Il n'est cependant pas possible d'échanger un service qui commence dans moins de 24h pour des raisons de logistiques. Si tu ne peux pas venir tu seras donc comptabilisé-e absent-e. Tu basculeras en statut "Rattrapage" et ne pourras plus faire tes courses. Il te faudra sélectionner dans ton espace membre un rattrapage à faire dans les 6 prochains mois pour basculer en statut "Délai" et pouvoir faire de nouveau tes courses.<br />
</div>
<div class="block_service_exchange">
Attention : si tu souhaites contacter le BDM pour prévenir que tu seras absent-e à ton service cela ne sert à rien ! Il te faut déplacer ton service via ton espace membre. Il n'est cependant pas possible d'échanger un service qui commence dans moins de 24h pour des raisons de logistiques. Si tu ne peux pas venir tu seras donc comptabilisé-e absent-e. Tu basculeras en statut "Rattrapage" et ne pourras plus faire tes courses. Il te faudra sélectionner dans ton espace membre un rattrapage à faire dans les 6 prochains mois pour basculer en statut "Délai" et pouvoir faire de nouveau tes courses.<br />
Merci de ne pas contacter le Bureau des membres pour cela, il te donnera exactement la même réponse.<br />
</div>
<div class="faq_link_button_area">
<a
href="javascript:void(0);"
......
......@@ -70,9 +70,12 @@
Échange de services
</div>
<div class="tile_content">
<div>
<div class="block_service_exchange">
Un empêchement ? J'anticipe et déplace mes services jusqu'à 24h avant leur début !
</div>
<div class="free_service_exchange">
Un empêchement ? J'anticipe et déplace mon service le plus tôt possible !
</div>
<div class="home_link_button_area">
<button type="button" class="btn--primary home_link_button" id="go_to_shifts_calendar">
Accéder au calendrier d'échange de services
......@@ -129,7 +132,8 @@
</div>
<script>
var max_begin_hour = "{{max_begin_hour}}",
type = 2;
type = 2,
exemptions_shift_id = '{{exemptions_shift_id}}';
</script>
{% endif %}
</div>
......@@ -93,7 +93,7 @@
</div>
<div id="cant_have_delay_msg_template">
<h3>Bonjour, tu avais 6 mois pour rattraper tes services manqués et il semblerait que tu ne l'aies pas fait.</h3>
<h3>Bonjour, tu avais {{extension_duration}} mois pour rattraper tes services manqués et il semblerait que tu ne l'aies pas fait.</h3>
<h3>Tu ne peux plus sélectionner de rattrapages sur ton espace membre pour le moment,
merci de contacter le bureau des membres pour résoudre ce problème en remplissant ce formulaire : </h3>
</div>
......@@ -101,6 +101,14 @@
<h3>Vous êtes inscrit.e dans le service des comités, vous n'avez pas accès au calendrier d'échange des services car vous vous organisez directement avec le responsable du comité. Si vous avez des rattrapages à réaliser, merci de contacter le responsable du comité qui vous aidera à planifier les rattrapages ou trouver une solution.</h3>
<h3>Le calendrier ci-dessous est en lecture seule</h3>
</div>
<div id="comite_my_shifs_message">
<p data-type="nb_of_shifs_state">
Vous avez actuellement <span data-type="shifts_nb"></span> <span data-type="service_txt">service</span> d'avance.
</p>
<p data-type="next_ftop_shift">
Le système informatique viendra prélever un service sur votre compteur le <span data-type="next_ftop_shift_date"></span>.
</p>
</div>
</div>
</div>
......@@ -129,14 +137,16 @@
"partner_id":"{{partnerData.id}}",
"name":"{{partnerData.display_name|safe}}",
"shift_type":"{{partnerData.shift_type}}",
"final_ftop_point":{{partnerData.final_ftop_point}},
"final_standard_point":{{partnerData.final_standard_point}},
"date_delay_stop":"{{partnerData.date_delay_stop}}",
"cooperative_state":"{{partnerData.cooperative_state}}",
"regular_shift_name":"{{partnerData.regular_shift_name}}",
"can_have_delay" : "{{partnerData.can_have_delay}}",
"makeups_to_do" : "{{partnerData.makeups_to_do}}",
"barcode_base" : "{{partnerData.barcode_base}}",
"street" : "{{partnerData.street|safe}}",
"street2" : "{{partnerData.street2|safe}}",
"street" : "{{partnerData.street}}",
"street2" : "{{partnerData.street2}}",
"zip" : "{{partnerData.zip}}",
"city" : "{{partnerData.city|safe}}",
"mobile" : "{{partnerData.mobile}}",
......@@ -154,7 +164,9 @@
"extra_shift_done": parseInt("{{partnerData.extra_shift_done}}", 10)
};
var block_actions_for_attached_people = '{{block_actions_for_attached_people}}';
var block_service_exchange_24h_before = '{{block_service_exchange_24h_before}}';
const canAddShift = {{canAddShift}};
const extension_duration = {{extension_duration}};
</script>
<script src="{% static "js/all_common.js" %}?v=1651853225"></script>
<script src="{% static "js/common.js" %}?v=1651853225"></script>
......
......@@ -18,7 +18,7 @@
<div id="suspended_content" class="shifts_exchange_page_content">
<h3>
J'ai <span class="makeups_nb"></span> rattrapage(s) à effectuer, je dois le(s) sélectionner pour pouvoir refaire mes courses.
J'ai 6 mois de délai pour le(s) rattraper.
J'ai {{extension_duration}} mois de délai pour le(s) rattraper.
</h3>
<h3>
Si besoin, je peux contacter le Bureau des membres via la rubrique "J'ai une demande" pour expliquer ma situation.
......
......@@ -15,9 +15,6 @@
( jusqu'au <span class="delay_date_stop"></span> )
</div>
<div id="member_status_action">
<a href="#" target="_blank" class="btn--warning unsuscribed_form_link">
J'accède au formulaire
</a>
<button type="button" class="btn--danger choose_makeups">
Je sélectionne mes rattrapages
</button>
......
{% extends "base.html" %}
{% load static %}
{% block additionnal_scripts %}
{% endblock %}
{% block content %}
<form enctype="multipart/form-data" action="/export_orders" method="post">
{% csrf_token %}
<label for="from">Entre :</label>
<input type="date" name="from">
<label for="to">et :</label>
<input type="date" name="to">
<input type="submit" class='btn--primary' value="OK">
</form>
<script src="{% static "js/all_common.js" %}?v=1651853225"></script>
{% endblock %}
\ No newline at end of file
......@@ -245,6 +245,7 @@
var add_all_left_is_good_prices = "{{ADD_ALL_LEFT_IS_GOOD_PRICES}}"
var add_products_pswd = "{{add_products_pswd}}"
var update_qty_pswd = "{{update_qty_pswd}}"
var allow_four_digits_in_reception_price = '{{allow_four_digits_in_reception_price}}';
</script>
<script src="{% static "js/all_common.js" %}?v=1651853225"></script>
<script src='{% static "js/barcodes.js" %}?v=1651853225'></script>
......
......@@ -59,7 +59,7 @@
</div>
<div class="col-3 center">
<label for="edition_input" id="edition_input_label">Qté : </label>
<input type="text" class="number input_small" id="edition_input" autocomplete="off" required>
<input type="number" class="input_small" id="edition_input" autocomplete="off" required>
<span id="product_uom"></span>
<i class="fa fa-undo" id="reset_to_previous_qty" style="display:none;"></i>
</div>
......
......@@ -24,6 +24,11 @@
<p><button type="submit">Connexion</button></p>
</form>
<p style="color:red;">{{msg}}</p>
{% if permanent_message %}
<div id="permanent_message">
{{permanent_message}}
</div>
{% endif %}
<!--<p><a href="oubli_pass">Mot de passe oublié</a></p>-->
<script>
try {
......
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