Commit b621a5c6 by Gwenaël Léger

Merge remote-tracking branch 'origin/migration-v12' into refonte_espace_membre_sc

parents 060f43e2 d0b7d9b1
Pipeline #3958 failed with stage
......@@ -50,7 +50,9 @@ Le code source de ce dépôt est celui qui fournit actuellement les services sui
* contient le code de 'mon-espace-prive' (formulaire de confirmation données et services échanges)
## Installation (sous distribution linux)
# Installation (sous distribution linux)
### 1. Installation
Prérequis : une version de python >= 3.8.12
......@@ -83,3 +85,20 @@ Lancer le serveur Web avec la commande `./launch.sh` (chmod u+x préalable si nÃ
L'application sera accessible via http://127.0.0.1:34001/
L'adresse d'écoute et le numero de port peuvent être modifiés en les passant en paramètre de la commande `./launch.sh`, par exemple `./launch.sh 192.168.0.2 5678`
### 2. Mise en place des crons
Les cronjobs situés dans le répertoire racine cronscripts, sont indispensables au bon fonctionnement du logiciel.
Ils se chargent de :
- la cloture des créneaux (record_absences.sh)
- la finalisation des réceptions (process_picking.sh)
Ils sont activés en renseignant le crontab de django ainsi, dans le cas où third-party est à la racine de /home/django :
```
# m h dom mon dow command
*/5 6,7,8,9,10,11,12,13,14,15,16,17,18,19 * * * ~/third-party/cronscripts/process_picking.sh
0 8,11,14,17,20,23 * * * ~/third-party/cronscripts/record_absences.sh
```
Ajoutez ```--user x -- password y``` si le service est protégé par une barrière HTTP
\ No newline at end of file
......@@ -40,6 +40,8 @@ CASH_PAYMENT_ID = 18
STOCK_LOC_ID = 12
CATEG_FRUIT = 151
CATEG_LEGUME = 152
FR_CATEGS = [CATEG_FRUIT]
VEG_CATEGS = [CATEG_LEGUME]
COEFF_MAG_ID = 1
......
......@@ -42,6 +42,8 @@ STOCK_LOC_ID = 12
CATEG_FRUIT = 151
CATEG_LEGUME = 152
FR_CATEGS = [CATEG_FRUIT]
VEG_CATEGS = [CATEG_LEGUME]
VRAC_CATEGS = [166, 167, 174, 179]
#EXPORT_POS_CAT_FOR_SCALES = True
FLV_CSV_NB = 2
......
......@@ -48,6 +48,8 @@ CASH_PAYMENT_ID = 18
STOCK_LOC_ID = 12
CATEG_FRUIT = 151
CATEG_LEGUME = 152
FR_CATEGS = [CATEG_FRUIT]
VEG_CATEGS = [CATEG_LEGUME]
COEFF_MAG_ID = 1
......
......@@ -47,6 +47,8 @@ MEALS_PICKING_TYPE_ID = 17
CATEG_FRUIT = 189
CATEG_LEGUME = 189
FR_CATEGS = [CATEG_FRUIT]
VEG_CATEGS = [CATEG_LEGUME]
VRAC_CATEGS = [197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207]
EXPORT_POS_CAT_FOR_SCALES = True
SHELF_LABELS_ADD_FIELDS = ['code', 'category_print_id', 'base_price', 'categ_id', 'country_id', 'label_ids', 'uom_id', 'suppliers']
......
......@@ -49,8 +49,10 @@ STOCK_LOC_ID = 12
CATEG_FRUIT = 151
CATEG_LEGUME = 152
FR_CATEGS = [CATEG_FRUIT]
VEG_CATEGS = [CATEG_LEGUME]
VRAC_CATEGS = [166, 167, 174, 179]
FLV_CSV_NB = 2
FLV_CSV_NB = 4
COEFF_MAG_ID = 1
......
# Tâches programmées (crons pour utilisateur django)
## Pour les services
```
# m h dom mon dow command
0 8,11,14,17,20,23 * * * /etc/record_absences.sh
0 2 * * 6 wget -O /var/log/django-cronlog/cloture_volant_$(date +%F_%T).log [url_appli_django]/members/close_ftop_service
```
## Pour les réceptions
```
# m h dom mon dow command
*/5 6,7,8,9,10,11,12,13,14,15,16,17,18,19 * * * wget -O /var/log/django-cronlog/reception_process_$(date +%F_%T).log [url_appli_django]/reception/po_process_picking
```
Ajoutez ```--user x -- password y``` si le service est protégé par une barrière HTTP
# Scripts appelés par les crons
### record_abscences.sh
```
#! /bin/bash
fn=$(date +%Y-%m-%d_%H-%M-%S).json
curl [url_appli_django]/members/record_absences > /home/django/absences/$fn
```
Lorsque le service est protégé par une barrière HTTP, ajoutez l'option ``` -u user:password```
#! /bin/bash
# Récupère le dossier contenant ce script
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/vars"
log_dir=~/cronlogs/process_picking
mkdir -p "$log_dir" # Crée le dossier s'il n'existe pas
MAXFILES=12960 # ~ 3 months every 5 minutes, 12 hours a day
curl -o ${log_dir}/reception_process_$(date +%F_%T).log http://$ip:$dj_port/reception/po_process_picking
ls -t ${log_dir}/*.log | sed 1,$MAXFILES\d | while read file ; do rm "$file"; done
#! /bin/bash
# Récupère le dossier contenant ce script
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/vars"
log_dir=~/cronlogs/shifts
mkdir -p "$log_dir" # Crée le dossier s'il n'existe pas
MAXFILES=2920 # 1 year (8 times a day)
curl -o ${log_dir}/$(date +%F_%T)_absences.log http://$ip:$dj_port/members/record_absences
ls -t ${log_dir}/*_absences.log | sed 1,$MAXFILES\d | while read file ; do rm "$file"; done
#! /bin/bash
ip=$(ip -f inet a show eth0|grep -oP "(?<=inet ).+(?=\/)")
dj_port=2012
......@@ -2,6 +2,7 @@ from django.db import models
from outils.common import OdooAPI
from outils.common import CouchDB
from outils.common import MARSHALL_ERROR
from outils.common_imports import *
from decimal import *
import os
......@@ -366,7 +367,7 @@ class CagetteInventory(models.Model):
missed.append({'product': p, 'msg': str(e)})
if len(missed) == 0:
api.execute('stock.inventory', 'action_done', [inv])
CagetteInventory.stock_inventory_action_validate(inv)
done.append('Closed inventory')
return {'missed': missed, 'unchanged': unchanged, 'done': done}
......@@ -415,10 +416,12 @@ class CagetteInventory(models.Model):
missed.append({'product': p, 'msg': str(e)})
# Set inventory as 'done' even if some products missed
api.execute('stock.inventory', 'action_done', [inv])
CagetteInventory.stock_inventory_action_validate(inv)
done.append('Closed inventory')
except Exception as e:
coop_logger.error(str(e))
if not (MARSHALL_ERROR in str(e)):
coop_logger.error("update_products_stock %s", str(e))
errors.append(str(e))
return {'errors': errors,
......@@ -428,6 +431,23 @@ class CagetteInventory(models.Model):
'inv_id': inv}
@staticmethod
def stock_inventory_action_validate(inv):
api = OdooAPI()
res = api.execute('stock.inventory', 'action_validate', [inv])
if res: # if action_validate returns something, there is something wrong
if isinstance(res, dict) and 'name' in res:
want_to_return_wiz_err_msg\
= ('Action validate wanted to return wizard named '
+ res['name'] + ' which is not possible from third-party app.')
coop_logger.error(want_to_return_wiz_err_msg)
raise Exception(want_to_return_wiz_err_msg)
else:
action_validate_generic_issue = "Action validate returned something."
coop_logger.error(action_validate_issue)
raise Exception(action_validate_generic_issue)
@staticmethod
def raz_archived_stock():
missed = []
done = []
......@@ -452,7 +472,7 @@ class CagetteInventory(models.Model):
done.append({'product': p, 'id': li})
except Exception as e:
missed.append({'product': p, 'msg': str(e)})
api.execute('stock.inventory', 'action_done', [inv])
CagetteInventory.stock_inventory_action_validate(inv)
return {'missed': missed, 'done': done}
@staticmethod
......@@ -480,7 +500,7 @@ class CagetteInventory(models.Model):
done.append({'product': p, 'id': li})
except Exception as e:
missed.append({'product': p, 'msg': str(e)})
api.execute('stock.inventory', 'action_done', [inv])
CagetteInventory.stock_inventory_action_validate(inv)
return {'missed': missed, 'done': done}
@staticmethod
......@@ -507,7 +527,7 @@ class CagetteInventory(models.Model):
done.append({'product': p, 'id': li})
except Exception as e:
missed.append({'product': p, 'msg': str(e)})
api.execute('stock.inventory', 'action_done', [inv])
CagetteInventory.stock_inventory_action_validate(inv)
return {'missed': missed, 'done': done}
......
......@@ -333,7 +333,7 @@ def manage_makeups(request):
context = {'title': 'BDM - Rattrapages',
'module': 'Membres',
'has_committe_shift': committees_shift_id is not None,
'extension_duration': m.get_extension_duration()
'extension_duration': m.get_months_extension_duration()
}
return HttpResponse(template.render(context, request))
......@@ -356,7 +356,10 @@ def manage_regular_shifts(request):
template = loader.get_template('members/admin/manage_regular_shifts.html')
committees_shift_id = CagetteServices.get_committees_shift_id()
committees_shift_name = getattr(settings, 'COMMITTEES_SHIFT_NAME', "service des Comités")
if getattr(settings, 'USE_EXEMPTIONS_SHIFT_TEMPLATE', False) is True:
exemptions_shift_id = CagetteServices.get_exemptions_shift_id()
else:
exemptions_shift_id = 0
context = {
'title': 'BDM - Créneaux',
'module': 'Membres',
......@@ -410,7 +413,7 @@ def update_members_makeups_core(members_data, res):
if member_data["member_shift_type"] == "standard":
# Set points to minus the number of makeups to do + the makeups to come (limited to -2)
cs = CagetteShift()
shift_data = cs.get_shift_partner(int(member_data["member_id"]))
[shift_data, is_ftop] = cs.get_shift_partner(int(member_data["member_id"]))
target_points = - int(member_data["target_makeups_nb"]) - sum(1 for value in shift_data if value['is_makeup'])
if (target_points < -2):
target_points = -2
......
......@@ -5,6 +5,7 @@ from outils.images_imports import *
from outils.common import OdooAPI
from outils.common import CouchDB
from outils.common import Verification
from products.models import OFF
from envelops.models import CagetteEnvelops
......@@ -15,8 +16,6 @@ import re
import dateutil.parser
from datetime import date
FUNDRAISING_CAT_ID = {'A': 1, 'B': 2, 'C': 3}
class CagetteMember(models.Model):
......@@ -34,6 +33,75 @@ class CagetteMember(models.Model):
self.id = int(id)
self.o_api = OdooAPI()
@staticmethod
def get_new_password_link(data):
result = {}
if 'email' in data:
email = data['email'].strip()
validator = validators.EmailValidator()
try:
validator(email)
api = OdooAPI()
cond = [['email', '=', email]]
m_res = api.search_read('res.partner', cond, ['id'])
if m_res and 'id' in m_res[0]:
res = api.execute('res.partner', 'send_new_password_email', [m_res[0]['id']])
if 'error' in res:
result['error'] = res['error']
else:
result['error'] = 'get_new_password_link django error : res_partner not found'
except Exception as e:
result['error'] = 'get_new_password_link error while calling odoo api : ' + str(e)
if "Only users with the following access level are currently allowed to do that" in str(e):
result['error'] += (" Il s'agit peut-être d'un problème de permissions de l'utilisateur api : "
"donnez lui le droit Administration/Settings (Configuration en français)")
else:
result['error'] = 'get_new_password_link django error : no email in data'
return result
@staticmethod
def set_new_password(received_pwd, token):
if len(token) > 32 and len(received_pwd) >= 10:
api = OdooAPI()
db_uuid = api.get_system_param('database.uuid')
cond = [['reset_password_token', '=', token.replace(db_uuid,'')]]
res_m = api.search_read('res.partner', cond, ['id'])
if res_m:
import argon2
ph = argon2.PasswordHasher()
fields = {'hashed_password': ph.hash(received_pwd),
'reset_password_token': ''}
api.update('res.partner', [res_m[0]['id']], fields)
else:
raise Exception('Invalid token')
else:
raise Exception('Invalid arguments')
return 'successful_reset_password'
def get_preferences(self, key=None):
preferences = {}
try:
cond = [['id', '=', self.id]]
fields = ['external_apps_preferences']
res = self.o_api.search_read('res.partner', cond, fields)
if res:
stored_pref = res[0]['external_apps_preferences']
if stored_pref:
p = json.loads(stored_pref)
if key is None:
preferences = p
elif key in p:
preferences = p[key]
except Exception as e:
preferences['error'] = str(e)
coop_logger.info("retrieved pref = %s", str(preferences))
return preferences
def set_preferences(self, data):
return self.o_api.update('res.partner', [self.id], {'external_apps_preferences': json.dumps(data)})
def update_from_ajax(self, request):
result = {}
try:
......@@ -98,7 +166,6 @@ class CagetteMember(models.Model):
def save_partner_info(self, partner_id, fieldsDatas):
return self.o_api.update('res.partner', partner_id, fieldsDatas)
@staticmethod
def retrieve_data_according_keys(keys, full=False):
api = OdooAPI()
......@@ -107,14 +174,14 @@ class CagetteMember(models.Model):
cond.append([k, '=', keys[k]])
if full is True:
fields = ['image_medium', 'barcode_base', 'barcode', 'create_date',
'cooperative_state', 'name', 'birthdate', 'street', 'street2',
'cooperative_state', 'name', 'birthdate_date', 'street', 'street2',
'zip', 'city', 'email', 'mobile', 'phone', 'total_partner_owned_share',
'amount_subscription', 'active_tmpl_reg_line_count', 'is_exempted',
'shift_type', 'current_template_name',
'final_standard_point', 'final_ftop_point',
'date_alert_stop','date_delay_stop', 'sex']
else:
fields = ['name', 'email', 'birthdate',
fields = ['name', 'email', 'birthdate_date',
'sex', 'country_id', 'total_partner_owned_share',
'barcode_base', 'tmpl_reg_line_ids']
return api.search_read('res.partner', cond, fields, 1, 0,
......@@ -138,21 +205,38 @@ class CagetteMember(models.Model):
cond.append(['is_member', '=', True])
cond.append(['is_associated_people', '=', True])
fields = ['name', 'email', 'birthdate', 'create_date', 'cooperative_state', 'is_associated_people', 'barcode_base']
fields = ['name', 'email', 'birthdate_date', 'create_date', 'cooperative_state', 'is_associated_people', 'barcode_base']
if getattr(settings, 'USE_MEMBERS_CUSTOM_PASSWORD', False) is True:
fields.append('hashed_password')
res = api.search_read('res.partner', cond, fields)
if (res and len(res) >= 1):
coop_id = None
hashed_password = None
for item in res:
coop = item
if item["birthdate"] is not False:
coop_birthdate = item['birthdate']
if 'hashed_password' in item:
hashed_password = item['hashed_password']
if item["birthdate_date"] is not False:
coop_birthdate = item['birthdate_date']
coop_state = item['cooperative_state']
if item["is_associated_people"] == True:
coop_id = item['id']
secret_verified = False
if hashed_password:
import argon2
ph = argon2.PasswordHasher()
try:
ph.verify(hashed_password, password)
secret_verified = True
except Exception as e:
coop_logger.info("Wrong password : %s", str(e))
else:
y, m, d = coop_birthdate.split('-')
password = password.replace('/', '')
if (password == d + m + y):
secret_verified = True
if secret_verified is True:
if coop_id is None:
coop_id = coop['id']
data['id'] = coop_id
......@@ -289,6 +373,12 @@ class CagetteMember(models.Model):
def create_capital_subscription_invoice(self, amount, date):
"""Make CapitalFundraisingWizard entities creation."""
api = OdooAPI()
if getattr(settings, 'ASK_FOR_CAPITAL_PAYMENT', True) is True:
shares_qty = int(int(amount) / settings.PARTS_A_PRICE_UNIT)
else:
shares_qty = 1
f1 = {'type': 'out_invoice',
'date_invoice': date,
'journal_id': settings.CAP_JOURNAL_ID,
......@@ -305,7 +395,7 @@ class CagetteMember(models.Model):
'product_id': settings.PARTS_A_PRODUCT_ID,
'price_unit': settings.PARTS_A_PRICE_UNIT,
'name': 'Parts A',
'quantity': int(int(amount) / settings.PARTS_A_PRICE_UNIT),
'quantity': shares_qty,
'account_id': settings.CAP_INVOICE_LINE_ACCOUNT_ID
}
invoice_line_id = api.create('account.invoice.line', f2)
......@@ -370,11 +460,11 @@ class CagetteMember(models.Model):
def is_associated(id_parent):
api = OdooAPI()
cond = [['parent_id', '=', int(id_parent)]]
fields = ['id','name','parent_id','birthdate']
fields = ['id','name','parent_id','birthdate_date']
res = api.search_read('res.partner', cond, fields, 10, 0, 'id DESC')
already_have_adult_associated = False
for partner in res:
birthdate = partner['birthdate']
birthdate = partner['birthdate_date']
if(birthdate):
today = date.today()
date1 = datetime.datetime.strptime(birthdate, "%Y-%m-%d")
......@@ -454,6 +544,7 @@ class CagetteMember(models.Model):
ask_4_sex = getattr(settings, 'SUBSCRIPTION_ASK_FOR_SEX', False)
ask_4_job = getattr(settings, 'SUBSCRIPTION_ASK_FOR_JOB', False)
concat_order = getattr(settings, 'CONCAT_NAME_ORDER', 'FL')
ask_for_capital_payment = getattr(settings, 'ASK_FOR_CAPITAL_PAYMENT', True)
sex = 'o'
function = ''
......@@ -484,7 +575,7 @@ class CagetteMember(models.Model):
else:
name = post_data['firstname'] + name_sep + post_data['lastname']
f = {'name': name,
'birthdate': birthdate,
'birthdate_date': birthdate,
'sex': sex,
'street': post_data['address'],
'zip': post_data['zip'],
......@@ -498,7 +589,7 @@ class CagetteMember(models.Model):
if ('country' in post_data):
if (post_data['country'].lower() == 'france' or
post_data['country'] == ''):
f['country_id'] = 76
f['country_id'] = getattr(settings, 'FRANCE_ID', 75)
if 'street2' in post_data:
f['street2'] = post_data['street2']
if ('phone' in post_data) and len(post_data['phone']) > 0:
......@@ -541,9 +632,13 @@ class CagetteMember(models.Model):
else:
# New member
# Create capital subscription, base & barcode
if 'shares_euros' in post_data:
shares_euros = post_data['shares_euros']
elif getattr(settings, 'ASK_FOR_CAPITAL_PAYMENT', True) is False:
shares_euros = 0
today = datetime.date.today().strftime("%Y-%m-%d")
res['subs'] = \
m.create_capital_subscription_invoice(post_data['shares_euros'], today)
m.create_capital_subscription_invoice(shares_euros, today)
res['bc'] = m.generate_base_and_barcode(post_data)
# if the new member is associated with an already existing member
......@@ -555,7 +650,7 @@ class CagetteMember(models.Model):
associated_member = {
'email': post_data['_id'],
'name': name,
'birthdate': birthdate,
'birthdate_date': birthdate,
'sex': sex,
'street': post_data['address'],
'zip': post_data['zip'],
......@@ -594,7 +689,7 @@ class CagetteMember(models.Model):
res['error'] = 'Erreur après souscription du capital'
coop_logger.error("Erreur après souscription : %s \n %s", str(res), str(e))
if ask_for_capital_payment is True:
# Create or update envelop(s) with coop payment data
payment_data = {
'partner_id': partner_id,
......@@ -665,12 +760,12 @@ class CagetteMember(models.Model):
if (well_formatted_dob is True):
# Prepare data for odoo
f = {'name': data['Prénom'] + ' ' + data['Nom'],
'birthdate': birthdate,
'birthdate_date': birthdate,
'sex': 'o',
'street': data['adresse rue'],
'zip': data['code postal'],
'city': data['ville'],
'country_id': 76,
'country_id': getattr(settings, 'FRANCE_ID', 75),
'phone': data['tel'],
'email': data['mail'],
'barcode_rule_id': settings.COOP_BARCODE_RULE_ID
......@@ -1087,7 +1182,7 @@ class CagetteMembers(models.Model):
api = OdooAPI()
cond = ['|', ('email', 'ilike', needle), ('display_name', 'ilike', needle)]
fields = ['barcode_base', 'barcode', 'create_date',
'cooperative_state', 'name', 'birthdate', 'street', 'street2',
'cooperative_state', 'name', 'birthdate_date', 'street', 'street2',
'zip', 'city', 'email', 'mobile', 'phone', 'total_partner_owned_share',
'amount_subscription', 'active_tmpl_reg_line_count',
'shift_type', 'current_template_name', 'sex']
......@@ -1330,7 +1425,7 @@ class CagetteMembers(models.Model):
if res:
cs = CagetteShift()
for idx, partner in enumerate(res):
shift_data = cs.get_shift_partner(int(partner['id']))
[shift_data, is_ftop] = cs.get_shift_partner(int(partner['id']))
res[idx]['makeups_to_come'] = sum(1 for value in shift_data if value['is_makeup'])
@staticmethod
......@@ -1393,5 +1488,42 @@ class CagetteUser(models.Model):
return answer
@staticmethod
def get_preferences(request, key=None):
preferences = {}
try:
api = OdooAPI()
cond = [['id', '=', request.COOKIES['uid']]]
fields = ['partner_id']
res = api.search_read('res.users', cond, fields)
if res:
preferences = CagetteMember(res[0]['partner_id'][0]).get_preferences(key)
except Exception as e:
preferences['error'] = str(e)
return preferences
@staticmethod
def set_preferences(request, data, key=None):
"""Be careful : if key is None, preferences will be overwritten by received data."""
res = {}
if CagetteUser.are_credentials_ok(request):
try:
api = OdooAPI()
cond = [['id', '=', request.COOKIES['uid']]]
fields = ['partner_id']
res_user = api.search_read('res.users', cond, fields)
if res_user:
if key is None:
external_apps_preferences = data
else:
external_apps_preferences = CagetteUser.get_preferences(request)
external_apps_preferences[key] = data
CagetteMember(res_user[0]['partner_id'][0]).set_preferences(external_apps_preferences)
res['success'] = True
except Exception as e:
res['error'] = str(e)
return res
from shifts.models import CagetteShift
......@@ -28,7 +28,7 @@ video {max-width:none;}
#photo_studio {font-size:125%; min-width:250px; max-width:250px;margin-right:25px;}
#webcam_button {min-width:115px; max-width:115px;}
#multi_results_preview button {margin:2px;}
#member_slide {grid-gap:0;padding:5px;display:none;}
#member_slide {grid-gap:0;padding:5px;display:none; position:absolute; top:150px;}
#member_slide .coop-info {background: #449d44;}
#image_medium {width:128px;float:left;}
#image_medium:hover {cursor: url(/static/img/ip-camera.png), url(/static/img/ip-camera.svg) 5 5, pointer;}
......@@ -72,7 +72,7 @@ h1 .member_name {font-weight: bold;}
.msg-big {font-size: xx-large; background: #fff; padding:25px; text-align: center;}
#member_advice {background: #FFF; color: red;}
#member_advice {position: absolute;background: #FFF; color: red;}
.easy_shift_validate {text-align: center; margin-top: 3em;}
......
......@@ -63,3 +63,22 @@ p.important {border: #ff0000 1px solid; padding: 15px; margin-top:15px;}
#form_delete, #problem_delete {display:none;}
#dashboard {margin-bottom:25px;}
.geo_suggestions {
border: 1px solid rgb(239, 239, 239);
background: rgb(255, 255, 255);
z-index: 1000;
padding: 0 10px;
}
.geo_suggestions ul {
list-style-type: none;
padding: 0;
}
.geo_suggestions li {
cursor: pointer;
border-bottom: 1px solid rgb(239, 239, 239);
margin-bottom: 2px;
}
.geo_suggestions li i, .geo_suggestions li svg{
margin-right: 8px;
}
......@@ -358,14 +358,14 @@ function update_members_makeups(member_ids, action, description) {
closeModal();
},
error: function(data) {
err = {msg: "erreur serveur pour décrémenter les rattrapages", ctx: 'decrement_makeups'};
err = {msg: "erreur serveur pour incrémenter ou décrémenter les rattrapages", ctx: 'update_members_makeups'};
if (typeof data.responseJSON != 'undefined' && typeof data.responseJSON.error != 'undefined') {
err.msg += ' : ' + data.responseJSON.error;
}
report_JS_error(err, 'members_admin-manage_makeups');
closeModal();
alert('Erreur serveur pour décrémenter les rattrapages. Veuillez contacer le service informatique.');
alert('Erreur serveur pour incrémenter ou décrémenter les rattrapages. Veuillez contacter le service informatique.');
}
});
}
......@@ -411,7 +411,7 @@ function extend_member_delay(member) {
report_JS_error(err, 'members_admin-manage_makeups');
closeModal();
alert('Erreur serveur pour créer un délai. Veuillez contacer le service informatique.');
alert('Erreur serveur pour créer un délai. Veuillez contacter le service informatique.');
}
});
}
......
......@@ -374,7 +374,7 @@ function modify_current_coop() {
ncoop_view.find('input[name="email"]').val(current_coop._id);
payment_meaning.find('option').removeAttr('selected');
payment_meaning.find('option[value="'+current_coop.payment_meaning+'"]').attr('selected', 'selected');
if (current_coop.checks_nb.length > 0) {
if (current_coop.checks_nb && current_coop.checks_nb.length > 0) {
ch_qty.val(current_coop.checks_nb);
ch_qty.show();
} else {
......
......@@ -279,6 +279,10 @@ function save_current_coop(callback) {
if ((date_test.getDate() !== parseInt(jj)) || ((date_test.getMonth()+1) !== parseInt(mm)) || (date_test.getFullYear() !== parseInt(aaaa)) || !date_test.isValid()) {
birthdate_error = true;
}
// do not allow years starting with a 0 as it causes bugs later in odoo
if (aaaa < 1000) {
birthdate_error = true;
}
} catch (Exception) {
birthdate_error = true;
}
......@@ -737,5 +741,7 @@ $(document).ready(function() {
home();
}
});
$('[name="birthdate"]').helpFillDate();
$('[name="phone"]').helpFillTel();
$('[name="mobile"]').helpFillTel();
});
......@@ -37,6 +37,8 @@ urlpatterns = [
url(r'^verify_final_state$', views.verify_final_state),
url(r'^update_couchdb_barcodes$', views.update_couchdb_barcodes),
url(r'^add_shares_to_member$', views.add_shares_to_member),
url(r'^ask_for_new_password$', views.ask_for_new_password),
url(r'^reset_password/([0-9-a-z\-]+)$', views.reset_new_password),
# Borne accueil
url(r'^search/([^\/.]+)/?([0-9]*)', views.search),
url(r'^save_photo/([0-9]+)$', views.save_photo, name='save_photo'),
......
......@@ -108,6 +108,7 @@ def inscriptions(request, type=1):
'mag_place_string': settings.MAG_NAME,
'office_place_string': settings.OFFICE_NAME,
'max_begin_hour': settings.MAX_BEGIN_HOUR,
'ask_for_capital_payment': getattr(settings, 'ASK_FOR_CAPITAL_PAYMENT', True),
'payment_meanings': settings.SUBSCRIPTION_PAYMENT_MEANINGS,
'force_firstname_hyphen': getattr(settings, 'FORCE_HYPHEN_IN_SUBSCRIPTION_FIRSTNAME', True),
'input_barcode': getattr(settings, 'SUBSCRIPTION_INPUT_BARCODE', False),
......@@ -118,6 +119,7 @@ def inscriptions(request, type=1):
'POUCHDB_VERSION': getattr(settings, 'POUCHDB_VERSION', ''),
'max_chq_nb': getattr(settings, 'MAX_CHQ_NB', 12),
'show_ftop_button': getattr(settings, 'SHOW_FTOP_BUTTON', True),
'ask_for_capital_payment': getattr(settings, 'ASK_FOR_CAPITAL_PAYMENT', True),
'db': settings.COUCHDB['dbs']['member'],
'ASSOCIATE_MEMBER_SHIFT' : getattr(settings, 'ASSOCIATE_MEMBER_SHIFT', ''),
'can_create_binome': getattr(settings, 'CAN_CREATE_BINOME', True),
......@@ -160,6 +162,7 @@ def prepa_odoo(request):
'ask_for_street2': getattr(settings, 'SUBSCRIPTION_ADD_STREET2', False),
'ask_for_second_phone': getattr(settings, 'SUBSCRIPTION_ADD_SECOND_PHONE', False),
'show_ftop_button': getattr(settings, 'SHOW_FTOP_BUTTON', True),
'ask_for_capital_payment': getattr(settings, 'ASK_FOR_CAPITAL_PAYMENT', True),
'db': settings.COUCHDB['dbs']['member'],
'committees_shift_id': committees_shift_id,
'exemptions_shift_id': exemptions_shift_id,
......@@ -198,6 +201,7 @@ def validation_inscription(request, email):
'ask_for_street2': getattr(settings, 'SUBSCRIPTION_ADD_STREET2', False),
'ask_for_second_phone': getattr(settings, 'SUBSCRIPTION_ADD_SECOND_PHONE', False),
'show_ftop_button': getattr(settings, 'SHOW_FTOP_BUTTON', True),
'ask_for_capital_payment': getattr(settings, 'ASK_FOR_CAPITAL_PAYMENT', True),
'em_url': settings.EM_URL,
'WELCOME_ENTRANCE_MSG': settings.WELCOME_ENTRANCE_MSG,
'WELCOME_SUBTITLE_ENTRANCE_MSG': getattr(settings, 'WELCOME_SUBTITLE_ENTRANCE_MSG', '')}
......@@ -324,7 +328,7 @@ def record_service_presence(request):
overrided_date = ""
if app_env != "prod":
import re
o_date = re.search(r'/([^\/]+?)$', request.META.get('HTTP_REFERER'))
o_date = re.search(r'/([^\/]+?)$', request.META.get('HTTP_REFERER').replace('%20', ' '))
if o_date:
overrided_date = re.sub(r'(%20)',' ', o_date.group(1))
......@@ -336,7 +340,7 @@ def record_service_presence(request):
if res['rattrapage'] is True:
res['update'] = 'ok'
else:
if (CagetteServices.registration_done(rid, overrided_date, typeAction) is True):
if (CagetteServices.registration_done(request, rid, overrided_date, typeAction) is True):
res['update'] = 'ok'
else:
res['update'] = 'ko'
......@@ -413,6 +417,56 @@ def create_from_csv(request):
res['error'] = "Forbidden"
return JsonResponse(res, safe=False)
def ask_for_new_password(request):
succeeded = False
res = {}
try:
data = json.loads(request.body.decode())
result = CagetteMember.get_new_password_link(data)
if 'error' in result:
res['error'] = result['error']
else:
succeeded = True
except Exception as e:
coop_logger.error("ask_for_new_password : %s", str(e))
res['error'] = 'ask_for_new_password django error : ' + str(e)
res['succeeded'] = succeeded
if succeeded is True:
return JsonResponse(res, safe=False)
else:
return JsonResponse(res, status=500)
def reset_new_password(request, token):
"""Return form or redirect to login is POST process succeeded"""
external_msg = ''
received_pwd = request.POST.get('password')
is_valid_pwd = received_pwd and len(received_pwd) >=10
if is_valid_pwd is True:
try:
# CagetteMember.lksfllkkl_generate_error('doesnt exist')
external_msg = CagetteMember.set_new_password(received_pwd, token)
except Exception as e:
is_valid_pwd = False
external_msg = "reset_password_failure"
coop_logger.error("reset_new_password : %s", str(e))
# Let's return content to visitor
if request.method == 'GET' or is_valid_pwd is False:
template = loader.get_template('website/change_pwd.html')
context = {'token': token,
'password_placeholder': 'Nouveau mot de passe',
'title': 'Changement du mot de passe',
'external_msg': external_msg}
response = HttpResponse(template.render(context, request))
else:
landing_url = getattr(settings, 'AFTER_NEW_PASS_SETTING_REDIRECT', '/')
if getattr(settings, 'APP_ENV', '') == 'dev':
landing_url = '/members_space/'
response = redirect(landing_url + '?msg=' + external_msg)
return response
def panel_get_purchases(request):
"""Return INRA panel purchases (possible filter : month (w/wo year))"""
if request.method == 'GET':
......
......@@ -27,12 +27,18 @@ class CagetteMembersSpace(models.Model):
answer = True
return answer
def get_extension_duration(self):
"""Return nb of months"""
# TODO : add a unit parameter and convert if not month
extension_duration = OdooAPI().get_system_param('lacagette_membership.extension_duration')
nb, unit = extension_duration.split(' ')
return nb
def get_months_extension_duration(self):
duration = 2
try:
import re
p = self.o_api.get_system_param('lacagette_membership.extension_duration')
pattern = re.compile(r'([0-9]+) month')
m = pattern.search(p)
if m:
duration = m.group(1)
except Exception as e:
coop_logger.error('get_months_extension_duration : %s', str())
return duration
def get_shifts_history(self, partner_id, limit, offset, date_from):
""" Get partner shifts history """
......
......@@ -12,6 +12,11 @@ body {
}
}
/* -- Calendar */
#shift_choice .shift {
padding-left: 5px;
}
/* -- Tiles */
.tiles_container {
......
......@@ -25,21 +25,6 @@ $(document).on('click', "#shift_exchange_btn", () => {
goto('echange-de-services');
});
$(document).on('click', '.accordion', function() {
/* Toggle between adding and removing the "active" class,
to highlight the button that controls the panel */
this.classList.toggle("active");
/* Toggle between hiding and showing the active panel */
var panel = this.nextElementSibling;
if (panel.style.display === "block") {
panel.style.display = "none";
} else {
panel.style.display = "block";
}
});
function display_messages_for_attached_people() {
if (block_actions_for_attached_people === "False") {
$(".attached-unblocked").show();
......
......@@ -91,6 +91,11 @@ function add_or_change_shift(new_shift_id) {
load_partner_shifts(partner_data.concerned_partner_id)
.then(() => {
init_shifts_list();
if (partner_data.shift_type === 'ftop' && ftop_can_delete_shift === "True") {
init_delete_registration_buttons();
}
closeModal();
setTimeout(() => {
......@@ -125,10 +130,7 @@ function add_or_change_shift(new_shift_id) {
`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.`);
alert(not_allowed_shift_op);
} 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. ` +
......@@ -138,6 +140,8 @@ function add_or_change_shift(new_shift_id) {
alert(`Une erreur est survenue. ` +
`Il est néanmoins possible que la requête ait abouti, ` +
`veuillez patienter quelques secondes puis vérifier vos services enregistrés.`);
} else if (error.status === 422) {
alert('Désolé ! Ce service ne peut pas être ajouté en raison des règles établies.');
} else {
alert(`Une erreur est survenue. ` +
`Il est néanmoins possible que la requête ait abouti, ` +
......@@ -373,8 +377,12 @@ function init_shifts_list() {
}
}
// Set delete registration button if shift isn't a makeup
if (partner_data.extra_shift_done > 0 && shift.is_makeup === false) {
// Set delete registration button if shift isn't a makeup or user is ftop and has right to delete shifts
if (
partner_data.extra_shift_done > 0 && shift.is_makeup === false
|| partner_data.shift_type === 'ftop' && ftop_can_delete_shift === "True"
) {
if (shift_line_template.find(".delete_registration_button").length === 0) {
let delete_reg_button_template = $("#delete_registration_button_template");
......@@ -458,6 +466,20 @@ function init_shifts_list() {
}
}
function add_week_letter_to_elt(elt) {
const date_string = $(elt.el).data('date'),
date = new Date(date_string);
if (date.getDay() == 1) {
const wl = date.getABCDWeekLetter();
let week_letter_div = document.createElement('div');
week_letter_div.innerHTML = '<span>' + wl + '</span>';
week_letter_div.classList.add('week-letter');
$('td[data-date="' + date_string + '"]').append(week_letter_div);
}
}
/**
* Inits the page when the calendar is displayed
*/
......@@ -510,6 +532,10 @@ function init_calendar_page() {
$("#delete_future_registration").on("click", init_delete_registration_buttons);
}
if (partner_data.shift_type === 'ftop' && ftop_can_delete_shift === "True") {
init_delete_registration_buttons();
}
let default_initial_view = "";
let header_toolbar = {};
......@@ -600,7 +626,7 @@ function init_calendar_page() {
},
"Valider"
);
} else if (selected_shift === null && can_exchange_shifts()) {
} else if (partner_data.shift_type !== 'ftop' && selected_shift === null && can_exchange_shifts()) {
if (adding_mode === false) {
/* could exchange shift but no old shift selected */
openModal(
......@@ -626,7 +652,7 @@ function init_calendar_page() {
);
}
} else if (should_select_makeup()) {
} else if (should_select_makeup() || partner_data.shift_type == 'ftop') {
/* choose a makeup service */
// Check if selected new shift is in less than extension end
if (partner_data.date_delay_stop !== 'False') {
......@@ -657,7 +683,7 @@ function init_calendar_page() {
}
}
},
eventDidMount: function() {
eventsSet: function() {
// Calendar is hidden at first on mobile to hide header change when data is loaded
$(".loading-calendar").hide();
$("#calendar").show();
......@@ -667,6 +693,11 @@ function init_calendar_page() {
} else {
$(".fc .fc-header-toolbar").removeClass("resp-header-toolbar");
}
},
dayCellDidMount: function(dayRenderInfo) {
add_week_letter_to_elt(dayRenderInfo);
return dayRenderInfo.el;
}
});
......@@ -761,7 +792,7 @@ async function init_read_only_calendar_page() {
eventDisplay: "block",
hiddenDays: hidden_days,
events: event_src,
eventDidMount: function() {
eventsSet: function() {
// Calendar is hidden at first on mobile to hide header change when data is loaded
$(".loading-calendar").hide();
$("#calendar").show();
......@@ -781,7 +812,7 @@ function init_delete_registration_buttons() {
$(".delete_registration_button").off();
$(".delete_registration_button").hide();
if (partner_data.extra_shift_done > 0) {
if (partner_data.extra_shift_done > 0 || partner_data.shift_type === 'ftop' && ftop_can_delete_shift === "True") {
$(".delete_registration_button").on("click", function() {
let shift_name = $(this).closest("div")
.parent()
......
......@@ -10,6 +10,7 @@ from shifts.models import CagetteShift
from members_space.models import CagetteMembersSpace
import hashlib
import re
def _get_response_according_to_credentials(request, credentials, context, template):
......@@ -41,8 +42,13 @@ def index(request, exception=None):
# Bad credentials (or none)
template = loader.get_template('website/connect.html')
context['msg'] = ''
context['external_msg'] = request.GET.get('msg')
if 'msg' in credentials:
context['msg'] = credentials['msg']
if getattr(settings, 'USE_MEMBERS_CUSTOM_PASSWORD', False) is True:
context['password_placeholder'] = "Mot de passe ou naissance"
context['reset_password_available'] = True
else:
context['password_placeholder'] = 'Naissance (jjmmaaaa)'
context['is_member_space'] = True
elif ('validation_state' in credentials) and credentials['validation_state'] == 'waiting_validation_member':
......@@ -87,6 +93,11 @@ def index(request, exception=None):
partnerData = cs.get_data_partner(partner_id)
default_not_allowed_shift_op = """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."""
if 'create_date' in partnerData:
md5_calc = hashlib.md5(partnerData['create_date'].encode('utf-8')).hexdigest()
partnerData['verif_token'] = md5_calc
......@@ -124,19 +135,28 @@ def index(request, exception=None):
partnerData["associated_partner_name"] = str(associated_partner["barcode_base"]) + ' - ' + partnerData["associated_partner_name"]
m = CagetteMembersSpace()
context['extension_duration'] = m.get_extension_duration()
context['extension_duration'] = m.get_months_extension_duration()
context['show_faq'] = getattr(settings, 'MEMBERS_SPACE_FAQ_TEMPLATE', 'members_space/faq.html')
context['show_abcd_calendar'] = getattr(settings, 'SHOW_ABCD_CALENDAR_TAB', True)
partnerData["comite"] = m.is_comite(partner_id)
context['partnerData'] = partnerData
context['mag_name'] = getattr(settings, 'MAG_NAME', '')
# Days to hide in the calendar
days_to_hide = "0"
context['SHIFTS_MOVING_ALLOWED'] = getattr(settings, 'SHIFTS_MOVING_ALLOWED', '')
context['ALLOW_FTOP_TO_DELETE_SHIFT'] = getattr(settings, 'ALLOW_FTOP_TO_DELETE_SHIFT', False)
context['ADDITIONAL_INFO_SHIFT_PAGE'] = getattr(settings, 'ADDITIONAL_INFO_SHIFT_PAGE', '')
if hasattr(settings, 'SHIFT_EXCHANGE_DAYS_TO_HIDE'):
days_to_hide = settings.SHIFT_EXCHANGE_DAYS_TO_HIDE
context['daysToHide'] = days_to_hide
# message shown when not allowed to move shift
context['not_allowed_shift_op'] = getattr(settings, 'NOT_ALLOWED_SHIFT_OP_MSG', default_not_allowed_shift_op)
context['not_allowed_shift_op'] = re.sub(r"\r\s( *)", "\n", context['not_allowed_shift_op'], flags=re.MULTILINE)
context['not_allowed_shift_op'] = re.sub(r"\n( *)", "\n", context['not_allowed_shift_op'], flags=re.MULTILINE)
can_add_shift = getattr(settings, 'CAN_ADD_SHIFT', False)
context['canAddShift'] = "true" if can_add_shift is True else "false"
......@@ -238,7 +258,7 @@ def shifts_exchange(request):
context = {
'title': 'Échange de Services',
'canAddShift': getattr(settings, 'CAN_ADD_SHIFT', False),
'extension_duration': m.get_extension_duration()
'extension_duration': m.get_months_extension_duration()
}
return HttpResponse(template.render(context, request))
......@@ -249,6 +269,7 @@ def faqBDM(request):
template = loader.get_template(template_path)
context = {
'title': 'foire aux questions',
'MEMBERS_GUIDE_URL': getattr(settings, 'MEMBERS_GUIDE_URL', None)
}
content = template.render(context, request)
......
......@@ -165,7 +165,7 @@ class Order(models.Model):
def export(self):
res = {'success': True}
try:
f = ["id", "name", "date_order", "partner_id", "date_planned", "amount_untaxed", "amount_total", "x_reception_status"]
f = ["id", "name", "date_order", "partner_id", "date_planned", "amount_untaxed", "amount_total", "reception_status"]
c = [['id', '=', self.id]]
order = self.o_api.search_read('purchase.order', c, f)
if order:
......@@ -226,6 +226,7 @@ class Order(models.Model):
def get_custom_barcode_labels_to_print(self):
import re
forced_quantity = getattr(settings, 'RECEPTION_PDT_LABELS_NB_FORCE_TO_NB', None)
fixed_prefix = getattr(settings, 'FIXED_BARCODE_PREFIX', '0490')
labels_data = {'total': 0, 'details': []}
lines_data = self.get_lines()
......@@ -233,6 +234,8 @@ class Order(models.Model):
bc_pattern = re.compile('^' + fixed_prefix)
for l in lines:
if ('barcode' in l) and not (bc_pattern.match(str(l['barcode'])) is None):
if forced_quantity is not None:
l['product_qty'] = forced_quantity
labels_data['details'].append(l)
labels_data['total'] += l['product_qty']
return labels_data
......@@ -253,6 +256,7 @@ class Order(models.Model):
@staticmethod
def create(supplier_id, date_planned, order_lines):
if len(list(order_lines)) > 0:
order_data = {
"partner_id": int(supplier_id),
"partner_ref": False,
......@@ -316,6 +320,11 @@ class Order(models.Model):
'supplier_id': supplier_id,
'date_planned': date_planned
}
else:
res = {
'id_po': 0,
'supplier_id': supplier_id
}
return res
......@@ -354,6 +363,7 @@ class Orders(models.Model):
import re
labels_data = {}
try:
forced_quantity = getattr(settings, 'RECEPTION_PDT_LABELS_NB_FORCE_TO_NB', None)
fixed_prefix = getattr(settings, 'FIXED_BARCODE_PREFIX', '0490')
bc_pattern = re.compile('^' + fixed_prefix)
lines_data = Orders.get_lines(oids)
......@@ -361,6 +371,9 @@ class Orders(models.Model):
if not (bc_pattern.match(str(l['barcode'])) is None):
if not (l['product_tmpl_id'] in labels_data):
labels_data[l['product_tmpl_id']] = 0
if forced_quantity is not None:
labels_data[l['product_tmpl_id']] = int(forced_quantity)
else:
labels_data[l['product_tmpl_id']] += int(l['product_qty'])
except Exception as e:
coop_logger.error('Orders get_custom_barcode_labels_to_print(oids) : %s', str(e))
......
......@@ -13,6 +13,21 @@
top: 0;
left: 0;
right: 0;
z-index: 500;
}
.preferences_area {
display: block;
width: fit-content;
float: left;
position: relative;
z-index: 501;
}
.preferences_area button {cursor: pointer;}
.ui-autocomplete {
z-index: 9999 !important;
}
.pill {
......@@ -495,6 +510,36 @@
/* - Miscellaneous */
.modal_input_area form {
width: 100%;
}
.modal_input_area form label {
display: block;
float: left;
clear: left;
width: 75%;
text-align: left;
}
.modal_input_area form .input-wrapper {
display: block;
float: left;
width: 23%;
}
.modal_input_area form .input-wrapper.checkboxes div {
float: left;
margin-right: 5px;
}
footer {
display: none;
}
@media screen and (max-width: 1600px) {
#order_forms_container {
font-size: small;
}
#supplier_form {
flex: min-content;
text-align: left;
}
}
\ No newline at end of file
......@@ -393,6 +393,19 @@ function compute_products_coverage_qties() {
});
}
function get_uom_factor_data(product) {
let factor = 1;
if (typeof uoms.list != "undefined" && uoms.list.length > 0) {
for (let uom of uoms.list) {
if (uom.id == product.uom_po_id[0]) {
factor = uom.factor;
break;
}
}
}
return factor;
}
/**
* Update order products data in case they have changed.
*/
......@@ -1305,7 +1318,7 @@ function create_orders() {
'name': p.name,
'product_qty_package': p_supplierinfo.qty,
'product_qty': p_supplierinfo.qty * p_supplierinfo.package_qty,
'product_uom': p.uom_id[0],
'product_uom': p.uom_po_id[0],
'price_unit': p_supplierinfo.price,
'supplier_taxes_id': p.supplier_taxes_id,
'product_variant_ids': p.product_variant_ids,
......@@ -1327,6 +1340,7 @@ function create_orders() {
// Display new orders
for (let new_order of result.res.created) {
if (new_order.id_po > 0) {
const supplier_name = suppliers_list.find(s => s.id == new_order.supplier_id).display_name;
const date_planned = new_order.date_planned
......@@ -1350,6 +1364,7 @@ function create_orders() {
$('#created_orders_area').append(new_order_template.html());
}
}
// Prepare buttons to download order attachment
get_order_attachments();
......@@ -1559,15 +1574,16 @@ function _compute_product_data(product) {
/* Coverage related data */
const coverage_days = (order_doc.coverage_days !== null) ? order_doc.coverage_days : 0;
const uom_factor = get_uom_factor_data(product);
let qty_not_covered = 0;
let days_covered = 0;
if (product.daily_conso !== 0) {
qty_not_covered = product.daily_conso * coverage_days - product.qty_available - product.incoming_qty - purchase_qty;
qty_not_covered = (product.daily_conso * coverage_days - product.qty_available - product.incoming_qty) * uom_factor - purchase_qty;
qty_not_covered = -Math.ceil(qty_not_covered); // round up: display values that are not fully covered
qty_not_covered = (qty_not_covered > 0) ? 0 : qty_not_covered; // only display qty not covered (neg value)
days_covered = (product.qty_available + product.incoming_qty + purchase_qty) / product.daily_conso;
days_covered = ((product.qty_available + product.incoming_qty) * uom_factor + purchase_qty) / (product.daily_conso * uom_factor);
days_covered = Math.floor(days_covered);
}
......@@ -1592,6 +1608,7 @@ function prepare_datatable_data(product_ids = []) {
}
for (product of products_to_format) {
try {
const item = {
id: product.id,
name: product.name,
......@@ -1600,6 +1617,7 @@ function prepare_datatable_data(product_ids = []) {
qty_available: +parseFloat(product.qty_available).toFixed(3),
daily_conso: product.daily_conso,
purchase_ok: product.purchase_ok,
po_uom: product.uom_po_id[1],
uom: product.uom_id[1],
stats: `Ecart type: ${product.sigma} / Jours sans vente: ${Math.round((product.vpc) * 100)}%`
};
......@@ -1609,6 +1627,10 @@ function prepare_datatable_data(product_ids = []) {
const full_item = { ...item, ...computed_data };
data.push(full_item);
} catch (e) {
console.log(e);
console.log(product);
}
}
return data;
......@@ -1622,7 +1644,6 @@ function prepare_datatable_columns() {
{
data: "id",
title: `<div id="table_header_select_all" class="txtcenter">
<!--<span class="select_all_text">Sélectionner</span>-->
<label for="select_all_products_cb">Tout</label>
<input type="checkbox" class="select_product_cb" id="select_all_products_cb" name="select_all_products_cb" value="all">
</div>`,
......@@ -1679,6 +1700,16 @@ function prepare_datatable_columns() {
}
];
// specific column ordering for some coop
if (company_code === 'lacoope') {
columns.push({
data: "uom",
title: "UDM",
className: "dt-body-center",
width: "4%"
});
}
for (const supplier of selected_suppliers) {
columns.push({
data: supplier_column_name(supplier),
......@@ -1730,6 +1761,39 @@ function prepare_datatable_columns() {
width: "4%"
});
// specific column ordering for some coop
if (company_code === 'lacoope') {
columns.push({
data: "purchase_qty",
title: "Qté Achat",
className: "dt-body-center",
width: "4%"
});
columns.push({
data: "qty_not_covered",
title: "Besoin non couvert (qté)",
className: "dt-body-center",
width: "4%"
});
columns.push({
data: "po_uom",
title: "UDM",
className: "dt-body-center",
width: "4%"
});
columns.push({
data: "price",
title: "Prix HT",
className: "dt-body-center",
render: function (data) {
return (data === 'X') ? data : `${data} €`;
},
width: "4%"
});
} else {
columns.push({
data: "uom",
title: "UDM",
......@@ -1761,6 +1825,7 @@ function prepare_datatable_columns() {
// className: "dt-body-center",
// width: "4%"
// });
}
columns.push({
data: "days_covered",
......@@ -2540,11 +2605,83 @@ function update_common_info(content) {
});
}
function set_preferences_using_preferences_form() {
const text_or_assimilate_types = [
'text',
'number'
];
$('input.modal').each(function(i, e) {
const input = $(e);
if (input.prop("checked") == true || text_or_assimilate_types.indexOf(input.attr('type')) > -1) {
preferences[input.attr('name')] = input.val();
}
});
return preferences;
}
function display_preferences_form() {
let content = $('#modal_preferences_form').clone(),
force_0s0c_order = preferences.force_0s0c_order || true;
content.find('input').addClass('modal');
if (force_0s0c_order == true) {
content.find('[name="force_0s0c_order"][value="1"]').attr('checked', true);
} else {
content.find('[name="force_0s0c_order"][value="0"]').attr('checked', true);
}
openModal(
content.html(),
() => {
let p = set_preferences_using_preferences_form();
$.ajax({
type: 'POST',
url: '/orders/set_user_preferences',
data: JSON.stringify({preferences: p}),
dataType:"json",
traditional: true,
contentType: "application/json; charset=utf-8",
success: function(data) {
console.log('success');
console.log(data);
},
error: function(data) {
console.log('error');
console.log(data);
}
});
console.log(p);
closeModal();
},
'Enregistrer', false
);
}
function fix_input_value() {
// Needed until reason why dom change is not made when click input in modal window
let clicked = $(this);
clicked.closest('.input-wrapper').find('input')
.each(function(i, e) {
if ($(e).attr('name') == clicked.attr('name') && $(e).attr('value') == clicked.attr('value')) {
$(e).attr('checked', true);
} else {
$(e).attr('checked', false);
}
});
}
$(document).ready(function() {
if (coop_is_connected()) {
$('#new_order_form').show();
$('#existing_orders_area').show();
$('#common_info_area').show();
$('.preferences_area').show();
fingerprint = new Fingerprint({canvas: true}).get();
$.ajaxSetup({ headers: { "X-CSRFToken": getCookie('csrftoken') } });
......@@ -2902,21 +3039,6 @@ $(document).ready(function() {
}
});
$(document).on('click', '.accordion', function() {
/* Toggle between adding and removing the "active" class,
to highlight the button that controls the panel */
this.classList.toggle("active");
/* Toggle between hiding and showing the active panel */
var panel = this.nextElementSibling;
if (panel.style.display === "block") {
panel.style.display = "none";
} else {
panel.style.display = "block";
}
});
if (/Firefox\//.exec(userAgent)) {
// needed to prevent bug using number input arrow to change quantity (https://bugzilla.mozilla.org/show_bug.cgi?id=1012818)
// Have to capture mousedown and mouseup events, instead of using only click event
......@@ -2937,4 +3059,7 @@ $(document).ready(function() {
} else {
$('#not_connected_content').show();
}
$('#preferences-settings').click(display_preferences_form);
$(document).on('click','input.modal[type="radio"], input.modal[type="checkbox"]', fix_input_value);
});
......@@ -9,11 +9,12 @@ urlpatterns = [
url(r'^export/([a-z]+)', views.export_regex),
url(r'^get_pdf_labels$', views.get_pdf_labels),
url(r'^print_product_labels$', views.print_product_labels),
url(r'^helper$', views.helper),
url(r'^helper(/?[a-z]*)$', views.helper),
url(r'^get_suppliers$', views.get_suppliers),
url(r'^get_supplier_products$', views.get_supplier_products),
url(r'^associate_supplier_to_product$', views.associate_supplier_to_product),
url(r'^end_supplier_product_association$', views.end_supplier_product_association),
url(r'^create_orders$', views.create_orders),
url(r'^get_orders_attachment$', views.get_orders_attachment),
url(r'^set_user_preferences$', views.set_user_preferences),
]
......@@ -4,6 +4,7 @@ from outils.common import OdooAPI
from orders.models import Order, Orders, CagetteSuppliers
from products.models import CagetteProduct, CagetteProducts
from members.models import CagetteUser
from openpyxl import Workbook
from openpyxl.writer.excel import save_virtual_workbook
......@@ -15,13 +16,25 @@ def as_text(value): return str(value) if value is not None else ""
def index(request):
return HttpResponse('Orders')
def helper(request):
def helper(request, params_query):
"""params_query is query string subpart, after /order in url."""
can_customize = getattr(settings, 'ORDERS_HELPER_CUSTOMIZE', False)
uoms = CagetteProducts.get_uoms()
preferences = {}
if can_customize is True:
preferences = CagetteUser.get_preferences(request, 'third_party_order_helper')
context = {
'title': 'Aide à la commande',
'couchdb_server': settings.COUCHDB['url'],
'db': settings.COUCHDB['dbs']['orders'],
'odoo_server': getattr(settings, 'ODOO_PUBLIC_URL', settings.ODOO['url']),
'metabase_url': getattr(settings, 'ORDERS_HELPER_METABASE_URL', ''),
'uoms': json.dumps(uoms),
'can_customize_parameters': can_customize,
'uoms': json.dumps(uoms),
'preferences': json.dumps(preferences),
'nb_past_days_to_compute_sales_average': OdooAPI().get_system_param('lacagette_products.nb_past_days_to_compute_sales_average'),
'nb_of_consecutive_non_sale_days_considered_as_break': OdooAPI().get_system_param('lacagette_products.nb_of_consecutive_non_sale_days_considered_as_break')
}
......@@ -30,6 +43,17 @@ def helper(request):
return HttpResponse(template.render(context, request))
def set_user_preferences(request):
res = {}
can_customize = getattr(settings, 'ORDERS_HELPER_CUSTOMIZE', False)
if can_customize is True:
data = json.loads(request.body.decode())
res = CagetteUser.set_preferences(request, data, 'third_party_order_helper')
else:
res['error'] = 'Customization not available'
return JsonResponse(res, status=403)
return JsonResponse({'res': res})
def get_suppliers(request):
""" Get suppliers list """
res = {}
......@@ -44,10 +68,15 @@ def get_suppliers(request):
def get_supplier_products(request):
""" Get supplier products """
with_fake_data = False
# Fake data can never be set to True in production env.
if getattr(settings, 'APP_ENV', '') == 'dev':
if '/fake' in request.META.get('HTTP_REFERER'):
with_fake_data = True
suppliers_id = request.GET.getlist('sids', '')
stats_from = request.GET.get('stats_from')
res = CagetteProducts.get_products_for_order_helper(suppliers_id, [], stats_from)
res = CagetteProducts.get_products_for_order_helper(suppliers_id, [], stats_from, with_fake_data)
if 'error' in res:
return JsonResponse(res, status=500)
......
......@@ -7,20 +7,26 @@ import logging
coop_logger = logging.getLogger("coop.framework")
MARSHALL_ERROR = "cannot marshal None unless allow_none is enabled" # api user need to use english language
class OdooAPI:
"""Class to handle Odoo API requests."""
url = settings.ODOO['url']
user = settings.ODOO['user']
passwd = settings.ODOO['passwd']
db = settings.ODOO['db']
url = None
user = None
passwd = None
db = None
common = None
uid = None
models = None
def __init__(self):
def __init__(self, odoo=settings.ODOO):
"""Initialize xmlrpc connection."""
try:
self.url = odoo['url']
self.user = odoo['user']
self.passwd = odoo['passwd']
self.db = odoo['db']
common_proxy_url = '{}/xmlrpc/2/common'.format(self.url)
object_proxy_url = '{}/xmlrpc/2/object'.format(self.url)
self.common = xmlrpc.client.ServerProxy(common_proxy_url)
......@@ -67,8 +73,11 @@ class OdooAPI:
def create(self, entity, fields):
"""Create entity instance with given fields values."""
context = {
'context': {'lang': 'fr_FR', 'tz': 'Europe/Paris'}
}
return self.models.execute_kw(self.db, self.uid, self.passwd,
entity, 'create', [fields])
entity, 'create', [fields], context)
def delete(self, entity, ids):
"""Destroy entity instance by given ids."""
......@@ -76,8 +85,15 @@ class OdooAPI:
entity, 'unlink', [ids])
def execute(self, entity, method, ids, params={}):
return self.models.execute_kw(self.db, self.uid, self.passwd,
res = []
try:
res = self.models.execute_kw(self.db, self.uid, self.passwd,
entity, method, [ids], params)
except Exception as e:
if not (MARSHALL_ERROR in str(e)):
coop_logger.error("Error while api execute: %s", str(e))
raise RuntimeError('Failed api execute : ' + str(e)) from e
return res
def authenticate(self, login, password):
return self.common.authenticate(self.db, login, password, {})
......@@ -262,3 +278,24 @@ class Verification:
if token == md5_calc:
match = True
return match
@staticmethod
def has_right_to_overriden_entrance_date(request):
from members.models import CagetteUser # Error if declared on top....
from urllib.parse import unquote
answer = False
u_grants = getattr(settings, 'ODOO_USERS_GRANTS', [])
if len(u_grants) > 0 and CagetteUser.are_credentials_ok(request):
try:
login = unquote(request.COOKIES['login'])
for user in u_grants:
if user['login'] == login:
if 'bdm' in user:
if (('all' in user['bdm'] and user['bdm']['all'] is True)
or ('record_absence' in user['bdm'] and user['bdm']['record_absence'] is True)):
answer = True
except Exception as e:
coop_logger.error("has right to overriden date : %s", str(e))
return answer
......@@ -2,6 +2,7 @@
"""Import which are used in most of modules files."""
from django.conf import settings
from django.core import validators
from .common_functions import *
import json, time, datetime, pytz
import logging
......
......@@ -152,6 +152,10 @@
- CATEG_LEGUME = 152
- FR_CATEGS = [CATEG_FRUIT]
- FR_CATEGS = [CATEG_LEGUME]
- FIXED_BARCODE_PREFIX = '0491'
- FLV_CSV_NB = 4
......@@ -299,6 +303,14 @@
If set to True, a "personnal data" menu is shown, permitting connected member to modify its data.
- SHIFTS_MOVING_ALLOWED = True
If set to False, ABCD members cannot move their shifts using calendar
- FTOP_SHIFTS_VIEW_LIMIT = 2
By default, no limit is set. Unit must be weeks
- CALENDAR_NO_MORE_LINK = True
If True, in shifts calendar view (to choose one or exchange one)
......@@ -327,6 +339,26 @@
Message shown to people when they connect to the Member Space
- MEMBERS_GUIDE_URL = 'https://.....'
URL to Members guide page
- MIN_SHIFT_DURATION = 3
Define minimum shift duration (in hours). 2 is not set.
- FTOP_BLOCK_SERVICE_EXCHANGE_DELAY = 1
Define duration, in hours, before shift starts within exchange is not more available, for ftop shift_type member
- STANDARD_BLOCK_SERVICE_EXCHANGE_DELAY = 48
Define duration, in hours, before shift starts within exchange is not more available, for standard shift_type member
- FTOP_SERVICES_RULES = {'successive_shifts_allowed': 0, 'max_shifts_per_cycle': 3}
Define ftop members constraints (Les amis de La Coopé ex.)
- 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
......@@ -391,6 +423,10 @@
- RECEPTION_PDT_LABELS_TEXT = 'Cliquez sur ce bouton pour imprimer les étiquettes code-barres à coller sur les produits'
- RECEPTION_PDT_LABELS_NB_FORCE_TO_NB = 65
If set, force , by example, to 65 labels per product
- RECEPTION_SHELF_LABEL_PRINT = True
- DISPLAY_COL_AUTRES = True
......@@ -401,6 +437,12 @@
DB coeff id, needed to compute product shelf price
### Orders helper
- ORDERS_HELPER_CUSTOMIZE = False
If lacagette_connection Odoo module is actived, True can be set, in order to allow user to record its preferences
### Stocks
- STOCK_LOC_ID = 12
......@@ -464,6 +506,10 @@
True by default. Remove 15 minutes to Odoo shift end (https://redmine.cooperatic.fr/issues/1680)
- ALLOW_FTOP_TO_DELETE_SHIFT = False
If True, allow ftop member to delete futur shifts
### BDM Admin
- BDM_SHOW_FTOP_BUTTON = True (by default)
......@@ -474,6 +520,12 @@
By defaut, True. Show "Gestion des binômes" in bdm admin
### Odoo users mapping
- ODOO_USERS_GRANTS = [ {'login': 'admin', 'bdm': {'all': True}}, {'login': gdm@lacoope.fr', 'bdm': {'record_absence': True}} ]
If not set, any authentified Odoo user can make any action
### Miscellious
- EXPORT_COMPTA_FORMAT = 'Quadratus'
......
......@@ -2,6 +2,8 @@
from outils.common import OdooAPI
import base64
import logging
from openpyxl import load_workbook
from io import BytesIO
coop_logger = logging.getLogger("coop.framework")
......@@ -174,14 +176,14 @@ def make_daily_full_sum_up(lines):
def retrieve_odoo_coop_data (coop_ids):
api = OdooAPI()
fields = ['name', 'barcode_base']
cond = [['barcode_base', 'in', coop_ids ]]
fields = ['name', 'barcode_base', 'id']
cond = [['id', 'in', coop_ids]]
coops = api.search_read('res.partner', cond, fields, 3000)
coops_dict = {}
for coop in coops:
if not (str(coop['barcode_base']) in coops_dict.keys()):
coops_dict[str(coop['barcode_base'])] = \
if not (str(coop['id']) in coops_dict.keys()):
coops_dict[str(coop['id'])] = \
str(coop['barcode_base']) + ' - ' + coop['name'].replace(';','')
return coops_dict
......@@ -208,23 +210,25 @@ def process_received_lines(received_data, coops, full_sumup=False):
def generate_arithmethique_compatible_file (report):
coop_ids = []
received_data = []
content = base64.b64decode(report['datas']).decode('utf-8')
for line in content.split("\r\n"):
items = line.split(';')
if len(items) == 8:
if (items[4].isnumeric() and items[0] != 'VEN'):
if not (items[4] in coop_ids):
coop_ids.append(items[4])
if (items[3] == '409600'):
items[3] == '419600'
if not (items[0] == 'CAP' and items[3] == '101300' and
items[5] != ""):
received_data.append(items)
xlsx_content = base64.b64decode(report['datas'])
excel_file = BytesIO(xlsx_content)
workbook = load_workbook(excel_file, read_only=True)
sheet = workbook.active
#use min_row = 2 to skip headers
for row in sheet.iter_rows(min_row=2, max_row=sheet.max_row, values_only=True):
row = [str(item) for item in row]
if len(row) == 8:
if row[4] and row[4].isnumeric() and row[0] != 'VEN':
if not (row[4] in coop_ids):
coop_ids.append(row[4])
if not (row[0] == 'CAP' and row[3] == '101300' and row[5] != ""):
received_data.append(row)
coops = retrieve_odoo_coop_data(coop_ids)
summarized_lines = process_received_lines(received_data, coops)
res = make_csv_content_from_lines(summarized_lines)
file_path = 'data/' + report['name']
file_path = 'data/' + report['name'].replace(".xlsx", ".csv")
file = open(file_path, 'wb')
file.write(res['csv_content'])
file.close()
......@@ -237,23 +241,25 @@ def generate_quadratus_compatible_file(report):
import io
coop_ids = []
received_data = []
content = base64.b64decode(report['datas']).decode('utf-8')
# content = ''
# with open('outils/exportMaiComptesCorriges.csv', "r", encoding='utf-8') as csvfile:
# content = csvfile.read()
for line in content.split("\n"):
# for line in content.split("\r\n"):
items = line.split(';')
if len(items) == 8:
if (items[4].isnumeric() and items[0] != 'VEN'):
if not (items[4] in coop_ids):
coop_ids.append(items[4])
received_data.append(items)
xlsx_content = base64.b64decode(report['datas'])
excel_file = BytesIO(xlsx_content)
workbook = load_workbook(excel_file, read_only=True)
sheet = workbook.active
#use min_row = 2 to skip headers
for row in sheet.iter_rows(min_row=2, max_row=sheet.max_row, values_only=True):
row = [str(item) for item in row]
if len(row) == 8:
if row[4] and row[4].isnumeric() and row[0] != 'VEN':
if not (row[4] in coop_ids):
coop_ids.append(row[4])
received_data.append(row)
coops = retrieve_odoo_coop_data(coop_ids)
summarized_lines = process_received_lines(received_data, coops, True)
res = generate_quadratus_file(summarized_lines)
files = []
fsuffix = report['name'].replace(".csv", ".txt")
fsuffix = report['name'].replace(".xlsx", ".txt")
if len(res['V']) > 0:
vfn = 'data/ventes_' + fsuffix
vf = open(vfn, 'wb')
......@@ -267,7 +273,7 @@ def generate_quadratus_compatible_file(report):
kf.close()
files.append(kfn)
file_path = 'data/' + report['name'].replace(".csv", ".zip")
file_path = 'data/' + report['name'].replace(".xlsx", ".zip")
with zipfile.ZipFile(file_path, 'w') as zipf:
for f in files:
zipf.write(f)
......@@ -314,4 +320,4 @@ def get_members_capital_at_date(date,only_active):
return OdooAPI().execute("lacagette.exports", 'get_members_capital_at_date', {'date': date, 'only_active': only_active})
except Exception as e:
coop_logger.error("Erreur get_members_capital_at_date : %s", str(e))
return None
\ No newline at end of file
return {'error': "Erreur get_members_capital_at_date : " + str(e)}
\ No newline at end of file
......@@ -34,6 +34,9 @@ footer { position: fixed;
font-style: italic;
color: blue;
}
.change_passwd_info {background-color: blue;}
#deconnect, #password_change {float:right; margin-left: 5px;}
/* The Overlay (background) */
......
......@@ -373,6 +373,7 @@ function show_admin_menu() {
function store_credentials(data) {
createCookie("authtoken", data.authtoken);
createCookie("uid", data.uid);
createCookie("login", data.login);
}
function coop_authenticate(callback) {
......@@ -499,26 +500,56 @@ Number.prototype.pad = function(size) {
return s;
};
// Accordions
var acc = document.getElementsByClassName("accordion");
var i;
Date.prototype.getABCDWeekLetter = function(debug) {
//high level const week_a_date must be declared (done in base.html template)
try {
const weeks = ['A', 'B', 'C', 'D'],
target_year = this.getFullYear(),
target_month = this.getMonth();
let letter = '';
this.setUTCFullYear(target_year)
this.setUTCMonth(target_month)
this.setUTCHours(0)
const difference = this.getTime() - new Date(week_a_date).getTime();
const w_index = Math.floor(difference / (1000 * 3600 * 24 * 7)) % 4;
letter = weeks[w_index];
if (debug) {
console.log(this)
console.log(letter + ' (' + (w_index) + ')')
}
return letter
} catch(e) {
// not critic
console.log(e);
return null;
}
}
for (i = 0; i < acc.length; i++) {
acc[i].addEventListener("click", function() {
// Accordions
$(document).on('click', '.accordion', function(){
/* Toggle between adding and removing the "active" class,
to highlight the button that controls the panel */
this.classList.toggle("active");
/* Toggle between hiding and showing the active panel */
var panel = this.nextElementSibling;
if (panel.style.display === "block") {
panel.style.display = "none";
} else {
panel.style.display = "block";
}
if (panel.style.maxHeight) {
panel.style.maxHeight = null;
} else {
panel.style.maxHeight = panel.scrollHeight + "px";
}
});
}
});
function report_JS_error(e, m) {
try {
......@@ -544,3 +575,33 @@ function isMacUser() {
if (isMacUser() && isSafari()) $('.mac-msg').show();
show_enqueued_messages();
(function($){
$.fn.helpFillDate = function (sep='/') {
var elt = this
elt.on('keyup touchend', function(){
var text = elt.val();
var keyChar = text.substr(-1);
var reg = /^\d+$/;
if (reg.test(keyChar) == false || text.length == 11) text = text.slice(0, -1);
if (text.length == 2 || text.length == 5) text += sep
elt.val(text);
});
};
$.fn.helpFillTel = function (sep=' ') {
var elt = this
elt.on('keyup touchend', function(){
var text = elt.val();
var keyChar = text.substr(-1);
var reg = /^\d+$/;
if (reg.test(keyChar) == false || text.length == 15) text = text.slice(0, -1);
if (text.length == 2 || text.length == 5 || text.length == 8 || text.length == 11) text += sep
elt.val(text);
});
};
}(jQuery));
\ No newline at end of file
......@@ -25,7 +25,7 @@ jQuery(document).ready(function($) {
dataType :'json'
})
.done(function(rData) {
if (rData.response && rData.response == true) {
if (rData.response) {
newWaitingMessage('Le document Odoo est terminé.<br/>Les ventes journalières sont compilées et les identifiants de coop. substitués.');
$.ajax({url : url + '?phase=2&export_id='+export_id+ '&final_format=' + $('[name="final_format"]').val(),
dataType :'json'
......
(function($){
// Suppose input name="zip" and name="city" are in form
// A div with class="geo_suggestions" has to be available too
$.fn.addSearchAutocomplete = function () {
var elt = this,
prev_key_evt_datetime = null,
prev_search_datetime = null,
prev_answer_datetime = null,
suggest_list = document.querySelector('.geo_suggestions');
const get_element_geom = function(el) {
var rect = el.getBoundingClientRect(),
scrollLeft = window.pageXOffset || document.documentElement.scrollLeft,
scrollTop = window.pageYOffset || document.documentElement.scrollTop;
return { top: rect.top + scrollTop, left: rect.left + scrollLeft, height: el.offsetHeight, width: el.offsetWidth}
}
const clear_suggestion_list = function() {
if (suggest_list) suggest_list.innerHTML = '';
}
const fill_with_suggestion = function(item) {
$('[name="address"]').val(item.getAttribute('data-street'));
$('[name="zip"]').val(item.getAttribute('data-zip'));
$('[name="city"]').val(item.getAttribute('data-city'));
suggest_list.style.display = 'none';
}
const display_results = function(features) {
let geom = get_element_geom(elt.get(0)),
list = document.createElement("ul");
suggest_list.style.position = 'absolute';
suggest_list.style.display = 'block';
suggest_list.style.top = (geom.top + geom.height + 2) + 'px';
suggest_list.style.left = geom.left + 'px';
suggest_list.style.width = geom.width + 'px';
features.forEach(function(f){
let item = document.createElement("li"),
cat = street = '';
if (f.properties.type == 'municipality') {
cat = 'fa fa-building';
} else if (f.properties.type == 'street') {
cat = 'fa fa-road';
} else {
cat = 'fa fa-map-marker';
}
item.classList.add('geo-suggestion')
item.setAttribute('data-zip', f.properties.postcode);
item.setAttribute('data-city', f.properties.city);
if (f.properties.type == 'housenumber' || f.properties.type == "street") {
street = f.properties.name;
}
item.setAttribute('data-street',street);
item.innerHTML = '<i class="' + cat + '"></i>' + f.properties.label
list.appendChild(item)
});
suggest_list.innerHTML = list.outerHTML;
}
//fa fa-building fa fa-road fa fa-map-marker
const search_address = function(q) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
try {
const results = JSON.parse(this.responseText)
prev_answer_datetime = (new Date()).getTime();
if (results.features.length > 0) {
display_results(results.features)
}
} catch (err) {
console.log(err)
}
}
};
xhttp.open("GET", "https://api-adresse.data.gouv.fr/search/?q=" + q);
xhttp.send();
}
const launch_search_if_needed = function(text, now) {
// launch search, excepted if one has been launched few time ago
if (text.length > 0 && (prev_search_datetime == null || (now - prev_search_datetime >= 500))) {
prev_search_datetime = now;
clear_suggestion_list();
search_address(text);
}
}
elt.on('keyup touchend', function(){
suggest_list.style.display = 'none';
var text = elt.val().trim(),
now = (new Date()).getTime();
if (prev_key_evt_datetime == null) {
prev_key_evt_datetime = now;
} else {
if ((now - prev_key_evt_datetime) > 750 && text.length > 0) {
// elapsed time is enough to consider it as a possible end
launch_search_if_needed(text, now);
}
}
prev_key_evt_datetime = now
setTimeout(function(){
// needed to launch search if no more keyup
let now = (new Date()).getTime();
launch_search_if_needed(elt.val().trim(), now);
}, 500);
});
document.addEventListener('click', function (event) {
if (event.target.tagName == "LI" && event.target.matches('.geo-suggestion')) {
fill_with_suggestion(event.target);
} else {
suggest_list.style.display = 'none';
}
});
};
}(jQuery));
......@@ -150,17 +150,4 @@ function get_module_settings() {
}
get_module_settings();
$(document).on('click', '.accordion', function(){
/* Toggle between adding and removing the "active" class,
to highlight the button that controls the panel */
this.classList.toggle("active");
/* Toggle between hiding and showing the active panel */
var panel = this.nextElementSibling;
if (panel.style.display === "block") {
panel.style.display = "none";
} else {
panel.style.display = "block";
}
});
......@@ -17,6 +17,8 @@ from openpyxl import Workbook
from openpyxl.writer.excel import save_virtual_workbook
from orders.models import Orders
import traceback
def test_compta(request):
generate_quadratus_compatible_file('bidon')
......@@ -127,8 +129,9 @@ class ExportCompta(View):
response = JsonResponse({'response': val})
except Exception as e:
val = str(e)
response = JsonResponse({'error': val})
# Capture the full stack trace
stack_trace = traceback.format_exc()
response = JsonResponse({'error': str(e), 'stack_trace': stack_trace})
return response
......@@ -204,7 +207,7 @@ class ExportPOS(View):
# p['name'] is a sequence generated string
# Test order is important as CHEQDEJ contains CHEQ for ex.
# p['journal'] could be used but easier to change in Odoo interface
sub_amount = round(p['total_amount'], 2)
sub_amount = round(float(p['total_amount']), 2) if p['total_amount'] else 0.00
if 'CSH' in p['name']:
csh = sub_amount
elif 'CHEQDEJ' in p['name']:
......@@ -317,11 +320,11 @@ class ExportPOS(View):
totals = {}
for r in res:
if (r['total_amount'] > 0):
if r['total_amount'] and float(r['total_amount']) > 0:
# d = time.strptime(r['min_date'], tf)
# date = str(d.tm_mday) + '-' + str(d.tm_mon) + '-' + str(d.tm_year)
date, hours = r['min_date'].split(' ')
total = round(r['total_amount'], 2)
total = round(float(r['total_amount']), 2)
cb = csh = chq = 0
caisse = r['config_id'][1]
......
......@@ -8,6 +8,8 @@ import tempfile
import pymysql.cursors
import datetime
import re
import sys
import traceback
vcats = []
......@@ -55,8 +57,8 @@ class CagetteProduct(models.Model):
api = OdooAPI()
cond = [['product_tmpl_id.id', '=', template_id]]
fields = ['barcode', 'product_tmpl_id', 'pricetag_rackinfos',
'price_weight_net', 'price_volume', 'list_price',
'weight_net', 'volume', 'to_weight', 'meal_voucher_ok']
'price_weight', 'price_volume', 'list_price',
'weight', 'volume', 'to_weight', 'meal_voucher_ok']
if not getattr(settings, 'SHOW_MEAL_VOUCHER_OK_LINE_IN_PRODUCT_INFO_FOR_LABEL', True):
fields.remove('meal_voucher_ok')
additionnal_fields = getattr(settings, 'SHELF_LABELS_ADD_FIELDS', [])
......@@ -78,24 +80,36 @@ class CagetteProduct(models.Model):
res = {}
try:
p = CagetteProduct.get_product_info_for_label_from_template_id(templ_id)
coop_logger.info("Generate label info : %s ", str(p))
if (p and p[0]['product_tmpl_id'][0] == int(templ_id)):
product = p[0]
txt = ''
meal_voucher_found = False
if price is not None and len(price) == 0:
price = None
for k, v in product.items():
if type(v) == list and len(v) > 0 :
v = v[-1]
if k == 'product_tmpl_id':
k = 'name'
if k == 'list_price' and len(price) > 0 and float(price) > 0:
if k == 'price_weight':
k = 'price_weight_net'
if k == 'weight':
k = 'weight_net'
if k == 'meal_voucher_ok':
meal_voucher_found = True
if k == 'list_price' and price is not None and float(price) > 0:
v = price
if k == 'price_weight_net' and len(v) > 0 and len(price) > 0 and float(price) > 0:
if k == 'price_weight_net' and len(str(v)) > 0 and float(v) > 0 and price is not None and float(price) > 0:
v = round(float(price) / float(product['weight_net']), 2)
if k == 'price_volume' and len(v) > 0 and len(price) > 0 and float(price) > 0:
if k == 'price_volume' and len(str(v)) > 0 and float(v) > 0 and price is not None and float(price) > 0:
v = round(float(price) / float(product['volume']), 2)
if directory != "/product_labels/" or (directory == "/product_labels/" and k != "meal_voucher_ok"):
# add parameter to text unless it's for a product label and parameter is meal_voucher_ok
txt += k + '=' + str(v).strip() + "\r\n"
if directory == '/labels/' and meal_voucher_found is False:
txt += 'meal_voucher_ok=' + "\r\n"
if not (nb is None) and len(nb) > 0:
txt += 'nb_impression=' + str(nb) + "\r\n"
res['txt'] = txt
......@@ -107,7 +121,8 @@ class CagetteProduct(models.Model):
file.close()
except Exception as e:
res['error'] = str(e)
coop_logger.error("Generate label : %s %s", templ_id, str(e))
trace=traceback.extract_tb(sys.exc_info()[2])
coop_logger.error("Generate label trace : %s %s", templ_id, str(trace))
return res
@staticmethod
......@@ -334,13 +349,18 @@ class CagetteProducts(models.Model):
if ('image_medium' in p):
p['image'] = p['image_medium']
p['image_medium'] = ''
else:
p['image'] = ''
if 'image_small' in p:
p['image'] = p['image_small']
p['image_small'] = ''
# if ('image' in p):
# p['image'] = __process_img_data(p, 'image')
if type(p['image']) is bool:
p['image'] = ''
if p['categ_id'][0] == settings.CATEG_FRUIT:
if p['categ_id'][0] in settings.FR_CATEGS:
p['categ'] = 'F'
elif p['categ_id'][0] == settings.CATEG_LEGUME:
elif p['categ_id'][0] in settings.VEG_CATEGS:
p['categ'] = 'L'
elif (p['name'].lower().find(' vrac') > -1) or (p['categ_id'][0] in vcats):
p['categ'] = 'V'
......@@ -385,7 +405,7 @@ class CagetteProducts(models.Model):
@staticmethod
def get_fl_products(withCandidate=False, fields=[]):
api = OdooAPI()
flv_cats = [settings.CATEG_FRUIT, settings.CATEG_LEGUME]
flv_cats = settings.FR_CATEGS + settings.VEG_CATEGS
cond = [['active', '=', True],
['available_in_pos', '=', True],
['categ_id', 'in', flv_cats]]
......@@ -396,7 +416,7 @@ class CagetteProducts(models.Model):
@staticmethod
def get_products_for_label_appli(withCandidate=False):
fields = ['sale_ok', 'uom_id', 'barcode',
'name', 'display_name', 'list_price', 'categ_id', 'image_medium']
'name', 'display_name', 'list_price', 'categ_id', 'image_small']
if getattr(settings, 'EXPORT_POS_CAT_FOR_SCALES', False) is True:
fields.append('pos_categ_id')
to_weight = CagetteProducts.get_products_to_weight(withCandidate, fields)
......@@ -441,12 +461,7 @@ class CagetteProducts(models.Model):
for p in res['list']:
# transcode result to compact format (for bandwith save and browser memory)
# real size / 4 (for 2750 products)
# following 2 lines is only useful for La Cagette (changing uom_id in Database has cascade effects...)
# TODO : Use mapping list in config.py
if p['uom_id'] == 3:
p['uom_id'] = 21
if p['uom_id'] == 20:
p['uom_id'] = 1
result['pdts'][p['barcode']] = [
p['display_name'],
p['sale_ok'],
......@@ -469,8 +484,8 @@ class CagetteProducts(models.Model):
api = OdooAPI()
try:
cond = [['active', '=', True]]
fields = ['display_name', 'uom_type']
res = api.search_read('product.uom', cond, fields)
fields = ['name', 'uom_type', 'factor']
res = api.search_read('uom.uom', cond, fields)
result['list'] = res
except Exception as e:
result['error'] = str(e)
......@@ -597,7 +612,7 @@ class CagetteProducts(models.Model):
return res
@staticmethod
def get_products_for_order_helper(supplier_ids, pids = [], stats_from = None):
def get_products_for_order_helper(supplier_ids, pids = [], stats_from = None, with_fakedata=False):
"""
supplier_ids: Get products by supplier if one or more supplier id is set. If set, pids is ignored.
pids: If set & supplier_ids is None/empty, get products specified in pids. In this case, suppliers info won't be fetched.
......@@ -630,12 +645,13 @@ class CagetteProducts(models.Model):
# Get products templates
f = [
"id",
"state",
"active",
"name",
"default_code",
"qty_available",
"incoming_qty",
"uom_id",
"uom_po_id",
"purchase_ok",
"supplier_taxes_id",
"product_variant_ids",
......@@ -643,7 +659,9 @@ class CagetteProducts(models.Model):
]
c = [['id', 'in', ptids], ['purchase_ok', '=', True], ['active', '=', True]]
products_t = api.search_read('product.template', c, f)
filtered_products_t = [p for p in products_t if p["state"] != "end" and p["state"] != "obsolete"]
# state replaced by active in product_template table
filtered_products_t = [p for p in products_t if p["active"]]
sales_average_params = {
'ids': ptids,
......@@ -676,6 +694,8 @@ class CagetteProducts(models.Model):
'product_code': psi_item["product_code"],
'sequence': psi_item["sequence"]
})
if len(sales) == 0:
filtered_products_t[i]['daily_conso'] = 0
for s in sales:
if s["id"] == fp["id"]:
......@@ -683,6 +703,10 @@ class CagetteProducts(models.Model):
filtered_products_t[i]['sigma'] = s["sigma"]
filtered_products_t[i]['vpc'] = s["vpc"]
if with_fakedata is True:
for p in filtered_products_t:
p['daily_conso'] = 100
res["products"] = filtered_products_t
except Exception as e:
coop_logger.error('get_products_for_order_helper %s (%s)', str(e))
......
......@@ -186,6 +186,8 @@ def commit_actions_on_product(request):
if res_inventory['errors'] or res_inventory['missed']:
res["code"] = "error_stock_update"
res["error"] = res_inventory['errors']
res["done"] = res_inventory["done"]
res["inv_id"] = res_inventory["inv_id"]
return JsonResponse(res, status=500)
except Exception as e:
res["code"] = "error_stock_update"
......@@ -269,7 +271,7 @@ def labels_appli_csv(request, params):
for c in file_copies:
copyfile(os_file, c)
res['fichiers_generes'] = len(file_copies) + 1
except Exception as e:
res['error'] = str(e)
return JsonResponse({'res': res})
......
from django.contrib import admin
from outils.common_imports import *
from outils.for_view_imports import *
from django.views.generic import View
from django.http import HttpResponse
from django.http import JsonResponse
# Register your models here.
import os
from datetime import date
from openpyxl import Workbook
from openpyxl import load_workbook
from openpyxl.styles import Alignment
from reception.models import CagetteReception
from outils.common import OdooAPI
from members.models import CagetteUser
from products.models import CagetteProduct
def index(request):
"""Accueil admin"""
if 'reception' in settings.COUCHDB['dbs']:
context = {
'title': 'Admin Reception',
}
template = loader.get_template('reception/admin.html')
return HttpResponse(template.render(context, request))
else:
return HttpResponse("Need to configure reception couchdb db in settings_secret.py")
def get_backups(request):
orders = []
po_ids = []
for file in os.listdir('data/receptions_backup'):
if '.json' in file:
with open('data/receptions_backup/' + file, 'r') as json_file:
bup = json.load(json_file)
for oid, o in bup['orders'].items():
if 'br_valid' in file:
[bup['id'], timestamp] = file.split('_br_valid_')
else:
[bup['id'], timestamp] = file.split('_qty_valid_')
bup['date'] = datetime.datetime.fromtimestamp(int(timestamp.replace('.json', ''))/1000).strftime("%d/%m/%Y")
if len(o['po']) > 0:
bup['supplier'] = o['po'][0]['partner_id'][1]
bup['id'] = o['po'][0]['id_po']
else:
po_ids.append(int(bup['id']))
orders.append(bup)
if len(po_ids) > 0:
api = OdooAPI()
cond = [['id', 'in', po_ids]]
fields = ['partner_id']
res = api.search_read('purchase.order', cond, fields)
for r in res:
for order in orders:
if 'id' in order:
if str(order['id']) == str(r['id']):
order['supplier'] = r['partner_id'][1]
continue
return JsonResponse({'data': orders, 'po_ids': po_ids}, safe=False)
......@@ -35,14 +35,18 @@ class CagetteReception(models.Model):
pids.append(int(r['purchase_id'][0]))
if len(pids):
f=["id","name","date_order", "partner_id", "date_planned", "amount_untaxed", "amount_total", "x_reception_status", 'create_uid']
f=["id","name","date_order", "partner_id", "date_planned", "amount_untaxed", "amount_total", "reception_status", 'create_uid']
# Only get orders that need to be treated in Reception
c = [['id', 'in', pids], ["x_reception_status", "in", [False, 'qty_valid', 'valid_pending', 'br_valid']]]
c = [
['id', 'in', pids],
["reception_status", "in", [False, 'qty_valid', 'valid_pending', 'br_valid']],
["state", "not in", ["cancel", "done"]]
]
orders = api.search_read('purchase.order', c, f)
except Exception as e:
print(str(e))
coop_logger.error("CagetteReception.get_orders : %s", str(e))
return orders
def get_order_unprocessable_products(id_po):
......@@ -68,7 +72,7 @@ class CagetteReception(models.Model):
except Exception as e:
print(str(e))
coop_logger.error("CagetteReception.get_mail_create_po : %s", str(e))
return res
......@@ -77,7 +81,8 @@ class CagetteReception(models.Model):
lines_data = Order(self.id).get_lines()
bc_pattern = re.compile('^0493|0499') # TODO : Adjust for other pattern (such as Supercoop)
for l in lines_data['lines']:
if not (bc_pattern.match(str(l['barcode'])) is None):
if 'barcode' in l and not (bc_pattern.match(str(l['barcode'])) is None):
# "'barcode' in l" has been added after actual case where it was missing !
answer = True
# print ('answer=' + str(answer))
return answer
......@@ -111,7 +116,7 @@ class CagetteReception(models.Model):
def update_order_status(self, id_po, updateType):
"""Update purchase.order with new reception status """
f = {'x_reception_status':updateType}
f = {'reception_status':updateType}
res = self.o_api.update('purchase.order', int(id_po), f)
return res
......@@ -173,31 +178,6 @@ class CagetteReception(models.Model):
return processed_lines
def update_products_price(self):
processed = 0
errors = []
order_lines_data = CagetteReception.get_order_lines_by_po(self.id)
order_lines = order_lines_data['lines']
if order_lines and len(order_lines) > 0:
# Exceptions are due to the fact API returns None whereas the action is really done !...
marshal_none_error = 'cannot marshal None unless allow_none is enabled'
processed = 0
for line in order_lines:
try:
self.o_api.execute('purchase.order.line','update_po_price_to_vendor_price',[int(line['id'])])
processed += 1
except Exception as e:
if not (marshal_none_error in str(e)):
errors.append(str(e))
else:
processed += 1
if processed == len(order_lines):
success = True
else:
success = False
return {'errors': errors, 'processed': processed, 'success': success, 'lines': order_lines}
def print_shelf_labels_for_updated_prices(self, lines):
import requests
# don't print barcode which begin with these codes
......@@ -222,53 +202,59 @@ class CagetteReception(models.Model):
if len(to_reset) > 0:
self.o_api.update('product.product', to_reset, {'to_print': 0})
def update_products_price_v12(self):
result = {'success': False}
try:
result['success'] = self.o_api.execute('purchase.order', 'update_po_price_to_vendor_price', [self.id])
except Exception as e:
coop_logger.error("update_products_price_v12 : %s", str(e))
result['error'] = str(e)
return result
def finalyze_picking(self):
"""stock_picking is created to make,
stock immediate transfer is done,
products are updated with new vendor prices"""
def stock_picking_update(self, order_line):
result = None
try:
res = self.o_api.search_read('stock.move', [['purchase_line_id', '=', order_line['id']]], ['id'])
if res:
fields = {
'package_qty': float(order_line['package_qty']),
'product_qty_package': float(order_line['product_qty_package']),
'product_uom_qty': float(order_line['package_qty']) * float(order_line['product_qty_package']),
'price_unit': float(order_line['price_unit'])
}
ids = []
for r in res:
ids.append(r['id'])
self.o_api.update('stock.move', ids, fields)
result = True
if self.o_api == None:
return 'error : cant reach odoo'
except Exception as e:
coop_logger.error("Stock picking update : %s ", str(e))
result = False
return result
res = self.o_api.execute('purchase.order', 'action_view_picking', [self.id])
new_x_reception_status = ''
if res:
sp = self.o_api.search_read('stock.picking',[['id','=', int(res['res_id'])]],['pack_operation_ids','state'],1)
if sp:
if sp[0]['state'] == 'assigned':
pack_operation_ids = sp[0]['pack_operation_ids']
cpt = self.make_immediate_transfer(pack_operation_ids)
if cpt == len(pack_operation_ids):
def finalyze_picking_v12(self):
result = None
try:
self.o_api.execute('stock.picking','do_transfer', [int(sp[0]['id'])])
res = self.o_api.execute('purchase.order', 'stock_immediate_transfer', [self.id])
done = 0
for r in res:
if r in ['done', 'cancel']:
done += 1
if done == len(res):
result = 'processed'
except:
result = 'error: transfer'
new_x_reception_status = 'error_transfer'
else:
result = 'error: pack operations'
new_x_reception_status = 'error_pack_op'
else:
result = 'already done'
else:
result = 'error: cant access stock picking'
new_x_reception_status = 'error_picking'
if result == 'processed':
price_update = self.update_products_price()
price_update = self.update_products_price_v12()
if price_update['success'] is False:
result = 'error: price update'
new_x_reception_status += '/error_uprice'
if new_x_reception_status == '':
new_x_reception_status = 'done'
else:
if getattr(settings, 'RECEPTION_SHELF_LABEL_PRINT', False) is True:
self.print_shelf_labels_for_updated_prices(price_update['lines'])
else:
result = False
if result != 'already done':
self.o_api.update('purchase.order', [self.id], {'x_reception_status': new_x_reception_status})
except Exception as e:
coop_logger.error("Finalyze picking : %s ", str(e))
return result
......@@ -303,7 +289,7 @@ class CagetteReception(models.Model):
if len(to_process) > 0:
for p in to_process:
m = CagetteReception(int(p['id']))
fp = m.finalyze_picking()
fp = m.finalyze_picking_v12()
if fp == 'processed':
print_label = m.implies_scale_file_generation()
if fp == 'processed' or fp == 'already done':
......
var orders = [],
table_orders = null,
op_details = null;
function replay() {
}
/**
* Display the main orders table
*/
function display_orders_table() {
if (table_orders) {
table_orders.clear().destroy();
$('#orders').empty();
}
table_orders = $('#orders').DataTable({
data: orders,
columns:[
{
data:"id",
title:"Sélectionner",
className:"dt-body-center",
render: function (data) {
return '<input type="checkbox" id="select_bc_'+data+'" value="'+data+'">';
},
width: "4%",
orderable: false
},
{data:"date", "title":"Date traitement", "width": "8%", "className":"dt-body-center"},
{
data: "id",
title: "Id. commande",
},
{
data:"supplier",
title:"Fournisseur",
render: function (data, type, full) {
// Add tooltip with PO over partner name
return '<div class="tooltip">' + data + ' <span class="tooltiptext">' + full.id + '</span> </div>';
}
},
{
title: "Etape",
data:"update_type",
className:"dt-body-center",
orderable: false,
width: "20%"
},
],
dom: 'rtip',
order: [
[
1,
"asc"
]
],
iDisplayLength: 25,
language: {url : '/static/js/datatables/french.json'}
});
}
$('#orders').on('click', 'tbody td', function () {
var row_data = table_orders.row($(this)).data();
var data_to_show = [];
if (op_details) {
op_details.clear().destroy();
$('#operation_details').empty();
}
var modal_content = $('#show_detail').clone()
modal_content.find('table').attr('id', 'operation_details')
modal_content.find('h3').html(row_data.supplier)
modal_content.find('p').html("Commande id. " + row_data.id + ", étape " + row_data.update_type)
for (oid in row_data.orders) {
row_data.orders[oid]['po'].forEach((r) => {data_to_show.push(r)})
}
var table_columns = [{
data: "barcode",
title: "Code-barre",
},
{
title: "Article",
render: function (data, type, full) {
return full.product_id[1];
}
},
{
title: "UdM",
render: function (data, type, full) {
return full.product_uom[1];
}
}
];
if (row_data.update_type == 'qty_valid') {
table_columns.push({data: "old_qty", title: "Qté prévue"});
table_columns.push({data: "product_qty", title: "Qté reçue"});
} else {
table_columns.push({data: "old_price_unit", title: "Prix prévu"});
table_columns.push({data: "price_unit", title: "Prix constaté"});
}
console.log(data_to_show)
openModal(modal_content.html(), replay, 'Rejouer', false);
op_details = $('#operation_details').DataTable({
data: data_to_show,
columns: table_columns,
dom: 'rtip',
order: [
[
1,
"asc"
]
],
iDisplayLength: 25,
language: {url : '/static/js/datatables/french.json'}
});
})
$(document).ready(function() {
if (coop_is_connected()) {
openModal();
// Set date format for DataTable so date ordering can work
$.fn.dataTable.moment('D/M/Y');
// Get orders
$.ajax({
type: 'GET',
url: "/reception/get_backups",
dataType:"json",
traditional: true,
contentType: "application/json; charset=utf-8",
success: function(data) {
orders = data.data;
display_orders_table();
closeModal();
},
error: function(data) {
err = {msg: "erreur serveur lors de la récupération des commandes", ctx: 'get_list_orders'};
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 récupération des commandes, rechargez la page plus tard.');
}
});
}
});
......@@ -2,6 +2,7 @@
from django.conf.urls import url
from . import views
from . import admin
urlpatterns = [
url(r'^$', views.home),
......@@ -17,5 +18,9 @@ urlpatterns = [
url(r'^reception_pricesValidated', views.reception_pricesValidated),
# url(r'^update_order_status/([0-9]+)$', views.tmp_update_order_status),
url(r'^po_process_picking$', views.po_process_picking),
url(r'^send_mail_no_barcode', views.send_mail_no_barcode)
url(r'^send_mail_no_barcode', views.send_mail_no_barcode),
url(r'^check/prices$', views.check_prices),
# Admin
url(r'^admin$', admin.index),
url(r'^get_backups$', admin.get_backups)
]
......@@ -10,6 +10,9 @@ from datetime import date
from openpyxl import Workbook
from openpyxl import load_workbook
from openpyxl.styles import Alignment
from openpyxl.writer.excel import save_virtual_workbook
import dateutil.parser
from reception.models import CagetteReception
from outils.common import OdooAPI
......@@ -69,7 +72,7 @@ def get_list_orders(request):
"date_planned" : order["date_planned"],
"amount_untaxed" : round(order["amount_untaxed"],2),
"amount_total" : round(order["amount_total"],2),
"reception_status" : str(order["x_reception_status"])
"reception_status" : str(order["reception_status"])
}
if get_order_lines is True:
......@@ -199,7 +202,7 @@ def update_orders(request):
float(order_line['package_qty']),
float(order_line['product_qty_package']),
float(order_line['price_unit']))
if not (update is True):
if update is not True:
# indicative_package may have been changed since data have been loaded in browser, retry
m.remove_package_restriction(order_line)
update = m.update_line(int(order_line['id']),
......@@ -207,9 +210,12 @@ def update_orders(request):
float(order_line['package_qty']),
float(order_line['product_qty_package']),
float(order_line['price_unit']))
if not (update is True):
if update is not True:
errors.append(order_line['id'])
if update is True:
spu = m.stock_picking_update(order_line)
# If update succeded, and supplier shortage set, try to register the supplier shortage
if update is True and 'supplier_shortage' in order_line:
try:
......@@ -661,3 +667,64 @@ def send_mail_no_barcode(request):
return JsonResponse("ok", safe=False)
def check_prices(request):
"""Check if stock transfers and price for br_valid purchase orders have been completed."""
# Grouped orders are not processed
data = {}
found = []
try:
oids = []
api = OdooAPI()
min_date = datetime.datetime.now() - datetime.timedelta(weeks=2)
cond = [['reception_status', '=', 'br_valid'], ['write_date', '>=', min_date.isoformat()]]
fields = ['name', 'parent_id', 'order_line']
orders = api.search_read('purchase.order', cond, fields)
if len(orders) > 0:
id_fn_pattern = re.compile(r'^([0-9]+)_br_valid_([0-9]+).json')
pdt_oids = []
for o in orders:
oids.append(o['id'])
for file in os.listdir('data/receptions_backup'):
id_search = id_fn_pattern.search(file)
if id_search:
current_id = id_search.group(1)
if int(current_id) in oids:
found.append(str(current_id))
with open('data/receptions_backup/' + file, 'r') as json_file:
bup = json.load(json_file)
for oid, o in bup['orders'].items():
if len(o['po']) > 0:
products = []
for pol in o['po']:
products.append({'id': pol['product_id'],
'partner_id': pol['partner_id'],
'price': pol['price_unit'],
'update': datetime.datetime.fromtimestamp(int(id_search.group(2))/1000).strftime("%d/%m/%Y")})
if int(pol['product_id'][0]) not in pdt_oids:
pdt_oids.append(pol['product_id'][0])
data[oid] = {'products': products}
if len(pdt_oids) > 0:
# WARNING !!! Supplier discount is not considered !!!
# (need to get product_tmpl_id and request product.supplierinfo ! (or make Odoo special api method))
cond = [['id', 'in', pdt_oids]]
fields = ['base_price']
p_res = api.search_read('product.product', cond, fields)
for p in p_res:
for oid, d in data.items():
i = 0
for pdt in d['products']:
if pdt['id'][0] == p['id']:
if float(p['price']) == float(pdt['base_price']):
data[oid]['products'][i]['price_ok'] = True
else:
data[oid]['products'][i]['price_ok'] = False
data[oid]['products'][i]['found_base_price'] = p['base_price']
i += 1
except Exception as e1:
coop_logger.error("check PO %s", str(e1))
return JsonResponse({'data': data})
......@@ -50,6 +50,18 @@ def delete(request):
result['error'] = "Forbidden"
return JsonResponse({'res': result})
def print_labels(request, shelf_id):
result = {}
is_connected_user = CagetteUser.are_credentials_ok(request)
if is_connected_user is True:
try:
result = Shelf(shelf_id).print_labels()
except Exception as e:
result['error'] = str(e)
else:
result['error'] = "Forbidden"
return JsonResponse({'res': result})
def add_products(request):
import json
result = {}
......
......@@ -2,7 +2,7 @@ from django.db import models
from outils.common_imports import *
from outils.common import OdooAPI
from products.models import CagetteProducts
from products.models import CagetteProducts, CagetteProduct
from inventory.models import CagetteInventory
import os
......@@ -38,7 +38,7 @@ class Shelf(models.Model):
res['error'] = "Le rayon n'a pas pu être trouvé (" + str(e) + ")"
return res
def get_products(self):
def get_products(self, additional_fields=[]):
res = {}
try:
c = [['shelf_id', '=', self.id]]
......@@ -53,6 +53,7 @@ class Shelf(models.Model):
'active'
]
f += additional_fields
pdts = self.o_api.search_read('product.product', c, f)
for p in pdts:
for k, v in p.items():
......@@ -116,7 +117,11 @@ class Shelf(models.Model):
if 'shelf_losses' in params:
f['last_inv_losses_percentage'] = params['shelf_losses']
try:
res['update'] = self.o_api.update('product.shelfs', self.id, f)
except Exception as e:
coop_logger.error("Error while updating products shelf %s", stre(e))
res['error'] = "Error while updating products shelf (" + str(e) + ")"
return res
......@@ -148,6 +153,16 @@ class Shelf(models.Model):
res['error'] = "Le rayon n'a pas pu être détruit"
return res
def print_labels(self):
res = {}
try:
for p in self.get_products(['product_tmpl_id'])['data']:
CagetteProduct().generate_label_for_printing(p['product_tmpl_id'][0], '/labels/')
res['success'] = "Le rayon a correctement été imprimé"
except Exception as e:
coop_logger.error("Rayon, print labels : %s", str(e))
res['error'] = "Le rayon n'a pas pu être imprimé"
return res
def _get_pdts_from_barcodes(self, barcodes):
c = [['barcode', 'in', barcodes]]
......@@ -541,16 +556,24 @@ class Shelf(models.Model):
class Shelfs(models.Model):
def get_all(precision='full'):
res = []
shelfs = []
try:
api = OdooAPI()
if precision == 'simple':
res = api.search_read('product.shelfs', [], ['name', 'sort_order'], order='sort_order asc')
else:
res = api.execute('product.shelfs', 'get', {})
for r in res:
if precision != 'simple':
if r['ongoing_inv_start_datetime'] == "1-01-01 00:00:00":
r['ongoing_inv_start_datetime'] = ""
if r['date_last_product_added'] == "1-01-01":
r['date_last_product_added'] = ""
coop_logger.info(str(r))
shelfs.append(r)
except Exception as e:
coop_logger.error("Rayons, get_all : %s", str(e))
return res
return shelfs
@staticmethod
def get_shelfs_sortorder(shelf_ids=[]):
......
......@@ -24,7 +24,7 @@ function init_datatable() {
if (type == "sort" || type == 'type')
return data;
if (data == '0001-01-01 00:00:00')
if (data == '0001-01-01 00:00:00' || data == '')
return "";
else {
var date = new Date(data);
......@@ -49,7 +49,7 @@ function init_datatable() {
if (type == "sort" || type == 'type')
return data;
if (data == '0001-01-01')
if (data == '0001-01-01' || data == '')
return "";
else {
var date = new Date(data);
......
......@@ -26,4 +26,5 @@ urlpatterns = [
url(r'^admin/update$', admin.update),
url(r'^admin/delete$', admin.delete),
url(r'^admin/add_products$', admin.add_products),
url(r'^print_labels/(?P<shelf_id>\d+)$', admin.print_labels),
]
......@@ -233,7 +233,7 @@ def do_shelf_inventory(request):
except Exception as e:
# Don't validate if error anywhere in inventory process
res['error'] = type(e).__name__
res['error'] = str(e)
coop_logger.error("Shelf inv. : %s", str(e))
except Exception as err_json:
res['error'] = "Unable to parse received JSON"
......
......@@ -2,7 +2,9 @@ from django.db import models
from outils.common_imports import *
from outils.common import OdooAPI
from outils.common import Verification
from members.models import CagetteMember
from members.models import CagetteUser
from pytz import timezone
......@@ -11,6 +13,8 @@ import re
import dateutil.parser
tz = pytz.timezone("Europe/Paris")
class CagetteShift(models.Model):
"""Class to handle cagette Odoo Shift."""
......@@ -110,9 +114,65 @@ class CagetteShift(models.Model):
try:
return listService[0]
except Exception as e:
print(str(e))
coop_logger.error("get_shift %s", str(e))
return None
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_data_partner(self, id):
"""Retrieve partner data useful to make decision about shift options"""
cond = [['id', '=', id]]
......@@ -138,6 +198,13 @@ class CagetteShift(models.Model):
if partnerData['shift_type'] == 'standard':
partnerData['in_ftop_team'] = False
# Because 'in_ftop_team' doesn't seem to be reset to False in Odoo
else:
if partnerData['final_ftop_point'] < 0 and partnerData['makeups_to_do'] == 0 and partnerData['cooperative_state'] == "suspended":
partnerData['makeups_to_do'] = int(abs(partnerData['final_ftop_point']))
try:
self.o_api.update('res.partner', [id], {'makeups_to_do': partnerData['makeups_to_do']})
except Exception as e:
coop_logger.error("update res.partner.makeups_to_do %s", str(e))
if partnerData['is_associated_people']:
cond = [['partner_id.id', '=', partnerData['parent_id'][0]]]
else:
......@@ -176,14 +243,26 @@ class CagetteShift(models.Model):
return partnerData
def get_shift_partner(self, id):
def get_shift_partner(self, id, start_date=None, end_date=None):
"""Récupère les shift du membre"""
fields = ['date_begin', 'date_end','final_standard_point',
shifts = []
is_ftop = False
not_before = datetime.datetime.now().isoformat()
if start_date:
not_before = start_date.isoformat()
fields = ['date_begin', 'date_end',
'shift_id', 'shift_type','partner_id', "id", "associate_registered", "is_makeup"] # res.partner
cond = [['partner_id.id', '=', id],['state', '=', 'open'],
['date_begin', '>', datetime.datetime.now().isoformat()]]
['date_begin', '>', not_before]]
if end_date:
cond.append(['date_begin', '<', end_date.isoformat()])
shiftData = self.o_api.search_read('shift.registration', cond, fields, order ="date_begin ASC")
return shiftData
for s in shiftData:
if not ('Equipe volante' in s['shift_id'][1]):
shifts.append(s)
else:
is_ftop = True
return [shifts, is_ftop]
def get_partners_with_makeups_to_come(self):
"""Returns a dictionary with : keys = the partners ids having at least one makeup to come ; values = #makeups_to_come"""
......@@ -207,8 +286,9 @@ class CagetteShift(models.Model):
def get_shift_calendar(self, id, start, end):
def get_shift_calendar(self, is_ftop, start, end):
"""Récupère les shifts à partir de maintenant pour le calendier"""
max_weeks_ahead = getattr(settings,'FTOP_SHIFTS_VIEW_LIMIT', None)
cond = [['date_begin', '>', datetime.datetime.now().isoformat()],
['state', '!=', 'cancel']]
try:
......@@ -221,6 +301,10 @@ class CagetteShift(models.Model):
cond.append(['date_end', '<=', end_d.isoformat()])
except:
pass
if max_weeks_ahead and is_ftop:
max_end = datetime.datetime.now() + datetime.timedelta(weeks=max_weeks_ahead)
cond.append(['date_end', '<=', max_end.isoformat()])
# 2018-11-25 seats_available instead of seats_max
fields = ['date_begin_tz',
'date_end_tz', 'name',
......@@ -270,7 +354,7 @@ class CagetteShift(models.Model):
st_r_id = self.o_api.create('shift.registration', fieldsDatas)
except Exception as e:
coop_logger.error("Set shift : %s, %s", str(e), str(data))
if 'This partner is already registered on this Shift' in str(e):
if 'This partner is already registered on this Shift' in str(e) or 'sql_constraint' in str(e):
res = self.reopen_shift(data)
if res:
st_r_id = True
......@@ -466,11 +550,14 @@ class CagetteShift(models.Model):
def get_test(self, odooModel, cond, fieldsDatas):
return self.o_api.search_read(odooModel, cond, fieldsDatas, limit = 1000)
def decrement_makeups_to_do(self, partner_id):
""" Decrements partners makeups to do if > 0 """
def get_member_makeups_to_do(self, partner_id):
cond = [['id', '=', partner_id]]
fields = ['makeups_to_do']
makeups_to_do = self.o_api.search_read('res.partner', cond, fields)[0]["makeups_to_do"]
return self.o_api.search_read('res.partner', cond, fields)[0]["makeups_to_do"]
def decrement_makeups_to_do(self, partner_id):
""" Decrements partners makeups to do if > 0 """
makeups_to_do = self.get_member_makeups_to_do(partner_id)
if makeups_to_do > 0:
makeups_to_do -= 1
......@@ -513,13 +600,8 @@ class CagetteServices(models.Model):
# 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*)")
title = re.compile(r"^(\w{1})(\w{2,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)
......@@ -527,12 +609,11 @@ class CagetteServices(models.Model):
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]
line['name'] = l['name']
t_elts = title.search(l['name'])
if t_elts:
line['day'] = t_elts.group(2)
......@@ -624,8 +705,11 @@ class CagetteServices(models.Model):
return services
@staticmethod
def registration_done(registration_id, overrided_date="", typeAction=""):
def registration_done(request, registration_id, overrided_date="", typeAction=""):
"""Equivalent to click present in presence form."""
has_right_to_overriden_entrance_date = Verification.has_right_to_overriden_entrance_date(request)
if (len(overrided_date) == 0 or has_right_to_overriden_entrance_date):
api = OdooAPI()
f = {'state': 'done'}
......@@ -656,6 +740,8 @@ class CagetteServices(models.Model):
else:
return False
return api.update('shift.registration', [int(registration_id)], f)
else:
return False
@staticmethod
def reopen_registration(registration_id, overrided_date=""):
......
......@@ -96,7 +96,7 @@ def _is_middled_filled_considered(reserved, max):
def get_list_shift_calendar(request, partner_id):
cs = CagetteShift()
registerPartner = cs.get_shift_partner(partner_id)
[registerPartner, is_ftop] = cs.get_shift_partner(partner_id)
use_new_members_space = getattr(settings, 'USE_NEW_MEMBERS_SPACE', False)
remove_15_minutes_at_shift_end = getattr(settings, 'REMOVE_15_MINUTES_AT_SHIFT_END', True)
......@@ -109,7 +109,7 @@ def get_list_shift_calendar(request, partner_id):
start = request.GET.get('start')
end = request.GET.get('end')
listService = cs.get_shift_calendar(partner_id, start, end)
listService = cs.get_shift_calendar(is_ftop, start, end)
events = []
for value in listService:
......@@ -134,7 +134,7 @@ def get_list_shift_calendar(request, partner_id):
event["start"] = dateIsoUTC(value['date_begin_tz'])
datetime_object = datetime.datetime.strptime(value['date_end_tz'], "%Y-%m-%d %H:%M:%S")
datetime_object = datetime.datetime.strptime(value['date_end_tz'], "%Y-%m-%d %H:%M:%S") - datetime.timedelta(minutes=15)
if remove_15_minutes_at_shift_end is True:
datetime_object -= datetime.timedelta(minutes=15)
event["end"] = dateIsoUTC(datetime_object.strftime("%Y-%m-%d %H:%M:%S"))
......@@ -178,7 +178,7 @@ def get_list_shift_calendar(request, partner_id):
def get_list_shift_partner(request, partner_id):
cs = CagetteShift()
shiftData = cs.get_shift_partner(partner_id)
[shiftData, is_ftop] = cs.get_shift_partner(partner_id)
for value in shiftData:
value['date_begin'] = value['date_begin'] + "Z"
......@@ -281,16 +281,29 @@ def add_shift(request):
cs = CagetteShift()
if 'idNewShift' in request.POST and 'idPartner' in request.POST:
partner_id = int(request.POST['idPartner'])
id_shift = int(request.POST['idNewShift'])
data = {
"idPartner": int(request.POST['idPartner']),
"idShift":int(request.POST['idNewShift']),
"shift_type":request.POST['shift_type'],
"idPartner": partner_id,
"idShift": id_shift,
"shift_type": request.POST['shift_type'],
"is_makeup": False
}
if 'is_makeup' in request.POST and request.POST['is_makeup'] == "1":
data['is_makeup'] = True
if request.POST['shift_type'] == "ftop":
if cs.is_matching_ftop_rules(partner_id, id_shift) is True:
# Need to find out if a makeup has to be choosen
makeups_to_do = cs.get_member_makeups_to_do(partner_id)
if makeups_to_do != 0:
data['is_makeup'] = True
else:
response = {'msg': "FTOP rules not respected"}
return JsonResponse(response, status=422)
#Insertion du nouveau shift
st_r_id = False
try:
......@@ -304,7 +317,8 @@ def add_shift(request):
response = {'result': False}
# decrement makeups_to_do
res_decrement = False
response["decrement_makeups"] = False
if data['is_makeup'] is True:
try:
res_decrement = cs.decrement_makeups_to_do(int(request.POST['idPartner']))
except Exception as e:
......@@ -313,9 +327,6 @@ def add_shift(request):
if res_decrement:
response["decrement_makeups"] = res_decrement
else:
response["decrement_makeups"] = False
else:
response = {'result': False}
return JsonResponse(response)
else:
......@@ -450,3 +461,7 @@ def get_list(request):
liste.append(val[fields[0]])
return JsonResponse(liste, safe=False)
def get_current_cycle_week(request):
cs = CagetteShift()
return JsonResponse(cs.get_current_cycle_week_data(), safe=False)
......@@ -4,6 +4,7 @@ from outils.common_imports import *
from outils.common import OdooAPI
from decimal import *
from datetime import datetime
from inventory.models import CagetteInventory
class CagetteStock(models.Model):
......@@ -59,16 +60,17 @@ class CagetteStock(models.Model):
return {'errors': errors, 'picking_id': picking}
picking_name += datetime.now().strftime("%d/%m/%Y %H:%M:%S")
picking_short_name = picking_name
picking_name += ' - ' + stock_movement_data['operator']['name'] + ' ('+ str(stock_movement_data['operator']['barcode_base']) + ')'
fields = {
'company_id': 1,
'name': picking_name,
'picking_type_id' : picking_type, # mouvement type
'picking_type_id': picking_type, # mouvement type
'location_id': settings.STOCK_LOC_ID, # movement origin
'location_dest_id': destination, # movement dest
'move_lines': [],
'pack_operation_ids': [],
# 'pack_operation_ids': [],
'operator_id': stock_movement_data['operator']['id']
}
......@@ -95,35 +97,55 @@ class CagetteStock(models.Model):
}
])
# Add stock.pack.operation to stock.picking
fields['pack_operation_ids'].append([
0,
False,
{
"product_qty": str(qty),
"qty_done": str(qty),
"location_id": settings.STOCK_LOC_ID,
"location_dest_id": destination,
"product_id": p['id'],
"name": p['name'],
"product_uom_id": p['uom_id'],
"state": 'done',
"fresh_record": False
}
])
# Exception rises when odoo method returns nothing
marshal_none_error = 'cannot marshal None unless allow_none is enabled'
try:
picking = api.create('stock.picking', fields)
picking_id = api.create('stock.picking', fields)
if picking_id:
# Récupérer les lignes de mouvement associées au picking
picking_move_lines = api.search_read(
'stock.move',
[('picking_id', '=', picking_id)], # Filtre sur le picking_id
['id', 'product_id', 'product_qty', 'product_uom'] # Champs nécessaires
)
# Pour chaque mouvement de picking, vérifier la disponibilité avant de procéder
for move in picking_move_lines:
product_id = move['product_id'][0] # ID du produit
product_qty = move['product_qty'] # Quantité demandée pour ce mouvement
stock_quant_data = api.search_read(
'stock.quant',
[('product_id', '=', product_id), ('location_id', '=', settings.STOCK_LOC_ID)],
['quantity','reserved_quantity']
)
if stock_quant_data:
# Calcul de la quantité réservable
net_available_stock = stock_quant_data[0]['quantity'] - stock_quant_data[0]['reserved_quantity']
# Vérifier si le stock net disponible est suffisant pour le mouvement actuel
if product_qty > net_available_stock:
#Le stock réservable n'est pas suffisant. On effectue un inventaire pour augmenter le stock.
#à la valeur de la quantité que l'on souhaite transférer + la quantité réservée
p = {
'id': product_id,
'uom_id': move['product_uom'],
'qty': product_qty + stock_quant_data[0]['reserved_quantity']
}
inventory_data = {
'name': 'Augmentation de stock avant mouvement : ' + picking_short_name,
'products': [p]
}
if not (picking is None):
# Set stock.picking done
api.execute('stock.picking', 'action_done', [picking])
res = CagetteInventory.update_products_stock(inventory_data)
if 'errors' in res and res['errors']:
raise Exception('Erreur lors de l\'augmentation de stock précédant le transfert : ' + res['errors'][0])
# Generate accounting writings for this picking
api.execute('stock.picking', 'generate_expense_entry', picking)
# Si le stock est suffisant, on effectue l'action de transfert immédiat
api.execute('stock.picking', 'stock_immediate_transfer', [picking_id])
except Exception as e:
if not (marshal_none_error in str(e)):
......@@ -131,7 +153,7 @@ class CagetteStock(models.Model):
errors.append(str(e))
return {'errors': errors,
'picking_id': picking}
'picking_id': picking_id}
### NOT IN USE ###
......@@ -306,13 +328,13 @@ class CagetteStock(models.Model):
invLi = o_api.create('stock.inventory.line', fields)
if not (invLi is None):
try:
o_api.execute('stock.inventory', 'action_done', [inv])
except:
print ('Probleme action_done avec ' + str(data['idArticle']))
CagetteInventory.stock_inventory_action_validate(inv)
except Exception as e:
print('Probleme action_validate avec ' + str(data['idArticle']) + ' ' + str(e))
else:
print('Probleme invLi avec ' + str(data['idArticle']))
else:
print ('Probleme inv avec ' + str(data['idArticle']))
print('Probleme inv avec ' + str(data['idArticle']))
return inv
......
......@@ -493,7 +493,15 @@ function confirm_movement() {
error_in_table = true;
}
tmp_products_data.push(data);
//Prepare the data container for the sending phase
//Deep copy is required not to modify data as,
//in case of error (more frequent now with stock availability on),
//we may want to retry the stock movement
//(modifying data, especially removing uom and replacing it by uom_id,
//as done before sending data, will cause an error)
let product_deep_copy = JSON.parse(JSON.stringify(data));
tmp_products_data.push(product_deep_copy);
return 0;
});
......@@ -718,7 +726,12 @@ function do_stock_movement() {
closeModal();
reset_focus();
$.notify("Erreur lors de l'opération.", {
let error_text_displayed = "Erreur lors de l'opération";
if(data.responseJSON.errors.length > 0) {
error_text_displayed += " : " + data.responseJSON.errors[0]
}
error_text_displayed += ".";
$.notify(error_text_displayed, {
globalPosition:"top right",
className: "error"
});
......
......@@ -12,7 +12,9 @@ from django.shortcuts import render
def movements_page(request):
"""Page de selection de produits pour créer des mouvements de stock"""
context = {
'title': 'Mouvements de stock'
'title': 'Mouvements de stock',
'autoconso': getattr(settings, 'AUTOCONSO_LOC_ID', None),
'formeals': getattr(settings, 'MEALS_LOC_ID', None)
}
template = loader.get_template('stock/stock_movements.html')
......@@ -21,7 +23,9 @@ def movements_page(request):
def movements_view(request):
"""Page d'extraction des mouvements de stocks"""
context = {
'title': 'Mouvements de stock'
'title': 'Mouvements de stock',
'autoconso': getattr(settings, 'AUTOCONSO_LOC_ID', None),
'formeals': getattr(settings, 'MEALS_LOC_ID', None)
}
template = loader.get_template('stock/stock_movements_view.html')
......
......@@ -29,6 +29,7 @@
history.pushState(null, null, location.pathname)
}
const False = false // prevent error if api return python False as a value
const week_a_date = "{{week_a_date}}";
</script>
<script src="{% static "js/fp.js" %}"></script>
<script src="{% static "js/jquery-3.3.1.min.js" %}"></script>
......
......@@ -11,6 +11,7 @@
{% block additionnal_scripts %}
<script type="text/javascript" src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/datatables/datatables.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/notify.min.js' %}?v="></script>
{% endblock %}
{% block content %}
......
......@@ -11,6 +11,7 @@
{% block additionnal_scripts %}
<script type="text/javascript" src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/datatables/datatables.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/notify.min.js' %}?v="></script>
{% endblock %}
{% block content %}
......
......@@ -11,6 +11,7 @@
{% block additionnal_scripts %}
<script type="text/javascript" src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/datatables/datatables.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/notify.min.js' %}?v="></script>
{% endblock %}
{% block content %}
......
......@@ -66,6 +66,8 @@
{% include "members/job_input.html" %}
</p>
{% endif %}
{% if ask_for_capital_payment %}
<p>
<input type="number" step="10" min="10" placeholder="Montant souscription" name="subs_cap" id="subs_cap" required/>
<select name="payment_meaning" id="payment_meaning" autocomplete="off" required >
......@@ -76,8 +78,8 @@
{% endfor %}
</select>
<input type="number" min="1" placeholder="Nb de chèques" name="ch_qty" id="ch_qty" style="display:none;"/>
</p>
{% endif %}
{% if input_barcode %}
<p>
......
......@@ -9,6 +9,7 @@
</style>
{% endblock %}
{% block additionnal_scripts %}
<script src="{% static "js/geoloc.js" %}"?v=></script>
<script type="text/javascript">
var type = 2;
var context = 'validation';
......@@ -93,6 +94,7 @@
<div style="display:none" id="admin_elts">
</div>
<div class="geo_suggestions" style="display:none" ></div>
<script src="{% static "js/pouchdb.min.js" %}"></script>
<script type="text/javascript">
var couchdb_dbname = '{{db}}';
......@@ -106,6 +108,7 @@
var mag_place_string = '{{mag_place_string}}';
var office_place_string = '{{office_place_string}}'
var max_begin_hour = '{{max_begin_hour}}'
$('[name="address"]').addSearchAutocomplete();
var committees_shift_id = '{{committees_shift_id}}';
var exemptions_shift_id = '{{exemptions_shift_id}}';
</script>
......
......@@ -38,6 +38,7 @@
{% include "members/job_input.html" %}
</p>
{% endif %}
{% if ask_for_capital_payment %}
<p>
<input type="number" step="1" min="1" placeholder="Nombre de parts" name="shares_nb" id="shares_nb" class="b_yellow" required disabled/>
<label for="shares_nb">Parts sociales</label>
......@@ -60,6 +61,7 @@
<div class="check_details">
</div>
</div>
{% endif %}
{% if input_barcode %}
<p>
<input type="text" name="m_barcode" id="m_barcode" disabled/>
......
......@@ -8,6 +8,7 @@
</h1>
</div>
{% if company_code == "lacagette" %}
<div class="tiles_container">
<div class="tile full_width_tile">
<div class="tile_content">
......@@ -522,5 +523,14 @@
</div>
</div>
</div>
{% else %}
<p class="txtcenter">
Merci de lire le manuel des membres, dans lequel toutes les règles concernant les services sont décrites.
{% if MEMBERS_GUIDE_URL %}
<br/>
Vous pouvez le lire en cliquant <a href="{{MEMBERS_GUIDE_URL}}" target="_blank">ici</a>
{% endif %}
</p>
{% endif %}
</div>
......@@ -9,7 +9,11 @@
<a href="javascript:void(0);" class="nav_item active" id="nav_home">Espace Membre</a>
<a href="javascript:void(0);" class="nav_item" id="nav_my_info">Mes Infos</a>
<a href="javascript:void(0);" class="nav_item" id="nav_my_shifts">Mes Services</a>
{% if company_code == "lacoope" %}
<a href="javascript:void(0);" class="nav_item" id="nav_shifts_exchange">Choix et reports de services</a>
{% else %}
<a href="javascript:void(0);" class="nav_item" id="nav_shifts_exchange">Échange de services</a>
{% endif %}
{% if show_faq %}
<a href="javascript:void(0);" class="nav_item" id="nav_faq">Problèmes et Demandes</a>
{% endif %}
......
......@@ -67,18 +67,30 @@
<div class="tile small_tile" id="home_tile_services_exchange">
<div class="tile_title">
<i class="fas fa-exchange-alt tile_icon"></i>
{% if company_code == "lacoope" %}
Choix et reports de services
{% else %}
Échange de services
{% endif %}
</div>
<div class="tile_content">
<div class="block_service_exchange">
Un empêchement ? J'anticipe et déplace mes services jusqu'à 24h avant leur début !
</div>
<div class="free_service_exchange">
{% if company_code == "lacoope" %}
Je suis volant ? j'ai un empêchement ? Je sélectionne ou déplace mes services
{% else %}
Un empêchement ? J'anticipe et déplace mon service le plus tôt possible !
{% endif %}
</div>
<div class="home_link_button_area">
<button type="button" class="btn--primary home_link_button" id="go_to_shifts_calendar">
{% if company_code == "lacoope" %}
Accéder au calendrier de choix et reports de services
{% else %}
Accéder au calendrier d'échange de services
{% endif %}
</button>
</div>
</div>
......
......@@ -118,6 +118,7 @@
<script>
var app_env = '{{app_env}}';
var ftop_can_delete_shift = "{{ALLOW_FTOP_TO_DELETE_SHIFT}}";
var forms_link = '{{forms_link}}';
var unsuscribe_form_link = '{{unsuscribe_form_link}}';
var request_form_link = '{{request_form_link}}';
......@@ -142,8 +143,6 @@
"partner_id":"{{partnerData.id}}",
"name":"{{partnerData.display_name|safe}}",
"shift_type":"{{partnerData.shift_type}}",
"final_ftop_point":{{partnerData.final_ftop_point}},
"final_standard_point":{{partnerData.final_standard_point}},
"date_delay_stop":"{{partnerData.date_delay_stop}}",
"cooperative_state":"{{partnerData.cooperative_state}}",
"regular_shift_name":"{{partnerData.regular_shift_name}}",
......@@ -167,12 +166,15 @@
"verif_token" : "{{partnerData.verif_token}}",
"leave_stop_date": "{{partnerData.leave_stop_date}}",
"comite": "{{partnerData.comite}}",
"extra_shift_done": parseInt("{{partnerData.extra_shift_done}}", 10)
"extra_shift_done": parseInt("{{partnerData.extra_shift_done}}", 10),
"final_ftop_point": parseInt("{{partnerData.final_ftop_point}}", 10),
"final_standard_point": parseInt("{{partnerData.final_standard_point}}", 10)
};
var block_actions_for_attached_people = '{{block_actions_for_attached_people}}';
var block_service_exchange_24h_before = '{{block_service_exchange_24h_before}}';
const canAddShift = {{canAddShift}};
const extension_duration = {{extension_duration}};
const not_allowed_shift_op = `{{not_allowed_shift_op|safe}}`;
</script>
<script src="{% static "js/all_common.js" %}?v=1651853225"></script>
<script src="{% static "js/common.js" %}?v=1651853225"></script>
......
<div id="faqBDM" class=" mt-3">
<div class="page_title txtcenter"><h1> Problèmes et demandes </h1></div>
<div class="txtcenter">
<p>
Si vous rencontrez des difficultés à vous inscrire, <br/>si vous avez des interrogations, ou si tout simplement vous avez besoin d’accompagnement,<br/> contactez le Bureau des Membres :
<br/>
<img src="/static/img/telephone.png" alt="Téléphone" style="height:25px;"> 02 38 22 54 65
<br/>hello@la-gabare-orleans.coop
</p>
<p>
Des tutoriels sont à votre disposition :
<div>
Tutoriel Espace Membre<br>
Tutoriel Borne d’Entrée<br>
Tutoriel Coopérateur & son binôme<br>
</div>
<div>
Pour y accéder, cliquez sur le lien ici : <a href="https://cloud.la-gabare-orleans.coop/s/nQj7dLF6p2xCXfb" target="_blank">Tutoriels</a>
</div>
</p>
</div>
</div>
<div id="faqBDM" class=" mt-3">
<div class="page_title txtcenter"><h1> Problèmes et demandes </h1></div>
<div class="txtcenter">
<p>
En cas de difficulté à vous inscrire en suivant la <a href="https://www.le-troglo.fr/wp-content/uploads/2023/03/Tuto-espace-membre.pdf">procédure</a>,<br />
ou si vous rencontrez un dysfonctionnement, vous pouvez écrire à bureau.coops@le-troglo.fr
</p>
</div>
</div>
......@@ -21,6 +21,12 @@
<div class="login_area">
{% include "common/conn_admin.html" %}
</div>
{% if can_customize_parameters %}
<div class="preferences_area" style="display:none;">
<button type="button" id="preferences-settings">Paramétrage</button>
</div>
{% endif %}
<div id="new_order_area">
<h2>Créer une nouvelle commande</h2>
<div class="txtcenter" id="not_connected_content" style="display:none;">
......@@ -71,9 +77,11 @@
</button>
</div>
</div>
{% if metabase_url != "" %}
<a class='btn--warning link_as_button' id="access_metabase" style="display:none;" href="{{metabase_url}}" target="_blank">
Stats Métabase
</a>
{% endif %}
</div>
</div>
......@@ -359,8 +367,25 @@
</div>
</div>
</div>
<div id="modal_preferences_form">
<div class="modal_input_area">
<form>
<div>
<label>Forcer commande de 1 colis si stock = 0 et conso = 0 ?</label>
<div class="input-wrapper checkboxes">
<div>
<input type="radio" class="radio" name="force_0s0c_order" value="1"> Oui
</div>
<div>
<input type="radio" class="radio" name="force_0s0c_order" value="0"> Non
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div> <!-- templates -->
</div> <!-- page body -->
<script src="{% static "js/pouchdb.min.js" %}"></script>
......@@ -369,6 +394,9 @@
var couchdb_server = '{{couchdb_server}}' + couchdb_dbname;
var odoo_server = '{{odoo_server}}';
var metabase_url = '{{metabase_url}}';
var uoms = {{uoms|safe}};
var company_code = '{{company_code}}';
var preferences = {{preferences|safe}};
</script>
<script src="{% static "js/all_common.js" %}?v=1651853225"></script>
<script type="text/javascript" src="{% static 'js/orders_helper.js' %}?v=1651853225"></script>
......
{% extends "base.html" %}
{% load static %}
{% block additionnal_css %}
<link rel="stylesheet" href="{% static 'css/datatables/jquery.dataTables.css' %}">
<link rel="stylesheet" href="{% static 'css/reception_style.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/moment.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/datatables/dataTables.plugin.moment_sorting.js' %}"></script>
{% endblock %}
{% block content %}
<div class="login_area">
{% include "common/conn_admin.html" %}
</div>
<div class="header txtcenter">
<h1>Commandes réceptionnées</h1>
</div>
<br>
<div class="main">
<table id="orders" class="display" width="90%" cellspacing="0" ></table>
</div>
<div id="templates" style="display:none;">
<div id="show_detail">
<h3></h3>
<p></p>
<table class="display" width="90%" cellspacing="0" ></table>
</div>
</div>
<script src="{% static "js/all_common.js" %}?v="></script>
<script type="text/javascript" src="{% static 'js/reception_admin.js' %}?v="></script>
{% endblock %}
......@@ -27,14 +27,18 @@
Pertes
<span class="movement_type_button_icons"><i class="fas fa-arrow-right"></i></span>
</button><br>
{% if autoconso %}
<button type="button" class="btn--primary movement_type_button" id="autoconso_type_button">
Autoconsommation
<span class="movement_type_button_icons"><i class="fas fa-arrow-right"></i></span>
</button><br>
{% endif %}
{% if formeals %}
<button type="button" class="btn--primary movement_type_button" id="meals_type_button">
Repas salariés
<span class="movement_type_button_icons"><i class="fas fa-arrow-right"></i></span>
</button>
{% endif %}
</div>
</div>
......
......@@ -30,8 +30,12 @@
<select class="select_movement_element select_movement_input" id="movement_type_selector" name="">
<option value="">-- Choisissez un type de mouvement --</option>
<option value="losses">Pertes</option>
{% if formeals %}
<option value="meals">Repas salariés</option>
{% endif %}
{% if autoconso %}
<option value="autoconso">Autoconsomation</option>
{% endif %}
</select>
<p class="select_movement_element">De : <input type="text" id="from" class="select_movement_input"></p>
<p class="select_movement_element">À : <input type="text" id="to" class="select_movement_input"></p>
......
<div>
<span id="ask_new_password">Changer de mot de passe</span> <span id="passwd_helper"><i class="fa fa-info-circle fa-lg change_passwd_info"> </i></span>
<div style="display:none;" id="help_content">
Pour changer de mot de passe, remplissez le champ "Email", <br/>
et cliquez sur "Changer de mot de passe"
</div>
</div>
<script>
//Minimum JS has been loaded, so write in pure JS (no extra lib dependent)
let last_call;
const ask_pwd_span = document.querySelector("#ask_new_password");
let getCookie = function(name) {
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
let is_time_to_call = function() {
let answer = false;
var last_date = last_call || 0;
console.log(last_date)
var d = new Date();
var now = d.getTime();
if (last_date == 0 || (now - last_date) >= 5000) {
answer = true;
last_call = now;
}
return answer;
}
let display_change_password_help = function() {
let help_content = document.querySelector("#help_content");
help_content.style.display = 'block';
setTimeout(function() {
help_content.style.display = 'none'}, 5000);
}
let ask_for_new_password = function() {
if (is_time_to_call() === true) {
try {
const email = document.querySelector('input[name="login"]').value;
if (email.trim().length > 0) {
ask_pwd_span.textContent = "Traitement en cours...."
let load = {'email' : email,
'csrfmiddlewaretoken': document.querySelector('input[name="csrfmiddlewaretoken"]').value}
let xhr = new XMLHttpRequest();
xhr.open('POST', '/members/ask_for_new_password', true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
xhr.onreadystatechange = function() {
//readyState => (0,UNSENT), (1,OPENED), (2, HEADER_RECEIVED), (3, LOADING), (4, DONE)
if (this.readyState == 4) {
const response = JSON.parse(xhr.response);
if (this.status == 200 && response.succeeded === true) {
alert("Un email vient de vous être envoyé à l'adresse indiquée pour réinitialiser le mot de passe.");
} else {
alert("Une erreur est survenue pendant le traitement de la demande : " + response.error);
}
ask_pwd_span.textContent = "Changer de mot de passe";
}
};
xhr.send(JSON.stringify(load));
} else {
alert("Veuillez remplir le champ 'Email' avant de cliquer ici.");
}
} catch(e) {
alert("Une erreur est survenue.")
}
}
}
document.querySelector("#passwd_helper").addEventListener('click', display_change_password_help);
ask_pwd_span.addEventListener('click', ask_for_new_password);
</script>
\ No newline at end of file
......@@ -2,7 +2,7 @@
{% block content %}
<div id="change_pwd_form_template" style="text-align:center;">
<form method="POST">
<form method="POST" id="change_pwd_form">
<p><input type="password" name="password" placeholder="{{password_placeholder}}" /></p>
{% csrf_token %}
<input type="hidden" name="fp" value="" />
......@@ -11,11 +11,27 @@
<script>
try {
const new_passwd_input = document.querySelector('input[name="password"]'),
form = document.querySelector('#change_pwd_form'),
external_msg = '{{external_msg}}';
window.addEventListener("DOMContentLoaded", (event) => {
var fp = document.getElementsByName('fp')
if (fp.length == 1)
fp[0].value = new Fingerprint({canvas: true}).get()
})
form.addEventListener("submit", e => {
e.preventDefault();
if (new_passwd_input.value.length >= 10) {
form.submit()
} else {
alert('Le mot de passe doit faire au moins 10 caractères.')
}
});
if (external_msg == "reset_password_failure") {
alert("Erreur lors de l'enregistrement.")
}
} catch (e) {
var msg = 'Ce navigateur ne permet pas de vous identifier. Merci de signaler l\'erreur suivante:\n'
msg += JSON.stringify(e)
......
......@@ -17,6 +17,10 @@
<br/>
<span style="font-size: small;font-style: italic;">{{password_notice}}</span>
{% endif %}
{% if reset_password_available %}
<br/>
{% include "website/change_password_availibility.html" %}
{% endif %}
</p>
{% csrf_token %}
......@@ -47,7 +51,9 @@
<script>
// For the members space, reset url to home when accessing connect page
const is_member_space = '{{is_member_space}}';
const is_member_space = '{{is_member_space}}',
external_msg = '{{external_msg}}';
if (is_member_space === "True") {
var app_env = '{{app_env}}';
......@@ -58,5 +64,8 @@
history.replaceState({}, '', 'home');
}
}
if (external_msg == "successful_reset_password") {
alert("Changement de mot de passe réussi.")
}
</script>
{% endblock %}
......@@ -14,7 +14,7 @@
sex : '{{data.sex}}',
firstname : '{{data.firstname}}',
lastname : '{{data.lastname}}',
birthdate : '{{data.birthdate}}',
birthdate : '{{data.birthdate_date}}',
street : '{{data.street}}',
street2 : '{{data.street2}}',
zip : '{{data.zip}}',
......@@ -69,7 +69,7 @@
<select name="jj"></select>
<select name="mm"></select>
<select name="yyyy"></select>
<input type="hidden" name="birthdate" value="{{data.birthdate}}" />
<input type="hidden" name="birthdate" value="{{data.birthdate_date}}" />
</p>
<p>
{% include "members/job_input.html" %}
......
......@@ -7,8 +7,10 @@
{% load static %}
<link rel="shortcut icon" type="image/png" href="{% static "favicon.ico" %}"/>
<link rel="stylesheet" href="{% static "fontawesome/css/fa-svg-with-js.css" %}?v=">
{% block additionnal_css %}{% endblock %}
<script src="{% static "js/fp.js" %}"></script>
<script src="{% static "fontawesome/js/fontawesome-all.min.js" %}"></script>
{% block additionnal_scripts %}{% endblock %}
</head>
<style>
......@@ -55,8 +57,19 @@
* and the following is set, and everything will render well in iOS.
*/
/*-webkit-appearance: none;*/
width: 15em
}
.change_passwd_info {color: blue;}
#help_content {
width: 15em;
margin: auto;
border: blue 2px solid;
border-radius: 20px;
background-color: #c7e7ff;
color: black;
}
#ask_new_password {cursor: pointer;}
</style>
<body>
......
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