Commit 87eb9ed2 by Administrator

Initial commit

parents
*/__pycache__/*
*.pyc
temp/*
data/*
dav/*
log/*
/static/
outils/settings.py
outils/settings_secret.py
outils/config.py
outils/texts/*
outils/js_errors.log
db.sqlite3
*/max_timeslot_carts.txt
.gitlab-ci.yml
shop/shop_admin_settings.json
shop/errors.log
# Django "La Cagette"
Le code source de ce dépôt est celui qui fournit actuellement les services suivants :
## Répertoires
### members
* Borne d'accueil (pour vérifier le statut de celles et ceux qui entrent faire leurs courses, et pour enregistrer les présences de celles et ceux qui viennent faire leur service)
* Inscription des nouveaux coopérateurs (partie utilisée au magasin et formulaire de confirmation affiché dans l'Espace Membre)
### reception
* Gestion des réceptions de marchandises (Sélection de Demandes de Prix, vérification des quantités et des prix de la marchandise livrée, génération de rapport, etc)
### shifts
* Choix de rattrapage (ou de services à venir pour les volants) et échanges de services (encapsulé dans une iframe de l'Espace Membre)
## stock
* Traçage des ruptures de stocks
## inventory
* Gestion des inventaires (préparation, inventaire, mise à jour Odoo)
### orders
* contient le code de gestion des commandes
## products
* Pour impression des étiquettes à la demande et génération fichiers pour les appli balances
### outils
* contient le code commun
### shop
* contient le code de la boutique en ligne
### shelfs
* contient le code de la gestion des rayons (dont une partie utilisée par shop)
### website
* contient le code de 'mon-espace-prive' (formulaire de confirmation données et services échanges)
## Installation (sous distribution linux)
Prérequis : une version de python3
Avec `virtualenvwrapper` (`sudo pip install virtualenvwrapper`)
Cloner le projet, se placer à la racine, puis :
```
mkvirtualenv dj_lacagette --python=$(which python3)
(il est possible de devoir faire 'source /usr/local/bin/virtualenvwrapper.sh' avant)
pip install Django==2.1.3 django-cors-headers couchdb python-dateutil requests pymysql openpyxl reportlab argon2-cffi
```
Copier le fichier `outils/settings_example.py` en le renommant `outils/settings.py`
Copier le fichier `outils/settings_secret_example.py` en le renommant `outils/settings_secret.py` et en adaptant les identifiants
Lancer le serveur Web avec la commande `./launch.sh` (chmod u+x préalable si nécessaire)
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`
# 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```
from django.contrib import admin
# Register your models here.
from django.apps import AppConfig
class EnvelopsConfig(AppConfig):
name = 'envelops'
from django.db import models
from django.conf import settings
from outils.common import OdooAPI
from outils.common import CouchDB
import datetime
class CagetteEnvelops(models.Model):
"""Class to manage operations on envelops"""
def __init__(self):
"""Init with odoo id."""
self.o_api = OdooAPI()
self.c_db = CouchDB(arg_db='envelops')
for pm in settings.SUBSCRIPTION_PAYMENT_MEANINGS:
if pm['code'] == 'cash':
self.cash_code = pm['journal_id']
elif pm['code'] == 'ch':
self.check_code = pm['journal_id']
elif pm['code'] == 'cb':
self.cb_code = pm['journal_id']
elif pm['code'] == 'vir':
self.vir_code = pm['journal_id']
def get_all(self):
envelops = []
alldocs = self.c_db.getAllDocs()
if len(alldocs) > 0:
for e in alldocs:
if 'type' in e:
envelops.append(e)
return envelops
def get_ids_in_all(self):
ids = []
envelops = self.get_all()
if len(envelops) > 0:
for e in envelops:
for key, val in e['envelop_content'].items():
if not (key in ids):
ids.append(key)
return ids
def save_payment(self, data):
"""Save a partner payment"""
res = {
"done": False
}
try:
# Get invoice
cond = [['partner_id', '=', data['partner_id']]]
fields = ['id', 'name', 'number', 'partner_id', 'residual_signed']
invoice_res = self.o_api.search_read('account.invoice', cond, fields)
# Check if invoice exists
if len(invoice_res) > 0:
invoice = invoice_res[0]
else:
res['error'] = 'No invoice found for this partner, can\'t create payment.'
return res
# Set payment type
if data['type'] == "cash":
payment_type_code = self.cash_code
elif data['type'] == "ch":
payment_type_code = self.check_code
elif data['type'] == "cb":
payment_type_code = self.cb_code
elif data['type'] == "vir":
payment_type_code = self.vir_code
args = {
"writeoff_account_id": False,
"payment_difference_handling": "open",
"payment_date": datetime.date.today().strftime("%Y-%m-%d"),
"currency_id": 1,
"amount": data['amount'],
"payment_method_id": 1,
"journal_id": payment_type_code,
"partner_id": data['partner_id'],
"partner_type": "customer",
"payment_type": "inbound",
"communication": invoice['number'],
"invoice_ids": [(4, invoice['id'])]
}
payment_id = self.o_api.create('account.payment', args)
# Exception rises when odoo method returns nothing
marshal_none_error = 'cannot marshal None unless allow_none is enabled'
try:
# Second operation to complete payment registration
self.o_api.execute('account.payment', 'post', [payment_id])
except Exception as e:
if not (marshal_none_error in str(e)):
res['error'] = repr(e)
if not ('error' in res):
try:
if int(float(data['amount']) * 100) == int(float(invoice['residual_signed']) * 100):
# This payment is what it was left to pay, so change invoice state
self.o_api.update('account.invoice', [invoice['id']], {'state': 'paid'})
except Exception as e:
res['error'] = repr(e)
except Exception as e:
res['error'] = repr(e)
if not ('error' in res):
res['done'] = True
res['payment_id'] = payment_id
return res
def delete_envelop(self, envelop):
return self.c_db.delete(envelop)
def generate_envelop_display_id(self):
"""Generate a unique incremental id to display"""
c_db = CouchDB(arg_db='envelops')
display_id = 0
# Get last created envelop: the one with the highest display_id
envelops = c_db.getAllDocs(descending=True)
if envelops:
last_env = envelops[0]
if 'display_id' in last_env:
try:
if int(last_env['display_id']) > display_id:
display_id = int(last_env['display_id'])
except:
pass
display_id += 1
return display_id
@staticmethod
def create_or_update_envelops(payment_data):
"""Create or update one or multiple envelops according to member payment data"""
c_db = CouchDB(arg_db='envelops')
m = CagetteEnvelops()
answer = None
doc = {
'type' : payment_data['payment_meaning'],
'creation_date' : datetime.date.today().strftime("%Y-%m-%d"),
'envelop_content': {
payment_data['partner_id'] : {
'partner_name' : payment_data['partner_name']
}
}
}
# Create or update today's envelop when payment is cash
if payment_data['payment_meaning'] == 'cash':
# Generate envelop id
today = datetime.date.today()
envelop_id = 'cash_' + str(today.year) + '_' + str(today.month) + '_' + str(today.day)
try:
doc = c_db.getDocById(envelop_id) #today's envelop already exists
doc['envelop_content'][payment_data['partner_id']] = {
'partner_name': payment_data['partner_name'],
'amount': int(payment_data['shares_euros'])
}
answer = c_db.dbc.update([doc])
except: #doesn't exist, create today's envelop
doc['_id'] = envelop_id
doc['envelop_content'][payment_data['partner_id']]['amount'] = int(payment_data['shares_euros'])
doc.pop('_rev', None)
answer = c_db.dbc.save(doc)
# When payment by check
else:
# Get the oldest check envelops, limited by the number of checks
docs = []
for item in c_db.dbc.view('index/by_type', key='ch', include_docs=True, limit=payment_data['checks_nb']):
docs.append(item.doc)
# If only 1 check to save
if int(payment_data['checks_nb']) == 1:
# No existing envelop, create one
if len(docs) == 0:
doc['_id'] = 'ch_' + str(datetime.datetime.now().timestamp())
doc['display_id'] = m.generate_envelop_display_id()
doc['envelop_content'][payment_data['partner_id']]['amount'] = int(payment_data['shares_euros'])
doc.pop('_rev', None)
answer = c_db.dbc.save(doc)
# Update existing envelop
else:
docs[0]['envelop_content'][payment_data['partner_id']] = doc['envelop_content'][payment_data['partner_id']]
docs[0]['envelop_content'][payment_data['partner_id']]['amount'] = int(payment_data['shares_euros'])
answer = c_db.dbc.update(docs)
# If more than 1 check
else:
checks_cpt = 0
# Put the first checks in the first existing envelops
for item in docs:
item['envelop_content'][payment_data['partner_id']] = doc['envelop_content'][payment_data['partner_id']]
item['envelop_content'][payment_data['partner_id']]['amount'] = payment_data['checks'][checks_cpt]
checks_cpt += 1
answer = c_db.dbc.update([item])
# If there is no existing envelop for the reminding checks, create them
env_display_id = m.generate_envelop_display_id()
for i in range(checks_cpt, int(payment_data['checks_nb'])): # rangeMAX excluded -> no loop if no remaining checks
doc['_id'] = 'ch_' + str(datetime.datetime.now().timestamp() + i)
doc['display_id'] = env_display_id
doc['envelop_content'][payment_data['partner_id']]['amount'] = payment_data['checks'][checks_cpt]
doc.pop('_rev', None) # For some reason, the _rev of the previously created doc is stored and set to the next new doc
answer = c_db.dbc.save(doc)
checks_cpt += 1
env_display_id += 1
return answer
.envelop_section {
margin-bottom: 10px;
}
.custom_alert {
display: none;
cursor: pointer;
margin-bottom: 15px;
}
#cash_envelops {
margin-top: 30px;
}
#ch_envelops {
margin-top: 30px;
}
/* Accordion style */
/* Style the buttons that are used to open and close the accordion panel */
.accordion {
background-color: #dee2e6;
color: #212529;
cursor: pointer;
padding: 18px;
/* width: 80%; */
text-align: left;
border: none;
outline: none;
transition: 0.4s;
}
.archive_button {
padding: 18px;
/* width: 20%; */
}
hr {
margin-top: 32px;
}
/* Add a background color to the button if it is clicked on (add the .active class with JS), and when you move the mouse over it (hover) */
.active, .accordion:hover {
background-color: #acb3c2;
}
/* Style the accordion panel. Note: hidden by default */
.panel {
padding: 0 30px;
max-height: 0;
overflow: hidden;
transition: max-height 0.2s ease-out;
}
.accordion:after {
content: '\02795'; /* Unicode character for "plus" sign (+) */
font-size: 13px;
color: #777;
float: right;
margin-left: 5px;
}
.active:after {
content: "\2796"; /* Unicode character for "minus" sign (-) */
}
.panel.active:after {
margin-bottom: 30px;
}
var cash_envelops = []
var ch_envelops = []
function reset() {
$('#cash_envelops').empty()
$('#ch_envelops').empty()
cash_envelops = []
ch_envelops = []
}
function toggle_error_alert() {
$('#envelop_cashing_error').toggle(250)
}
function toggle_success_alert() {
$('#envelop_cashing_success').toggle(250)
}
// Set an envelop content on the document
function set_envelop_dom(envelop, envelop_name, envelop_content_id, envelop_index) {
var envelops_section = $('#' + envelop.type + '_envelops')
// Calculate envelop total amount
var total_amount = 0
for (partner_id in envelop.envelop_content) {
total_amount += envelop.envelop_content[partner_id].amount
}
var new_html = '<div class="envelop_section">'
+ '<div class="flex-container">'
// Allow checking for all cash and first check envelops
if (envelop.type == 'cash' || envelop.type == 'ch' && envelop_index == 0) {
new_html += '<button class="accordion w80">' + envelop_name + ' - <i>' + total_amount + '€</i></button>'
+ '<button class="btn--success archive_button item-fluid" onClick="openModal(\'<h3>Êtes-vous sûr ?</h3>\', function() {archive_envelop(\'' + envelop.type + '\', ' + envelop_index + ');}, \'Encaisser\')">Encaisser</button>'
} else {
new_html += '<button class="accordion w100">' + envelop_name + ' - <i>' + total_amount + '€</i></button>'
}
new_html += '</div>'
+ '<div class="panel"><ol id="' + envelop_content_id + '"></ol></div>'
+ '</div>'
$(new_html).appendTo(envelops_section);
for (node in envelop.envelop_content) {
var li_node = document.createElement("LI"); // Create a <li> node
var content = envelop.envelop_content[node].partner_name + ' : ' + envelop.envelop_content[node].amount + '€';
if ('payment_id' in envelop.envelop_content[node]) {
content += " - déjà comptabilisé."
}
var textnode = document.createTextNode(content); // Create a text node
li_node.appendChild(textnode); // Append the text to <li>
document.getElementById(envelop_content_id).appendChild(li_node);
}
}
// Set the envelops data according to their type
function set_envelops(envelops) {
var cash_index = 0
var ch_index = 0
reset()
for(var i= 0; i < envelops.length; i++) {
var envelop = envelops[i].doc
if (envelop.type == "cash") {
cash_envelops.push(envelop)
let split_id = envelop._id.split('_');
let envelop_date = split_id[3] + "/" + split_id[2] + "/" + split_id[1];
var envelop_name = 'Enveloppe du ' + envelop_date
var envelop_content_id = 'content_cash_list_' + cash_index
set_envelop_dom(envelop, envelop_name, envelop_content_id, cash_index)
cash_index += 1
} else if (envelop.type == "ch") {
ch_envelops.push(envelop)
var envelop_name = 'Enveloppe #' + envelop.display_id
var envelop_content_id = 'content_ch_list_' + ch_index
set_envelop_dom(envelop, envelop_name, envelop_content_id, ch_index)
ch_index += 1
}
}
if (cash_index == 0)
$('#cash_envelops').html("<p class='txtcenter'>Aucune enveloppe.</p>")
if (ch_index == 0)
$('#ch_envelops').html("<p class='txtcenter'>Aucune enveloppe.</p>")
// Set accordions
var acc = document.getElementsByClassName("accordion");
for (var i = 0; i < acc.length; i++) {
acc[i].addEventListener("click", 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.parentNode.nextElementSibling; // depends on html structure
if (panel.style.maxHeight) {
panel.style.maxHeight = null;
} else {
panel.style.maxHeight = panel.scrollHeight + "px";
}
});
}
}
function archive_envelop(type, index) {
$('#envelop_cashing_error').hide()
$('#envelop_cashing_success').hide()
// Loading on
openModal()
if (type == "cash") {
envelop = cash_envelops[index]
} else {
envelop = ch_envelops[index]
}
// Proceed to envelop cashing
$.ajax({
type: "POST",
url: "/envelops/archive_envelop",
headers: { "X-CSRFToken": getCookie("csrftoken") },
dataType: "json",
traditional: true,
contentType: "application/json; charset=utf-8",
data: JSON.stringify(envelop),
success: function(response) {
closeModal()
var display_success_alert = true
// Handle errors when saving payments
var error_payments = response.error_payments
var error_message = ""
for (var i = 0; i < error_payments.length; i++) {
if (error_payments[i].done == false) {
error_message += "<p>Erreur lors de l'enregistrement du paiement de <b>" + error_payments[i]['partner_name']
+ "</b> (id odoo : " + error_payments[i]['partner_id'] + ", valeur à encaisser : " + error_payments[i]['amount'] + "€)."
error_message += "<br/><b>L'opération est à reprendre manuellement dans Odoo pour ce paiement.</b></p>"
}
}
// If error during envelop deletion
var response_envelop = response.envelop
if (response_envelop == "error") {
error_message += "<p>Erreur lors de la suppression de l'enveloppe.<br/>"
error_message += "<b>Sauf contre-indication explicite, les paiements ont bien été enregistrés.</b><br/>"
error_message += "Les paiements déjà comptabilisés ne le seront pas à nouveau, vous pouvez ré-essayer. Si l'erreur persiste, l'enveloppe devra être supprimée manuellement.</p>"
display_success_alert = false
}
if (error_message !== "") {
$('#error_alert_txt').html(error_message)
toggle_error_alert()
}
if (display_success_alert) {
toggle_success_alert()
}
},
error: function() {
closeModal()
alert('Erreur serveur. Merci de ne pas ré-encaisser l\'enveloppe qui a causé l\'erreur.')
}
});
}
// Get all the envelops from couch db
function get_envelops() {
dbc.allDocs({
include_docs: true,
attachments: true
}).then(function (result) {
set_envelops(result.rows);
}).catch(function (err) {
alert('Erreur lors de la récupération des enveloppes.')
console.log(err);
});
}
// Hande change in couc db
sync.on('change', function (info) {
// handle change
if (info.direction == 'pull') {
get_envelops();
}
}).on('error', function (err) {
// handle error
console.log('erreur sync')
console.log(err)
});
$(document).ready(function() {
if (typeof must_identify == "undefined" || coop_is_connected()) {
get_envelops()
}
});
"""."""
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.index),
url(r'^archive_envelop', views.archive_envelop)
]
from outils.common_imports import *
from outils.for_view_imports import *
from envelops.models import CagetteEnvelops
from members.models import CagetteMember
def index(request):
"""Display envelops"""
must_identify = False
must_identify = getattr(settings, 'BRINKS_MUST_IDENTIFY', False)
context = {'title': 'Enveloppes',
'couchdb_server': settings.COUCHDB['url'],
'must_identify': must_identify,
'db': settings.COUCHDB['dbs']['envelops']}
template = loader.get_template('envelops/index.html')
return HttpResponse(template.render(context, request))
def archive_envelop(request):
"""Save members payment and destroy the envelop"""
m = CagetteEnvelops()
res_payments = []
res_envelop = ""
envelop = json.loads(request.body.decode())
# save each partner payment
for partner_id in envelop['envelop_content']:
# If payment_id in payment details: payment already saved. Skip saving.
if not ('payment_id' in envelop['envelop_content'][partner_id]):
try: #Additionnal security to ensure process
data = {
'partner_id' : int(partner_id),
'partner_name' : envelop['envelop_content'][partner_id]['partner_name'],
'amount' : envelop['envelop_content'][partner_id]['amount'],
'type' : envelop['type']
}
res = m.save_payment(data)
except Exception as e:
res = {
"done": False,
"error": repr(e)
}
if res['done']:
# Immediately save a token than this payment has been saved
# If an error occurs, this payment won't be saved again
envelop['envelop_content'][partner_id]['payment_id'] = res['payment_id']
updated_envelop = m.c_db.updateDoc(envelop);
envelop['_rev'] = updated_envelop['_rev']
else:
# Handling error when saving payment, return data to display error message
res['partner_id'] = partner_id
try:
res['partner_name'] = envelop['envelop_content'][partner_id]['partner_name']
res['amount'] = envelop['envelop_content'][partner_id]['amount']
except:
res['error'] = "Wrong envelop structure"
res_payments.append(res)
try:
# Log the error
lf = open("/tmp/erreurs_django.log", "a")
lf.write(datetime.date.today().strftime("%Y-%m-%d") + " - Erreur lors de l'enregistrement du paiement de " + res['partner_name'] + "(odoo_id:" + partner_id + " )")
lf.write(res['error'] + "\n")
lf.close()
msg = 'Erreur lors de l\'enregistrement du paiement ' + envelop['type']
msg += ' ' + envelop['envelop_content'][partner_id]['amount'] + ' euros '
msg += ' (' + res['error'] + ')'
CagetteMember(int(partner_id)).attach_message(msg)
except:
pass
try:
# Delete envelop from couchdb
res_envelop = m.delete_envelop(envelop)
except Exception as e:
res_envelop = "error"
return JsonResponse({'error_payments': res_payments, 'envelop': res_envelop})
from django.contrib import admin
# Register your models here.
from django.apps import AppConfig
class InventoryConfig(AppConfig):
name = 'inventory'
.actions {margin-top:10px;}
.dataTables_wrapper button {margin-right:5px;}
.dataTables_wrapper tr.even {background-color:#efefef;}
td.shelf_qty {background-color: rgba(232,231,178,0.3)}
td.stock_qty {background-color: rgba(228,208,242,0.3)}
tr.highlight .shelf_qty, tr.highlight .stock_qty, tr.highlight .name {
color:#0da0ad;
}
.hidden {display:none;}
.editable-cell {background-color:#e8e7b2;}
.local-data-info, .global-data-info {padding:5px; margin: 10px 0;}
.local-data-info {background-color:#0da0ad ;}
.global-data-info {background-color:#f2ee7d;}
.global-data-info span {cursor: help; font-style: italic; color: red;}
button[name="local_to_global_sync"], button[name="global_to_local_sync"]
{height: 10px; font-size:12px; padding-top: 5px; padding-bottom: 15px;margin-left:10px; margin-bottom:2px;}
tr.to_weight td.shelf_qty, tr.to_weight td.stock_qty
{
background-image: url('/static/img/poids.png');
background-position: center;
background-repeat: no-repeat;
}
.error_msg {color: #d9534f;}
@media screen and (max-width: 767px) {
.barcode, .delta {display: none;}
}
\ No newline at end of file
var shelfs_table = null
function init_datatable() {
return $('#lists').DataTable( {
data: lists, // data passed at page loading
rowId: 'id',
columns:[
{data: "id", title:"id", "visible": false},
{
data:"datetime_created",
title:"Liste",
render: function (data, type, full, meta) {
return "Liste du " + data
}
},
{data:"p_nb", title:"Nombre de réfs", width: "10%", className:"dt-body-center"},
{
data:"inventory_status",
title:"Inventaire à faire",
className:"dt-body-center",
width: "15%",
render: function (data, type, full, meta) {
if (data == '')
return "<button class='btn--primary do_inventory'>Inventaire en rayon</button>"
else
return "<button class='btn--success do_inventory'>Inventaire en réserve</button>"
}
}
],
dom: 'rtip',
order: [[ 1, "asc" ]],
iDisplayLength: 25,
language: {url : '/static/js/datatables/french.json'}
})
}
function go_to_inventory() {
openModal()
var clicked = $(this)
var row_data = shelfs_table.row(clicked.parents('tr')).data()
// Use local storage to pass data to next page
if (Modernizr.localstorage) {
var stored_list = JSON.parse(localStorage.getItem('custom_list_' + row_data.id))
// Set local storage if key doesn't exist
if (stored_list == null) {
localStorage.setItem("custom_list_" + row_data.id , JSON.stringify(row_data))
}
}
document.location.href = "custom_list_inventory/" + row_data.id
}
$(document).ready(function() {
console.log(lists)
shelfs_table = init_datatable()
$(document).on('click', 'button.do_inventory', go_to_inventory)
})
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
from django.test import TestCase
# Create your tests here.
"""."""
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.home),
url(r'^custom_lists$', views.custom_lists),
url(r'^custom_list_inventory/([0-9]+)$', views.custom_list_inventory),
url(r'^get_custom_list_data$', views.get_custom_list_data),
url(r'^do_custom_list_inventory$', views.do_custom_list_inventory),
url(r'^get_credentials$', views.get_credentials),
url(r'^get_product_categories$', views.get_product_categories),
url(r'^create_inventory$', views.create_inventory),
url(r'^update_odoo_stock$', views.update_odoo_stock),
url(r'^raz_archived_stock$', views.raz_archived_stock),
url(r'^raz_negative_stock$', views.raz_negative_stock),
url(r'^raz_not_saleable$', views.raz_not_saleable),
url(r'^cancel_buggy_pos_sales_waiting_transfer$', views.cancel_buggy_pos_sales_waiting_transfer),
url(r'^process_pos_sales_waiting_transfer$', views.process_pos_sales_waiting_transfer)
]
from django.shortcuts import render
from django.http import HttpResponse
from django.http import JsonResponse
from django.template import loader
from django.conf import settings
from members.models import CagetteUser
from .models import CagetteInventory
import json
def home(request):
"""Page de selection de la commande suivant un fournisseurs"""
context = {'title': 'Inventaires',
'TOOLS_SERVER': settings.TOOLS_SERVER,
'couchdb_server': settings.COUCHDB['url'],
'db': settings.COUCHDB['dbs']['inventory']}
template = loader.get_template('inventory/index.html')
return HttpResponse(template.render(context, request))
def custom_lists(request):
"""Affichage des listes de produits à inventorier"""
lists = CagetteInventory.get_custom_lists()
context = {'title': 'Listes de produits à inventorier',
'lists' : json.dumps(lists),
}
template = loader.get_template('inventory/custom_lists.html')
return HttpResponse(template.render(context, request))
def custom_list_inventory(request, id):
"""Inventaire d'une liste de produits"""
products = CagetteInventory.get_custom_list_products(id)
if 'error' in products:
print(products)
products['data'] = []
context = {'title': 'Inventaire',
'products' : json.dumps(products['data']),
}
# Reuse shelf inventory template: same process
template = loader.get_template('shelfs/shelf_inventory.html')
return HttpResponse(template.render(context, request))
def get_custom_list_data(request):
id = request.GET.get('id', '')
try:
lists = CagetteInventory.get_custom_lists(id)
return JsonResponse({'res': lists[0]})
except Exception as e:
return JsonResponse(id, status=500)
def do_custom_list_inventory(request):
res = {}
data = json.loads(request.body.decode())
inventory_data = {
'id': data['id'],
'name': 'Inventaire personnalisé du '+ data['datetime_created'],
'user_comments': data['user_comments'],
'products': data['list_processed']
}
try:
if data['inventory_status'] == '' :
# First step: update inventory file
res = CagetteInventory.update_custom_inv_file(inventory_data)
else:
# Get data from step 1
full_inventory_data = CagetteInventory.get_full_inventory_data(inventory_data)
# Proceed with inventory
res['inventory'] = CagetteInventory.update_stock_with_shelf_inventory_data(full_inventory_data)
# remove file
CagetteInventory.remove_custom_inv_file(inventory_data['id'])
except Exception as e:
res['error'] = {'inventory' : str(e)}
if 'error' in res:
return JsonResponse(res, status=500)
else:
return JsonResponse({'res': res})
def get_credentials(request):
"""Receiving user mail + password, returns id, rights and auth token"""
return JsonResponse(CagetteUser.get_credentials(request))
def get_product_categories(request):
return JsonResponse(CagetteInventory.get_product_categories(), safe=False)
def create_inventory(request):
res = {}
if CagetteUser.are_credentials_ok(request):
import json
cats = json.loads(request.POST.get('cats'))
res['products'] = CagetteInventory.get_products_from_cats(cats)
else:
res['msg'] = 'Forbidden'
return JsonResponse(res)
def update_odoo_stock(request):
res = {}
if CagetteUser.are_credentials_ok(request):
try:
doc_id = request.POST.get('doc_id')
res['action'] = CagetteInventory.update_stock_with_inventory_data(doc_id)
except Exception as e:
res['msg'] = str(e)
else:
res['msg'] = 'Forbidden'
return JsonResponse(res)
def raz_archived_stock(request):
res = {}
if CagetteUser.are_credentials_ok(request):
try:
res['action'] = CagetteInventory.raz_archived_stock()
except Exception as e:
res['msg'] = str(e)
else:
res['msg'] = 'Forbidden'
return JsonResponse(res)
def raz_negative_stock(request):
res = {}
if CagetteUser.are_credentials_ok(request):
try:
res['action'] = CagetteInventory.raz_negative_stock()
except Exception as e:
res['msg'] = str(e)
else:
res['msg'] = 'Forbidden'
return JsonResponse(res)
def raz_not_saleable(request):
res = {}
if CagetteUser.are_credentials_ok(request):
try:
res['action'] = CagetteInventory.raz_not_saleable_stock()
except Exception as e:
res['msg'] = str(e)
else:
res['msg'] = 'Forbidden'
return JsonResponse(res)
def cancel_buggy_pos_sales_waiting_transfer(request):
res = {}
try:
res['action'] = CagetteInventory.cancel_buggy_pos_sales_waiting_transfer()
except Exception as e:
res['msg'] = str(e)
return JsonResponse(res)
def process_pos_sales_waiting_transfer(request):
# TODO : priority is to stop what's making error !!
return JsonResponse(res)
#! /bin/sh
port=34001
ip=127.0.0.1
if [ ! -z "$1" ]
then
ip=$1
fi
if [ ! -z "$2" ]
then
port=$2
fi
current_path=$(pwd)
export PYTHONPATH="$current_path:$current_path/lib:$PYTHONPATH"
echo yes | django-admin collectstatic --settings=outils.settings
django-admin runserver $ip:$port --settings=outils.settings
#!/usr/bin/env python3
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "outils.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
from django.contrib import admin
from outils.common_imports import *
from outils.for_view_imports import *
from members.models import CagetteUser
from members.models import CagetteMembers
from members.models import CagetteMember
from outils.common import MConfig
default_msettings = {'msg_accueil': {'title': 'Message borne accueil',
'type': 'textarea',
'value': ''
},
'no_picture_member_advice': {'title': 'Message avertissement membre sans photo',
'type': 'textarea',
'value': ''
},
}
def config(request):
"""Page de configuration."""
template = loader.get_template('outils/config.html')
context = {'title': 'Configuration module Membres',
'module': 'Membres'}
return HttpResponse(template.render(context, request))
def get_settings(request):
result = {}
is_connected_user = CagetteUser.are_credentials_ok(request)
if is_connected_user is True:
try:
msettings = MConfig.get_settings('members')
if len(msettings) == 0:
msettings = default_msettings
# take care that every params will be shown (consider newly added params)
for k, v in default_msettings.items():
if not (k in msettings):
msettings[k] = v
result['settings'] = msettings
except Exception as e:
result['error'] = str(e)
else:
result['error'] = "Forbidden"
return JsonResponse({"res": result}, safe=False)
def save_settings(request):
result = {}
is_connected_user = CagetteUser.are_credentials_ok(request)
if is_connected_user is True:
try:
params = json.loads(request.POST.get('params'))
result['save'] = MConfig.save_settings('members', params)
except Exception as e:
result['error'] = str(e)
else:
result['error'] = "Forbidden"
return JsonResponse({"res": result}, safe=False)
def module_settings(request):
if request.method == 'GET':
return get_settings(request)
else:
return save_settings(request)
def add_pts_to_everybody(request, pts, reason):
result = {}
is_connected_user = CagetteUser.are_credentials_ok(request)
if is_connected_user is True:
try:
fields = ['in_ftop_team']
cond = [['is_member', '=', True]]
all_members = CagetteMembers.get(cond, fields)
if all_members and len(all_members) > 0:
ftop_ids = []
standard_ids = []
for m in all_members:
if m['in_ftop_team'] is True:
ftop_ids.append(m['id'])
else:
standard_ids.append(m['id'])
if len(standard_ids) > 0:
result['standard'] = CagetteMembers.add_pts_to_everyone('standard', standard_ids, pts, reason)
else:
result['standard'] = 'No standard found ! '
if len(ftop_ids) > 0:
result['ftop'] = CagetteMembers.add_pts_to_everyone('ftop', ftop_ids, pts, reason)
else:
result['ftop'] = 'No FTOP found !'
# result['ftop'] = ftop_ids
# result['standard'] = standard_ids
except Exception as e:
result['error'] = str(e)
else:
result['error'] = "Forbidden"
return JsonResponse({'res': result})
def manage_mess(request):
"""Admin part to manage mess - uncomplete subscription"""
is_connected_user = CagetteUser.are_credentials_ok(request)
template = loader.get_template('members/manage_mess.html')
context = {'title': 'Gestion des inscriptions problématiques',
'couchdb_server': settings.COUCHDB['url'],
'db': settings.COUCHDB['dbs']['member_mess'],
'is_connected_user': is_connected_user}
return HttpResponse(template.render(context, request))
# JsonResponse({'error' : str(e)}, status=500)
def raw_search(request):
res = {}
is_connected_user = CagetteUser.are_credentials_ok(request)
if is_connected_user is True:
try:
needle = str(request.GET.get('needle'))
members = CagetteMembers.raw_search(needle)
res = {'members': members}
except Exception as e:
res['error'] = str(e)
response = JsonResponse(res)
else:
response = JsonResponse(res, status=403)
return response
def problematic_members(request):
res = {}
is_connected_user = CagetteUser.are_credentials_ok(request)
if is_connected_user is True:
try:
members = CagetteMembers.get_problematic_members()
res = {'members': members}
except Exception as e:
res['error'] = str(e)
response = JsonResponse(res)
else:
response = JsonResponse(res, status=403)
return response
def remove_member_from_mess_list(request):
res = {}
is_connected_user = CagetteUser.are_credentials_ok(request)
if is_connected_user is True:
try:
res = CagetteMember.remove_from_mess_list(request)
except Exception as e:
res['error'] = str(e)
response = JsonResponse(res)
else:
response = JsonResponse(res, status=403)
return response
def generate_barcode(request, member_id):
res = {}
is_connected_user = CagetteUser.are_credentials_ok(request)
if is_connected_user is True:
try:
res['done'] = CagetteMember(member_id).generate_barcode()
except Exception as e:
res['error'] = str(e)
response = JsonResponse(res, safe=False)
else:
response = JsonResponse(res, status=403)
return response
def generate_base_and_barcode(request, member_id):
res = {}
is_connected_user = CagetteUser.are_credentials_ok(request)
if is_connected_user is True:
try:
res['done'] = CagetteMember(member_id).generate_base_and_barcode()
except Exception as e:
res['error'] = str(e)
response = JsonResponse(res, safe=False)
else:
response = JsonResponse(res, status=403)
return response
def create_envelops(request):
res = {}
is_connected_user = CagetteUser.are_credentials_ok(request)
if is_connected_user is True:
try:
res['result'] = CagetteMember.standalone_create_envelops(request)
except Exception as e:
res['error'] = str(e)
response = JsonResponse(res, safe=False)
else:
response = JsonResponse(res, status=403)
return response
\ No newline at end of file
from .newCorsMiddleware import newCorsMiddleware
from django.utils.deprecation import MiddlewareMixin
class newCorsMiddleware(MiddlewareMixin):
def process_response(self, req, resp):
from urllib.parse import urlparse
referer = '*'
try:
# referer = urlparse(req.META['HTTP_REFERER']).hostname
referer = req.META['HTTP_REFERER']
except:
pass
# resp["X-Frame-Options"] = "ALLOW-FROM " + referer
resp["X-Frame-Options"] = "ALLOWALL"
# resp["Content-Security-Policy"] = "frame-src " + req.META['HTTP_HOST']
return resp
#shift_choice td {padding: 0.6em;}
#shift_choice .firstcol {width: 60px;}
.main_content .shift.full {display: block; background: #FF0000;cursor: not-allowed;}
.shift {margin:2px; padding:2px; cursor: cell; background: #FF0000 !important; color:#FFF;min-width:40px; font-size: 13px; border: 0;}
.shift.alert,span.alert {background: #00FF00 !important; border: 0;color:#000;}
.b_more_than_50pc {background:#FF0000;}
.b_less_than_50pc {background:#00FF00 !important; color: #000;}
.b_less_than_25pc {color:#000;}
.nav .fr {margin-left:10px;}
#shift_choice > div, #new_coop, #coop_list_view,#coop_registration_details {display:none;}
#subs_cap {width: 200px;}
.lat_menu button {margin-bottom:5px;}
.oddeven_selector {margin-right:25px;}
.main_content .shift {float:left;}
.main_content .shift.full {display:none;}
.shift {margin:2px; padding:2px; cursor: cell;}
.shift.alert,span.alert {border-bottom: 3px #e52121 solid;}
.highlighted {box-shadow: 10px 10px 5px grey;}
.lat_menu.highlighted {border: 2px #fffc07 dotted;}
#coop_list_view td.coop:hover,
#coop_list_view td.c_shift:hover {background:#fffc07; cursor:pointer;}
.b_red {color:#ffffff;}
.shift[data-type="compact"] {border: 1px solid #000000;border-radius: 5px; padding:5px; cursor: pointer;}
.shift_template, .next_shift {font-weight: bold;}
/** ff0000 **/
[data-week="1"] {border: 2px #02ff1a solid;}
[data-week="2"] {border: 2px #0088fb solid;}
[data-week="3"] {border: 2px #000000 solid;}
[data-week="4"] {border: 2px #eed000 solid;}
#new_coop [name="email"] {width:25em;}
#mail_generation {position:absolute; bottom:30px;}
#sex {padding: 0;}
label {display:block;}
.sres, #list .content .item {
border: 1px solid black;
border-radius: 5px;
padding:5px;
cursor: pointer;
margin-top: 3px;}
.sres {max-width: 50%;}
#list, #working_area {
border: 3px solid blue;
border-radius: 15px;
padding: 15px;
margin-top: 15px;
}
#working_area {
border: 3px solid red;
}
#working_area button {
display: block;
margin:auto;
margin-bottom: 10px;
}
.mconfirm .content_details {text-align:left;}
[name="checks[]"] {max-width: 100px;}
\ No newline at end of file
body {background: #c8deff; margin:5px;}
#webcam{
width: 280px;
height: 360px;
border: 1px solid black;
}
video {max-width:none;}
.search_box_wrapper .label {margin-bottom:10px;}
#shopping_entry ,
#service_entry,
#service_entry_validation,
#service_entry_success,
#rattrapage_1, #rattrapage_2,
#current_shift_title,
#photo_advice, #photo_studio,
#service_entry_success .compteur
{display:none;}
#first_page .btn {height:50px; font-size: xx-large; margin-top:150px;}
#post_take_buttons .btn--primary,#post_take_buttons .btn--inverse {margin-bottom:15px;}
#shopping_entry .btn--primary, #service_entry .btn--primary,
#rattrapage_1 .btn--primary,
#rattrapage_2 .btn--primary {color:#fff !important;}
#rattrapage_2 .txtcenter [data-next] {margin-top: 25px;}
#post_take_buttons input {display: block; float:right;width: 100px;}
#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 .coop-info {background: #fbfbd5;}
#image_medium {width:128px;float:left;}
#image_medium:hover {cursor: url(/static/img/ip-camera.png), url(/static/img/ip-camera.svg) 5 5, pointer;}
#barcode {height:128px;}
#barcode_base {width:50px;float:left;}
.coop-info {min-width: 300px;padding:5px;}
#lat_menu button {margin-bottom:5px;}
.col-6.big {font-size:200%; border: 2px solid red; padding:10px; text-align:center; background: #ffffff;}
#cooperative_state {font-size:150%; font-weight:bold;}
h1 .member_name {font-weight: bold;}
#current_shift_title, .members_list
{border:1px solid #000; border-radius: 5px; padding:5px; margin-bottom:15px;background:#FFF;}
.members_list {list-style: none;}
.members_list li {display:block;margin-bottom:5px;}
.members_list li.btn--inverse {background: #449d44 !important; cursor:not-allowed;}
#service_entry_success {font-size: x-large;}
#service_entry_success .explanations {margin: 25px 0; font-size: 18px;}
#service_entry_success .points,
#service_entry_success .next_shift
{border: 1px solid #000;
border-radius: 5px;
padding: 5px;
background: #eed000;}
#service_entry_success .btn,
#member_slide .btn[data-next]
{margin-top: 35px; background: #449d44; color:#FFF;}
[data-next="rattrapage_1"] {float:right;color:#171A17 !important; margin-top: 25px;}
#service_en_cours .info a {line-height: 24px; font-size:18px; margin-top:15px;}
.outside_list a {margin-left:15px;}
#rattrapage_1 .advice {margin-top:15px;}
.btn.present {background:#50C878;}
.btn.return {background: #ff3333; color:#FFF !important; margin-top:25px;}
.msg-big {font-size: xx-large; background: #fff; padding:25px; text-align: center;}
#member_advice {background: #FFF; color: red;}
\ No newline at end of file
#dashboard h2 {margin:25px; padding:5px; border:1px solid #efefef; background:#ffffff;}
.validate_container {background:#00b0f0;}
.errors_container {background:#f000c0;}
div.coop {margin:15px;}
div.coop span
{padding:5px; border: 1px solid #000000;border-radius: 15px; background:#ffffff; cursor:pointer;}
div.coop_no_select span {background:#ccc !important; cursor:default;}
#coop_page, #warning_slide {display:none;}
.title p {font-weight:bold;}
#coop_validation_form {width:980px;margin:auto;}
#coop_validation_form input , #warning_slide input{
text-indent: 5px;
}
#coop_validation_form [name="email"]
{max-width:545px;}
[name="barcode_base"],
#coop_validation_form [name="shares_nb"],
#coop_validation_form [name="shares_euros"],
#coop_validation_form [name="checks_nb"],
#coop_validation_form input.check_item
{width:80px;}
#coop_validation_form [name="ftop"] {width:70px;display: none;}
#coop_validation_form [name="address"], #coop_validation_form [name="street2"] {width:100%;}
#coop_validation_form [name="street2"] {margin-top: 3px;}
#coop_validation_form [name="zip"] {width:100px;}
#coop_validation_form .phone-wrapper-2 {
clear: both;
display: block;
margin-top: 8px;
}
#choosen_shift input {width:50px;}
#choosen_shift [name="week"] {width:30px;}
#choosen_shift [name="place"]{width:75px;}
#choosen_shift [name="hour"] {width:60px;}
#coop_validation_form [disabled] {text-indent:1px; color:#404040;}
[name="signaler"] {color:#ffffff !important;}
#warning_slide h1, #coop_page h1 {text-transform: uppercase;}
#warning_slide .main-content {width:980px;margin:auto;}
#new_warning_form textarea {width:100%;margin-bottom:15px;}
#ready_for_odoo, #waiting_validation {display:none;margin-top:35px;}
#ready_for_odoo div.coop span,
#done div.coop span,
#done h2
{background: #60B000;}
#msg_box {display:none;}
#coop_validation_form p.buttons {margin-top:20px;}
p.coop_msg {color: #ff0000 ;}
p.important {border: #ff0000 1px solid; padding: 15px; margin-top:15px;}
#change_shift_template {display:none; color: #ffffff;}
#shift_choice > div {display:none;}
#form_delete, #problem_delete {display:none;}
#dashboard {margin-bottom:25px;}
var coop_page = $('#coop_page');
function show_checks_nb() {
$('#coop_validation_form').find('[name="checks_nb"]').show();
$('#coop_validation_form').find('[id="checks_nb_label"]').show();
}
function hide_checks_nb() {
$('#coop_validation_form').find('[name="checks_nb"]').hide();
$('#coop_validation_form').find('[name="checks_nb"]').val(0);
$('#coop_validation_form').find('[id="checks_nb_label"]').hide();
}
function open_shift_choice() {
schoice_view.show();
coop_page.hide();
retrieve_and_draw_shift_tempates();
}
function display_current_coop_form() {
let form = $('#coop_validation_form'),
chgt_shift_btn = $('#change_shift_template');
var ftop_shift = $('#choosen_shift [name="ftop"]'),
m_barcode = form.find('[name="m_barcode"]'),
sex = $('#sex')
let street2_input = form.find('[name="street2"]'),
phone_input = form.find('[name="phone"]')
chgt_shift_btn.hide();
chgt_shift_btn.off('click',open_shift_choice);
form.find('[name="firstname"]').val(current_coop.firstname);
form.find('[name="lastname"]').val(current_coop.lastname);
if (m_barcode.length > 0 && typeof current_coop.m_barcode != "undefined") {
m_barcode.val(current_coop.m_barcode)
}
//console.log(current_coop)
if (sex.length > 0 && typeof current_coop.sex != "undefined") {
$("#" + current_coop.sex + "_sex").prop('checked', true)
}
// form.find('[name="barcode_base"]').val(current_coop.barcode_base);
form.find('[name="email"]').val(current_coop._id);
if (current_coop.shift_template &&
current_coop.shift_template.data.type == 2) {
$('#choosen_shift input').hide();
ftop_shift.val('Volant');
ftop_shift.show();
} else {
// Bien laisser dans cet ordre
$('#choosen_shift input').show();
ftop_shift.hide();
}
form.find('[name="birthdate"]').val(current_coop.birthdate || '');
form.find('[name="address"]').val(current_coop.address || '');
form.find('[name="city"]').val(current_coop.city || '');
form.find('[name="zip"]').val(current_coop.zip || '');
form.find('[name="country"]').val(current_coop.country || 'France');
form.find('[name="mobile"]').val(current_coop.mobile || '');
form.find('[name="shares_nb"]').val(current_coop.shares_nb || '');
form.find('[name="shares_euros"]').val(current_coop.shares_euros || '');
form.find('[name="payment_meaning"]').val(current_coop.payment_meaning || '');
form.find('[name="checks_nb"]').val(current_coop.checks_nb || 0);
if (street2_input.length > 0) {
street2_input.val(current_coop.street2 || '')
}
if (phone_input.length > 0) {
phone_input.val(current_coop.phone || '')
}
// Checks
form.find('[name="checks_nb"]').hide();
form.find('[id="checks_nb_label"]').hide();
$('#checks').hide();
var check_details = $('#checks').find('.check_details');
$(check_details).html('');
// Display checks number if paid by checks
if (current_coop.payment_meaning == "ch") {
show_checks_nb();
// Display check details if in payment validation step and more than 1 check
if (current_coop.validation_state == "waiting_validation_employee" && current_coop.checks_nb > 1) {
$('#checks').show();
for (var i = 1; i <= current_coop.checks_nb; i++) {
$(check_details).append('<p>Chèque #' + i +' : <input type="text" name="check_' + i + '" class="b_green check_item" required/> € </p>');
}
}
}
var show_change_shift = false;
if (current_coop.shift_template) {
var st = current_coop.shift_template.data;
form.find('[name="week"]').val(weeks_name[st.week]);
form.find('[name="day"]').val(st.day);
form.find('[name="hour"]').val(st.begin);
var place = st.place;
if (place == mag_place_string) {
place = 'Magasin';
} else if (place == office_place_string) {
place = 'Bureau';
}
form.find('[name="place"]').val(place);
if (current_coop.coop_msg) {
show_change_shift = true;
}
} else {
show_change_shift = true;
}
if (show_change_shift == true) {
chgt_shift_btn.show();
chgt_shift_btn.on('click',open_shift_choice);
}
if (typeof(coop_page) != "undefined"){coop_page.show();}
}
$('#payment_meaning').change(function(){
if ($(this).val() == 'ch'){
show_checks_nb()
} else {
hide_checks_nb()
}
});
let vform = $('#coop_validation_form'),
wform = $('#coop_warning_form'),
m_barcode = vform.find('[name="m_barcode"]'),
street2 = vform.find('input[name="street2"]'),
phone = vform.find('input[name="phone"]'),
sex = $('#sex');
wform.hide();
vform.find('input').attr('required','required');
// Setting required to fields doesn't prevent submit with empty fields anymore !!!
// TODO : Find out why
if (street2.length > 0) street2.get(0).removeAttribute('required')
if (phone.length > 0) phone.get(0).removeAttribute('required')
vform.find('[name="shares_nb"]').attr('disabled','disabled');
vform.find('[name="shares_euros"]').attr('disabled','disabled');
vform.find('[name="checks_nb"]').attr('disabled','disabled');
vform.find('[name="country"]').attr('disabled','disabled');
vform.find('[name="email"]').attr('disabled','disabled');
if (m_barcode.length > 0) {
m_barcode.attr('disabled','disabled')
}
function show_warning_form(){
vform.hide();
wform.show();
}
function show_coop_form() {
wform.hide();
vform.show();
}
function process_form_submission(event){
event.preventDefault();
var clicked = $(this),
fname = clicked.attr('name');
if (fname == 'valider'){
var form_data = new FormData(vform.get(0)),
has_empty_values = false;
if (sex.length > 0) {
//value attrribute is emptied when form is loaded !!
//so, we have to retrive sex value using unusual way
form_data.set('sex',
$('input[name="sex"]:checked').attr('id').replace('_sex',''))
}
for (var pair of form_data.entries()) {
let val = pair[1],
key = pair[0]
if ($('input[name="' + key +'"]').get(0).hasAttribute('required') && val.length == 0) {
has_empty_values = true
}
}
if (has_empty_values == true) {
alert('Vous devez remplir tous les champs pour valider.');
} else {
form_data.set('firstname',
vform.find('input[name="firstname"]').val().toFormatedFirstName());
form_data.set('lastname',
vform.find('input[name="lastname"]').val().toFormatedLastName());
form_data.set('odoo_id', current_coop.odoo_id);
form_data.set('shift_template', JSON.stringify(current_coop.shift_template));
form_data.set('shares_euros',vform.find('input[name="shares_euros"]').val())
form_data.set('email',current_coop._id);
form_data.set('shares_nb',current_coop.shares_nb);
form_data.set('checks_nb', current_coop.checks_nb);
form_data.set('country', current_coop.country);
if (m_barcode.length > 0) {
form_data.set('m_barcode', current_coop.m_barcode)
}
openModal()
post_form('/members/coop_validated_data',form_data,
function(err,result){
closeModal();
if (!err) {
var msg = "Vous êtes maintenant enregistré ! ";
msg += "<a href='" + em_url + "'>Cliquez ici</a> ";
msg += "pour découvrir l'espace membre";
$('p.intro').remove();
vform.remove();
display_msg_box(msg);
}
});
}
} else if (fname =='warning') {
var msg = $('textarea[name="message"]').val();
var data = {'odoo_id': current_coop.odoo_id,
'msg': msg,'_rev': current_coop._rev,'_id': current_coop._id};
openModal()
post_form('/members/coop_warning_msg',data,
function(err,result){
closeModal();
if (!err) {
$('#main_content').remove()
display_msg_box('Message enregistré ! Le bureau des membres est averti.');
}
});
}
}
try {
current_coop = coop;
display_current_coop_form();
var w_msg = current_coop.coop_msg || '';
wform.find('textarea').val(w_msg);
} catch(e) {
console.log(e);
}
$('button').click(process_form_submission);
$('a[name="signaler"]').click(show_warning_form);
$('a[name="retour"]').click(show_coop_form);
from django.test import TestCase
# Create your tests here.
"""."""
from django.conf.urls import url
from . import views
from . import admin
urlpatterns = [
url(r'^$', views.index, name='index'),
url(r'^([0-9\-\ \:]+)$', views.index_date, name='index_date'),
url(r'^config$', admin.config),
url(r'^settings$', admin.module_settings),
url(r'^get_all_shift_templates/$', views.get_all_shift_templates),
url(r'^shift_template/next_shift/([0-9]+)$',
views.get_shift_templates_next_shift),
url(r'^create_from_buffered_data/$', views.create_from_buffered_data),
url(r'^import_from_csv', views.create_from_csv),
url(r'^inscriptions/$', views.inscriptions),
url(r'^inscriptions/([0-9]*)$', views.inscriptions),
url(r'^prepa-odoo/$', views.prepa_odoo),
url(r'^validation_inscription/(.+)$', views.validation_inscription),
url(r'^manage_mess$', admin.manage_mess),
url(r'^problematic_members$', admin.problematic_members),
url(r'^remove_member_from_mess_list$', admin.remove_member_from_mess_list),
url(r'^generate_barcode/([0-9]+)$', admin.generate_barcode),
url(r'^generate_base_and_barcode/([0-9]+)$', admin.generate_base_and_barcode),
url(r'^create_envelops$', admin.create_envelops),
url(r'^raw_search$', admin.raw_search),
url(r'^coop_warning_msg$', views.coop_warning_msg),
url(r'^coop_validated_data$', views.coop_validated_data),
url(r'^latest_coop_id/$', views.latest_coop_id),
url(r'^get/([0-9]+)$', views.get),
url(r'^exists/([a-zA-Z0-9_\-\.\+@]+)$', views.exists),
url(r'^get_couchdb_odoo_markers/(.+)$', views.get_couchdb_odoo_markers),
url(r'^menu/$', views.menu),
url(r'^verify_final_state$', views.verify_final_state),
url(r'^update_couchdb_barcodes$', views.update_couchdb_barcodes),
# Borne accueil
url(r'^search/(.+)', views.search),
url(r'^save_photo/([0-9]+)$', views.save_photo, name='save_photo'),
url(r'^services_at_time/([0-9TZ\-\: \.]+)/([0-9\-]+)$', views.services_at_time),
url(r'^service_presence/$', views.record_service_presence),
url(r'^record_absences$', views.record_absences),
url(r'^close_ftop_service$', views.close_ftop_service),
url(r'^get_credentials$', views.get_credentials),
url(r'^remove_data_from_couchdb$', views.remove_data_from_CouchDB),
url(r'^image/([0-9]+)', views.getmemberimage),
url(r'^add_pts_to_everybody/([0-9]+)/([a-zA-Z0-9_ ]+)$', admin.add_pts_to_everybody),
# conso / groupe recherche / socio
url(r'^panel_get_purchases$', views.panel_get_purchases),
]
from django.contrib import admin
# Register your models here.
from django.apps import AppConfig
class OrdersConfig(AppConfig):
name = 'orders'
"""
https://gist.github.com/lkrone/04ca0e3ae3a78f434e5ac84cfd9ca6b1
https://docs.djangoproject.com/fr/3.0/howto/outputting-pdf/
"""
from reportlab.lib.pagesizes import A4
from reportlab.graphics.shapes import Drawing, String
from reportlab.graphics.barcode.eanbc import Ean13BarcodeWidget
from reportlab.graphics import renderPDF
from reportlab.pdfgen import canvas
from reportlab.pdfbase.pdfmetrics import stringWidth
from reportlab.lib.units import mm
import io
PAGESIZE = A4
SHEET_TOP = PAGESIZE[1]
NUM_LABELS_X = 5
NUM_LABELS_Y = 13
TEXT_Y = 13 * mm
BARCODE_Y = 0
MM_LABEL_WIDTH = 38.1 + 1.95 # adjusted with actual printer and sheet
MM_LABEL_HEIGHT = 21.2 + 1.1 # adjusted with actual printer and sheet
MM_X_LABEL_PADDING = 2
MM_X_MARGIN = 19.5 / 2 - 4 # adjusted with actual printer and sheet
MM_Y_MARGIN = 21.4 / 2 - 10 # adjusted with actual printer and sheet
BAR_HEIGHT = 30.0
LABEL_WIDTH = MM_LABEL_WIDTH * mm
LABEL_HEIGHT = MM_LABEL_HEIGHT * mm
MAX_TEXT_WIDTH = LABEL_WIDTH - 1.5 * MM_X_LABEL_PADDING * mm
LABEL_FONT = "Helvetica"
FONT_SIZE = 7
def label(ean13, name):
"""
Generate a drawing with EAN-13 barcode and descriptive text.
:param ean13: The EAN-13 Code.
:param name: Product name
:return: Drawing with barcode and name
"""
name_part2 = ''
textWidth = stringWidth(name, LABEL_FONT, FONT_SIZE)
if textWidth > MAX_TEXT_WIDTH:
max_char = int(MAX_TEXT_WIDTH/textWidth * len(name))
if textWidth > 2 * MAX_TEXT_WIDTH:
name_part2 = name[max_char-1:max_char*2+1]
name = name[0:max_char-1]
else:
last_sp_idx = name.rindex(" ")
name_part2 = name[last_sp_idx+1:]
name = name[0:last_sp_idx]
if last_sp_idx > max_char:
#transfer some part of name to name_part2
while (name.rindex(" ") > 0 and len(name) > max_char):
last_sp_idx = name.rindex(" ")
name_part2 = name[last_sp_idx+1:] + ' ' + name_part2
name = name[0:last_sp_idx]
# truncate name
text1 = String(0, TEXT_Y, name, fontName=LABEL_FONT, fontSize=FONT_SIZE, textAnchor="start")
text1.x = 0
text2 = String(0, TEXT_Y - 2 *mm, name_part2, fontName=LABEL_FONT, fontSize=FONT_SIZE, textAnchor="start")
text2.x = 0
barcode = Ean13BarcodeWidget(ean13)
# bounds = barcode.getBounds()
# width = bounds[2] - bounds[0]
#height = bounds[3] - bounds[1]
# barcode.barWidth = BAR_WIDTH
barcode.barHeight = BAR_HEIGHT
barcode.x = 0
barcode.y = BARCODE_Y # spacing from label bottom (pt)
label_drawing = Drawing(LABEL_WIDTH, LABEL_HEIGHT)
label_drawing.add(text1)
label_drawing.add(text2)
label_drawing.add(barcode)
return label_drawing
def draw_page(c,sheet_data):
"""Exemple
{'qty': 18.0, 'ean13': '0490010003694', 'name': 'Bière Blonde Brasserie Aveze 75cl'}
{'qty': 18.0, 'ean13': '0490010003243', 'name': 'Bière IPA Brasserie Aveze 75cl'}
{'qty': 12.0, 'ean13': '0490010003236', 'name': 'Bière IPA Brasserie Aveze 33cl'}
{'qty': 6.0, 'ean13': '0490010003267', 'name': 'Bière Rousse Brasserie Aveze 75cl'}
"""
labels_to_draw = []
for l in sheet_data:
for k in range(0,int(l['qty'])):
labels_to_draw.append(label(l['ean13'],l['name']))
i = 0
y0 = SHEET_TOP - LABEL_HEIGHT - (MM_Y_MARGIN * mm)
for l2d in labels_to_draw:
x = MM_X_MARGIN * mm + (i%NUM_LABELS_X) * LABEL_WIDTH
y = y0 - int(i/NUM_LABELS_X)*LABEL_HEIGHT
renderPDF.draw(l2d, c, x, y)
i+=1
c.showPage() # stop drawing on the current page and any further operations will draw on a subsequent page
def pdf_generate(sheets):
# Create a file-like buffer to receive PDF data.
buffer = io.BytesIO()
# Create the PDF object, using the buffer as its "file."
p = canvas.Canvas(buffer)
for s in sheets:
draw_page(p, s)
p.save()
# FileResponse sets the Content-Disposition header so that browsers
# present the option to save the file.
buffer.seek(0)
return buffer
from django.test import TestCase
# Create your tests here.
"""."""
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.index),
url(r'^export/([0-9]+)', views.export_one),
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)
]
from outils.common_imports import *
from outils.for_view_imports import *
from orders.models import Order, Orders
from products.models import CagetteProduct
from openpyxl import Workbook
from openpyxl.writer.excel import save_virtual_workbook
def as_text(value): return str(value) if value is not None else ""
def index(request):
return HttpResponse('Orders')
def export_one(request, oid):
msg = ''
try:
oid = int(oid)
order = Order(oid)
order_data = order.export()
if ('success' in order_data) and (order_data['success'] is True):
import datetime
now = datetime.datetime.now()
taxes = 0
company_name = ''
if hasattr(settings, 'COMPANY_NAME'):
company_name = settings.COMPANY_NAME
wb = Workbook()
ws1 = wb.create_sheet("Commande " + order_data['order']['name'], 0)
ws1.merge_cells('A1:I1')
ws1.merge_cells('A2:I2')
ws1.merge_cells('A3:I3')
ws1['A1'].value = 'Date : ' + now.strftime("%d/%m/%Y")
ws1['A2'].value = 'Commande ' + company_name + ' / ' + order_data['order']['partner_id'][1]
ws1['A3'].value = 'Ref : ' + order_data['order']['name']
ws1.append([])
ws1.append(['Produit', 'Nb. colis', 'Colisage',
'Qté', 'Référence', 'code-barre','Prix Unitaire', 'Remise', 'Sous-total'])
for line in order_data['lines']:
taxes += line['price_tax']
ws1.append([line['product_id'][1], line['product_qty_package'], line['package_qty'],
line['product_qty'], line['supplier_code'], line['barcode'], line['price_unit'], line['discount'], line['price_subtotal']])
ws1.append([])
ws1.append(['', '', '', '', '', '', '', 'Montant HT', order_data['order']['amount_untaxed'], 'euros'])
ws1.append(['', '', '', '', '', '', '', 'Taxes', taxes, 'euros'])
ws1.append(['', '', '', '', '', '', '', 'Montant TTC', order_data['order']['amount_total'], 'euros'])
# "Auto fit" columns width to content
for column_cells in ws1.columns:
length = max(len(as_text(cell.value)) for cell in column_cells)
ws1.column_dimensions[column_cells[3].column_letter].width = length
partner_name = order_data['order']['partner_id'][1]
partner_name = partner_name.replace("/", "-").replace(" ", "-")
wb_name = 'commande_' + order_data['order']['name'] + "_" + partner_name + now.strftime("_%Y_%m_%d") + '.xlsx'
file_path = 'temp/' + wb_name
wb.save(filename=file_path)
order.attach_file(file_path)
msg = 'done'
# response = HttpResponse(content=save_virtual_workbook(wb),content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
# response['Content-Disposition'] = 'attachment; filename=' + wb_name
# return response
except Exception as e:
msg = str(e)
return JsonResponse({"msg": msg}, safe=False)
def export_regex(request, string):
return HttpResponse(string + '???')
# Get labels to print for each order (ids received in GET parameter separated by comas)
def get_pdf_labels(request):
import math
import io
from django.http import FileResponse
msg = ''
LABELS_PER_SHEET = 65 # 13 rows of 5
l_data = {'total': 0, 'details': []}
# Concatenate labels to print for each order received
order_ids = request.GET['oids'].split(",")
for id in order_ids:
order_l_data = Order(id).get_custom_barcode_labels_to_print()
if order_l_data['total'] > 0:
l_data['total'] += order_l_data['total']
l_data['details'] += order_l_data['details']
if l_data['total'] > 0:
try:
from .labels_pdf_generation import pdf_generate
sheets = []
labels = 0
for i in range(0, math.ceil((l_data['total']/LABELS_PER_SHEET))):
sheets.append([])
for l in l_data['details']:
if len(str(l['barcode'])) > 0:
# Dispatch all labels to print into sheets
nb_before_insert = labels
labels += l['product_qty']
first_sheet_2_insert = int(nb_before_insert / LABELS_PER_SHEET)
first_sheet_places_available = (first_sheet_2_insert + 1) * LABELS_PER_SHEET - nb_before_insert
left_to_insert = l['product_qty']
ean13 = l['barcode']
name = l['product_id'][1]
if first_sheet_places_available >= left_to_insert:
sheets[first_sheet_2_insert].append({"qty": left_to_insert, "ean13": ean13, "name": name})
else:
left_to_insert -= first_sheet_places_available
sheets[first_sheet_2_insert].append({"qty": first_sheet_places_available, "ean13": ean13, "name": name })
sheet_idx = first_sheet_2_insert + 1
while left_to_insert > 0:
if left_to_insert > 65:
qty = 65
else:
qty = left_to_insert
sheets[sheet_idx].append({"qty": qty, "ean13": ean13, "name": name })
left_to_insert -= 65
sheet_idx += 1
pdf = pdf_generate(sheets)
return FileResponse(pdf, as_attachment=True, filename='codebarres.pdf')
except Exception as e:
msg = str(e)
else:
msg = "Nothing to do !"
return JsonResponse({"msg": msg}, safe=False)
def print_product_labels(request):
"""Orders ids are given as parameters, to print "own" product labels."""
res = {}
oids = []
try:
for oid in request.GET['oids'].split(","):
try:
oids.append(int(oid))
except:
pass # was not an int
if len(oids) >= 1:
ldatas = Orders.get_custom_barcode_labels_to_print(oids)
for tmpl_id, nb in ldatas.items():
pres = CagetteProduct.generate_label_for_printing(str(tmpl_id), '/product_labels/' , '0', str(nb))
if 'error' in pres:
if not ('errors' in res):
res['errors'] = []
res['errors'].append(pres)
except Exception as e:
res['error_ext'] = str(e)
return JsonResponse({'res': res}, safe=False)
\ No newline at end of file
"""commons functions to collect data through API."""
from django.conf import settings
import xmlrpc.client
import couchdb
import logging
coop_logger = logging.getLogger("coop.framework")
class OdooAPI:
"""Class to handle Odoo API requests."""
url = settings.ODOO['url']
user = settings.ODOO['user']
passwd = settings.ODOO['passwd']
db = settings.ODOO['db']
common = None
uid = None
models = None
def __init__(self):
"""Initialize xmlrpc connection."""
try:
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)
self.uid = self.common.authenticate(self.db,
self.user, self.passwd, {})
self.models = xmlrpc.client.ServerProxy(object_proxy_url)
except:
coop_logger.error("Impossible d'initialiser la connexion API Odoo")
def get_entity_fields(self, entity):
fields = self.models.execute_kw(self.db, self.uid, self.passwd,
entity, 'fields_get',
[],
{'attributes': ['string', 'help',
'type']})
return fields
def search_count(self, entity, cond=[]):
"""Return how many lines are matching the request."""
return self.models.execute_kw(self.db, self.uid, self.passwd,
entity, 'search_count', [cond])
def search_read(self, entity, cond=[], fields={}, limit=3500, offset=0,
order='id ASC'):
"""Main api request, retrieving data according search conditions."""
fields_and_context = {'fields': fields,
'context': {'lang': 'fr_FR','tz':'Europe/Paris'},
'limit': limit,
'offset': offset,
'order': order
}
return self.models.execute_kw(self.db, self.uid, self.passwd,
entity, 'search_read', [cond],
fields_and_context)
def update(self, entity, ids, fields):
"""Update entities which have ids, with new fields values."""
return self.models.execute_kw(self.db, self.uid, self.passwd,
entity, 'write', [ids, fields])
def create(self, entity, fields):
"""Create entity instance with given fields values."""
return self.models.execute_kw(self.db, self.uid, self.passwd,
entity, 'create', [fields])
def execute(self, entity, method, ids, params={}):
return self.models.execute_kw(self.db, self.uid, self.passwd,
entity, method, [ids], params)
def authenticate(self, login, password):
return self.common.authenticate(self.db, login, password, {})
class CouchDB:
"""Class to handle interactions with CouchDB"""
url = settings.COUCHDB['url']
dbs = settings.COUCHDB['dbs']
db = None
dbc = None
def __init__(self, arg_db=''):
url = self.url
dbs = self.dbs
if len(arg_db) > 0:
db = arg_db
couchserver = couchdb.Server(url)
self.dbc = couchserver[dbs[db]]
def getDocById(self, id):
# https://gist.github.com/marians/8e41fc817f04de7c4a70
return self.dbc[id]
def getView(self, view, my_key=None):
# return self.dbc.view(view, my_key,include_docs=False)
return self.dbc.view(view, include_docs=True)
def getAllDocs(self, key=None, value=None, with_key=True, descending=False):
"""
Get all documents
If key and value provided :
Filter with the key/value pair if 'with_key' is True
Filter without key or without value if 'with_key' if False
"""
res = []
for item in self.dbc.view('_all_docs', include_docs=True, descending=descending):
m = item.doc
if key and value:
if with_key:
if (key in m) and m[key] == value:
res.append(m)
else:
if not(key in m) or m[key] != value:
res.append(m)
else:
res.append(m)
return res
def updateDoc(self, data, key_name='_id', remove_keys=[]):
"""
Update a document with data
Fetch the document using provided key_name
- key must be in data
- key must be unique (duh)
Remove the remove_keys from document
Returns None in case of error.
"""
existing = None
try:
if key_name in data:
try:
key_value = int(data[key_name])
except:
key_value = data[key_name]
_rev = None
# Find existing doc
document = None
try:
# Use view if it exists for this key
index = self.dbc['_design/index']
if index and ('by_' + key_name) in index['views']:
for item in self.dbc.view('index/by_' + key_name, key=key_value, include_docs=True, limit=1):
document = item.doc
except:
pass
# else fetch in all docs
if document is None:
for item in self.dbc.view('_all_docs', include_docs=True):
if key_name in item.doc:
if item.doc[key_name] == key_value:
document = item.doc
if not (document is None):
existing = document
if ('_rev' in data) and data['_rev'] == document['_rev']:
existing = document
if not (existing is None):
for key in data:
value = data[key]
if (key == 'odoo_id'):
value = int(value)
existing[key] = value
for rk in remove_keys:
existing.pop(rk, None)
if ('_rev' in data):
(_id, _rev) = self.dbc.save(existing)
else:
res = self.dbc.update([existing])
if res[0] and (res[0][0] is True):
_rev = res[0][2]
if not(_rev is None):
existing['_rev'] = _rev
else:
coop_logger.warning('CouchDB : Document not found')
else:
coop_logger.warning('CouchDB : Key not found in data.')
except Exception as e:
coop_logger.error('Update couchdb: %s, %s', str(e), str(data))
return existing
def delete(self, doc):
# Database has to be purged to completly remove data
# http://docs.couchdb.org/en/stable/api/database/misc.html
res = ''
try:
res = self.dbc.delete(doc)
# only admin can definitly purge docs
except Exception as e:
res = str(e)
return res
class MConfig:
"""Module configuration"""
def get_settings(module):
import json
try:
with open(module + '/settings.json') as json_file:
msettings = json.load(json_file)
# file automatically closed with 'with..as' statement
except Exception as e:
msettings = {}
return msettings
def save_settings(module, data):
import json # TODO : which performance to declare here instead of file headings
res = False
try:
with open(module + '/settings.json', 'w') as outfile:
json.dump(data, outfile)
res = True
except Exception as e:
coop_logger.error(str(e))
return res
class Verification:
@staticmethod
def verif_token(token, coop_id):
import hashlib
match = False
api = OdooAPI()
cond = [['id', '=', coop_id]]
fields = ['create_date']
res = api.search_read('res.partner', cond, fields, 1)
if (res and len(res) == 1):
coop = res[0]
md5_calc = hashlib.md5(coop['create_date'].encode('utf-8')).hexdigest()
if token == md5_calc:
match = True
return match
# -*- coding: utf-8 -*-
"""commons apps functions ."""
from django.conf import settings
# -*- coding: utf-8 -*-
"""Import which are used in most of modules files."""
from django.conf import settings
from .common_functions import *
import json, time, datetime, pytz
import logging
"""
Following declaration needs to be inserted in settings.py
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
'style': '{',
},
'simple': {
'format': '{levelname} {asctime} {message}',
'style': '{',
},
},
'filters': {
'special': {
'()': 'django.utils.log.CallbackFilter',
'callback': custom_process_error
},
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': {
'console': {
'level': 'INFO',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'simple'
},
'file': {
'level': 'INFO',
'class': 'logging.FileHandler',
'filters': ['require_debug_true'],
'filename': os.path.join(BASE_DIR, 'log/debug.log'),
'formatter': 'simple'
},
'errors_file': {
'level': 'ERROR',
'class': 'logging.FileHandler',
'filename': os.path.join(BASE_DIR, 'log/errors.log'),
'formatter': 'simple'
},
'mail_admins': {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler',
'filters': ['special']
}
},
'loggers': {
'django': {
'handlers': ['console'],
'propagate': True,
},
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': False,
},
'coop.trace': {
'handlers': ['file', 'errors_file'],
'level': 'INFO'
},
'coop.framework': {
'handlers': ['file', 'mail_admins'],
'level': 'INFO',
'filters': ['special']
}
}
}
"""
coop_logger = logging.getLogger("coop.framework")
# Company specific data values
(TODO : Specify which one are mandatory)
### General
- OPEN_ON_SUNDAY = True
Used to show Sunday on calendars (set SHIFT_EXCHANGE_DAYS_TO_HIDE = '' to show sunday shifts)
- EMAIL_DOMAIN = 'lesgrainsdesel.fr'
- MAG_NAME = ''
Could be "Cleme" for example, at Cagette (Location are associated to shift template)
- OFFICE_NAME = ''
Was used for La Cagette (shift template)
- MAX_BEGIN_HOUR = '19:00'
Used to draw weeks planning
- COMPANY_NAME = 'Les Grains de Sel'
- ADMIN_IDS = [13]
Used to show hidden things. for example, input barcode in shelf adding product (Odoo user id array)
- ADMINS = ['webmaster@coop.dom']
- TOOLS_SERVER = 'https://outils.lacagette-coop.fr'
Used for file generation (scale or print purpose)
- BRINKS_MUST_IDENTIFY = True
If True, visitor has to use its Odoo user to identify
- UNITE_UOM_ID = 1
DB uom unit id (only use for shares invoice)
### Member subscription
- FORCE_HYPHEN_IN_SUBSCRIPTION_FIRSTNAME = False
- CONCAT_NAME_ORDER = 'LF'
- SUBSCRIPTION_ADD_SECOND_PHONE = True
- SUBSCRIPTION_ADD_STREET2 = True
- SUBSCRIPTION_ASK_FOR_SEX = True
- SUBSCRIPTION_INPUT_BARCODE = True
- SUBSCRIPTION_NAME_SEP = ', '
- COOP_BARCODE_RULE_ID = 11
- FUNDRAISING_CAT_ID = 1
- PARTS_PRICE_UNIT = 10.0
- PARTS_A_PRICE_UNIT = PARTS_PRICE_UNIT
- PARTS_A_PRODUCT_ID = 5
- PARTS_B_PRODUCT_ID = 6
- PARTS_C_PRODUCT_ID = 7
- CAP_APPELE_NON_VERSE_ACCOUNT_ID = 529
- CAP_APPELE_VERSE_ACCOUNT_ID = 8
- CAP_INVOICE_LINE_ACCOUNT_ID = 8
- CAP_JOURNAL_ID = 9
- CASH_PAYMENT_ID = 18
- CB_PAYMENT_ID = 15
- CHECK_PAYMENT_ID = 8
- VIREMENT_PAYMENT_ID = 16
- HELLO_ASSO_PAYMENT_ID = 29
- SUMUP_PAYMENT_ID = 30
- SUBSCRIPTION_PAYMENT_MEANINGS = [
{'code': 'cash', 'title': 'Espèces','journal_id': CASH_PAYMENT_ID},
{'code': 'ch', 'title': 'Chèque', 'journal_id': CHECK_PAYMENT_ID},
{'code': 'cb', 'title': 'Carte bancaire', 'journal_id': CB_PAYMENT_ID},
{'code': 'vir', 'title': 'Virement', 'journal_id': VIREMENT_PAYMENT_ID}
]
Used to generate payment meanings in subscription form
### Scales and labels files generation
- DAV_PATH = '/data/dav/cagette'
DAV_PATH is a directory managed by Apache2 dav module
- CATEG_FRUIT = 151
- CATEG_LEGUME = 152
- FIXED_BARCODE_PREFIX = '0491'
- FLV_CSV_NB = 4
How many distinct file for scale input have to be generated
- VRAC_CATEGS = [166, 167, 174]
### Shop module
- SHOP_CAN_BUY = False
If set to False, visitors can only see products (quantities and prices)
- SHOP_SLOT_SIZE = 30
How many minutes for a slot
- DEFAULT_MAX_TIMESLOT_CARTS = 5
How many carts can be produced by slot
This value can be changed in shop admin page
- HOURS_FOR_VALIDATION_SHOP = 2
How many hours are left for people to complete cart (for the choosen date)
Once time has left, booked picking hour is canceled.
Visitor has to choose a new picking date and hour
- MIN_DELAY_FOR_SLOT = 4
How far has to be the closest picking date (in hours)
For example, with 4 value, if it's 08:00 am, the picking date could not be before 12:00 am (even if a slot is free before)
- DISCOUNT_SHELFS_IDS = [74]
- EXCLUDE_SHOP_CATEGORIES = [108]
- FL_SHELFS = [16, 17, 18]
Fruits et Légumes shelves (Odoo database ids)
- VRAC_SHELFS = [20, 38]
- PROMOTE_SHELFS_IDS = [68]
- CART_VALIDATION_BOTTOM_MSG = "Pour des raisons d'hygiène les commandes seront préparées dans des sacs en papier kraft qui vous seront facturées, 0,24€ pour les petits et 0,77€ pour les grands. Merci de votre compréhension"
- SHOP_CATEGORIES = {}
- SHOP_EXTRA_MENUS = ['shop/planning_livraison_pains.html', 'shop/combien_ca_pese.html']
- SHOP_HEADER_IMG = 'https://supercafoutch.fr/wp-content/uploads/2018/02/SC-logo-4-invert@1x.png'
- SHOP_LIMIT_PRODUCTS = ['relatively_available', 'no_shelf']
- SHOP_OPENING = {'mar.': [{'start': '10:30', 'end': '14:00'}, {'start': '15:30', 'end': '20:00'}],
'mer.': [{'start': '10:30', 'end': '14:00'}, {'start': '15:30', 'end': '20:00'}],
'jeu.': [{'start': '10:30', 'end': '14:00'}, {'start': '15:30', 'end': '20:00'}],
'ven.': [{'start': '10:30', 'end': '14:00'}, {'start': '15:30', 'end': '20:00'}],
'sam.': [{'start': '10:30', 'end': '14:00'}, {'start': '15:30', 'end': '20:00'}]
}
- SHOP_OPENING_START_DATE = '2020-06-02'
- SHOP_SURVEY_LINK = 'https://docs.google.com/forms/d/e/1FAIpQLSczl0mMRwx3s9LbUSPYwwFTiiRa6agx7YkQM9cL41eiQnXNUw/viewform'
- SHOW_SUBSTITUTION_OPTION = False
- VALIDATION_ORDER_MAIL_TEMPLATE = 'shop/supercafoutch_validation_mail.html'
### Entrance module
- ENTRANCE_COME_FOR_SHOPING_MSG = "Hey coucou toi ! Cet été nous sommes plus de <strong>1000 acheteur·euses</strong> pour seulement <strong>300 coopérateur·rice·s</strong> en service. <br />Tu fais tes courses à La Cagette cet été ?<br/> Inscris-toi sur ton espace membre !"
- ENTRANCE_FTOP_BUTTON_DISPLAY = False
Hide the "I come as FTOP" button when set on False
### Member space
- EM_URL = ''
Url to redirect once member has validated its data (empty in simple use)
- WITH_WEBSITE_MENU = True
If set to True, a "personnal data" menu is shown, permitting connected member to modify its data.
- CALENDAR_NO_MORE_LINK = True
If True, in shifts calendar view (to choose one or exchange one)
all shifts are shown (if False, a link to show more shifts is shown)
- CAL_INITIAL_VIEW = 'dayGridWeek'
If not set, default view is 'dayGridMonth'
- SHIFT_EXCHANGE_DAYS_TO_HIDE = ''
By default, if this variable is not set, sunday is hidden
To hide Sunday and Monday, set this to "0,1"
- SHIFT_INFO = """A la cagette, un service est une plage de trois heures un jour en particulier, par exemple : le mardi 25/09/2018 à 13h15.
<br />A l'inverse, un créneau est une plage de trois heures régulière, par exemple, tous les mardi de semaine A à 13h15."""
- PB_INSTRUCTIONS = """Si j'ai un problème, que je suis désinscrit, que je veux changer de créneaux ou quoi que ce soit, merci de vous rendre dans la section \"J'ai un problème\" sur le site web de <a href=\"https://lacagette-coop.fr/?MonEspaceMembre\">La Cagette</a>"""
- UNSUBSCRIBED_MSG = 'Vous êtes désincrit·e, merci de remplir <a href="https://docs.google.com/forms/d/e/1FAIpQLSfPiC2PkSem9x_B5M7LKpoFNLDIz0k0V5I2W3Mra9AnqnQunw/viewform">ce formulaire</a> pour vous réinscrire sur un créneau.<br />Vous pouvez également contacter le Bureau des Membres en remplissant <a href="https://docs.google.com/forms/d/e/1FAIpQLSeZP0m5-EXPVJxEKJk6EjwSyZJtnbiGdYDuAeFI3ENsHAOikg/viewform">ce formulaire</a>'
Message shown to people when they connect to the Member Space
### Reception
- RECEPTION_ADD_ADMIN_MODE = True
- RECEPTION_ADD_ALL_LEFT_IS_GOOD = True
A second button appears to make all pending products (left list) to be considered as "good"
(RECEPTION_ADD_ADMIN_MODE needs to be set at True)
- RECEPTION_MERGE_ORDERS_PSWD = 'pass2makeApause'
Password to enter to validate merge orders processing
It has been set only to stop member action, considering the impact of the merge
- RECEPTION_PDT_LABELS_BTN_TEXT = 'Lancer l\'impression'
- RECEPTION_PDT_LABELS_FN = 'print_product_labels()'
- RECEPTION_PDT_LABELS_TEXT = 'Cliquez sur ce bouton pour imprimer les étiquettes code-barres à coller sur les produits'
- RECEPTION_SHELF_LABEL_PRINT = True
- COEFF_MAG_ID = 1
DB coeff id, needed to compute product shelf price
### Miscellious
- EXPORT_COMPTA_FORMAT = 'Quadratus'
If not set, export format is the one used by La Cagette
Quadratus has been introduced to fit with Supercafoutch need.
- STOCK_LOC_ID = 12
Only used in Inventory module, which is no more in use
- CUSTOM_CSS_FILES = {'all': ['common_lgds.css'],
'members': ['inscription_lgds.css']}
To insert a CSS to all modules, key is 'all' (files actually put in outils/static/css)
from django.conf import settings
import os
from os import path
def custom_css(request):
css_files = {}
if hasattr(settings, 'CUSTOM_CSS_FILES'):
for module_name, files in settings.CUSTOM_CSS_FILES.items():
module_key = module_name
if module_name == 'all':
module_name = 'outils'
for fn in files:
module_relative_path = '/static/css/' + fn
fpath = settings.BASE_DIR + '/' + module_name + module_relative_path
if path.exists(fpath) is True:
if not (module_key in css_files):
css_files[module_key] = []
css_files[module_key].append(module_relative_path.replace('/static',''))
return {'custom_css': css_files}
def context_setting(request):
"""adding settings variable to context (can be overloaded in views)."""
context = {'odoo': settings.ODOO['url']}
return context
\ No newline at end of file
"""Custom logging, entry point for reacting to errors."""
from django.conf import settings
import logging
import traceback
logger = logging.getLogger("coop.trace")
def custom_process_error(record):
"""
dir(record)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'args', 'created', 'exc_info', 'exc_text', 'filename', 'funcName', 'getMessage', 'levelname', 'levelno', 'lineno', 'module', 'msecs', 'msg', 'name', 'pathname', 'process', 'processName', 'relativeCreated', 'request', 'stack_info', 'status_code', 'thread', 'threadName']
"""
"""
dir(record.request)
['COOKIES', 'FILES', 'GET', 'META', 'POST', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_cached_user', '_cors_enabled', '_current_scheme_host', '_encoding', '_files', '_get_full_path', '_get_post', '_get_raw_host', '_get_scheme', '_initialize_handlers', '_load_post_and_files', '_mark_post_parse_error', '_messages', '_post', '_post_parse_error', '_read_started', '_set_post', '_stream', '_upload_handlers', 'body', 'build_absolute_uri', 'close', 'content_params', 'content_type', 'csrf_processing_done', 'encoding', 'environ', 'get_full_path', 'get_full_path_info', 'get_host', 'get_port', 'get_raw_uri', 'get_signed_cookie', 'is_ajax', 'is_secure', 'method', 'parse_file_upload', 'path', 'path_info', 'read', 'readline', 'readlines', 'resolver_match', 'scheme', 'session', 'upload_handlers', 'user', 'xreadlines']
"""
"""
print(record.getMessage())
print(record.module)
print(record.stack_info)
print(record.threadName)
print(record.processName)
#print(record.exc_info)
print(record.filename)
print(record.funcName)
print(record.request.get_full_path())
# print(record.request.method)
"""
if record.levelname == 'ERROR':
if hasattr(record, 'request'):
logger.error(record.request.get_full_path() + ' --> ' + str(traceback.format_exc()))
else:
logger.error(record.getMessage())
return True
# -*- coding: utf-8 -*-
"""Import which are used in most of modules view files."""
from django.http import HttpResponse
from django.http import JsonResponse
from django.http import HttpResponseNotFound
from django.http import HttpResponseForbidden
from django.http import HttpResponseServerError
from django.template import loader
from django.shortcuts import render
from django.shortcuts import redirect
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_exempt
\ No newline at end of file
from django import forms
from outils.lib.MonthYearWidget import *
import datetime
class OdooEntityFieldsForm(forms.Form):
entity = forms.CharField()
class ExportComptaForm(forms.Form):
mois = forms.DateField(
required=True,
widget=MonthYearWidget()
)
#fichier = forms.FileField()
# CHOICES = [('zip', '1 fichier par journal'),('compact', '1 seul fichier')]
# pref = forms.ChoiceField(choices=CHOICES, widget=forms.RadioSelect())
class GenericExportMonthForm(forms.Form):
mois = forms.DateField(
required=True,
widget=MonthYearWidget(years=range(datetime.date.today().year-2,datetime.date.today().year+1))
)
"""commons computation functions ."""
import logging
coop_logger = logging.getLogger("coop.framework")
def computeEAN13Check(s12):
odd_sum = 0
even_sum = 0
for i, char in enumerate(s12):
if i % 2 == 0:
even_sum += int(char)
else:
odd_sum += int(char)
total_sum = (odd_sum * 3) + even_sum
mod = total_sum % 10
if mod == 0:
computed_check = 0
else:
computed_check = 10 - mod
return computed_check
def checkEAN13(bc):
bc = str(bc)
if (len(bc) != 13):
raise Exception("Invalid length")
code = bc[:-1]
check = bc[-1:]
computed_check = computeEAN13Check(code)
return (computed_check == int(check))
def getMonthFromRequestForm(request):
month = request.POST.get('mois_month')
year = request.POST.get('mois_year')
res = {}
try:
m = int(month)
y = int(year)
if (m < 10):
month = '0' + month
if (m > 0 and y > 0):
res['month'] = year + '-' + month
else:
today = datetime.date.today()
year = str(today.year)
month = str(today.month)
if (len(month) == 1):
month = '0' + month
res['month'] = year + '-' + month
except Exception as e:
res['error'] = str(e)
return res
def extract_firstname_lastname(fullname, sep):
firstname = lastname = fullname
try:
elts = fullname.split(sep)
if len(elts) > 1:
firstname = elts[0]
lastname = ' '.join(elts[1:])
except Exception as e:
coop_logger.error('extract_firstname_lastname : %s', str(e))
return {'firstname': firstname, 'lastname': lastname}
def is_present_period(d1, d2, dformat='%Y-%m-%d'):
"""Is present included between the two datetime objects
Parameters:
d1 (string): start of period
d2 (string): end of period
dformat (string): Needed to create datetime object using
Returns:
boolean : If True, the present is included in the period
"""
from datetime import datetime
now = dt1 = dt2 = datetime.now()
try:
dt1 = datetime.strptime(d1, dformat)
except:
pass
try:
dt2 = datetime.strptime(d2, dformat)
except:
pass
return (now - dt1).total_seconds() > 0 and (now - dt2).total_seconds() <= 0
\ No newline at end of file
# -*- coding: utf-8 -*-
"""Import which are used when image data are processed."""
import base64
import imghdr
import datetime
import re
from six import string_types
from django.forms.widgets import Widget, Select
from django.utils.dates import MONTHS
from django.utils.safestring import mark_safe
__all__ = ('MonthYearWidget',)
RE_DATE = re.compile(r'(\d{4})-(\d\d?)-(\d\d?)$')
class MonthYearWidget(Widget):
"""
A Widget that splits date input into two <select> boxes for month and year,
with 'day' defaulting to the first of the month.
Based on SelectDateWidget, in
django/trunk/django/forms/extras/widgets.py
"""
none_value = (0, '---')
month_field = '%s_month'
year_field = '%s_year'
def __init__(self, attrs=None, years=None, required=True):
# years is an optional list/tuple of years to use in the "year" select box.
self.attrs = attrs or {}
self.required = required
if years:
self.years = years
else:
this_year = datetime.date.today().year
self.years = range(this_year-1, this_year+1)
def render(self, name, value, attrs=None, renderer=None):
try:
year_val, month_val = value.year, value.month
except AttributeError:
year_val = month_val = None
if isinstance(value, string_types):
match = RE_DATE.match(value)
if match:
year_val, month_val, day_val = [int(v) for v in match.groups()]
output = []
if 'id' in self.attrs:
id_ = self.attrs['id']
else:
id_ = 'id_%s' % name
month_choices = list(MONTHS.items())
if not (self.required and value):
month_choices.append(self.none_value)
month_choices.sort()
local_attrs = self.build_attrs(base_attrs=self.attrs)
s = Select(choices=month_choices)
select_html = s.render(self.month_field % name, month_val, local_attrs)
output.append(select_html)
year_choices = [(i, i) for i in self.years]
if not (self.required and value):
year_choices.insert(0, self.none_value)
local_attrs['id'] = self.year_field % id_
s = Select(choices=year_choices)
select_html = s.render(self.year_field % name, year_val, local_attrs)
output.append(select_html)
return mark_safe(u'\n'.join(output))
def id_for_label(self, id_):
return '%s_month' % id_
id_for_label = classmethod(id_for_label)
def value_from_datadict(self, data, files, name):
y = data.get(self.year_field % name)
m = data.get(self.month_field % name)
if y == m == "0":
return None
if y and m:
return '%s-%s-%s' % (y, m, 1)
return data.get(name, None)
\ No newline at end of file
"""Quadratus export."""
from collections import OrderedDict
import logging
coop_logger = logging.getLogger("coop.framework")
"""
https://help.eurecia.com/hc/fr/articles/360000581669-Format-CEGID-QUADRATUS-COMPTABILITE
Fichier texte (extension TXT)
Fichier position
Pas de lignes d’en-tête
Format des dates : JJMMAA
Ordre Colonnes Début Longueur Commentaires
1 Type de ligne 1 1 M
2 Compte 2 8   6 + 2 espace
3 Code journal 10 2 VT par ex
4 Folio 12 3 000 en dur
5 Date d’écriture 15 6 Date sélectionnée lors de l’export
6 Vide 21 1 Vide
7 Libellé 22 20 Description (complété par espace)
8 Sens de l’écriture 42 1 "C si crédit D si débit"
----- A partir de la, ça diffère pour SuperCafoutch ------
9 Montant 43 12 En centimes
10 Signe 55 1 + ou -
11 Contrepartie 56 8 Vide
12 Date d’échéance 64 6 Date de la dépense
13 ? 72 9 Vide
14 ? 81 1 0
15 ? 82 27 Vide
16 ? 109 1 0
17 Devise 110 3 EUR
18 Code journal 113 2
19 ? 115 1 Vide
20 ? 116 3 N0D
21 ? 119 32 Vide
22 Incrément 160 10 1, 2, 3..... ( complété à gauche par des vides)
23 ? Vide
24 Montant 162 12 En centimes 0 devant
25 Signe 174 1 + ou -
26 ? 175 24 Vide
Fin par cr / nl
------
9 Signe 43 1 + ou -
10 Montant 44 12 En centimes
11 Contrepartie 56 8 Vide
12 Date d’échéance 64 6 Date de la dépense
13 Lettrage 70 5 Vide
14 Numéro de pièce 75 5 Vide
15 Code analytique 80 10 Code du compte analytique sélectionné
16 Quantité 90 10 Vide
17 Numéro de pièce 100 8 Numéro de NDF
18 Devise 108 3 Devise de l’utilisateur
19 Code journal 111 3 "NDF par défaut Configuré dans la boîte à outil du connecteur"
20 Vide 114 3 Vide
21 Libellé de l’écriture 117 32 Description de la dépense
22 Numéro de pièce 149 10 Numéro de NDF complété par des 0 à gauche
23 Vide 1 59 73 Vide
Fichier reçu d'Odoo
[0] : Journal
[1] : Date
[2] : Libellé journal
[3] : compte
[4] : coop
[5] : Pièce compta
[6] : D (0) ou C (1)
[7] : montant
ATTENTION : Suppose modification du code de l'account_export de La Louve : ligne 381 commentée
"""
def line_generate(cpte, cj, date, libelle, sens, montant, signe, inc):
if (len(cpte) < 8):
while (len(cpte) < 8):
cpte += ' '
elif len(cpte) > 8:
cpte = cpte[0:8]
if len(libelle) > 20:
libelle = libelle[0:20]
else:
while (len(libelle) < 20):
libelle += ' '
line = 'M'
line += cpte
line += cj
line += '000'
line += date
line += ' '
line += libelle
line += sens
line += '{:0>12}'.format(montant)
line += signe
line += '{:<8}'.format(' ')
line += date
line += '{:<9}'.format(' ')
line += '0'
line += '{:<27}'.format(' ')
line += '0'
line += 'EUR'
line += cj
line += ' '
line += 'N0D'
line += '{:<32}'.format(' ')
line += '{:>10}'.format(str(inc))
line += ' '
line += '{:0>12}'.format(montant)
line += signe
line += '{:<24}'.format(' ')
return line
def generate_quadratus_file(rows):
try:
#rows = sorted(rows, key=lambda x: x[0], reverse=True)
lines = {'V': [], 'K': []}
daily_ops = {'V': {}, 'K': {}}
for k, data in rows.items():
if (k in ['V', 'K']):
for row in data:
[y, m, d] = row[1].split('-')
date = d + m + y[2:]
if not (date in daily_ops):
daily_ops[date] = []
libelle = row[2]
signe = '+'
if row[6] != 0:
sens = 'D'
if row[6] < 0:
signe = '-'
montant = str(row[6])
if row[7] != 0:
sens = 'C'
if row[7] < 0:
signe = '-'
montant = str(row[7])
if not (date in daily_ops[k].keys()):
daily_ops[k][date] = [] # !! It creates daily_ops[k][date] AND daily_ops[date] entries !!!
daily_ops[k][date].append({'cpte': row[3],
'cj': row[0],
'date': date,
'libelle': libelle,
'sens': sens,
'montant': montant,
'signe': signe})
except Exception as e:
coop_logger.error("Compta, quadratus, generate_quadratus_file : %s", str(e))
for k, daily_ops_data in daily_ops.items():
if (k in ['V', 'K']): # could be date !! (see above)
daily_ops_data = OrderedDict(sorted(daily_ops_data.items())) # sort by date
previous_date = ''
inc = 1
totaux = {'D': 0, 'C': 0}
for d, ops in daily_ops_data.items():
for op in ops:
if op['sens'] == 'D':
totaux['D'] += int(op['montant'])
else:
totaux['C'] += int(op['montant'])
line = line_generate(op['cpte'], op['cj'], op['date'], op['libelle'],
op['sens'], op['montant'], op['signe'], inc)
lines[k].append(line)
if previous_date != d:
previous_date = d
# let's verify if there is a corrective line to write
diff = totaux['D'] - totaux['C']
if diff != 0:
if diff > 0:
line = line_generate('758000', 'VT', d, 'correctif', 'C', diff, '+', inc)
else:
line = line_generate('658000', 'VT', d, 'correctif', 'D', abs(diff), '+', inc)
lines[k].append(line)
totaux = {'D': 0, 'C': 0}
inc += 1
contents = {'V': '\r\n'.join(lines['V']).encode(encoding="ascii", errors="replace"),
'K': '\r\n'.join(lines['K']).encode(encoding="ascii", errors="replace")}
return contents
\ No newline at end of file
from django.conf import settings
class CagetteMail:
@staticmethod
def sendWelcome(email):
from django.core.mail import send_mail
import re
from django.utils.html import strip_tags
from django.template.loader import render_to_string
html_msg = render_to_string(settings.WELCOME_MAIL_TEMPLATE, {})
msg = re.sub('[ \t]+', ' ', strip_tags(html_msg))
msg = msg.replace('\n ', '\n').strip()
send_mail(settings.WELCOME_MAIL_SUBJECT,
msg,
settings.DEFAULT_FROM_EMAIL,
[email],
fail_silently=False,
html_message=html_msg)
@staticmethod
def sendCartValidation(email, cart):
from django.core.mail import send_mail
from django.utils.html import strip_tags
from django.template.loader import render_to_string
from datetime import datetime
import pytz
tz = pytz.timezone("Europe/Paris")
d_obj = datetime.fromtimestamp(cart['submitted_time'], tz)
if ('comment' in cart) and len(cart['comment']) == 0:
del cart['comment']
ctx = {'mag': settings.COMPANY_NAME,
'cart': cart,
'order_date': d_obj.strftime('%d/%m/%Y à %Hh%S (UTC)')}
try:
ctx['survey_link'] = settings.SHOP_SURVEY_LINK
except:
pass
mail_template = 'shop/cart_validation_email.html'
try:
mail_template = settings.VALIDATION_ORDER_MAIL_TEMPLATE
except:
pass
html_msg = render_to_string(mail_template, ctx)
msg = strip_tags(html_msg)
send_mail("Votre commande en ligne à " + settings.COMPANY_NAME ,
msg,
settings.DEFAULT_FROM_EMAIL,
[email],
fail_silently=False,
html_message=html_msg)
from django.http import HttpResponse
from django.http import JsonResponse
from django.template import loader
from django.conf import settings
from members.models import CagetteUser
import json
def index(request):
template = loader.get_template('outils/monitor.html')
context = {'title': 'Monitor Django',
'couchdb_server': settings.COUCHDB['url']}
response = HttpResponse(template.render(context, request))
return response
def js_errors(request):
from os import path
is_connected_user = CagetteUser.are_credentials_ok(request)
if is_connected_user is True:
res = {}
try:
content = []
f_path = 'outils/js_errors.log'
if path.exists(f_path):
with open(f_path, 'r') as file:
rows = file.readlines()
for row in rows:
[d, mo, a, mg] = row.split('\t')
content.append({'date': d,
'module': mo,
'agent': a,
'data': json.loads(mg)
})
res['content'] = content
except Exception as e:
res['error'] = str(e)
return JsonResponse({'res': res})
return HttpResponse('ok') # always responds 'ok' if request doesn't match inside conditions
# coding: utf-8
"""Interact with Odoo by python code
Before launching script, launch the following command:
export DJANGO_SETTINGS_MODULE='scripts_settings'
(a file named scripts_settings.py is present in this directory)
"""
#
import sys, getopt, os
sys.path.append(os.path.abspath('../..'))
from outils.common import OdooAPI
def main():
api = OdooAPI()
# etc.....
if __name__ == "__main__":
main()
\ No newline at end of file
"""
Django settings for outils project.
Generated by 'django-admin startproject' using Django 1.8.7.
For more information on this file, see
https://docs.djangoproject.com/en/1.8/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.8/ref/settings/
"""
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
from .settings_secret import *
from .settings_constants import *
from .texts.cagette import *
from .config import *
from .customized_errors_filter import *
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'Mettre_plein_de_caracteres_aleatoires_iezezezeezezci'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['127.0.0.1']
# Application definition
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'corsheaders',
'members',
'shifts',
'reception',
'stock',
'inventory',
'products',
'envelops',
'website',
'orders',
'shop',
'shelfs',
# 'tests'
)
MIDDLEWARE = (
'corsheaders.middleware.CorsMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
'members.middleware.newCorsMiddleware'
)
ROOT_URLCONF = 'outils.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'outils.context_processors.custom_css',
'outils.context_processors.context_setting'
],
},
},
]
STATICFILES_DIRS = (
"members/static",
"outils/static",
"shifts/static",
"stock/static",
"inventory/static",
"products/static",
"envelops/static",
"website/static",
"shop/static",
"shelfs/static",
# "tests/static"
)
WSGI_APPLICATION = 'outils.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.8/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Internationalization
# https://docs.djangoproject.com/en/1.8/topics/i18n/
LANGUAGE_CODE = 'fr-FR'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.8/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
# https://docs.djangoproject.com/fr/2.2/topics/auth/passwords/
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
]
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
'style': '{',
},
'simple': {
'format': '{levelname} {asctime} {message}',
'style': '{',
},
},
'filters': {
'special': {
'()': 'django.utils.log.CallbackFilter',
'callback': custom_process_error
},
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': {
'console': {
'level': 'INFO',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'simple'
},
'file': {
'level': 'INFO',
'class': 'logging.FileHandler',
'filters': ['require_debug_true'],
'filename': os.path.join(BASE_DIR, 'log/debug.log'),
'formatter': 'simple'
},
'errors_file': {
'level': 'ERROR',
'class': 'logging.FileHandler',
'filename': os.path.join(BASE_DIR, 'log/errors.log'),
'formatter': 'simple'
},
'mail_admins': {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler',
'filters': ['special']
}
},
'loggers': {
'django': {
'handlers': ['console'],
'propagate': True,
},
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': False,
},
'coop.trace': {
'handlers': ['file', 'errors_file'],
'level': 'INFO'
},
'coop.framework': {
'handlers': ['file', 'mail_admins'],
'level': 'INFO',
'filters': ['special']
}
}
}
"""Secret data for DB connexion ."""
ODOO = {
'url': 'http://127.0.0.1:8069'
'user': 'api',
'passwd': 'xxxxxxxxxxxx',
'db': 'bd_test',
}
COUCHDB = {
'url': 'http://127.0.0.1:5984',
'dbs': {
'member': 'coops',
'inventory': 'inventory',
'envelops': 'envelop',
'shop': 'shopping_carts'
}
}
SQL_OFF = {
'db': 'open_food_facts',
'user': 'off_user',
'pwd': 'xxxxxxxx'
}
EMAIL_HOST = 'mail.proxy'
EMAIL_HOST_USER = 'nepasrepondre@mydomain.ext'
EMAIL_HOST_PASSWORD = 'xxxxx'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
DEFAULT_FROM_EMAIL = 'nepasrepondre@mydomain.ext'
ADMINS = [('myname', 'django@mydomain.ext')]
select.admin-autocomplete {
width: 20em;
}
.select2-container--admin-autocomplete.select2-container {
min-height: 30px;
}
.select2-container--admin-autocomplete .select2-selection--single,
.select2-container--admin-autocomplete .select2-selection--multiple {
min-height: 30px;
padding: 0;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection,
.select2-container--admin-autocomplete.select2-container--open .select2-selection {
border-color: #999;
min-height: 30px;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--single,
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--single {
padding: 0;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--multiple,
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--multiple {
padding: 0;
}
.select2-container--admin-autocomplete .select2-selection--single {
background-color: #fff;
border: 1px solid #ccc;
border-radius: 4px;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__rendered {
color: #444;
line-height: 30px;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__placeholder {
color: #999;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow {
height: 26px;
position: absolute;
top: 1px;
right: 1px;
width: 20px;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow b {
border-color: #888 transparent transparent transparent;
border-style: solid;
border-width: 5px 4px 0 4px;
height: 0;
left: 50%;
margin-left: -4px;
margin-top: -2px;
position: absolute;
top: 50%;
width: 0;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__clear {
float: left;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__arrow {
left: 1px;
right: auto;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single {
background-color: #eee;
cursor: default;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single .select2-selection__clear {
display: none;
}
.select2-container--admin-autocomplete.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #888 transparent;
border-width: 0 4px 5px 4px;
}
.select2-container--admin-autocomplete .select2-selection--multiple {
background-color: white;
border: 1px solid #ccc;
border-radius: 4px;
cursor: text;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered {
box-sizing: border-box;
list-style: none;
margin: 0;
padding: 0 5px;
width: 100%;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered li {
list-style: none;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__placeholder {
color: #999;
margin-top: 5px;
float: left;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
margin: 5px;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice {
background-color: #e4e4e4;
border: 1px solid #ccc;
border-radius: 4px;
cursor: default;
float: left;
margin-right: 5px;
margin-top: 5px;
padding: 0 5px;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove {
color: #999;
cursor: pointer;
display: inline-block;
font-weight: bold;
margin-right: 2px;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove:hover {
color: #333;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-search--inline {
float: right;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
margin-left: 5px;
margin-right: auto;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
margin-left: 2px;
margin-right: auto;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection--multiple {
border: solid #999 1px;
outline: 0;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--multiple {
background-color: #eee;
cursor: default;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection__choice__remove {
display: none;
}
.select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--multiple {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--multiple {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.select2-container--admin-autocomplete .select2-search--dropdown .select2-search__field {
border: 1px solid #ccc;
}
.select2-container--admin-autocomplete .select2-search--inline .select2-search__field {
background: transparent;
border: none;
outline: 0;
box-shadow: none;
-webkit-appearance: textfield;
}
.select2-container--admin-autocomplete .select2-results > .select2-results__options {
max-height: 200px;
overflow-y: auto;
}
.select2-container--admin-autocomplete .select2-results__option[role=group] {
padding: 0;
}
.select2-container--admin-autocomplete .select2-results__option[aria-disabled=true] {
color: #999;
}
.select2-container--admin-autocomplete .select2-results__option[aria-selected=true] {
background-color: #ddd;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option {
padding-left: 1em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__group {
padding-left: 0;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option {
margin-left: -1em;
padding-left: 2em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -2em;
padding-left: 3em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -3em;
padding-left: 4em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -4em;
padding-left: 5em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -5em;
padding-left: 6em;
}
.select2-container--admin-autocomplete .select2-results__option--highlighted[aria-selected] {
background-color: #79aec8;
color: white;
}
.select2-container--admin-autocomplete .select2-results__group {
cursor: default;
display: block;
padding: 6px;
}
/* CHANGELISTS */
#changelist {
position: relative;
width: 100%;
}
#changelist table {
width: 100%;
}
.change-list .hiddenfields { display:none; }
.change-list .filtered table {
border-right: none;
}
.change-list .filtered {
min-height: 400px;
}
.change-list .filtered .results, .change-list .filtered .paginator,
.filtered #toolbar, .filtered div.xfull {
margin-right: 280px;
width: auto;
}
.change-list .filtered table tbody th {
padding-right: 1em;
}
#changelist-form .results {
overflow-x: auto;
}
#changelist .toplinks {
border-bottom: 1px solid #ddd;
}
#changelist .paginator {
color: #666;
border-bottom: 1px solid #eee;
background: #fff;
overflow: hidden;
}
/* CHANGELIST TABLES */
#changelist table thead th {
padding: 0;
white-space: nowrap;
vertical-align: middle;
}
#changelist table thead th.action-checkbox-column {
width: 1.5em;
text-align: center;
}
#changelist table tbody td.action-checkbox {
text-align: center;
}
#changelist table tfoot {
color: #666;
}
/* TOOLBAR */
#changelist #toolbar {
padding: 8px 10px;
margin-bottom: 15px;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
background: #f8f8f8;
color: #666;
}
#changelist #toolbar form input {
border-radius: 4px;
font-size: 14px;
padding: 5px;
color: #333;
}
#changelist #toolbar form #searchbar {
height: 19px;
border: 1px solid #ccc;
padding: 2px 5px;
margin: 0;
vertical-align: top;
font-size: 13px;
}
#changelist #toolbar form #searchbar:focus {
border-color: #999;
}
#changelist #toolbar form input[type="submit"] {
border: 1px solid #ccc;
padding: 2px 10px;
margin: 0;
vertical-align: middle;
background: #fff;
box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
cursor: pointer;
color: #333;
}
#changelist #toolbar form input[type="submit"]:focus,
#changelist #toolbar form input[type="submit"]:hover {
border-color: #999;
}
#changelist #changelist-search img {
vertical-align: middle;
margin-right: 4px;
}
/* FILTER COLUMN */
#changelist-filter {
position: absolute;
top: 0;
right: 0;
z-index: 1000;
width: 240px;
background: #f8f8f8;
border-left: none;
margin: 0;
}
#changelist-filter h2 {
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 5px 15px;
margin-bottom: 12px;
border-bottom: none;
}
#changelist-filter h3 {
font-weight: 400;
font-size: 14px;
padding: 0 15px;
margin-bottom: 10px;
}
#changelist-filter ul {
margin: 5px 0;
padding: 0 15px 15px;
border-bottom: 1px solid #eaeaea;
}
#changelist-filter ul:last-child {
border-bottom: none;
padding-bottom: none;
}
#changelist-filter li {
list-style-type: none;
margin-left: 0;
padding-left: 0;
}
#changelist-filter a {
display: block;
color: #999;
text-overflow: ellipsis;
overflow-x: hidden;
}
#changelist-filter li.selected {
border-left: 5px solid #eaeaea;
padding-left: 10px;
margin-left: -15px;
}
#changelist-filter li.selected a {
color: #5b80b2;
}
#changelist-filter a:focus, #changelist-filter a:hover,
#changelist-filter li.selected a:focus,
#changelist-filter li.selected a:hover {
color: #036;
}
/* DATE DRILLDOWN */
.change-list ul.toplinks {
display: block;
float: left;
padding: 0;
margin: 0;
width: 100%;
}
.change-list ul.toplinks li {
padding: 3px 6px;
font-weight: bold;
list-style-type: none;
display: inline-block;
}
.change-list ul.toplinks .date-back a {
color: #999;
}
.change-list ul.toplinks .date-back a:focus,
.change-list ul.toplinks .date-back a:hover {
color: #036;
}
/* PAGINATOR */
.paginator {
font-size: 13px;
padding-top: 10px;
padding-bottom: 10px;
line-height: 22px;
margin: 0;
border-top: 1px solid #ddd;
}
.paginator a:link, .paginator a:visited {
padding: 2px 6px;
background: #79aec8;
text-decoration: none;
color: #fff;
}
.paginator a.showall {
padding: 0;
border: none;
background: none;
color: #5b80b2;
}
.paginator a.showall:focus, .paginator a.showall:hover {
background: none;
color: #036;
}
.paginator .end {
margin-right: 6px;
}
.paginator .this-page {
padding: 2px 6px;
font-weight: bold;
font-size: 13px;
vertical-align: top;
}
.paginator a:focus, .paginator a:hover {
color: white;
background: #036;
}
/* ACTIONS */
.filtered .actions {
margin-right: 280px;
border-right: none;
}
#changelist table input {
margin: 0;
vertical-align: baseline;
}
#changelist table tbody tr.selected {
background-color: #FFFFCC;
}
#changelist .actions {
padding: 10px;
background: #fff;
border-top: none;
border-bottom: none;
line-height: 24px;
color: #999;
}
#changelist .actions.selected {
background: #fffccf;
border-top: 1px solid #fffee8;
border-bottom: 1px solid #edecd6;
}
#changelist .actions span.all,
#changelist .actions span.action-counter,
#changelist .actions span.clear,
#changelist .actions span.question {
font-size: 13px;
margin: 0 0.5em;
display: none;
}
#changelist .actions:last-child {
border-bottom: none;
}
#changelist .actions select {
vertical-align: top;
height: 24px;
background: none;
color: #000;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
padding: 0 0 0 4px;
margin: 0;
margin-left: 10px;
}
#changelist .actions select:focus {
border-color: #999;
}
#changelist .actions label {
display: inline-block;
vertical-align: middle;
font-size: 13px;
}
#changelist .actions .button {
font-size: 13px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
cursor: pointer;
height: 24px;
line-height: 1;
padding: 4px 8px;
margin: 0;
color: #333;
}
#changelist .actions .button:focus, #changelist .actions .button:hover {
border-color: #999;
}
/* DASHBOARD */
.dashboard .module table th {
width: 100%;
}
.dashboard .module table td {
white-space: nowrap;
}
.dashboard .module table td a {
display: block;
padding-right: .6em;
}
/* RECENT ACTIONS MODULE */
.module ul.actionlist {
margin-left: 0;
}
ul.actionlist li {
list-style-type: none;
overflow: hidden;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
}
@font-face {
font-family: 'Roboto';
src: url('../fonts/Roboto-Bold-webfont.woff');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('../fonts/Roboto-Regular-webfont.woff');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('../fonts/Roboto-Light-webfont.woff');
font-weight: 300;
font-style: normal;
}
@import url('widgets.css');
/* FORM ROWS */
.form-row {
overflow: hidden;
padding: 10px;
font-size: 13px;
border-bottom: 1px solid #eee;
}
.form-row img, .form-row input {
vertical-align: middle;
}
.form-row label input[type="checkbox"] {
margin-top: 0;
vertical-align: 0;
}
form .form-row p {
padding-left: 0;
}
.hidden {
display: none;
}
/* FORM LABELS */
label {
font-weight: normal;
color: #666;
font-size: 13px;
}
.required label, label.required {
font-weight: bold;
color: #333;
}
/* RADIO BUTTONS */
form ul.radiolist li {
list-style-type: none;
}
form ul.radiolist label {
float: none;
display: inline;
}
form ul.radiolist input[type="radio"] {
margin: -2px 4px 0 0;
padding: 0;
}
form ul.inline {
margin-left: 0;
padding: 0;
}
form ul.inline li {
float: left;
padding-right: 7px;
}
/* ALIGNED FIELDSETS */
.aligned label {
display: block;
padding: 4px 10px 0 0;
float: left;
width: 160px;
word-wrap: break-word;
line-height: 1;
}
.aligned label:not(.vCheckboxLabel):after {
content: '';
display: inline-block;
vertical-align: middle;
height: 26px;
}
.aligned label + p, .aligned label + div.help, .aligned label + div.readonly {
padding: 6px 0;
margin-top: 0;
margin-bottom: 0;
margin-left: 170px;
}
.aligned ul label {
display: inline;
float: none;
width: auto;
}
.aligned .form-row input {
margin-bottom: 0;
}
.colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField {
width: 350px;
}
form .aligned ul {
margin-left: 160px;
padding-left: 10px;
}
form .aligned ul.radiolist {
display: inline-block;
margin: 0;
padding: 0;
}
form .aligned p.help,
form .aligned div.help {
clear: left;
margin-top: 0;
margin-left: 160px;
padding-left: 10px;
}
form .aligned label + p.help,
form .aligned label + div.help {
margin-left: 0;
padding-left: 0;
}
form .aligned p.help:last-child,
form .aligned div.help:last-child {
margin-bottom: 0;
padding-bottom: 0;
}
form .aligned input + p.help,
form .aligned textarea + p.help,
form .aligned select + p.help,
form .aligned input + div.help,
form .aligned textarea + div.help,
form .aligned select + div.help {
margin-left: 160px;
padding-left: 10px;
}
form .aligned ul li {
list-style: none;
}
form .aligned table p {
margin-left: 0;
padding-left: 0;
}
.aligned .vCheckboxLabel {
float: none;
width: auto;
display: inline-block;
vertical-align: -3px;
padding: 0 0 5px 5px;
}
.aligned .vCheckboxLabel + p.help,
.aligned .vCheckboxLabel + div.help {
margin-top: -4px;
}
.colM .aligned .vLargeTextField, .colM .aligned .vXMLLargeTextField {
width: 610px;
}
.checkbox-row p.help,
.checkbox-row div.help {
margin-left: 0;
padding-left: 0;
}
fieldset .field-box {
float: left;
margin-right: 20px;
}
/* WIDE FIELDSETS */
.wide label {
width: 200px;
}
form .wide p,
form .wide input + p.help,
form .wide input + div.help {
margin-left: 200px;
}
form .wide p.help,
form .wide div.help {
padding-left: 38px;
}
form div.help ul {
padding-left: 0;
margin-left: 0;
}
.colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField {
width: 450px;
}
/* COLLAPSED FIELDSETS */
fieldset.collapsed * {
display: none;
}
fieldset.collapsed h2, fieldset.collapsed {
display: block;
}
fieldset.collapsed {
border: 1px solid #eee;
border-radius: 4px;
overflow: hidden;
}
fieldset.collapsed h2 {
background: #f8f8f8;
color: #666;
}
fieldset .collapse-toggle {
color: #fff;
}
fieldset.collapsed .collapse-toggle {
background: transparent;
display: inline;
color: #447e9b;
}
/* MONOSPACE TEXTAREAS */
fieldset.monospace textarea {
font-family: "Bitstream Vera Sans Mono", Monaco, "Courier New", Courier, monospace;
}
/* SUBMIT ROW */
.submit-row {
padding: 12px 14px;
margin: 0 0 20px;
background: #f8f8f8;
border: 1px solid #eee;
border-radius: 4px;
text-align: right;
overflow: hidden;
}
body.popup .submit-row {
overflow: auto;
}
.submit-row input {
height: 35px;
line-height: 15px;
margin: 0 0 0 5px;
}
.submit-row input.default {
margin: 0 0 0 8px;
text-transform: uppercase;
}
.submit-row p {
margin: 0.3em;
}
.submit-row p.deletelink-box {
float: left;
margin: 0;
}
.submit-row a.deletelink {
display: block;
background: #ba2121;
border-radius: 4px;
padding: 10px 15px;
height: 15px;
line-height: 15px;
color: #fff;
}
.submit-row a.deletelink:focus,
.submit-row a.deletelink:hover,
.submit-row a.deletelink:active {
background: #a41515;
}
/* CUSTOM FORM FIELDS */
.vSelectMultipleField {
vertical-align: top;
}
.vCheckboxField {
border: none;
}
.vDateField, .vTimeField {
margin-right: 2px;
margin-bottom: 4px;
}
.vDateField {
min-width: 6.85em;
}
.vTimeField {
min-width: 4.7em;
}
.vURLField {
width: 30em;
}
.vLargeTextField, .vXMLLargeTextField {
width: 48em;
}
.flatpages-flatpage #id_content {
height: 40.2em;
}
.module table .vPositiveSmallIntegerField {
width: 2.2em;
}
.vTextField {
width: 20em;
}
.vIntegerField {
width: 5em;
}
.vBigIntegerField {
width: 10em;
}
.vForeignKeyRawIdAdminField {
width: 5em;
}
/* INLINES */
.inline-group {
padding: 0;
margin: 0 0 30px;
}
.inline-group thead th {
padding: 8px 10px;
}
.inline-group .aligned label {
width: 160px;
}
.inline-related {
position: relative;
}
.inline-related h3 {
margin: 0;
color: #666;
padding: 5px;
font-size: 13px;
background: #f8f8f8;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
}
.inline-related h3 span.delete {
float: right;
}
.inline-related h3 span.delete label {
margin-left: 2px;
font-size: 11px;
}
.inline-related fieldset {
margin: 0;
background: #fff;
border: none;
width: 100%;
}
.inline-related fieldset.module h3 {
margin: 0;
padding: 2px 5px 3px 5px;
font-size: 11px;
text-align: left;
font-weight: bold;
background: #bcd;
color: #fff;
}
.inline-group .tabular fieldset.module {
border: none;
}
.inline-related.tabular fieldset.module table {
width: 100%;
}
.last-related fieldset {
border: none;
}
.inline-group .tabular tr.has_original td {
padding-top: 2em;
}
.inline-group .tabular tr td.original {
padding: 2px 0 0 0;
width: 0;
_position: relative;
}
.inline-group .tabular th.original {
width: 0px;
padding: 0;
}
.inline-group .tabular td.original p {
position: absolute;
left: 0;
height: 1.1em;
padding: 2px 9px;
overflow: hidden;
font-size: 9px;
font-weight: bold;
color: #666;
_width: 700px;
}
.inline-group ul.tools {
padding: 0;
margin: 0;
list-style: none;
}
.inline-group ul.tools li {
display: inline;
padding: 0 5px;
}
.inline-group div.add-row,
.inline-group .tabular tr.add-row td {
color: #666;
background: #f8f8f8;
padding: 8px 10px;
border-bottom: 1px solid #eee;
}
.inline-group .tabular tr.add-row td {
padding: 8px 10px;
border-bottom: 1px solid #eee;
}
.inline-group ul.tools a.add,
.inline-group div.add-row a,
.inline-group .tabular tr.add-row td a {
background: url(../img/icon-addlink.svg) 0 1px no-repeat;
padding-left: 16px;
font-size: 12px;
}
.empty-form {
display: none;
}
/* RELATED FIELD ADD ONE / LOOKUP */
.add-another, .related-lookup {
margin-left: 5px;
display: inline-block;
vertical-align: middle;
background-repeat: no-repeat;
background-size: 14px;
}
.add-another {
width: 16px;
height: 16px;
background-image: url(../img/icon-addlink.svg);
}
.related-lookup {
width: 16px;
height: 16px;
background-image: url(../img/search.svg);
}
form .related-widget-wrapper ul {
display: inline-block;
margin-left: 0;
padding-left: 0;
}
.clearable-file-input input {
margin-top: 0;
}
/* LOGIN FORM */
body.login {
background: #f8f8f8;
}
.login #header {
height: auto;
padding: 5px 16px;
}
.login #header h1 {
font-size: 18px;
}
.login #header h1 a {
color: #fff;
}
.login #content {
padding: 20px 20px 0;
}
.login #container {
background: #fff;
border: 1px solid #eaeaea;
border-radius: 4px;
overflow: hidden;
width: 28em;
min-width: 300px;
margin: 100px auto;
}
.login #content-main {
width: 100%;
}
.login .form-row {
padding: 4px 0;
float: left;
width: 100%;
border-bottom: none;
}
.login .form-row label {
padding-right: 0.5em;
line-height: 2em;
font-size: 1em;
clear: both;
color: #333;
}
.login .form-row #id_username, .login .form-row #id_password {
clear: both;
padding: 8px;
width: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.login span.help {
font-size: 10px;
display: block;
}
.login .submit-row {
clear: both;
padding: 1em 0 0 9.4em;
margin: 0;
border: none;
background: none;
text-align: left;
}
.login .password-reset-link {
text-align: center;
}
/* TABLETS */
@media (max-width: 1024px) {
[dir="rtl"] .colMS {
margin-right: 0;
}
[dir="rtl"] #user-tools {
text-align: right;
}
[dir="rtl"] #changelist .actions label {
padding-left: 10px;
padding-right: 0;
}
[dir="rtl"] #changelist .actions select {
margin-left: 0;
margin-right: 15px;
}
[dir="rtl"] .change-list .filtered .results,
[dir="rtl"] .change-list .filtered .paginator,
[dir="rtl"] .filtered #toolbar,
[dir="rtl"] .filtered div.xfull,
[dir="rtl"] .filtered .actions {
margin-right: 0;
margin-left: 230px;
}
[dir="rtl"] .inline-group ul.tools a.add,
[dir="rtl"] .inline-group div.add-row a,
[dir="rtl"] .inline-group .tabular tr.add-row td a {
padding: 8px 26px 8px 10px;
background-position: calc(100% - 8px) 9px;
}
[dir="rtl"] .related-widget-wrapper-link + .selector {
margin-right: 0;
margin-left: 15px;
}
[dir="rtl"] .selector .selector-filter label {
margin-right: 0;
margin-left: 8px;
}
[dir="rtl"] .object-tools li {
float: right;
}
[dir="rtl"] .object-tools li + li {
margin-left: 0;
margin-right: 15px;
}
[dir="rtl"] .dashboard .module table td a {
padding-left: 0;
padding-right: 16px;
}
}
/* MOBILE */
@media (max-width: 767px) {
[dir="rtl"] .change-list .filtered .results,
[dir="rtl"] .change-list .filtered .paginator,
[dir="rtl"] .filtered #toolbar,
[dir="rtl"] .filtered div.xfull,
[dir="rtl"] .filtered .actions {
margin-left: 0;
}
[dir="rtl"] .aligned .add-another,
[dir="rtl"] .aligned .related-lookup,
[dir="rtl"] .aligned .datetimeshortcuts {
margin-left: 0;
margin-right: 15px;
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
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