Commit eb1e61d0 by Damien Moulard

Merge branch 'adaptation_supercoop_supercafoutch' into 'dev_cooperatic'

Adaptation supercoop supercafoutch

See merge request !196
parents c805631f dba4831f
Pipeline #2399 passed with stage
in 1 minute 23 seconds
......@@ -90,16 +90,19 @@ class CagetteInventory(models.Model):
p[k] = ''
# Get shelf sortorder
if p['shelf_id'] is not False:
if p['shelf_id'] is not False and len(p['shelf_id']) > 0:
c = [['id', '=', p['shelf_id'][0]]]
f = ['id', 'sort_order']
res_sortorder = api.search_read('product.shelfs', c, f)
if res_sortorder:
p['shelf_sortorder'] = res_sortorder[0]['sort_order']
else:
p['shelf_sortorder'] = ''
res['data'] = pdts
except Exception as e:
coop_logger.error("get_custom_list_products -> Erreur lors de la récupération des produits : %s", str(e))
res['error'] = "Erreur lors de la récupération des produits (" + str(e) + ")"
return res
......
......@@ -5,7 +5,7 @@ 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 outils.common import MConfig
from datetime import datetime
......@@ -313,7 +313,8 @@ def admin(request):
""" Administration des membres """
template = loader.get_template('members/admin/index.html')
context = {'title': 'BDM',
'module': 'Membres'}
'module': 'Membres',
'admin_binome_active': getattr(settings, 'ADMIN_BINOME_ACTIVE', True),}
return HttpResponse(template.render(context, request))
def manage_makeups(request):
......@@ -494,14 +495,19 @@ def shift_subscription(request):
and delete all existing shifts EXCEPT makeups.
"""
res = {}
if CagetteUser.are_credentials_ok(request):
data = json.loads(request.body.decode())
data = json.loads(request.body.decode())
partner_id = int(data["partner_id"])
is_allowed = CagetteUser.are_credentials_ok(request)
if is_allowed is False:
credentials = CagetteMember.get_credentials(request, with_id = True)
if 'success' in credentials and credentials['success'] is True and credentials['id'] == partner_id:
is_allowed = True
if is_allowed is True:
partner_id = int(data["partner_id"])
shift_type = data["shift_type"]
if shift_type == 1:
# 1 = standard
shift_template_id = data["shift_template_id"]
shift_template_id = int(data["shift_template_id"])
else:
# 2 = ftop
......@@ -529,7 +535,6 @@ def shift_subscription(request):
)
res["unsubscribe_member"] = m.unsubscribe_member(changing_shift = True)
m.create_coop_shift_subscription(shift_template_id, shift_type)
# Return necessary data
......
......@@ -121,7 +121,7 @@ class CagetteMember(models.Model):
'id DESC')
@staticmethod
def get_credentials(request, external=False):
def get_credentials(request, external=False, with_id=False):
import hashlib
data = {}
......@@ -191,6 +191,8 @@ class CagetteMember(models.Model):
calc_token = hashlib.sha256(res[0]['create_date'].encode('utf-8')).hexdigest()
if calc_token == request.COOKIES['token']:
data['success'] = True
if with_id is True:
data['id'] = res[0]['id']
else:
data['failure'] = True
data['errnum'] = 3
......@@ -792,7 +794,7 @@ class CagetteMember(models.Model):
else:
cond.append(['is_associated_people', '=', False])
# 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')
......@@ -838,7 +840,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:
......@@ -851,6 +853,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:
......@@ -1267,409 +1272,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']
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['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 member 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']
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']
res = api.search_read('shift.registration', cond, fields)
ids = []
partner_ids = []
excluded_partner = []
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 = 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']))
# 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}
if update_shift_reg_result['update'] is True:
update_shift_reg_result['process_status_res'] = api.execute('res.partner','run_process_target_status', [])
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 CagetteUser(models.Model):
@staticmethod
......
......@@ -49,6 +49,7 @@ h1 .member_name {font-weight: bold;}
.members_list li.btn--inverse.late {background-color: #de9b00; color: white}
.members_list li.btn--inverse.both {background-color: #0275d8 ; color: white}
.members_list.done li.btn {pointer-events: none;}
#service_entry_success {font-size: x-large;}
#service_entry_success .explanations {margin: 25px 0; font-size: 18px;}
#service_entry_success .points,
......
......@@ -284,7 +284,7 @@ function create_pair(payload) {
data.responseJSON.errors.map(function(error) {
message += ('\n' + error);
return null;
});
}
......
......@@ -293,7 +293,7 @@ $(document).ready(function() {
let search_str = $('#search_member_input').val();
$.ajax({
url: '/members/search/' + search_str,
url: `/members/search/${search_str}?search_type=shift_template_data`,
dataType : 'json',
success: function(data) {
$('#partner_data_area').hide();
......
......@@ -231,7 +231,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 = [];
......
......@@ -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,7 @@ 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);
local_in_process = getLocalInProcess();
if (getLocalInProcess().length > 0) {
empty_waiting_local_processes();
......@@ -271,11 +271,13 @@ function store_new_coop(event) {
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;
}
if (associated_data_ok === false) errors.push("Le membre 'titulaire' du binôme n'est pas défini");
......@@ -318,7 +320,7 @@ function store_new_coop(event) {
}
}
});
}
}
......@@ -361,6 +363,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';
......@@ -726,7 +729,7 @@ function display_possible_members() {
/**
* Search for members to associate a new member with an old one.
* Search for members to associate a new member with an old one.
*/
function searchMembersForAssociate() {
let search_str = $('#search_member_input').val();
......
......@@ -41,6 +41,7 @@ var coop_info = $('.coop-info');
var service_data = null;
const missed_begin_msg = $('#missed_begin_msg').html();
const current_shift_process_data_actions = $('#current_shift_process_data_actions');
let no_pict_msg = $('#no-picture-msg');
......@@ -70,6 +71,12 @@ var html_elts = {
var chars = []; //input chars buffer
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();
current_displayed_member = member;
......@@ -282,6 +289,9 @@ function fill_service_entry(s) {
// if (typeof s.late != "undefined" && s.late == true) {
// m_list = '<ul class="members_list late">';
// }
if (s.state == 'done') {
m_list = '<ul class="members_list done">';
}
$.each(s.members, function(i, e) {
var li_class = "btn";
var li_data = "";
......@@ -304,6 +314,9 @@ function fill_service_entry(s) {
} else {
li_data = ' data-rid="'+e.id+'" data-mid="'+e.partner_id[0]+'"';
}
if (s.state == 'done') {
li_data += ' disabled ';
}
m_list += '<li class="'+li_class+'" '+li_data+'>';
m_list += e.partner_id[1];
m_list += '</li>';
......@@ -311,6 +324,46 @@ function fill_service_entry(s) {
m_list += '</ul>';
}
if (coop_is_connected()) {
// Add shift process data
reset_shift_process_actions_zone();
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');
current_shift_process_data_actions.append(btn);
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');
try {
$.ajax({
url: '/members/record_shift_absences/' + s.id,
dataType : 'json'
})
.done(function(rData) {
if (typeof rData.res.update !== "undefined" && rData.res.update == true) {
enqueue_message_for_next_loading("Données de présences traitées.");
location.reload();
} else {
closeModal();
btn.removeAttr('disabled');
alert(JSON.stringify(rData.res));
}
});
} catch (e) {
console.log(e);
}
}, 'Confirmer');
});
} else {
current_shift_process_data_actions.append("<em>Traitement des présences : " + s.state + "</em>");
}
current_shift_process_data_actions.show();
}
rattrapage_ou_volant = null;
shift_members.html(m_list);
rattrapage_wanted.show();
......@@ -401,6 +454,8 @@ function get_service_entry_data() {
})
.done(function(rData) {
info_place.text('');
reset_shift_process_actions_zone();
var page_title = pages.service_entry.find('h1');
page_title.text('Qui es-tu ?');
......
......@@ -43,6 +43,7 @@ urlpatterns = [
url(r'^services_at_time/([0-9TZ\-\: \.]+)/([0-9\-]+)$', views.services_at_time),
url(r'^service_presence/$', views.record_service_presence),
url(r'^record_absences/?([0-9\-\ \:]*)$', views.record_absences),
url(r'^record_shift_absences/?([0-9]+)$', views.record_shift_absences),
url(r'^close_ftop_service$', views.close_ftop_service),
url(r'^get_credentials$', views.get_credentials),
url(r'^remove_data_from_couchdb$', views.remove_data_from_CouchDB),
......
......@@ -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
from shifts.models import CagetteServices, CagetteService
from outils.forms import GenericExportMonthForm
import datetime
......@@ -105,6 +105,7 @@ def inscriptions(request, type=1):
'show_ftop_button': getattr(settings, 'SHOW_FTOP_BUTTON', True),
'db': settings.COUCHDB['dbs']['member'],
'ASSOCIATE_MEMBER_SHIFT' : getattr(settings, 'ASSOCIATE_MEMBER_SHIFT', ''),
'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,
}
......@@ -347,6 +348,10 @@ def easy_validate_shift_presence(request):
def record_absences(request, date):
return JsonResponse({'res': CagetteServices.record_absences(date)})
def record_shift_absences(request, id):
shift = CagetteService(id)
return JsonResponse({'res': shift.record_absences(request)})
def close_ftop_service(request):
"""Close the closest past FTOP service"""
return JsonResponse({'res': CagetteServices.close_ftop_service()})
......
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
......
......@@ -39,6 +39,7 @@
justify-content: space-between;
}
@media screen and (max-width:992px) {
#calendar_top_info {
display: flex;
......
var calendar = null;
function init_my_shifts_tile() {
if (incoming_shifts.length === 0) {
$("#home_tile_my_services #home_incoming_services").text("Aucun service à venir...");
......@@ -20,6 +22,97 @@ 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;
openModal(
msg,
function() {
setTimeout(openModal, 300); // to show something happened , work in process
let data = {
partner_id: parseInt(partner_data.partner_id, 10),
shift_type: 1, //force to standard
shift_template_id: shift_t_id,
unsubscribe_first: true
};
$.ajax({
type: 'POST',
url: '/members/shift_subscription',
data: JSON.stringify(data),
dataType:"json",
traditional: true,
contentType: "application/json; charset=utf-8",
success: function(data) {
stdata = data.shift_template;
partner_data.regular_shift_name = stdata.name;
partner_data.shift_type = "standard";
init_my_info_data();
location.reload();
},
error: function(err_data) {
if (
err_data.status == 409
&& typeof (err_data.responseJSON) != "undefined"
&& err_data.responseJSON.code === "makeup_found"
) {
let modal_template = $("#modal_error_change_shift_template");
modal_template.find(".shift_template_name").text(shift_template_name);
closeModal();
openModal(
modal_template.html(),
() => {},
"Compris !",
true,
false
);
} else {
err = {
msg: "erreur serveur lors de l'inscription du membre au créneau",
ctx: 'members_space.shift_subscrition'
};
report_JS_error(err, 'members_space');
closeModal();
$.notify("Une erreur est survenue lors de l'inscription au créneau.", {
globalPosition:"top right",
className: "error"
});
}
}
});
},
'Valider',
true, // modal closes after validation
true,
edit_shift_template_registration // on cancel , reload calendar
);
}
function edit_shift_template_registration() {
let external = true;
if (calendar == null) calendar = $('#modal-calendar-choice').clone();
if ($('#modal-calendar-choice').html().length > 0) {
$('#modal-calendar-choice').empty();
}
calendar.find('.oddeven_selector').empty();
displayMsg(calendar.html());
$('#week_types').find('input')
.change(() => {
filter_weeks(external);
});
retrieve_and_draw_shift_tempates(external);
}
function init_home() {
$("#go_to_shifts_calendar").on("click", () => {
goto('echange-de-services');
......@@ -37,6 +130,13 @@ function init_home() {
goto('faq');
});
$(".member_shift_name_area").on("click", ".fa-edit", (e) => {
$('#week_types').find('input')
.change(filter_weeks);
e.preventDefault();
edit_shift_template_registration();
});
if (partner_data.is_in_association === false) {
$("#home .member_associated_partner_area").hide();
} else {
......
var calendar = null,
selected_shift = null,
vw = null;
vw = null,
adding_mode = false;
/* - Logic */
......@@ -51,6 +52,9 @@ function add_or_change_shift(new_shift_id) {
if (selected_shift === null) {
tUrl = '/shifts/add_shift';
if (partner_data.makeups_to_do > 0) {
tData += '&is_makeup=1';
}
} else {
tUrl = '/shifts/change_shift';
tData = tData + '&idOldShift='+ selected_shift.shift_id[0] +'&idRegister=' + selected_shift.id;
......@@ -121,6 +125,11 @@ function add_or_change_shift(new_shift_id) {
`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. ` +
......@@ -143,6 +152,8 @@ function add_or_change_shift(new_shift_id) {
}, 300);
}
});
adding_mode = false;
$('#start_adding_shift').prop('disabled', false);
}
return null;
......@@ -269,7 +280,7 @@ function offer_extra_shift() {
timeout: 3000,
success: function() {
partner_data.extra_shift_done -= 1;
$("#can_delete_future_registrations_area").hide();
$(".delete_registration_button").off();
$(".delete_registration_button").hide();
......@@ -315,6 +326,7 @@ function init_shifts_list() {
if (!can_exchange_shifts()) {
shift_line_template.find(".selectable_shift_line").addClass("btn");
shift_line_template.find(".checkbox").prop("disabled", "disabled");
$('#start_adding_shift').prop('disabled', true);
} else {
if (shift.is_makeup==true) {
shift_line_template.find(".selectable_shift_line").addClass("btn--warning");
......@@ -577,12 +589,28 @@ function init_calendar_page() {
"Valider"
);
} else if (selected_shift === null && can_exchange_shifts()) {
/* could exchange shift but no old shift selected */
openModal(
"Je dois sélectionner un service à échanger.",
closeModal,
"J'ai compris"
);
if (adding_mode === false) {
/* could exchange shift but no old shift selected */
openModal(
"Je dois sélectionner un service à échanger.",
closeModal,
"J'ai compris"
);
} 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(
modal_template.html(),
() => {
add_or_change_shift(new_shift_id);
},
"Valider"
);
}
} else if (should_select_makeup()) {
/* choose a makeup service */
// Check if selected new shift is in less than 6 months
......@@ -730,14 +758,15 @@ 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();
let shift_id = $(this).closest(".shift_line_container")
.attr('id')
.split('_')[2];
openModal(
`<p>Je m'apprête à supprimer ma présence au service du <b>${shift_name}</b></p>`,
() => {
......@@ -747,7 +776,7 @@ function init_delete_registration_buttons() {
false
);
});
$(".delete_registration_button").css('display', 'flex');
}
}
......@@ -811,6 +840,19 @@ function init_shifts_exchange() {
init_calendar_page();
}
$('#start_adding_shift').click((c) => {
openModal(
"<p>Je souhaite sélectionner un service supplémentaire.</p>",
() => {
$(c.target).prop('disabled', true);
adding_mode = true;
closeModal();
},
"Confirmer",
false
);
});
$(window).smartresize(function() {
// only apply if a width threshold is passed
if (
......
......@@ -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/' : '/';
......
......@@ -29,7 +29,8 @@ 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),
}
template = loader.get_template('members_space/index.html')
......@@ -120,6 +121,7 @@ def index(request, exception=None):
m = CagetteMembersSpace()
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)
context['partnerData'] = partnerData
......@@ -130,6 +132,8 @@ def index(request, exception=None):
if hasattr(settings, 'SHIFT_EXCHANGE_DAYS_TO_HIDE'):
days_to_hide = settings.SHIFT_EXCHANGE_DAYS_TO_HIDE
context['daysToHide'] = days_to_hide
can_add_shift = getattr(settings, 'CAN_ADD_SHIFT', False)
context['canAddShift'] = "true" if can_add_shift is True else "false"
msettings = MConfig.get_settings('members')
context['forms_link'] = msettings['forms_link']['value'] if 'forms_link' in msettings else ''
......@@ -169,9 +173,20 @@ def home(request):
Consequently, the front-end url should be unknown from the server so the user is redirected to the index,
then the front-end index will call this endpoint to load the home page
"""
template = loader.get_template('members_space/home.html')
template = loader.get_template(getattr(settings, 'MEMBERS_SPACE_HOME_TEMPLATE', 'members_space/home.html'))
coop_can_change_shift_template = getattr(settings, 'COOP_CAN_CHANGE_SHIFT_TEMPLATE', False)
if coop_can_change_shift_template is True:
# make further investigation only if COOP_CAN_CHANGE_SHIFT_TEMPLATE is True
if 'id' in request.COOKIES:
partner_id = request.COOKIES['id']
cs = CagetteShift()
partnerData = cs.get_data_partner(partner_id)
if partnerData['cooperative_state'] == "unsubscribed":
coop_can_change_shift_template = False
context = {
'title': 'Espace Membres',
'coop_can_change_shift_template': coop_can_change_shift_template,
'max_begin_hour': settings.MAX_BEGIN_HOUR,
}
# Get messages to display
msettings = MConfig.get_settings('members')
......@@ -186,7 +201,8 @@ def my_info(request):
template = loader.get_template('members_space/my_info.html')
context = {
'title': 'Mes Infos',
'understand_my_status': getattr(settings, 'MEMBERS_SPACE_SHOW_UNDERSTAND_MY_STATUS', True)
'understand_my_status': getattr(settings, 'MEMBERS_SPACE_SHOW_UNDERSTAND_MY_STATUS', True),
'understand_my_status_template': getattr(settings, 'MEMBERS_SPACE_UNDERSTAND_MY_STATUS_TEMPLATE', "members_space/understand_my_status.html")
}
return HttpResponse(template.render(context, request))
......@@ -203,6 +219,7 @@ def shifts_exchange(request):
template = loader.get_template('members_space/shifts_exchange.html')
context = {
'title': 'Échange de Services',
'canAddShift': getattr(settings, 'CAN_ADD_SHIFT', False)
}
return HttpResponse(template.render(context, request))
......
......@@ -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 */
......@@ -310,10 +310,11 @@ function compute_and_affect_product_supplier_quantities(coeff, days) {
const daily_conso = product.daily_conso;
let purchase_package_qty_for_coverage = compute_purchase_qty_for_coverage(product, coeff, stock, incoming_qty, daily_conso, 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
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;
......@@ -491,7 +492,7 @@ function check_products_data() {
}
report_JS_error(err, 'orders');
alert(`Erreur lors de la vérification des données des articles. Certaines données peuvent être erronées`);
$('.notifyjs-wrapper').trigger('notify-hide');
// Don't block process if this call fails
resolve();
......@@ -559,15 +560,15 @@ function update_product_ref(input_el, p_id, p_index) {
handle_unauthorize();
} else {
let msg = "erreur serveur lors de la sauvegarde de la référence";
msg += ` (product_tmpl_id: ${product.id}`;
err = {msg: msg, ctx: 'update_product_ref'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
alert('Erreur lors de la sauvegarde de la référence dans Odoo. Veuillez recharger la page et ré-essayer plus tard.');
}
}
......@@ -637,7 +638,7 @@ function add_supplier() {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
closeModal();
alert('Erreur lors de la récupération des produits, réessayer plus tard.');
}
......@@ -741,15 +742,15 @@ function save_supplier_product_association(product, supplier, cell) {
handle_unauthorize();
} else {
let msg = "erreur serveur lors de la sauvegarde de l'association product/supplier";
msg += ` (product_tmpl_id: ${product.id}; supplier_id: ${supplier.id})`;
err = {msg: msg, ctx: 'save_supplier_product_association'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
closeModal();
alert('Erreur lors de la sauvegarde de l\'association. Veuillez ré-essayer plus tard.');
}
......@@ -800,13 +801,13 @@ function end_supplier_product_association(product, supplier) {
} else {
let msg = "erreur serveur lors de la suppression de l'association product/supplier".
msg += ` (product_tmpl_id: ${product.id}; supplier_id: ${supplier.id})`;
err = {msg: msg, ctx: 'end_supplier_product_association'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
closeModal();
alert('Erreur lors de la suppression de l\'association. Veuillez ré-essayer plus tard.');
}
......@@ -980,13 +981,13 @@ function commit_actions_on_product(product, inputs) {
} else {
let msg = "erreur serveur lors de la sauvegarde".
msg += ` (product_tmpl_id: ${product.id})`;
err = {msg: msg, ctx: 'commit_actions_on_product'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
try {
if (data.responseJSON.code === "archiving_with_incoming_qty") {
alert("Ce produit a des quantités entrantes, vous ne pouvez pas l'archiver.");
......@@ -998,7 +999,7 @@ function commit_actions_on_product(product, inputs) {
} catch (error) {
alert('Erreur lors de la sauvegarde des données. Veuillez ré-essayer plus tard.');
}
check_products_data()
.then(() => {
update_cdb_order();
......@@ -1079,14 +1080,14 @@ function generate_inventory() {
} else {
$('#do_inventory').empty()
.append(`Faire un inventaire`);
let msg = "erreur serveur lors de la création de l'inventaire".
let msg = "erreur serveur lors de la création de l'inventaire",
err = {msg: msg, ctx: 'generate_inventory'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
alert("Erreur lors de la création de l'inventaire. Réessayez plus tard.");
}
}
......@@ -1362,13 +1363,13 @@ function create_orders() {
handle_unauthorize();
} else {
let msg = "erreur serveur lors de la création des product orders";
err = {msg: msg, ctx: 'save_supplier_product_association', data: orders_data};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
closeModal();
alert('Erreur lors de la création des commandes. Veuillez ré-essayer plus tard.');
}
......@@ -2826,7 +2827,7 @@ $(document).ready(function() {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'orders');
closeModal();
alert('Erreur lors de la récupération des fournisseurs, rechargez la page plus tard');
}
......
......@@ -123,7 +123,7 @@ def get_orders_attachment(request):
for item in res:
if 'error' in item:
return JsonResponse(res, status=500)
return JsonResponse(res, status=500, safe=False)
return JsonResponse({'res': res})
......@@ -133,6 +133,7 @@ def export_one(request, oid):
oid = int(oid)
order = Order(oid)
order_data = order.export()
if ('success' in order_data) and (order_data['success'] is True):
now = datetime.datetime.now()
taxes = 0
......@@ -176,7 +177,7 @@ def export_one(request, oid):
# return response
except Exception as e:
msg = str(e)
coop_logger.error("Order export error : %s", msg)
return JsonResponse({"msg": msg}, safe=False)
def export_regex(request, string):
......@@ -258,4 +259,4 @@ def print_product_labels(request):
res['errors'].append(pres)
except Exception as e:
res['error_ext'] = str(e)
return JsonResponse({'res': res}, safe=False)
\ No newline at end of file
return JsonResponse({'res': res}, safe=False)
......@@ -131,6 +131,10 @@
La Cagette use False to implement custom rules
- CAN_CREATE_BINOME = True (by default)
If set to True, in new member creation form, a member can be selected to be associated with.
- ASSOCIATE_MEMBER_SHIFT = ''
Id number of the associate shift template
......@@ -326,6 +330,11 @@
- MEMBERS_SPACE_FAQ_TEMPLATE = None
If set to None, "FAQ menu" will not be shown. To use a custom content add a template and set it's relative path
- MEMBERS_SPACE_HOME_TEMPLATE = 'members_space/supercafoutch/home.html'
If not set, 'members_space/home.html' (la Cagette)
- MEMBERS_SPACE_SHOW_UNDERSTAND_MY_STATUS = False
By default, is True. If False, tile showing explanations is not shown
......@@ -333,6 +342,10 @@
- BLOCK_ACTIONS_FOR_ATTACHED_PEOPLE = False
Attached people can or not change his services
- CAN_ADD_SHIFT = True
By default, False. Set if coop can or not add shifts in their memberspace calendar
### Reception
- RECEPTION_ADD_ADMIN_MODE = True
......@@ -410,8 +423,31 @@
- START_DATE_FOR_SHIFTS_HISTORY = "2018-01-01"
- AMNISTIE_DATE = "2021-11-24 00:00:00"
In members_space history display a special activity about amnistie
- MEMBERS_SPACE_FAQ_TEMPLATE = 'members_space/supercoop/faq.html'
Set alternative template for FAQ
- MEMBERS_SPACE_HOME_TEMPLATE = 'members_space/supercoop/home.html'
Set alternative template for home page
- MEMBERS_SPACE_UNDERSTAND_MY_STATUS_TEMPLATE = 'members_space/supercoop/understand_my_status.html'
Set alternative template for understand my status
- SHOW_ABCD_CALENDAR_TAB = False
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
### BDM Admin
......@@ -419,6 +455,10 @@
If True, in BDM Admin manage shift template, on the calendar when subscribing a partner to a shift, "Volant" button is included
- ADMIN_BINOME_ACTIVE = False
By defaut, True. Show "Gestion des binômes" in bdm admin
### Miscellious
- EXPORT_COMPTA_FORMAT = 'Quadratus'
......
......@@ -23,5 +23,6 @@ def context_setting(request):
"""adding settings variable to context (can be overloaded in views)."""
context = {'odoo': settings.ODOO['url'],
'app_env': getattr(settings, 'APP_ENV', "prod"),
'company_code': getattr(settings, 'COMPANY_CODE', '')}
'company_code': getattr(settings, 'COMPANY_CODE', ''),
'favicon_url': getattr(settings, 'FAVICON_URL', '/static/favicon.ico')}
return context
\ No newline at end of file
......@@ -23,7 +23,7 @@ var volant = null;
function get_displayed_weeks() {
displayed_weeks = [];
week_types.find('.selected_weeks :checked').each(function() {
$('#week_types').find('.selected_weeks :checked').each(function() {
displayed_weeks.push($(this).val());
});
......@@ -32,7 +32,7 @@ function get_displayed_weeks() {
function get_shift_name(s_data) {
var shift_name = "Inconnu";
if (typeof ASSOCIATE_MEMBER_SHIFT == "undefined") ASSOCIATE_MEMBER_SHIFT = "";
if (s_data && s_data.week) {
shift_name = weeks_name[s_data.week];
if (s_data.type == 2 && typeof manage_ftop != "undefined" && manage_ftop == true && s_data.id != ASSOCIATE_MEMBER_SHIFT) {
......@@ -112,7 +112,7 @@ function select_shift_among_compact(event, clicked_item = null, subscribe = true
var worst_score = 1;
displayed_weeks = get_displayed_weeks();
var place_cond = sc_lat.find('.highlighted').data('select');
var place_cond = sc_lat.find('.highlighted').data('select') || "";
$.each(shift_templates, function(i, e) {
if (e.data) {
......@@ -135,14 +135,23 @@ function select_shift_among_compact(event, clicked_item = null, subscribe = true
}
});
if (selected && subscribe === true)
subscribe_shift(selected);
if (selected && subscribe === true){
if (typeof partner_data !== "undefined" && typeof partner_data.barcode_base !== "undefined") {
//click is coming from memberspace
process_asked_shift_template_change(selected);
} else {
subscribe_shift(selected);
}
}
return selected
}
function draw_table(begin_hours, callback) {
if (shift_table.length == 0) shift_table = $('#shift_choice table');
shift_table.find('tbody tr').remove();
begin_hours.sort();
var days = [
......@@ -172,14 +181,15 @@ function draw_table(begin_hours, callback) {
}
function draw_shift_templates() {
function draw_shift_templates(external) {
if (typeof external !== "undefined" && external == true) shift_table = $('#shift_choice table');
var existing_shifts = shift_table.find('.shift');
existing_shifts.off("click", single_shift_click);
existing_shifts.off("click", select_shift_among_compact);
existing_shifts.remove();
var place_cond = sc_lat.find('.highlighted').data('select');
var place_cond = sc_lat.find('.highlighted').data('select') || "";
//warning MAG_NAME should correspond to data.place value of shift_templates objects
dispo = 0;
......@@ -199,7 +209,7 @@ function draw_shift_templates() {
if (e.data.begin <= max_begin_hour && e.data.max > 0 && (place_cond == 'both' || place_cond == place)) {
keep_it = true;
}
}
if (keep_it == true && displayed_weeks.indexOf(e.data.week.toString()) > -1) {
var known_begin_hour = false;
......@@ -272,7 +282,6 @@ function draw_shift_templates() {
boxes[e.data.day+'_'+e.data.begin]['profil'] = profile;
}
}
}
}
......@@ -280,6 +289,7 @@ function draw_shift_templates() {
}
});
if (type == 1) {
shift_table.find('.shift').on("click", single_shift_click);
}
......@@ -311,16 +321,19 @@ function draw_shift_templates() {
sc_lat.find('.info').html(dispo + ' places disponibles<br />(/'+max+')');
closeModal();
if (typeof external == "undefined") {
closeModal();
}
});
}
function retrieve_and_draw_shift_tempates() {
openModal();
function retrieve_and_draw_shift_tempates(external) {
if (shift_table.length == 0) shift_table = $('#shift_choice table');
if (!external) {
openModal();
}
shift_table.find('.shift').remove();
......@@ -333,7 +346,7 @@ function retrieve_and_draw_shift_tempates() {
if (e.data.type == 2 && volant == null) {
// has comitee shift
if (committees_shift_id !== undefined && committees_shift_id !== "None") {
if (typeof committees_shift_id !== "undefined" && committees_shift_id !== "None") {
if (e.data.id == parseInt(committees_shift_id)) {
volant = e.data.id
}
......@@ -342,33 +355,39 @@ function retrieve_and_draw_shift_tempates() {
}
}
});
dbc.allDocs({include_docs: true, descending: true}, function(err, resp) {
if (err) {
return console.log(err);
}
$.each(resp.rows, function(i, e) {
if (e.doc.shift_template && typeof(e.doc.shift_template.data) != "undefined") {
try {
if (typeof shift_templates[e.doc.shift_template.data.id]!= "undefined")
shift_templates[e.doc.shift_template.data.id]['data']['reserved'] += 1;
} catch (ec) {
console.log(ec);
}
if (typeof dbc !== "undefined") {
dbc.allDocs({include_docs: true, descending: true}, function(err, resp) {
if (err) {
return console.log(err);
}
$.each(resp.rows, function(i, e) {
if (e.doc.shift_template && typeof(e.doc.shift_template.data) != "undefined") {
try {
if (typeof e.doc.odoo_id == "undefined" || isNaN(e.doc.odoo_id)) {
if (typeof shift_templates[e.doc.shift_template.data.id] != "undefined")
shift_templates[e.doc.shift_template.data.id]['data']['reserved'] += 1;
}
} catch (ec) {
console.log(ec);
}
}
});
draw_shift_templates(external);
});
draw_shift_templates();
});
} else {
draw_shift_templates(external);
}
});
}
function filter_weeks() {
function filter_weeks(external) {
var clicked = $(this);
var week_types = $('#week_types');
var parent_div = clicked.closest('div');
var w1 = week_types.find('#dw1');
var w2 = week_types.find('#dw2');
......@@ -411,8 +430,7 @@ function filter_weeks() {
if (!w2.is(':checked') || !w4.is(':checked')) {
$('#odd_weeks').prop('checked', false);
}
draw_shift_templates();
draw_shift_templates(external);
}
function shift_loc_selection() {
......
......@@ -88,7 +88,7 @@ class FieldsView(View):
u"""Nous allons retourner les attributs Odoo correspondants."""
entity = request.POST.get("entity", "")
fields = []
if len(entity) > 1:
if getattr(settings, 'APP_ENV', "prod") == "dev" and len(entity) > 1:
api = OdooAPI()
fields = api.get_entity_fields(entity)
template = loader.get_template('common/entity_fields.html')
......
......@@ -1979,10 +1979,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();
}
}
......@@ -2024,8 +2026,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")
......@@ -2045,15 +2049,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({
......
......@@ -591,4 +591,4 @@ class Shelfs(models.Model):
except Exception as e:
res['error'] = str(e)
coop_logger.error("Rayons, make_products_shelf_links : %s", str(e))
return res
\ No newline at end of file
return res
......@@ -17,6 +17,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]]
......@@ -169,8 +250,8 @@ class CagetteShift(models.Model):
"origin": 'memberspace',
"is_makeup": data['is_makeup'],
"state": 'open'}
if shift_type == "standard" and data['is_makeup'] is not True:
fieldsDatas['template_created'] = 1 # It's not true but otherwise, presence add 1 standard point, which is not wanted
if (shift_type == "standard" and data['is_makeup'] is not True) or shift_type == "ftop":
fieldsDatas['template_created'] = 1 # It's not true but otherwise, presence add 1 standard point , which is not wanted
st_r_id = self.o_api.create('shift.registration', fieldsDatas)
except Exception as e:
......@@ -373,8 +454,536 @@ class CagetteShift(models.Model):
def member_can_have_delay(self, partner_id):
""" Can a member have a delay? """
return self.o_api.execute('res.partner', 'can_have_extension', [partner_id])
answer = False
try:
answer = self.o_api.execute('res.partner', 'can_have_extension', [partner_id])
except Exception as e:
coop_logger.error("member_can_have_delay : %s", str(e))
return answer
def update_counter_event(self, fields):
""" Add/remove points """
return self.o_api.create('shift.counter.event', fields)
\ No newline at end of file
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'}
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
......@@ -215,6 +215,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:
......@@ -281,8 +286,11 @@ def add_shift(request):
"idPartner": int(request.POST['idPartner']),
"idShift":int(request.POST['idNewShift']),
"shift_type":request.POST['shift_type'],
"is_makeup":True
"is_makeup": False
}
if 'is_makeup' in request.POST and request.POST['is_makeup'] == "1":
data['is_makeup'] = True
#Insertion du nouveau shift
st_r_id = False
......
......@@ -365,7 +365,17 @@ class CagetteStock(models.Model):
return res
@staticmethod
def get_valuable_stock():
articles = []
try:
api = OdooAPI()
cond = [['qty_available','>', 0], ['active', '=', True]]
fields = ["barcode", "display_name", "qty_available", "standard_price"]
articles = api.search_read('product.product', cond, fields, 1000000)
except Exception as e:
coop_logger.error("Erreur get_valuable_stock : %s", str(e))
return articles
def set_test():
o_api = OdooAPI()
......
$(document).ready(function() {
table_article = $('#tableArticle').DataTable({
"ajax": {
"url": "get_valuable_stock",
"data": ""
},
"columns":[
{data:"barcode", "title":"Code-barre", "width": "50%"},
{data:"display_name", "title":"Article", "width": "50%"},
{data:"qty_available", "title":"Stock", "width":"15%"
},
{data:"standard_price", "title":"Prix achat", "width":"15%"
}
],
"searching": true,
"order": [
[
2,
"desc"
]
],
"iDisplayLength": 50,
"language": {
"emptyTable": "Pas de donnée",
"info": "Affiché : lignes _START_ à _END_ sur _TOTAL_",
"infoEmpty": "Affiché : 0 ligne",
"infoFiltered": "(filtré de _MAX_ lignes au total)",
"thousands": ",",
"lengthMenu": "Afficher _MENU_ lignes",
"loadingRecords": "Loading...",
"processing": "Processing...",
"search": "Rechercher un article :",
"searchPlaceholder": "Référence, code-barre",
"zeroRecords": "Aucun résultat",
"paginate": {
"first": "Premier",
"last": "Dernier",
"next": "Suivant",
"previous": "Precedant"
},
"aria": {
"sortAscending": ": activate to sort column ascending",
"sortDescending": ": activate to sort column descending"
}
},
buttons: [
{
extend: 'excelHtml5',
text: 'Export en Excel',
className: 'btn--primary btn_export'
},
],
dom: '<lr<t>ip><"clear"><B>',
});
});
\ No newline at end of file
......@@ -46,4 +46,8 @@ urlpatterns = [
url(r'^get_saleWitheNotSale', views.get_saleWitheNotSale),
url(r'^get_test', views.get_test),
# Values
url(r'^stockValues', views.stockValues),
url(r'^get_valuable_stock', views.get_valuable_stock),
]
......@@ -550,8 +550,16 @@ def get_saleWitheNotSale(request):
return JsonResponse({"data":lArticleSale}, safe=True)
def stockValues(request):
"""Page valeurs du stock (quantités positives)."""
context = {'title': 'Stock (quantités positives valorisées)'}
template = loader.get_template('stock/stock_values.html')
return HttpResponse(template.render(context, request))
def get_valuable_stock(request):
articles = CagetteStock.get_valuable_stock()
return JsonResponse({"data":articles}, safe=True)
def get_test(request):
res = CagetteStock.get_sale_qty_by_from(1)
return JsonResponse({"data":res}, safe=False)
......@@ -14,7 +14,7 @@
{% endfor %}
{% endif %}
<link rel="shortcut icon" type="image/png" href="{% static "favicon.ico" %}"/>
<link rel="shortcut icon" type="image/png" href="{{favicon_url}}"/>
{% block additionnal_css %}{% endblock %}
<script type="text/javascript">
// Prevent back page
......
......@@ -29,10 +29,12 @@
Gestion des présences
<span class="management_type_button_icons"><i class="fas fa-arrow-right"></i></span>
</button><br>
{% if admin_binome_active %}
<button type="button" class="btn--primary management_type_button" id="manage_attached_button">
Gestion des binômes
<span class="management_type_button_icons"><i class="fas fa-arrow-right"></i></span>
</button><br>
{% endif %}
<button type="button" class="btn--primary management_type_button" id="manage_regular_shifts_button">
Gestion des créneaux
<span class="management_type_button_icons"><i class="fas fa-arrow-right"></i></span>
......
......@@ -79,7 +79,7 @@
</div>
</div>
<div id="modal_error_change_shift_template">
<h3 class="error_modal_title""">Action impossible</h3>
<h3 class="error_modal_title">Action impossible</h3>
<p>
Le ou la membre est inscrit.e à un rattrapage sur le créneau choisi (<span class="shift_template_name"></span>), cela empêche de l'inscrire sur ce créneau.
</p>
......
......@@ -141,7 +141,9 @@
<div class="col-2 row-2">
<a class="btn btn--primary" data-next="first_page" >Retour accueil</a>
</div>
<div class="col-2 row-2"></div>
<div class="col-2 row-2 txtcenter">
<div id="current_shift_process_data_actions" style="display:none;"></div>
</div>
<div class="col-2 row-2 login_area">
{% include "common/conn_admin.html" %}
</div>
......
......@@ -79,16 +79,18 @@
</p>
{% endif %}
{% if ASSOCIATE_MEMBER_SHIFT %}
{% if can_create_binome %}
<p id="add_binome" >+ Binomes (facultatif)</p>
<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
</div>
{% if ASSOCIATE_MEMBER_SHIFT %}
<div id="new_member_choice" class="member_choice">
A mettre en binome avec un.e nouveau membre
</div>
{% endif %}
</div>
<div id="existing_member_choice_action" style="display:none;">
......@@ -110,6 +112,7 @@
</div>
</div>
</div>
{% if ASSOCIATE_MEMBER_SHIFT %}
<div id="new_member_choice_action" style="display:none;">
<div >
<div>
......@@ -117,6 +120,7 @@
</div>
</div>
</div>
{% endif %}
</div>
{% endif %}
<div>
......
<table class="table">
<caption>Calendrier fixe</caption>
<thead>
<tr>
<th scope="col" class="firstcol"></th>
<th scope="col">Lundi</th>
<th scope="col">Mardi</th>
<th scope="col">Mercredi</th>
<th scope="col">Jeudi</th>
<th scope="col">Vendredi</th>
<th scope="col">Samedi</th>
{% if open_on_sunday %}
<th scope="col">Dimanche</th>
{% endif %}
</tr>
</thead>
<tbody>
</tbody>
</table>
<div id="week_types" class="clearfix">
<div class="oddeven_selector fl">
<input type="checkbox" class="switch" id="even_weeks" value='0' checked><label for="even_weeks">Paires</label>
<input type="checkbox" class="switch" id="odd_weeks" value='1' checked><label for="odd_weeks">Impaires</label>
</div>
<div class="selected_weeks fl">
<input type="checkbox" class="switch" id="dw1" value='1' checked><label for="c1">A</label>
<input type="checkbox" class="switch" id="dw2" value='2' checked><label for="c2">B</label>
<input type="checkbox" class="switch" id="dw3" value='3' checked><label for="c3">C</label>
<input type="checkbox" class="switch" id="dw4" value='4' checked><label for="c4">D</label>
</div>
</div>
{% if type and type == 1 %}
<!--<div class="clearfix mtop25">Le trait <span class="alert">rouge</span> signale que le créneau est rempli à moins de 75%</div>-->
{% endif %}
\ No newline at end of file
......@@ -12,41 +12,7 @@
<div class="info"></div>
</div>
<div class="col-5 main_content">
<table class="table">
<caption>Calendrier fixe</caption>
<thead>
<tr>
<th scope="col" class="firstcol"></th>
<th scope="col">Lundi</th>
<th scope="col">Mardi</th>
<th scope="col">Mercredi</th>
<th scope="col">Jeudi</th>
<th scope="col">Vendredi</th>
<th scope="col">Samedi</th>
{% if open_on_sunday %}
<th scope="col">Dimanche</th>
{% endif %}
</tr>
</thead>
<tbody>
</tbody>
</table>
<div id="week_types" class="clearfix">
<div class="oddeven_selector fl">
<input type="checkbox" class="switch" id="even_weeks" value='0' checked><label for="even_weeks">Paires</label>
<input type="checkbox" class="switch" id="odd_weeks" value='1' checked><label for="odd_weeks">Impaires</label>
</div>
<div class="selected_weeks fl">
<input type="checkbox" class="switch" id="dw1" value='1' checked><label for="c1">A</label>
<input type="checkbox" class="switch" id="dw2" value='2' checked><label for="c2">B</label>
<input type="checkbox" class="switch" id="dw3" value='3' checked><label for="c3">C</label>
<input type="checkbox" class="switch" id="dw4" value='4' checked><label for="c4">D</label>
</div>
</div>
{% if type and type == 1 %}
<!--<div class="clearfix mtop25">Le trait <span class="alert">rouge</span> signale que le créneau est rempli à moins de 75%</div>-->
{% endif %}
{% include "members/shift_template_calendar.html" %}
</div>
</section>
\ No newline at end of file
......@@ -13,6 +13,7 @@
{% if show_faq %}
<a href="javascript:void(0);" class="nav_item" id="nav_faq">Problèmes et Demandes</a>
{% endif %}
{% if show_abcd_calendar %}
<a
href="javascript:void(0);"
target="_blank"
......@@ -21,7 +22,7 @@
>
Calendrier ABCD
</a>
{% endif %}
{# Disconnection button must have this id (logic in all_common.js) #}
<a href="javascript:void(0);" id="deconnect">Déconnexion</a>
<a href="javascript:void(0);" class="icon" onclick="toggleHeader()">
......
......@@ -28,6 +28,9 @@
<div class="member_shift_name_area">
<span>Mon créneau : </span>
<span class="member_shift_name member_info"></span>
{% if coop_can_change_shift_template %}
<span><i class="fas fa-edit tile_icon edit-btn"></i></span>
{% endif %}
</div>
<div class="member_coop_number_area">
<span>Mon numéro de coop : </span>
......@@ -118,4 +121,15 @@
</div>
</div>
</div>
{% if coop_can_change_shift_template %}
<div id="modal-calendar-choice" style="display:none;">
<div id="shift_choice">
{% include "members/shift_template_calendar.html" %}
</div>
</div>
<script>
var max_begin_hour = "{{max_begin_hour}}",
type = 2;
</script>
{% endif %}
</div>
......@@ -17,6 +17,7 @@
<script type="text/javascript" src="{% static 'js/datatables/dataTables.responsive.min.js' %}"></script>
<script type="text/javascript" src="{% static 'fullcalendar-5.9.0/lib/main.min.js' %}"></script>
<script type="text/javascript" src="{% static 'fullcalendar-5.9.0/lib/locales/fr.js' %}"></script>
<script type="text/javascript" src="{% static 'js/notify.min.js' %}?v=1651853225"></script>
{% endblock %}
{% block content %}
......@@ -126,7 +127,7 @@
var days_to_hide = "{{daysToHide}}"
var partner_data = {
"partner_id":"{{partnerData.id}}",
"name":"{{partnerData.display_name}}",
"name":"{{partnerData.display_name|safe}}",
"shift_type":"{{partnerData.shift_type}}",
"date_delay_stop":"{{partnerData.date_delay_stop}}",
"cooperative_state":"{{partnerData.cooperative_state}}",
......@@ -134,27 +135,29 @@
"can_have_delay" : "{{partnerData.can_have_delay}}",
"makeups_to_do" : "{{partnerData.makeups_to_do}}",
"barcode_base" : "{{partnerData.barcode_base}}",
"street" : "{{partnerData.street}}",
"street2" : "{{partnerData.street2}}",
"street" : "{{partnerData.street|safe}}",
"street2" : "{{partnerData.street2|safe}}",
"zip" : "{{partnerData.zip}}",
"city" : "{{partnerData.city}}",
"city" : "{{partnerData.city|safe}}",
"mobile" : "{{partnerData.mobile}}",
"phone" : "{{partnerData.phone}}",
"email" : "{{partnerData.email}}",
"is_associated_people" : "{{partnerData.is_associated_people}}",
"parent_id" : "{{partnerData.parent_id}}",
"parent_name" : "{{partnerData.parent_name}}",
"parent_name" : "{{partnerData.parent_name|safe}}",
"parent_verif_token" : "{{partnerData.parent_verif_token}}",
"associated_partner_id" : "{{partnerData.associated_partner_id}}",
"associated_partner_name" : "{{partnerData.associated_partner_name}}",
"associated_partner_name" : "{{partnerData.associated_partner_name|safe}}",
"verif_token" : "{{partnerData.verif_token}}",
"leave_stop_date": "{{partnerData.leave_stop_date}}",
"comite": "{{partnerData.comite}}",
"extra_shift_done": parseInt("{{partnerData.extra_shift_done}}", 10)
};
var block_actions_for_attached_people = '{{block_actions_for_attached_people}}';
const canAddShift = {{canAddShift}};
</script>
<script src="{% static "js/all_common.js" %}?v=1651853225"></script>
<script src="{% static "js/common.js" %}?v=1651853225"></script>
<script src="{% static "js/members-space-home.js" %}?v=1651853225"></script>
<script src="{% static "js/members-space-my-info.js" %}?v=1651853225"></script>
<script src="{% static "js/members-space-my-shifts.js" %}?v=1651853225"></script>
......
......@@ -106,7 +106,7 @@
</div>
</div>
{% if understand_my_status %}
{% include "members_space/understand_my_status.html" %}
{% include ""|add:understand_my_status_template %}
{% endif %}
</div>
</div>
......@@ -50,6 +50,9 @@
<i class="fas fa-spinner fa-spin fa-lg"></i>
</div>
<div id="shifts_list"></div>
{% if canAddShift %}
<button class="btn--primary selectable_shift_line" id="start_adding_shift"><strong>+ Ajouter un service</strong></button>
{% endif %}
</div>
<div id="calendar_explaination_area"></div>
<button id="calendar_explaination_button" class="btn--success">Légende du calendrier</button>
......
<div id="faqBDM" class=" mt-3">
<div class="page_title txtcenter"><h1> Problèmes et demandes </h1></div>
<div class="tiles_container">
<div class="tile full_width_tile">
<div class="tile_content">
<div class="block">
<div class="faq_intro_texts">
<p>Bienvenue dans la Foire Aux Questions du Super Cafoutch. Nous espérons que tu y trouveras des réponses.</p>
</div>
<div class="param">
<button type="button" class="accordion btn_faq"><span class="full_width">1. Je suis indisponible ou dans l'incapacité d'effectuer un ou plusieurs services.</span>
</button>
<div class="input-container panel">
<div class="info_slots_shifts"><p><i class="fas fa-exclamation-circle"></i> <b>Créneau versus service,
c'est quoi la différence ?</b></p>
<p>Un créneau, c'est une plage récurrente, par exemple, tous les jeudis de semaine A de 15h15 à 18h15.</p>
<p>Un service, c'est une plage horaire en particulier, par exemple le jeudi 28 novembre de 15h15 à 18h15.</p>
</div>
<div class="grp_text"><h3><b>Je veux changer de créneau</b></h3>
<p class="attached-unblocked"> Envoie un mail au Bureau des Membres
<a href="mailto:bdm@supercafoutch.fr?subject=Demande de changement de créneau">bdm@supercafoutch.fr</a> en indiquant la semaine (A,B,C ou D), le jour de la semaine
et l’horaire du créneau. N’hésite pas à formuler plusieurs vœux car certains
créneaux sont déjà complets. </p>
</div>
<div class="grp_text"><h3><b>Je suis ponctuellement indisponible pour effectuer un service ou je m'absente pour une courte durée (4 semaines maximum)</b></h3>
<p class="attached-unblocked"> Tu peux échanger le ou les services que tu ne pourras pas assurer en autonomie sur ton espace
membre. Le lien ci-dessous te conduit sur l’onglet "Échange de services” de ton
espace
membre. Sélectionne ensuite le service auquel tu ne peux pas être présent et choisis
un
service de remplacement dans le calendrier. Nous te demandons de t’y prendre au moins 24
heures à l’avance pour des raisons logistiques. </p>
<div class="faq_link_button_area"><a href="javascript:void(0);" type="button"
class="btn--primary faq_link_button"
id="shift_exchange_btn"> J'échange mon service </a>
</div>
</div>
<div class="grp_text"><h3><b>Je suis dans l'incapacité de faire mes services pour raison de santé (plus de 4 semaines)</b></h3>
<p class="attached-unblocked"> Si ton état de santé ne te permet pas de faire tes
services, que ce soit
temporairement ou non, tu en seras exempté et tu pourras
continuer à faire tes courses. Explique ta situation au Bureau des Membres
<a href="mailto:bdm@supercafoutch.fr?subject=Incapacité à faire mes services pour raison de santé">bdm@supercafoutch.fr</a> et n'oublie pas de le dire à ton capitaine d'équipe. Si ton
incapacité est
temporaire, n’oublie pas de recontacter le Bureau des Membres à ton retour pour te
réinscrire sur un créneau. </p>
</div>
<div class="grp_text"><h3><b>Je m'absente plus longtemps pour une autre raison (plus de 4 semaines)</b></h3>
<p class="attached-unblocked"> Envoie un mail au Bureau des Membres
<a href="mailto:bdm@supercafoutch.fr?subject=Incapacité à faire mes services pour cause d'absence prolongée">bdm@supercafoutch.fr</a>
et à ton capitaine d'équipe en indiquant tes dates de début et de
fin d’absence. Pendant ton absence, tu ne fais pas de service et tu ne peux pas
faire tes courses. </p>
</div>
</div>
<button type="button" class="accordion btn_faq"><span class="full_width">2. Je suis en statut "Rattrapage" ou "Désinscrit.e" et je ne peux pas faire mes courses</span>
</button>
<div class="input-container panel">
<div class="grp_text"><h3><b>Dans quel cas ?</b></h3> Tu es en statut "Rattrapage" si tu as
manqué un service.<br/> Tu es désinscrit.e après avoir manqué trois services consécutifs sans
les rattraper. Tu es désinscrit.e de ton créneau afin d'y libérer une place. Il se peut aussi
que tu n'aies encore jamais été inscrit.e sur un créneau.
</div>
<div class="grp_text"><img src="/static/img/diagramme_etat_statut_cooperateurs.png"/>
<h3><b>Que faire ? </b></h3> <h4>Si tu es en statut "Rattrapage" :</h4>
<p class="attached-unblocked">Tu as manqué un service et tu ne t'es pas inscrit à un rattrapage. Tu ne peux plus faire tes courses.
Il faut t'inscrire au rattrapage sur ton espace membre, tu
as 6 mois pour l'effectuer. Quand tu seras inscrit à ton rattrapage, tu seras en statut
"Délai" et tu pourras de nouveau faire tes courses.<br/> Pour choisir tes rattrapages,
clique sur le bouton ci-dessous.</p>
<div class="faq_link_button_area"><a href="echange-de-services" type="button"
class="btn--primary faq_link_button"> Je
sélectionne mes rattrapages </a></div>
</div>
<div class="grp_text"><h4>Si tu es désinscrit.e :</h4> <p class="attached-unblocked">Tu as raté trois services consécutifs
sans les rattraper ou tu n'as pas effectué les rattrapages auxquels tu étais
inscrit.<br/>
Tu es désinscrit.e de ton créneau afin d'y libérer une place.<br/>Il se peut aussi
que tu n'aies encore jamais été inscrit sur un créneau.<br/><br/> Pour t'inscrire ou te
réinscrire, contacte le Bureau des Membres
<a href="mailto:bdm@supercafoutch.fr?subject=Inscription / réinscription créneau fixe">bdm@supercafoutch.fr</a>
en indiquant la semaine, le jour et l'horaire du créneau souhaité. Envoie
plusieurs vœux car certains créneaux sont déjà complets. Dès que tu es
réinscrit sur un créneau, n'oublie pas de t'inscrire à tes deux rattrapages.</p></div>
<div class="grp_text"><p class="attached-unblocked"> Si tu ne comprends pas pourquoi tu es désinscrit.e ou suspendu.e, tu
peux le signaler au Bureau des Membres <a href="mailto:bdm@supercafoutch.fr?subject=Problème désinscription créneau">bdm@supercafoutch.fr</a>
pour qu'il règle le problème.<br/> N'hésite pas à indiquer un
maximum d'informations sur ta situation pour nous aider à régler cette situation.</p>
</div>
</div>
<button type="button" class="accordion btn_faq"><span
class="full_width">3. Autres questions</span>
</button>
<div class="input-container panel">
<div class="grp_text"><h3><b>Je suis arrivé.e en retard ou j'ai oublié de pointer</b></h3>
<p class="attached-unblocked"> Si tu as effectué ton service mais que tu ne l’as pas
validé à l'entrée du magasin, il te faut prévenir le Bureau des Membres
<a href="mailto:bdm@supercafoutch.fr?subject=J'ai oublié de valider mon service">bdm@supercafoutch.fr</a> en indiquant la date et l’heure du service concerné. Le
Bureau des Membres pourra intervenir pour que tu n’aies pas à t’inscrire à un
service de rattrapage. </p>
</div>
<div class="grp_text"><h3><b>Je viens / nous venons d'avoir un bébé. Est-ce que je dois quand même faire mes services pour pouvoir faire mes courses ?</b></h3>
<p class="attached-unblocked"> Lors de la naissance (ou l’adoption) d’un enfant, les
coopérateurs peuvent continuer à faire leur courses sans faire de service pendant 12
mois.
Si les deux parents sont membres de la coopérative, ils peuvent se partager ces 12
mois comme ils l'entendent. Il peuvent prendre par exemple 6 mois chacun en même
temps ou 8 mois pour l'un puis 4 mois pour l'autre.
Envoie un mail au Bureau des Membres <a href="mailto:bdm@supercafoutch.fr?subject=Demande de congé parental de services">bdm@supercafoutch.fr</a>
avec les dates souhaitées. </p>
</div>
<div class="grp_text"><h3><b>Je veux changer d'adresse mail</b></h3>
<p class="attached-unblocked"> Il arrive qu’au moment de
l’inscription, une adresse mail erronée soit saisie. Il se peut aussi qu’un membre
change d’adresse mail. Or l'adresse mail est utilisée comme identifiant pour se
connecter à l'espace membre.
Envoie simplement un message à <a href="mailto:bdm@supercafoutch.fr?subject=Changement d'adresse mail">bdm@supercafoutch.fr</a> avec ta nouvelle adresse. </p>
</div>
<div class="grp_text"><h3><b>Je n'ai pas trouvé la réponse à ma question</b></h3>
<p class="attached-unblocked"> Envoie un mail à <a href="mailto:bdm@supercafoutch.fr?subject=Je n'ai pas trouvé la réponse à ma question">bdm@supercafoutch.fr</a>. On parie que tu
t’en doutais :-). <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 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. </p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="home">
<div class="page_title txtcenter">
<h1>Espace Membre</h1>
</div>
<div class="tiles_container">
<div class="tile high_tile" id="home_tile_my_info">
<div class="tile_title">
<i class="fas fa-user tile_icon"></i>
<span class="member_info member_name"></span>
</div>
<div class="tile_content">
{# <p><span class="member_info member_name"></span></p> #}
<p class="member_status_text_container">Mon statut : <span class="member_info member_status"></span></p>
<div class="delay_date_stop_container">
( 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>
<button type="button" class="btn--success remove_future_registration">
J'ai validé un service à deux, je peux supprimer une présence
</button>
</div>
<div class="member_shift_name_area">
<span>Mon créneau : </span>
<span class="member_shift_name member_info"></span>
</div>
<div class="member_coop_number_area">
<span>Mon numéro de coop : </span>
<span class="member_coop_number member_info"></span>
</div>
<div class="member_associated_partner_area">
<span>Je suis en binôme avec : </span>
<span class="member_associated_partner member_info"></span>
</div>
<div id="see_more_info">
<button type="button", class="btn btn--primary home_link_button" id="see_more_info_link">
Accéder à mes infos et comprendre mon statut
</button>
</div>
</div>
</div>
<div class="tile high_tile" id="home_tile_my_services">
<div class="tile_title">
<i class="fas fa-clipboard tile_icon"></i>
Mes Services
</div>
<div class="tile_content">
<h3>Services à venir</h3>
<div id="home_incoming_services">
<i class="fas fa-spinner fa-spin fa-lg"></i>
</div>
<div id="go_to_shift_history_area">
<button type="button", class="btn btn--primary" id="home_go_to_shift_history">
Accéder à mon historique
</button>
</div>
</div>
</div>
<div class="tile small_tile" id="home_tile_services_exchange">
<div class="tile_title">
<i class="fas fa-exchange-alt tile_icon"></i>
Échange de services
</div>
<div class="tile_content">
<div>
Un empêchement ? J'anticipe et déplace mes services jusqu'à 24h avant leur début !
</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
</button>
</div>
</div>
</div>
<div class="tile small_tile" id="home_tile_help">
<div class="tile_title">
<i class="fas fa-question-circle tile_icon"></i>
J'ai une demande
</div>
<div class="tile_content">
<div class="home_link_button_area">
<a
href="javascript:void(0);"
class="btn--primary home_link_button"
id="go_to_forms"
>
Accéder à la FAQ
</a>
</div>
</div>
</div>
<div class="tile small_tile" id="home_tile_shop_info">
<div class="tile_title">
<i class="fas fa-newspaper tile_icon"></i>
Informations magasins
</div>
<div id="shop_info_content">
<div class="shop_info_item shop_opening_hours">
<div class="shop_info_item_content">
<div class="opening_hours_title">
Horaires du magasin :
</div>
<div class="opening_hours_content">
{{shop_opening_hours|safe}}
</div>
</div>
</div>
<div class="shop_info_item shop_message">
<div class="shop_info_item_content shop_message_content">
{{msg_accueil|safe}}
</div>
</div>
</div>
</div>
</div>
</div>
<div id="faqBDM" class=" mt-3">
<div class="page_title txtcenter"><h1> Problèmes et demandes </h1></div>
<div class="tiles_container">
<div class="tile full_width_tile">
<p>
<a href="https://supercoop.notion.site/supercoop/Foire-Aux-Questions-sur-la-participation-et-Formulaires-correspondants-106143413d0b400a9f94023962375304" target="_blank">Foire-Aux-Questions-sur-la-participation-et-Formulaires</a>
</p>
<p>
<em>En cliquant sur le lien ci-dessus, un nouvel onglet ou une nouvelle fenêtre affichera le contenu de la page sur le site supercoop.notion.site.</em>
</p>
</div>
</div>
</div>
<div id="home">
<div class="page_title txtcenter">
<h1>Bienvenue sur ton nouvel espace membre ! 🎉</h1>
<strong>Si tu es ex-volant.e</strong>, il te faut absolument changer ton créneau volant (DDim 22:00)par un créneau régulier qui te convient ou le moins pire... (ci-dessous)<br/> Ensuite, tu peux déplacer ton prochain service programmé en allant sur "Echanges de services" 🤙<br/>
Si tu es régulier.e, rien ne change pour toi si ce n'est que tu peux anticiper une absence en déplaçant ton service. Tu n'as plus à rechercher de remplaçant ✌️<br/>
Concernant les <strong>binômes</strong>, il nous faut un peu plus de temps pour déployer cette nouveauté. On te tiens au courant via le mail hebdo 😉<br/>
Si jamais ça ne marche pas, contactes-nous à support@supercoop.fr 📧<br/>
</div>
<div>
</div>
<div class="tiles_container">
<div class="tile high_tile" id="home_tile_my_info">
<div class="tile_title">
<i class="fas fa-user tile_icon"></i>
<span class="member_info member_name"></span>
</div>
<div class="tile_content">
{# <p><span class="member_info member_name"></span></p> #}
<p class="member_status_text_container">Mon statut : <span class="member_info member_status"></span></p>
<div class="delay_date_stop_container">
( 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>
<button type="button" class="btn--success remove_future_registration">
J'ai validé un service à deux, je peux supprimer une présence
</button>
</div>
<div class="member_shift_name_area">
<span>Mon créneau : </span>
<span class="member_shift_name member_info"></span>
{% if coop_can_change_shift_template %}
<span><i class="fas fa-edit tile_icon edit-btn"></i></span>
{% endif %}
</div>
<div class="member_coop_number_area">
<span>Mon numéro de coop : </span>
<span class="member_coop_number member_info"></span>
</div>
<div class="member_associated_partner_area">
<span>Je suis en binôme avec : </span>
<span class="member_associated_partner member_info"></span>
</div>
<div id="see_more_info">
<button type="button", class="btn btn--primary home_link_button" id="see_more_info_link">
Accéder à mes infos et comprendre mon statut
</button>
</div>
</div>
</div>
<div class="tile high_tile" id="home_tile_my_services">
<div class="tile_title">
<i class="fas fa-clipboard tile_icon"></i>
Mes Services
</div>
<div class="tile_content">
<h3>Services à venir</h3>
<div id="home_incoming_services">
<i class="fas fa-spinner fa-spin fa-lg"></i>
</div>
<div id="go_to_shift_history_area">
<button type="button", class="btn btn--primary" id="home_go_to_shift_history">
Accéder à mon historique
</button>
</div>
</div>
</div>
<div class="tile small_tile" id="home_tile_services_exchange">
<div class="tile_title">
<i class="fas fa-exchange-alt tile_icon"></i>
Échange de services
</div>
<div class="tile_content">
<div>
Un empêchement ? J'anticipe et déplace mes services jusqu'à 24h avant leur début !
</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
</button>
</div>
</div>
</div>
<div class="tile small_tile" id="home_tile_help">
<div class="tile_title">
<i class="fas fa-question-circle tile_icon"></i>
J'ai une demande
</div>
<div class="tile_content">
<div class="home_link_button_area">
<a
href="javascript:void(0);"
class="btn--primary home_link_button"
id="go_to_forms"
>
Accéder aux formulaires
</a>
</div>
</div>
</div>
<div class="tile small_tile" id="home_tile_shop_info">
<div class="tile_title">
<i class="fas fa-newspaper tile_icon"></i>
Informations magasins
</div>
<div id="shop_info_content">
<div class="shop_info_item shop_opening_hours">
<div class="shop_info_item_content">
<div class="opening_hours_title">
Horaires du magasin :
</div>
<div class="opening_hours_content">
{{shop_opening_hours|safe}}
</div>
</div>
</div>
<div class="shop_info_item shop_message">
<div class="shop_info_item_content shop_message_content">
{{msg_accueil|safe}}
</div>
</div>
</div>
</div>
</div>
{% if coop_can_change_shift_template %}
<div id="modal-calendar-choice" style="display:none;">
<div id="shift_choice">
{% include "members/shift_template_calendar.html" %}
</div>
</div>
<div id="modal_error_change_shift_template" style="display:none;">
<h3 class="error_modal_title">Action impossible</h3>
<p>
Il y a un rattrapage sur le créneau choisi (<span class="shift_template_name"></span>), cela empêche de s'inscrire sur ce créneau.
</p>
<p>Vous pouvez essayer ce créneau une autre semaine.</p>
</div>
<script>
var max_begin_hour = "{{max_begin_hour}}",
type = 2;
</script>
{% endif %}
</div>
<div class="tile full_width_tile">
<div class="tile_title">
Comprendre mon statut
</div>
<div class="my_info_line_middle">
Il existe différents statuts donnant ou non le droit de faire ses courses. Voici un schéma explicatif expliquant le passage d'un statut à un autre. Pour toute question relative aux statuts, rendez&#x2011;vous dans la rubrique <a href='faq'>Problèmes&nbsp;&&nbsp;Demandes</a>.
</div>
<a href="/static/img/Diagramme_Etat_Statut_Cooperateurs_Supercoop_220909.jpg" target=”_blank”>
<img class="status_info_image" src="/static/img/Diagramme_Etat_Statut_Cooperateurs_Supercoop_220909.jpg" alt="diagramme_etat_statut_cooperateurs"/>
</a>
</div>
\ No newline at end of file
......@@ -31,6 +31,7 @@
<a class="dropdown-item" href="breakingArticleSet">Mettre en rupture un article sur odoo</a>
<a class="dropdown-item" href="stockQuantLastSale">Date de la dernière vente des articles</a>
<a class="dropdown-item" href="saleWithNotSale">Vente avec jours sans vente</a>
<a class="dropdown-item" href="stockValues">Stock (qtés > 0 valorisées)</a>
</div>
</li>
</ul>
......
{% extends "stock/stock_menu.html" %}
{% load static %}
{% block additionnal_css %}
<link rel="stylesheet" href="{% static 'css/datatables/jquery.dataTables.css' %}">
{% endblock %}
{% block additionnal_scripts %}
<script type="text/javascript" src="{% static 'js/datatables/jquery.dataTables.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/datatables/dataTables.plugins.js' %}"></script>
<script type="text/javascript" src="{% static 'js/datatables/datatables.buttons.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/datatables/buttons.html5.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/datatables/jszip.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/stock_values.js' %}"></script>
{% endblock %}
{% block content %}
<h1>Stock valorisé</h1>
<br>
<div class="main">
<table id="tableArticle" class="display" width="95%" cellspacing="0" ></table>
</div>
<br/>
<br/>
<script src="{% static "js/all_common.js" %}?v=1651853225"></script>
<script src="{% static "js/common.js" %}?v=1651853225"></script>
{% endblock %}
......@@ -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