1678 lines
67 KiB
Python
1678 lines
67 KiB
Python
# -*- coding: utf8 -*-
|
|
from pyramid.response import Response
|
|
from pyramid.renderers import render, get_renderer
|
|
from pyramid.view import (
|
|
view_config,
|
|
forbidden_view_config,
|
|
)
|
|
from pyramid.httpexceptions import (
|
|
HTTPFound,
|
|
HTTPNotFound,
|
|
HTTPForbidden,
|
|
)
|
|
from pyramid_mailer import get_mailer
|
|
from pyramid_mailer.message import Message, Attachment
|
|
from datetime import *
|
|
|
|
|
|
from PIL import Image
|
|
import os
|
|
import io
|
|
import shutil
|
|
import pdfkit
|
|
import imaplib
|
|
import base64
|
|
import email
|
|
|
|
from pdfminer3.layout import LAParams, LTTextBox, LTTextLine
|
|
from pdfminer3.pdfpage import PDFPage
|
|
from pdfminer3.pdfparser import PDFParser
|
|
from pdfminer3.pdfdocument import PDFDocument
|
|
from pdfminer3.pdfdevice import PDFDevice
|
|
from pdfminer3.pdfinterp import PDFResourceManager, PDFPageInterpreter
|
|
from pdfminer3.converter import PDFPageAggregator
|
|
|
|
from ..views.default import *
|
|
from ..models.default import *
|
|
from ..models.dossier import *
|
|
|
|
@view_config(route_name='dossier_lookup', renderer='../templates/dossier/dossier_lookup.pt', permission='view')
|
|
@view_config(route_name='dossier_select', renderer='../templates/dossier/dossier_lookup.pt', permission='view')
|
|
def dossier_lookup(request):
|
|
|
|
if 'dossier_select' in request.current_route_path() :
|
|
# récupérer les paramètres de l'appel de la view
|
|
datePlan = request.matchdict['date']
|
|
# sélectionner dossier -> goto planning
|
|
goto_url = '/dossier_selected/agenda/%s/' % datePlan
|
|
url = request.route_url('dossier_select', date=datePlan)
|
|
else:
|
|
# recherche dossier -> goto fiche dossier
|
|
goto_url = '/dossier_selected/dossier_view/%s/' % date.today().strftime('%Y-%m-%d')
|
|
url = request.route_url('dossier_lookup')
|
|
|
|
logged_in = request.authenticated_userid.upper()
|
|
message = ''
|
|
member = get_member_by_id(request, logged_in)
|
|
societe_defaut = member.societe
|
|
societe = societe_defaut
|
|
access_defaut = member.access
|
|
liste=[]
|
|
name = ''
|
|
cb_tous = "non"
|
|
|
|
if 'form.submitted' in request.params:
|
|
name = request.params['name']
|
|
societe = request.params['societe']
|
|
|
|
# lire les chantiers
|
|
chantiers = get_chantiers_byName(request, societe, name)
|
|
if len(chantiers) == 0:
|
|
message = "Chantier non trouvé : %s" % name
|
|
# construire la liste
|
|
for item in chantiers:
|
|
d = ('%s-%s' % (societe, item.numero),item.date.strftime('%d-%m-%Y'), item.nomcli, item.chantier, to_euro(item.montant),
|
|
item.nosin, item.libelle, item.usermaj)
|
|
liste.append(d)
|
|
|
|
if len(name) == 0 :
|
|
order_option = 'desc'
|
|
else:
|
|
order_option = 'asc'
|
|
|
|
return {
|
|
'page_title': "Rechercher un chantier",
|
|
'url': url,
|
|
'goto_url': goto_url,
|
|
'message': message,
|
|
'dt_data': json.dumps(liste),
|
|
'societe': societe,
|
|
'name': name,
|
|
'order_option': order_option,
|
|
}
|
|
|
|
@view_config(route_name='dossier_view', renderer='../templates/dossier/dossier_view.pt', permission='view')
|
|
def dossier_view(request):
|
|
nodossier = request.matchdict['nodossier']
|
|
societe = nodossier[0:2]
|
|
logged_in = request.authenticated_userid.upper()
|
|
url = request.route_url('dossier_view', nodossier=nodossier)
|
|
|
|
# lire son niveau d'accès
|
|
member = get_member_by_id(request, logged_in)
|
|
access = member.access
|
|
|
|
url = request.route_url("dossier_view", nodossier=nodossier)
|
|
|
|
dossier = get_dossier_by_no(request, nodossier)
|
|
if dossier is None:
|
|
request.session.flash(u"Le dossier no %s est introuvable" % (nodossier), 'danger')
|
|
return HTTPFound(location=request.route_url("dossier_lookup"))
|
|
# lire tous le suivi du dossier
|
|
details = get_dossier_rdv_by_no(request, nodossier, '0')
|
|
# lire toutes les devis et factures du chantier
|
|
documents = get_documents_byChantier(request, nodossier)
|
|
# lire toutes les dossiers similaires
|
|
similaires = get_similaires_byChantier(request, dossier.societe, dossier.C_NOM, dossier.C_ADR, dossier.C_CP, dossier.C_VILLE)
|
|
# lire tous les documents attachés
|
|
docs_attaches = get_docs_attaches(request, nodossier, 0, 'CLT', '')
|
|
# lire tous les documents techniques
|
|
docs_techniques = get_docs_attaches(request, nodossier, 0, 'FRN', '')
|
|
if nodossier.startswith('PL'):
|
|
# lire rapport de rdf
|
|
rapports = get_rapport_by_no(request, nodossier, '')
|
|
else:
|
|
rapports = []
|
|
# select background color according to society
|
|
bg_color = "bg-%s" % societe
|
|
|
|
status = get_status_by_id(request, '')
|
|
motifs = get_motifs(request)
|
|
# lire les notes du dossier
|
|
dem_notes = get_dem_notes(request, nodossier, '0', 'NOTE')
|
|
# lire les croquis du dossier
|
|
dem_croquis = get_dem_notes(request, nodossier, '0', 'CROQUIS')
|
|
|
|
if 'form.close' in request.params:
|
|
status = request.params["status"]
|
|
motif = request.params["motif"]
|
|
|
|
code, libelle = status.split(' | ')
|
|
code = int(code)
|
|
|
|
if code >= 10:
|
|
comment = 'Le dossier est ' + libelle + ' : ' + motif
|
|
insert_suivi(request, nodossier, comment)
|
|
|
|
update_dossier_cloture(request, nodossier, code, logged_in)
|
|
request.session.flash(u"Le dossier a été cloturé avec succès.", 'success')
|
|
return HTTPFound(url)
|
|
|
|
return {
|
|
'page_title': "Dossier : %s" % (nodossier),
|
|
'url': url,
|
|
'logged_in': logged_in,
|
|
'nodossier': nodossier,
|
|
'dossier': dossier,
|
|
'details': details,
|
|
'rapports': rapports,
|
|
'documents': documents,
|
|
'similaires': similaires,
|
|
'docs_attaches': docs_attaches,
|
|
'docs_techniques': docs_techniques,
|
|
'docs_url': request.static_url(request.registry.settings['mondumas.devfac_url']),
|
|
'bg_color': bg_color,
|
|
'access': access,
|
|
'status': status,
|
|
'motifs': motifs,
|
|
'motif': '',
|
|
'dem_notes': dem_notes,
|
|
'dem_croquis': dem_croquis,
|
|
}
|
|
|
|
@view_config(route_name='dossier_selected', permission='view')
|
|
def dossier_selected(request):
|
|
|
|
# récupérer les paramètres de l'appel de la view
|
|
goto = request.matchdict['goto']
|
|
datePlan = request.matchdict['date']
|
|
nodossier = request.matchdict['nodossier']
|
|
|
|
# fiche dossier
|
|
dossier = get_dossier_by_no(request, nodossier)
|
|
if goto == 'dossier_view':
|
|
return HTTPFound(location=request.route_url('dossier_view', nodossier=nodossier))
|
|
else:
|
|
return HTTPFound(location=request.route_url('agenda', date=datePlan))
|
|
|
|
@view_config(route_name='suivi_edit', renderer='../templates/dossier/suivi_edit.pt', permission='view')
|
|
def suivi_edit(request):
|
|
|
|
logged_in = request.authenticated_userid.upper()
|
|
nodossier = request.matchdict['nodossier']
|
|
nolig = request.matchdict['nolig']
|
|
url = request.route_url("suivi_edit", nodossier=nodossier, nolig=nolig)
|
|
|
|
message = ''
|
|
if nolig == '0':
|
|
# nouveau
|
|
suivi = {}
|
|
suivi['attached_text'] = ''
|
|
suivi['COMMENT'] = ''
|
|
suivi['USERMAJ'] = logged_in
|
|
suivi['DATEMAJ'] = datetime.now()
|
|
page_title= 'Nouveau suivi'
|
|
else:
|
|
# lire le suivi
|
|
suivi = get_dossier_rdv_by_no(request, nodossier, nolig)
|
|
if not suivi:
|
|
request.session.flash(u"Suivi non trouvé : %s" % nodossier, 'warning')
|
|
return HTTPFound(location=request.route_url('dossier_view', nodossier=nodossier))
|
|
|
|
page_title= "Modification du suivi %s" % (nolig)
|
|
|
|
if 'form.submitted' in request.params:
|
|
new_values = {}
|
|
for param, db_value in suivi.items():
|
|
if param in request.params and request.params[param] != db_value:
|
|
new_values[param] = request.params[param]
|
|
|
|
if new_values:
|
|
new_values['USERMAJ'] = logged_in
|
|
new_values['DATE'] = date.today()
|
|
|
|
update_suivi(request, nodossier, nolig, new_values)
|
|
request.session.flash(u"Le suivi a été mis à jour avec succès.", 'success')
|
|
|
|
return HTTPFound(location=request.route_url('dossier_view', nodossier=nodossier))
|
|
|
|
if 'form.deleted' in request.params:
|
|
delete_rdv(request, nodossier, nolig)
|
|
request.session.flash(u"Le suivi a été supprimé avec succès.", 'success')
|
|
return HTTPFound(location=request.route_url('dossier_view', nodossier=nodossier))
|
|
|
|
return {
|
|
'page_title': page_title,
|
|
'logged_in': logged_in,
|
|
'url': url,
|
|
'nodossier': nodossier,
|
|
'nolig': nolig,
|
|
'suivi': suivi,
|
|
'message': message,
|
|
}
|
|
|
|
@view_config(route_name='dossier_edit', renderer='../templates/dossier/dossier_edit.pt', permission='view')
|
|
def dossier_edit(request):
|
|
logged_in = request.authenticated_userid
|
|
nodossier = request.matchdict['nodossier']
|
|
url = request.route_url('dossier_edit', nodossier=nodossier)
|
|
message = ''
|
|
|
|
dossier = get_dossier_by_no(request, nodossier)
|
|
if not dossier:
|
|
request.session.flash(u"Le dossier no %s est introuvable" % (nodossier), 'danger')
|
|
return HTTPFound(location=request.route_url('dossier_lookup'))
|
|
|
|
code_postal = '%s - %s' % (dossier.C_CP, dossier.C_VILLE)
|
|
# lire table expert
|
|
experts = get_experts(request, dossier.CABINET, 0)
|
|
|
|
if 'form.submitted' in request.params:
|
|
new_values = {}
|
|
for param, db_value in dossier.items():
|
|
if param in request.params and request.params[param] != db_value:
|
|
new_values[param] = request.params[param]
|
|
|
|
# controle saisie code postal
|
|
code_postal = request.params['code_postal']
|
|
cp = code_postal.split(' - ')
|
|
if len(cp) < 2:
|
|
message = 'Code postal invalide. Veuillez sélectionner un parmi la liste.'
|
|
else:
|
|
new_values['c_cp'] = cp[0]
|
|
new_values['c_ville'] = cp[1]
|
|
new_values['USERMAJ'] = logged_in.upper()
|
|
|
|
# ascenseur coché ?
|
|
if 'ascenseur' in request.params:
|
|
new_values['c_ascenseur'] = 1
|
|
else:
|
|
new_values['c_ascenseur'] = 0
|
|
|
|
update_dossier(request, nodossier, new_values)
|
|
request.session.flash(u"Le dossier a été mis à jour avec succès.", 'success')
|
|
return HTTPFound(location=request.route_url('dossier_view', nodossier=nodossier))
|
|
|
|
return {
|
|
'page_title': "Modifier le dossier : %s" % nodossier,
|
|
'url': url,
|
|
'message': message,
|
|
'dossier': dossier,
|
|
'nodossier': nodossier,
|
|
'experts': experts,
|
|
'code_postal': code_postal,
|
|
}
|
|
|
|
@view_config(route_name='upload_doc', renderer='../templates/dossier/upload_doc.pt', permission='view')
|
|
def upload_doc(request):
|
|
logged_in = request.authenticated_userid.upper()
|
|
nodossier = request.matchdict['nodossier']
|
|
origine = request.matchdict['origine']
|
|
if origine == 'CLT':
|
|
page_title = "Télécharger un document PDF"
|
|
else:
|
|
page_title = "Télécharger un document technique"
|
|
|
|
societe = nodossier[0:2]
|
|
nochantier = nodossier[3:]
|
|
|
|
url = request.route_url("upload_doc", nodossier=nodossier, origine=origine)
|
|
message = ""
|
|
|
|
dossier = get_dossier_by_no(request, nodossier)
|
|
if dossier is None:
|
|
request.session.flash(u"Le dossier no %s est introuvable" % (nodossier), 'danger')
|
|
return HTTPFound(location=request.route_url("dossier_lookup"))
|
|
|
|
# lire tous les documents attachés
|
|
docs_attaches = get_docs_attaches(request, nodossier, 0, origine, '')
|
|
|
|
if 'form.submitted' in request.params:
|
|
# récupère le(s) fichier(s) download dans le dossier /tmp
|
|
fileslist = request.POST.getall('files')
|
|
for f in fileslist:
|
|
# récupère le fichier download dans le dossier /tmp
|
|
input_file = f.file
|
|
input_name = f.filename
|
|
ext_allowed = ['pdf',]
|
|
temp_file = downloadFile2Temp(input_file, input_name, ext_allowed)
|
|
if temp_file[:8] == 'ERREUR: ':
|
|
request.session.flash(temp_file, 'danger')
|
|
return HTTPFound(location=url)
|
|
|
|
# fabriquer le nom du document
|
|
filename = '%s-DD%s-%s' % (societe, nochantier, input_name)
|
|
tempFile2Dossier(request, societe, nochantier, '0', origine, temp_file, filename, logged_in)
|
|
|
|
request.session.flash('%s : Ce fichier est téléchargé avec succès.' % input_name, 'success')
|
|
|
|
# lire tous les documents attachés
|
|
docs_attaches = get_docs_attaches(request, nodossier, 0, origine, '')
|
|
|
|
return {
|
|
'page_title': page_title,
|
|
'url': url,
|
|
'nodossier': nodossier,
|
|
'dossier': dossier,
|
|
'docs_attaches': docs_attaches,
|
|
'docs_url': request.static_url(request.registry.settings['mondumas.devfac_url']),
|
|
}
|
|
|
|
@view_config(route_name='upload_img', renderer='../templates/dossier/upload_img.pt', permission='view')
|
|
def upload_img(request):
|
|
"""
|
|
Paramètres d'appel :
|
|
- gestion images d'un dossier :
|
|
norapport = numero du dossier
|
|
origine = 'CLT' si images du dossier
|
|
'FRN' si images techniques
|
|
- gestion images d'un rapport RDF :
|
|
norapport = numéro du rapport de RDF
|
|
origine = '1' ou '2', numéro du de la section du rapport
|
|
|
|
"""
|
|
origine = request.matchdict['origine']
|
|
# Gérer les photos d'un dossier ?
|
|
if len(origine) > 1:
|
|
norapport = '0'
|
|
# oui, lire le dossier
|
|
nodossier = request.matchdict['norapport']
|
|
rapport = get_dossier_by_no(request, nodossier)
|
|
if rapport is None:
|
|
request.session.flash(u"Le dossier no %s est introuvable" % (norapport), 'danger')
|
|
return HTTPFound(location=request.route_url("rdf_list"))
|
|
societe = rapport.societe
|
|
nochantier = rapport.NO_ID
|
|
url = request.route_url("upload_img", norapport=nodossier, origine=origine)
|
|
url_retour = request.route_url('dossier_view', nodossier=nodossier)
|
|
titre = "Gérer les photos du dossier %s" % (nodossier)
|
|
else:
|
|
norapport = request.matchdict['norapport']
|
|
rapport = get_rapport_by_no_id(request, norapport)
|
|
if rapport is None:
|
|
request.session.flash(u"Le rapport no %s est introuvable" % (norapport), 'danger')
|
|
return HTTPFound(location=request.route_url("rdf_list"))
|
|
societe = 'PL'
|
|
nochantier = rapport.nochantier
|
|
nodossier = '%s-%s' % (societe, nochantier)
|
|
url = request.route_url("upload_img", norapport=norapport, origine=origine)
|
|
url_retour = request.route_url('rdf_view', no_id=norapport)
|
|
titre = "Gérer les photos du rapport %s-%s/%s" % (societe, norapport, origine)
|
|
|
|
logged_in = request.authenticated_userid.upper()
|
|
message = ""
|
|
|
|
|
|
if 'form.submitted' in request.params:
|
|
# récupère le(s) fichier(s) download dans le dossier /tmp
|
|
fileslist = request.POST.getall('files')
|
|
for f in fileslist:
|
|
input_file = f.file
|
|
input_name = f.filename
|
|
ext_allowed = ['jpeg','jpg','png']
|
|
temp_file = downloadFile2Temp(input_file, input_name, ext_allowed)
|
|
if temp_file[:8] == 'ERREUR: ':
|
|
request.session.flash(temp_file, 'danger')
|
|
return HTTPFound(location=url)
|
|
|
|
# fabriquer le nom du rapport
|
|
filename = '%s-DD%s-%s-%s' % (societe, nochantier, norapport, input_name)
|
|
tempFile2Dossier(request, societe, nochantier, norapport, origine, temp_file, filename, logged_in)
|
|
|
|
request.session.flash('%s : Ce fichier est téléchargé avec succès.' % input_name, 'success')
|
|
|
|
# lire tous les photos attachées
|
|
photos = get_photos(request, nodossier, int(norapport), origine)
|
|
|
|
return {
|
|
'page_title': titre,
|
|
'url': url,
|
|
'url_retour': url_retour,
|
|
'nodossier': nodossier,
|
|
'nochantier': nochantier,
|
|
'norapport': norapport,
|
|
'origine': origine,
|
|
'photos': photos,
|
|
'total_size' : photos_size(photos),
|
|
'docs_url': request.static_url(request.registry.settings['mondumas.devfac_url']),
|
|
}
|
|
|
|
@view_config(route_name='rdf_edit', renderer='../templates/dossier/rdf_edit.pt', permission='view')
|
|
def rdf_edit(request):
|
|
logged_in = request.authenticated_userid.upper()
|
|
nodossier = request.matchdict['nodossier']
|
|
date_inter = request.matchdict['date_inter']
|
|
|
|
if date_inter == 'new':
|
|
date_inter = date.today().strftime('%Y-%m-%d')
|
|
rapport = get_rapport_by_no(request, nodossier, date_inter)
|
|
if not rapport:
|
|
# creer un nouveau rapport
|
|
insert_rapport(request, nodossier, logged_in)
|
|
# lire le rapport
|
|
rapport = get_rapport_by_no(request, nodossier, date_inter)
|
|
url = request.route_url('rdf_edit', nodossier=nodossier, date_inter=date_inter)
|
|
|
|
caracteristiques = ["Maison individuelle", "Immeuble collectif", "Copropriété", "Commerce", "Bureaux"]
|
|
equipements = get_rdf_causes(request, 'C01')
|
|
reseaux_int = get_rdf_causes(request, 'C02')
|
|
reseaux_ext = get_rdf_causes(request, 'C03')
|
|
reseaux_local = get_rdf_causes(request, 'C04')
|
|
elements_clos = get_rdf_causes(request, 'C05')
|
|
elements_couvert = get_rdf_causes(request, 'C06')
|
|
voisins = get_rdf_causes(request, 'C07')
|
|
tierce_personnes = get_rdf_causes(request, 'C08')
|
|
# liste des users avec agenda
|
|
users = get_users_agenda(request, '')
|
|
# lire code accès du user
|
|
access = get_userAccess(request, logged_in)
|
|
|
|
if 'form.submitted' in request.params:
|
|
if 'signature_svg' in request.params:
|
|
if len(request.params['signature_svg']) > 7000:
|
|
request.session.flash(u"La signature est trop grande !", 'danger')
|
|
return HTTPFound(location=url)
|
|
|
|
new_values = {}
|
|
for param, db_value in rapport.items():
|
|
if param in request.params and request.params[param] != db_value:
|
|
new_values[param] = request.params[param]
|
|
|
|
if 'date_inter' in request.params:
|
|
ddate = datetime.strptime(request.params['date_inter'], '%d-%m-%Y')
|
|
new_values['date_inter'] = ddate.strftime("%Y-%m-%d")
|
|
# case à cocher ?
|
|
if 'sonde_capa' in request.params:
|
|
new_values['sonde_capa'] = 1
|
|
else:
|
|
new_values['sonde_capa'] = 0
|
|
if 'sonde_cond' in request.params:
|
|
new_values['sonde_cond'] = 1
|
|
else:
|
|
new_values['sonde_cond'] = 0
|
|
if 'test_mano' in request.params:
|
|
new_values['test_mano'] = 1
|
|
else:
|
|
new_values['test_mano'] = 0
|
|
if 'test_gaz' in request.params:
|
|
new_values['test_gaz'] = 1
|
|
else:
|
|
new_values['test_gaz'] = 0
|
|
if 'visu_camera' in request.params:
|
|
new_values['visu_camera'] = 1
|
|
else:
|
|
new_values['visu_camera'] = 0
|
|
if 'visu_endoscope' in request.params:
|
|
new_values['visu_endoscope'] = 1
|
|
else:
|
|
new_values['visu_endoscope'] = 0
|
|
if 'visu_tele' in request.params:
|
|
new_values['visu_tele'] = 1
|
|
else:
|
|
new_values['visu_tele'] = 0
|
|
if 'rech_magnetique' in request.params:
|
|
new_values['rech_magnetique'] = 1
|
|
else:
|
|
new_values['rech_magnetique'] = 0
|
|
if 'rech_accoustique' in request.params:
|
|
new_values['rech_accoustique'] = 1
|
|
else:
|
|
new_values['rech_accoustique'] = 0
|
|
if 'test_accoustique' in request.params:
|
|
new_values['test_accoustique'] = 1
|
|
else:
|
|
new_values['test_accoustique'] = 0
|
|
if 'test_mise_en_eau' in request.params:
|
|
new_values['test_mise_en_eau'] = 1
|
|
else:
|
|
new_values['test_mise_en_eau'] = 0
|
|
if 'test_fumigenes' in request.params:
|
|
new_values['test_fumigenes'] = 1
|
|
else:
|
|
new_values['test_fumigenes'] = 0
|
|
|
|
if new_values:
|
|
update_rapport(request, nodossier, date_inter, new_values)
|
|
request.session.flash(u"Le dossier a été mis à jour avec succès.", 'success')
|
|
return HTTPFound(location=request.route_url('dossier_view', nodossier=nodossier))
|
|
|
|
return {
|
|
'page_title': "Rapport de RDF : %s du %s" % (nodossier, rapport.date_inter.strftime('%d-%m-%Y')),
|
|
'url': url,
|
|
'logged_in': logged_in,
|
|
'rapport': rapport,
|
|
'nodossier': nodossier,
|
|
'caracteristiques': caracteristiques,
|
|
'equipements': equipements,
|
|
'reseaux_int': reseaux_int,
|
|
'reseaux_ext': reseaux_ext,
|
|
'reseaux_local': reseaux_local,
|
|
'elements_clos': elements_clos,
|
|
'elements_couvert': elements_couvert,
|
|
'voisins':voisins,
|
|
'tierce_personnes': tierce_personnes,
|
|
'users': users,
|
|
'access': access,
|
|
}
|
|
|
|
@view_config(route_name='rdf_list', renderer='../templates/dossier/rdf_list.pt', permission='view')
|
|
def rdf_list(request):
|
|
# lire les rapports de RDF
|
|
items = get_rapport_rdf(request)
|
|
|
|
# construire la liste
|
|
liste=[]
|
|
for item in items:
|
|
if item.date_relu :
|
|
date_relu = item.date_relu.strftime('%d-%m-%Y')
|
|
else:
|
|
date_relu = ''
|
|
if item.date_rapport :
|
|
date_rapport = item.date_rapport.strftime('%d-%m-%Y')
|
|
else:
|
|
date_rapport = ''
|
|
|
|
d = (item.no_id, item.date_inter.strftime('%d-%m-%Y'), date_relu, date_rapport, '%s - %s %s' % (item.nochantier, item.C_QUALITE, item.C_NOM), item.NOMCLI, item.NOSIN,
|
|
item.auteur_code)
|
|
liste.append(d)
|
|
|
|
return {
|
|
'page_title': 'Liste des rapports de RDF',
|
|
'dt_data': json.dumps(liste),
|
|
}
|
|
|
|
@view_config(route_name='rdf_client', renderer='../templates/dossier/rdf_client.pt', permission='view')
|
|
def rdf_client(request):
|
|
logged_in = request.authenticated_userid.upper()
|
|
norapport = request.matchdict['no_id']
|
|
message = ''
|
|
|
|
# lire code accès du user
|
|
access = get_userAccess(request, logged_in)
|
|
if access == 0:
|
|
request.session.flash("Vous n'avez pas les droits nécessaires pour changer de client.", 'danger')
|
|
return HTTPFound(location=request.route_url('rdf_view', no_id=norapport))
|
|
|
|
url = request.route_url('rdf_client', no_id=norapport)
|
|
|
|
# lire le rapport
|
|
rapport = get_rapport_by_no_id(request, norapport)
|
|
nodossier = 'PL-' + str(rapport.nochantier)
|
|
|
|
if 'form.submitted' in request.params:
|
|
nomClient = request.params['name'].split(' | ')
|
|
if len(nomClient) == 2:
|
|
update_rapport_client(request, norapport, nomClient[0], nomClient[1])
|
|
request.session.flash("Le client du rapporta été modifié avec succès.", 'success')
|
|
return HTTPFound(location=request.route_url('dossier_view', nodossier=nodossier))
|
|
else:
|
|
message = "Veuillez saisir un nom de client ou Annuler"
|
|
|
|
return {
|
|
'page_title': "Changer le client du RDF n° %s" % norapport,
|
|
'url': url,
|
|
'message': message,
|
|
'access': access,
|
|
'norapport': norapport,
|
|
'rapport': rapport,
|
|
}
|
|
|
|
@view_config(route_name='rdf_nochantier', renderer='../templates/dossier/rdf_nochantier.pt', permission='view')
|
|
def rdf_nochantier(request):
|
|
logged_in = request.authenticated_userid.upper()
|
|
norapport = request.matchdict['no_id']
|
|
message = ''
|
|
|
|
# lire code accès du user
|
|
access = get_userAccess(request, logged_in)
|
|
if access == 0:
|
|
request.session.flash("Vous n'avez pas les droits nécessaires pour changer de client.", 'danger')
|
|
return HTTPFound(location=request.route_url('rdf_view', no_id=norapport))
|
|
|
|
url = request.route_url('rdf_nochantier', no_id=norapport)
|
|
|
|
# lire le rapport
|
|
rapport = get_rapport_by_no_id(request, norapport)
|
|
nodossier = 'PL-' + str(rapport.nochantier)
|
|
|
|
if 'form.submitted' in request.params:
|
|
new_nochantier = request.params['new_nochantier']
|
|
if len(new_nochantier) == 6:
|
|
retour = update_rapport_nochantier(request, norapport, new_nochantier)
|
|
if retour == "OK":
|
|
request.session.flash("Le numéro du dossier été modifié avec succès.", 'success')
|
|
return HTTPFound(location=request.route_url('rdf_view', no_id=norapport))
|
|
else:
|
|
message = "Le numéro du dossier n'existe pas : %s" % new_nochantier
|
|
else:
|
|
message = "Un numero de dossier doit avoir 6 chiffres"
|
|
|
|
return {
|
|
'page_title': "Changer le no du dossier du rapport n° %s" % norapport,
|
|
'url': url,
|
|
'message': message,
|
|
'access': access,
|
|
'norapport': norapport,
|
|
'rapport': rapport,
|
|
}
|
|
|
|
@view_config(route_name='rdf_view', renderer='../templates/dossier/rdf_view.pt', permission='view')
|
|
def rdf_view(request):
|
|
logged_in = request.authenticated_userid.upper()
|
|
# lire code accès du user
|
|
access = get_userAccess(request, logged_in)
|
|
norapport = request.matchdict['no_id']
|
|
url = request.route_url('rdf_view', no_id=norapport)
|
|
|
|
# lire le rapport
|
|
rapport = get_rapport_by_no_id(request, norapport)
|
|
nodossier = 'PL-' + str(rapport.nochantier)
|
|
if rapport.date_relu:
|
|
date_relu = rapport.date_relu.strftime('%d-%m-%Y')
|
|
else:
|
|
date_relu = ''
|
|
if rapport.date_rapport:
|
|
date_rapport = rapport.date_rapport.strftime('%d-%m-%Y')
|
|
else:
|
|
date_rapport = ''
|
|
if rapport.date_facture:
|
|
date_facture = rapport.date_facture.strftime('%d-%m-%Y')
|
|
else:
|
|
date_facture = ''
|
|
|
|
# lire tous les photos attachées
|
|
photos1 = get_photos(request, nodossier, norapport, '1')
|
|
photos2 = get_photos(request, nodossier, norapport, '2')
|
|
|
|
if 'form.generate' in request.params:
|
|
footer_url = request.static_url('mondumas:static/img/footer_PL.html')
|
|
# import pdb;pdb.set_trace()
|
|
options = {
|
|
'page-size': 'A4',
|
|
'margin-top': '1cm',
|
|
'margin-right': '1cm',
|
|
'margin-bottom': '2cm',
|
|
'margin-left': '1.5cm',
|
|
'footer-font-size':'7',
|
|
'footer-right': '[page] / [topage]',
|
|
}
|
|
|
|
# créer le répertoire du rapport s'il n'existe pas encore
|
|
path = '%s/%s/%s/%s' % (request.registry.settings['mondumas.devfac_dir'], 'PL', rapport.nochantier, norapport)
|
|
os.makedirs(path, exist_ok=True)
|
|
# générer le rapport en PDF
|
|
filename = "PL-DD%s-rapport_RDF_no_%s.pdf" % (str(rapport.nochantier), norapport)
|
|
dest = "mondumas/static/DEVFAC/DOCS_ATTACHES/PL/%s/%s" % (str(rapport.nochantier), filename)
|
|
|
|
# supprimer le fichier s'il existe déjà
|
|
if os.path.exists(dest):
|
|
os.remove(dest)
|
|
|
|
# developpement ou production
|
|
if request.registry.settings["mondumas.admin_email"].find('@entreprise-dumas.com') > 0:
|
|
origin = 'https://gestion.entreprise-dumas.com/rdf_rapport/%s' % norapport
|
|
pdfkit.from_url(origin, dest, options=options)
|
|
else:
|
|
origin = request.route_url('rdf_rapport', no_id=norapport)
|
|
config = pdfkit.configuration(wkhtmltopdf="C:\\Program Files\\wkhtmltopdf\\bin\\wkhtmltopdf.exe")
|
|
pdfkit.from_url(origin, dest, configuration=config, options=options)
|
|
|
|
insert_dossier_attaches(request, nodossier, 0, 'CLT', filename, '160 Ko', logged_in)
|
|
# si generation pdf, maj de la date du rapport
|
|
new_values = {}
|
|
new_values['date_rapport'] = date.today()
|
|
update_rapport(request, nodossier, rapport.date_inter.strftime('%Y-%m-%d'), new_values)
|
|
|
|
request.session.flash(u"Le rapport a été généré avec succès.", 'success')
|
|
return HTTPFound(location=request.route_url('dossier_view', nodossier=nodossier))
|
|
|
|
if 'form.validate' in request.params:
|
|
update_rapport_validate(request, norapport)
|
|
request.session.flash(u"Le rapporta été validé avec succès.", 'success')
|
|
return HTTPFound(location=request.route_url('dossier_view', nodossier=nodossier))
|
|
|
|
if 'form.delete' in request.params:
|
|
# le rapport a-t-il des photos ?
|
|
if len(photos1) > 0 or len(photos2) > 0:
|
|
request.session.flash(u"Veuillez supprimer les photos avant de supprimer Le rapport.", 'warning')
|
|
return HTTPFound(location=url)
|
|
|
|
delete_rapport(request, norapport)
|
|
request.session.flash(u"Le rapport a été supprimé avec succès.", 'success')
|
|
return HTTPFound(location=request.route_url('dossier_view', nodossier=nodossier))
|
|
|
|
return {
|
|
'page_title': "Rapport no %s du %s" % (norapport, rapport.date_inter.strftime('%d-%m-%Y')),
|
|
'url': url,
|
|
'pt_name': 'rdf_view',
|
|
'access': access,
|
|
'rapport': rapport,
|
|
'nodossier': nodossier,
|
|
'nochantier': rapport.nochantier,
|
|
'norapport': norapport,
|
|
'date_rapport': date_rapport,
|
|
'date_relu': date_relu,
|
|
'date_facture': date_facture,
|
|
'photos1': photos1,
|
|
'photos2': photos2,
|
|
'docs_url': request.static_url(request.registry.settings['mondumas.devfac_url']),
|
|
}
|
|
|
|
@view_config(route_name='rdf_rapport', renderer='../templates/dossier/rdf_view.pt')
|
|
def rdf_rapport(request):
|
|
norapport = request.matchdict['no_id']
|
|
|
|
logged_in = request.authenticated_userid
|
|
if logged_in:
|
|
# lire code accès du user
|
|
access = get_userAccess(request, logged_in)
|
|
else:
|
|
# user anonyme
|
|
access = 0
|
|
|
|
# lire le rapport
|
|
rapport = get_rapport_by_no_id(request, norapport)
|
|
nodossier = 'PL-' + str(rapport.nochantier)
|
|
|
|
if rapport.date_rapport:
|
|
date_rapport = rapport.date_rapport.strftime('%d-%m-%Y')
|
|
else:
|
|
date_rapport = ''
|
|
|
|
if rapport.date_relu:
|
|
date_relu = rapport.date_relu.strftime('%d-%m-%Y')
|
|
else:
|
|
date_relu = ''
|
|
|
|
# lire tous les photos attachées
|
|
photos1 = get_photos(request, nodossier, norapport, '1')
|
|
photos2 = get_photos(request, nodossier, norapport, '2')
|
|
|
|
return {
|
|
'page_title': '',
|
|
'url': '',
|
|
'pt_name': 'rdf_rapport',
|
|
'rapport': rapport,
|
|
'nodossier': nodossier,
|
|
'nochantier': rapport.nochantier,
|
|
'norapport': norapport,
|
|
'date_rapport': date_rapport,
|
|
'date_relu': date_relu,
|
|
'photos1': photos1,
|
|
'photos2': photos2,
|
|
'docs_url': request.static_url(request.registry.settings['mondumas.devfac_url']),
|
|
'access': access,
|
|
}
|
|
|
|
@view_config(route_name='delete_img', permission='view')
|
|
def delete_img(request):
|
|
logged_in = request.authenticated_userid.upper()
|
|
nodossier = request.matchdict['nodossier']
|
|
societe = nodossier[0:2]
|
|
nochantier = nodossier[3:]
|
|
norapport = request.matchdict['norapport']
|
|
origine = request.matchdict['origine']
|
|
nomfic = request.matchdict['nomfic']
|
|
|
|
# origine de l'image ?
|
|
if len(origine) > 1:
|
|
url_retour = location=request.route_url("upload_img", norapport=nodossier, origine=origine)
|
|
else:
|
|
url_retour = location=request.route_url("upload_img", norapport=norapport, origine=origine)
|
|
|
|
delete_photos(request, nodossier, norapport, origine, nomfic)
|
|
request.session.flash(u"La photo %s a été supprimée avec succès" % (nomfic), 'success')
|
|
return HTTPFound(url_retour)
|
|
|
|
@view_config(route_name='rotate_img', permission='view')
|
|
def rotate_img(request):
|
|
|
|
nodossier = request.matchdict['nodossier']
|
|
societe = nodossier[0:2]
|
|
nochantier = nodossier[3:]
|
|
norapport = request.matchdict['norapport']
|
|
origine = request.matchdict['origine']
|
|
nomfic = request.matchdict['nomfic']
|
|
angle = int(request.matchdict['angle'])
|
|
|
|
# afin de refresh l'image, il faut lui donner un nouveau nom
|
|
n = nomfic.split('.')
|
|
new_nomfic = n[0] + 'r.' + n[1]
|
|
|
|
# origine de l'image ?
|
|
if len(origine) > 1:
|
|
url_retour = location=request.route_url("upload_img", norapport=nodossier, origine=origine)
|
|
image_file = '%s/%s/%s/%s' % (request.registry.settings['mondumas.devfac_dir'], societe, nochantier, nomfic)
|
|
new_file = '%s/%s/%s/%s' % (request.registry.settings['mondumas.devfac_dir'], societe, nochantier, new_nomfic)
|
|
else:
|
|
url_retour = location=request.route_url("upload_img", norapport=norapport, origine=origine)
|
|
image_file = '%s/%s/%s/%s/%s' % (request.registry.settings['mondumas.devfac_dir'], societe, nochantier, norapport, nomfic)
|
|
new_file = '%s/%s/%s/%s/%s' % (request.registry.settings['mondumas.devfac_dir'], societe, nochantier, norapport, new_nomfic)
|
|
|
|
# using the Python Image Library (PIL) to rotate an image
|
|
img_in = Image.open(image_file)
|
|
img_out = img_in.rotate(angle, expand=True)
|
|
# create a new file name for saving the result
|
|
img_out.save(new_file)
|
|
|
|
# rename dans la BD
|
|
rename_photos(request, nodossier, norapport, origine, nomfic, new_nomfic)
|
|
|
|
return HTTPFound(url_retour)
|
|
|
|
|
|
@view_config(route_name='rdf_bill', renderer='../templates/dossier/rdf_bill.pt', permission='view')
|
|
def rdf_bill(request):
|
|
logged_in = request.authenticated_userid.upper()
|
|
norapport = request.matchdict['no_id']
|
|
url = request.route_url('rdf_bill', no_id=norapport)
|
|
|
|
# lire le rapport
|
|
rapport = get_rapport_by_no_id(request, norapport)
|
|
nodossier = 'PL-' + str(rapport.nochantier)
|
|
|
|
if not rapport.date_rapport:
|
|
request.session.flash(u"Le rapport n'a été encore généré. Facturation impossible", 'danger')
|
|
return HTTPFound(location=request.route_url('rdf_view', no_id=norapport))
|
|
|
|
# lire tous les articles RDF
|
|
articles = get_articles_rdf(request)
|
|
article = 'RDF1'
|
|
|
|
if 'form.submitted' in request.params:
|
|
# lire article à facturer
|
|
article = request.params['article']
|
|
insert_facture_rdf(request, 'PL', rapport.nochantier, rapport.CD_CLI, rapport.NOMCLI, logged_in, article, rapport.date_inter.strftime('%d-%m-%Y'))
|
|
|
|
# marquer le rapport comme facturé
|
|
update_rapport_facture(request, norapport)
|
|
|
|
request.session.flash("Le rapport a été généré avec succès.", 'success')
|
|
return HTTPFound(location=request.route_url('dossier_view', nodossier=nodossier))
|
|
|
|
return {
|
|
'page_title': "Générer une facture pour le rapport du %s" % (rapport.date_inter.strftime('%d-%m-%Y')),
|
|
'url': url,
|
|
'pt_name': 'rdf_view',
|
|
'logged_in': logged_in,
|
|
'rapport': rapport,
|
|
'nodossier': nodossier,
|
|
'nochantier': rapport.nochantier,
|
|
'norapport': norapport,
|
|
'articles': articles,
|
|
'article' : article,
|
|
}
|
|
|
|
@view_config(route_name='demandes', renderer='../templates/dossier/demandes.pt', permission='view')
|
|
def demandes(request):
|
|
|
|
def demandes_lister(societe, search_criteria):
|
|
# créer la liste des entêtes des messages à afficher
|
|
liste = []
|
|
# connecter au serveur de mail
|
|
conn = mailbox_connect(request, societe)
|
|
if conn == None:
|
|
return liste
|
|
|
|
# select INBOX
|
|
rv, data = conn.select('INBOX', readonly =True)
|
|
|
|
for criteria in search_criteria:
|
|
rv, data = conn.search(None, criteria)
|
|
if rv != 'OK':
|
|
request.session.flash("ERREUR de lecture de la boîte de réception", 'danger')
|
|
return HTTPFound(location=request.route_url('home'))
|
|
|
|
mail_ids = data[0]
|
|
for email_UID in mail_ids.split():
|
|
rv, msg_data = conn.fetch(email_UID, '(RFC822)')
|
|
if rv != 'OK':
|
|
request.session.flash("ERREUR de lecture du message %s" % email_UID, 'danger')
|
|
return HTTPFound(location=request.route_url('home'))
|
|
|
|
msg = email.message_from_bytes(msg_data[0][1])
|
|
hdr = email.header.make_header(email.header.decode_header(msg['Subject']))
|
|
email_subject = str(hdr)
|
|
email_from = email.utils.parseaddr(msg['from'])[1]
|
|
# Now convert to local date-time
|
|
date_tuple = email.utils.parsedate_tz(msg['Date'])
|
|
if date_tuple:
|
|
email_date = datetime.fromtimestamp(email.utils.mktime_tz(date_tuple))
|
|
else:
|
|
email_date = datetime.now()
|
|
|
|
d = {
|
|
"email_societe": societe,
|
|
"email_date": email_date,
|
|
"email_from": email_from.split('@')[1],
|
|
'email_subject':email_subject,
|
|
"email_uid": email_UID
|
|
}
|
|
liste.append(d)
|
|
|
|
# deconnexion du serveur
|
|
conn.close()
|
|
conn.logout()
|
|
return liste
|
|
|
|
# ------- main -------
|
|
logged_in = request.authenticated_userid.upper()
|
|
url = request.route_url('demandes')
|
|
message = ''
|
|
|
|
societes = ['PE','ME','PL','PO']
|
|
|
|
# critères de recherche des demandes d'interventions de la MAIF
|
|
# search_criteria = ['FROM service.sinistres@domus-services.fr SUBJECT "Ordre de mission DOMUS - Dossier" UNDELETED'] "
|
|
search_criteria = ['FROM gestionsinistre@maif.fr SUBJECT "Intervention entreprise partenaire"',
|
|
'FROM service.sinistres@domus-services.fr UNDELETED']
|
|
# 'FROM service.sinistres@domus-services.fr SUBJECT "Ordre de mission DOMUS - Dossier" UNDELETED']
|
|
|
|
emails=[]
|
|
# lister les demandes par societe
|
|
for societe in societes:
|
|
emails = emails + demandes_lister(societe, search_criteria)
|
|
|
|
# messages lus
|
|
msglus = bool(emails)
|
|
|
|
return {
|
|
'page_title': "Liste des emails de demandes d'intervention",
|
|
'url': url,
|
|
'emails': emails,
|
|
}
|
|
|
|
@view_config(route_name='demandes_dl', permission='view')
|
|
def demandes_dl(request):
|
|
"""
|
|
Lire le message et récupérer le PDF
|
|
Analyser le PDF et génère le dossier
|
|
"""
|
|
|
|
def generer_mission(request, societe, mbx_search, extracted_file, temp_file_path):
|
|
|
|
if 'maif.fr' in mbx_search:
|
|
# extraire les infos de la demmande MAIF
|
|
dem_info = get_pdf_infos_maif(extracted_file)
|
|
if societe == 'PE':
|
|
cd_cli = 2813
|
|
elif societe == 'ME':
|
|
cd_cli = 3428
|
|
else:
|
|
# VERSANIT
|
|
cd_cli = 1743
|
|
elif 'domus-services.fr' in mbx_search:
|
|
# extraire les infos de la demmande DOMUS
|
|
dem_info = get_pdf_infos_domus(extracted_file)
|
|
if societe == 'PE':
|
|
cd_cli = 8991
|
|
elif societe == 'ME':
|
|
cd_cli = 5276
|
|
else:
|
|
# VERSANIT
|
|
cd_cli = 3209
|
|
|
|
# extraction OK ? oui, créer une dem_devis et récupèrer son no_id
|
|
traite = 0
|
|
if dem_info['c_nom'] != '':
|
|
# oui, rechercher la dem_devis concerné par le no de sinistre
|
|
nosin = dem_info['no_sinistre']
|
|
dem_devis = get_dossier_by_sinistre(request,societe, nosin)
|
|
if dem_devis:
|
|
# dem_devis existe, ajouter le PDF dans ce dossier
|
|
nochantier = dem_devis.NO_ID
|
|
nodossier = "%s-%s" % (societe, nochantier)
|
|
# insérer une ligne de suivi ANNULATION
|
|
insert_suivi(request, nodossier, '!!MISSION CONFIRMEE ou MODIFIEE PAR la MAIF')
|
|
# log de nuit
|
|
print('--> MODIFIER DOSSIER sinistre %s <--' % nodossier)
|
|
else:
|
|
# dem_devis n'existe pas, creer nouveau dossier
|
|
nochantier = insert_dossier(request, societe, cd_cli, dem_info['c_nom'], dem_info['c_adr'], dem_info['c_adr2'], \
|
|
dem_info['c_cp'], dem_info['c_ville'], dem_info['c_telp'], dem_info['c_email'], nosin, '', dem_info['c_obs'], dem_info['tx_trav'])
|
|
nodossier = "%s-%s" % (societe, nochantier)
|
|
# log de nuit
|
|
print('--> CREER DOSSIER sinistre %s <--' % nodossier)
|
|
|
|
# récupère le nom du fichier et ajouter le no de dossier
|
|
filename = os.path.basename(temp_file_path)
|
|
filename = '%s-DD%s-%s' % (societe, nochantier, filename)
|
|
tempFile2Dossier(request, societe, nochantier, '0', 'CLT', temp_file_path, filename, 'EMAIL')
|
|
traite = 1
|
|
|
|
return traite
|
|
|
|
def generer_annul_maif(request, societe, extracted_file, temp_file_path):
|
|
# extraire les infos de la demmande MAIF
|
|
dem_info = get_pdf_infos_maif(extracted_file)
|
|
# extraction OK ? oui, rechercher la dem_devis concerné
|
|
|
|
traite = 0
|
|
if dem_info['c_nom'] != '':
|
|
# oui, rechercher la dem_devis concerné par le no de sinistre
|
|
nosin = dem_info['no_sinistre']
|
|
dem_devis = get_dossier_by_sinistre(request,societe, nosin)
|
|
if dem_devis:
|
|
nodossier = "%s-%s" % (societe, dem_devis.NO_ID)
|
|
|
|
# récupère le nom du fichier et ajouter le no de dossier
|
|
filename = '%s-DD%s-%s' % (societe, dem_devis.NO_ID, 'ANNULATION.pdf')
|
|
tempFile2Dossier(request, societe, dem_devis.NO_ID, '0', 'CLT', temp_file_path, filename, 'EMAIL')
|
|
# insérer une ligne de suivi ANNULATION
|
|
insert_suivi(request, nodossier, '!!MISSION ANNULEE PAR la MAIF')
|
|
# log de nuit
|
|
print('--> ANNULATION DOSSIER sinistre %s <--' % nodossier)
|
|
traite = 1
|
|
|
|
return traite
|
|
|
|
def generer_annul_domus(request, societe, nosin, temp_file_path):
|
|
traite = 0
|
|
# oui, rechercher la dem_devis concerné par le no de sinistre
|
|
dem_devis = get_dossier_by_sinistre(request,societe, nosin)
|
|
if dem_devis:
|
|
nodossier = "%s-%s" % (societe, dem_devis.NO_ID)
|
|
|
|
# récupère le nom du fichier et ajouter le no de dossier
|
|
filename = '%s-DD%s-%s' % (societe, dem_devis.NO_ID, 'ANNULATION.pdf')
|
|
tempFile2Dossier(request, societe, dem_devis.NO_ID, '0', 'CLT', temp_file_path, filename, 'EMAIL')
|
|
|
|
#import pdb;pdb.set_trace()
|
|
# insérer une ligne de suivi ANNULATION
|
|
insert_suivi(request, nodossier, '!!MISSION ANNULEE PAR DOMUS')
|
|
traite = 1
|
|
|
|
return traite
|
|
|
|
|
|
# ------- main -------
|
|
societe = request.matchdict['societe']
|
|
email_from = request.matchdict['email_from']
|
|
email_uid = request.matchdict['email_uid']
|
|
logged_in = request.authenticated_userid.upper()
|
|
message = ''
|
|
|
|
if 'maif' in email_from:
|
|
search_criteria = 'FROM gestionsinistre@maif.fr SUBJECT "Intervention entreprise partenaire" UNDELETED'
|
|
else:
|
|
search_criteria = 'FROM service.sinistres@domus-services.fr SUBJECT "Ordre de mission DOMUS - Dossier" UNDELETED'
|
|
|
|
# connecter au serveur de mail
|
|
conn = mailbox_connect(request, societe)
|
|
# select INBOX
|
|
rv, data = conn.select('INBOX')
|
|
conn.expunge()
|
|
|
|
# rechercher les emails de demandes dans le INBOX
|
|
rv, data = conn.search(None, search_criteria)
|
|
if rv != 'OK':
|
|
request.session.flash("ERREUR de lecture de la boîte de réception", 'danger')
|
|
return HTTPFound(location=request.route_url('messages'))
|
|
|
|
# lire le message avec UID
|
|
rv, msg_data = conn.fetch(email_uid, '(RFC822)')
|
|
if rv != 'OK':
|
|
request.session.flash("ERREUR de lecture du message %S-%s" % (email_from, email_uid), 'danger')
|
|
return HTTPFound(location=request.route_url('messages'))
|
|
|
|
email_message = email.message_from_bytes(msg_data[0][1])
|
|
|
|
# get the message's body
|
|
body = ''
|
|
for part in email_message.walk():
|
|
ctype = part.get_content_type()
|
|
cdispo = str(part.get('Content-Disposition'))
|
|
|
|
# skip any text/plain (txt) attachments
|
|
if ctype == 'text/html' and 'attachment' not in cdispo:
|
|
body = part.get_payload(decode=True) # decode
|
|
break
|
|
|
|
# downloading attachment
|
|
temp_file_path = download_pdf_to_tmp(email_message)
|
|
if temp_file_path != '':
|
|
# si attachement existe, convertir le fichier pdf en texte
|
|
texte, extracted_file = pdf_convert_to_txt(temp_file_path)
|
|
# mission annulée ?
|
|
if 'Objet : ANNULATION MISSION' in texte :
|
|
# genere ANNULATION mission MAIF
|
|
#import pdb;pdb.set_trace()
|
|
n = generer_annul_maif(request, societe, extracted_file, temp_file_path)
|
|
if n > 0:
|
|
# déplacer le message dans la poubelle
|
|
conn.store(email_uid, '+FLAGS', '\\Deleted')
|
|
elif 'ANNULATION ORDRE DE MISSION' in texte:
|
|
# genere ANNULATION mission DOMUS
|
|
nosin = texte.split('\n')[3] # 4ème ligne de texte
|
|
#import pdb;pdb.set_trace()
|
|
n = generer_annul_domus(request, societe, nosin, temp_file_path)
|
|
if n > 0:
|
|
# déplacer le message dans la poubelle
|
|
conn.store(email_uid, '+FLAGS', '\\Deleted')
|
|
else:
|
|
#import pdb;pdb.set_trace()
|
|
# genere le dossier d'après le mail
|
|
n = generer_mission(request, societe, email_from, extracted_file, temp_file_path)
|
|
if n > 0:
|
|
# déplacer le message dans la poubelle
|
|
conn.store(email_uid, '+FLAGS', '\\Deleted')
|
|
|
|
conn.expunge()
|
|
conn.close()
|
|
# deconnexion du serveur
|
|
conn.logout()
|
|
|
|
request.session.flash("La mission no %s de %s a été importée avec succès" % (email_uid, email_from), 'success')
|
|
return HTTPFound(request.route_url('demandes'))
|
|
|
|
|
|
def downloadFile2Temp(input_file, input_name, ext_allowed):
|
|
# récupère son extension
|
|
input_extension = input_name.split('.')[-1]
|
|
|
|
# extensions autorisées ?
|
|
if input_extension.lower() not in ext_allowed :
|
|
return "ERREUR: Le format du fichier n'est pas valide. Téléchargement impossible."
|
|
|
|
# Finally write the data to a temporary file
|
|
temp_file_path = os.path.join('/tmp/', input_name)
|
|
# supprimer le fichier s'il existe déjà
|
|
if os.path.exists(temp_file_path):
|
|
os.remove(temp_file_path)
|
|
|
|
input_file.seek(0)
|
|
with open(temp_file_path, 'wb') as output_file:
|
|
shutil.copyfileobj(input_file, output_file)
|
|
|
|
# controler la taille du fichier < 4 Mo
|
|
filesize = round(os.path.getsize(temp_file_path) / 1024)
|
|
if filesize > 4096 :
|
|
os.remove(temp_file_path)
|
|
return "ERREUR: La taille du fichier dépasse la limite autorisée. Téléchargement impossible."
|
|
|
|
if input_extension in ['jpeg','jpg','png']:
|
|
# using the Python Image Library (PIL) to resize an image
|
|
resize_photos(temp_file_path)
|
|
|
|
return temp_file_path
|
|
|
|
def tempFile2Dossier(request, societe, nochantier, norapport, origine, temp_file, filename, logged_in):
|
|
# créer le répertoire du chantier
|
|
if norapport == '0':
|
|
path = '%s/%s/%s' % (request.registry.settings['mondumas.devfac_dir'], societe, nochantier)
|
|
else:
|
|
path = '%s/%s/%s/%s' % (request.registry.settings['mondumas.devfac_dir'], societe, nochantier, norapport)
|
|
os.makedirs(path, exist_ok=True)
|
|
|
|
filepath = os.path.join('%s/%s' % (path, filename))
|
|
# supprimer le fichier s'il existe déjà
|
|
if os.path.exists(filepath):
|
|
os.remove(filepath)
|
|
|
|
# Finally move the temporary file to folder
|
|
shutil.move(temp_file, filepath)
|
|
|
|
filesize = round(os.path.getsize(filepath) / 1024)
|
|
insert_dossier_attaches(request, '%s-%s' % (societe, nochantier), norapport, origine, filename, '%s Ko' % str(filesize), logged_in)
|
|
|
|
def download_pdf_to_tmp(email_message):
|
|
# import pdb;pdb.set_trace()
|
|
temp_file_path = ''
|
|
# downloading attachments
|
|
for part in email_message.walk():
|
|
# this part comes from the snipped I don't understand yet...
|
|
if part.get_content_maintype() == 'multipart':
|
|
continue
|
|
if part.get('Content-Disposition') is None:
|
|
continue
|
|
fileName = part.get_filename()
|
|
if bool(fileName):
|
|
# copier le fichier PDF dans le dossier /tmp
|
|
temp_file_path = os.path.join('/tmp/', fileName)
|
|
if not os.path.isfile(temp_file_path) :
|
|
fp = open(temp_file_path, 'wb')
|
|
fp.write(part.get_payload(decode=True))
|
|
fp.close()
|
|
|
|
return temp_file_path
|
|
|
|
def pdf_convert_to_txt(path):
|
|
resource_manager = PDFResourceManager()
|
|
laparams = LAParams()
|
|
converter = PDFPageAggregator(resource_manager, laparams=laparams)
|
|
page_interpreter = PDFPageInterpreter(resource_manager, converter)
|
|
|
|
extracted_text = ""
|
|
with open(path, 'rb') as fh:
|
|
|
|
for page in PDFPage.get_pages(fh,
|
|
caching=True,
|
|
check_extractable=True):
|
|
page_interpreter.process_page(page)
|
|
# The converter renders the layout from interpreter
|
|
layout = converter.get_result()
|
|
# Out of the many LT objects within layout, we are interested in LTTextBox and LTTextLine
|
|
for lt_obj in layout:
|
|
if isinstance(lt_obj, LTTextBox) or isinstance(lt_obj, LTTextLine):
|
|
extracted_text += lt_obj.get_text()
|
|
|
|
# close open handles
|
|
converter.close()
|
|
fh.close()
|
|
# ecrire le texte dans un fichier
|
|
extracted_file = '/tmp/log_file.txt'
|
|
with open(extracted_file, "w", encoding="utf-8") as my_log:
|
|
my_log.write(extracted_text)
|
|
my_log.close()
|
|
|
|
return extracted_text, extracted_file
|
|
|
|
def get_pdf_infos_maif(extracted_file):
|
|
# à partir du fichier texte du pdf
|
|
# parcourir les lignes pour retrouver les infos utiles
|
|
with open(extracted_file, encoding="utf-8") as fp:
|
|
cnt = 1
|
|
line = fp.readline()
|
|
# première ligne doit être "MAIF"
|
|
if line[:-1] not in ['MAIF', 'FILIA-MAIF']:
|
|
return {'c_nom': ''}
|
|
|
|
c_obs = ''
|
|
tx_trav = ''
|
|
c_telp = ''
|
|
c_email = ''
|
|
while line:
|
|
if line.find('Nos références') == 0:
|
|
line = fp.readline()
|
|
line = fp.readline()
|
|
no_sinistre = line[:-1]
|
|
if line.find('Bénéficiaire des travaux :') == 0:
|
|
elt = line[:-1].split(' :')
|
|
if len(elt) == 1:
|
|
# le nom du chantier est sur la ligne suivante
|
|
line = fp.readline()
|
|
c_nom = line[:-1]
|
|
c_nom = c_nom[:40] # ne prendre que 40 carac
|
|
else:
|
|
# le nom du chantier est sur la même ligne
|
|
c_nom = elt[1].strip()[:40]
|
|
line = fp.readline()
|
|
line = fp.readline()
|
|
line = fp.readline()
|
|
c_adr = line[:-1]
|
|
line = fp.readline()
|
|
c_adr2 = line[:-1]
|
|
line = fp.readline()
|
|
c_adr3 = line[:-1]
|
|
# début 3ème ligne adr est un code postal ?
|
|
if to_int(c_adr3[0:5]) > 0 :
|
|
# oui, mémoriser le code postal et la ville
|
|
c_cp = c_adr3[0:5]
|
|
c_ville = c_adr3[6:]
|
|
else:
|
|
# non, le code postal et la ville se trouvent dans la 2è ligne
|
|
c_cp = c_adr2[0:5]
|
|
c_ville = c_adr2[6:]
|
|
c_adr2 = ''
|
|
if ' téléphone : ' in line:
|
|
# les 10 derniers caratères
|
|
# import pdb;pdb.set_trace()
|
|
c_telp = line[-11:-1]
|
|
if 'E-mail : ' in line:
|
|
# séparer l'émail
|
|
# import pdb;pdb.set_trace()
|
|
elt = line[:-1].split(' : ')
|
|
if len(elt) == 2:
|
|
c_email = elt[1]
|
|
if 'une franchise de ' in line:
|
|
i1 = line.find('franchise de ')
|
|
i2 = line.find(' € ')
|
|
c_obs = line[i1:i2+2]
|
|
if ' pour un montant de ' in line:
|
|
i1 = line.find('pour un montsant de ')
|
|
tx_trav = line[i1:-2].replace(',', '.')
|
|
|
|
# lire ligne suivante
|
|
line = fp.readline()
|
|
cnt += 1
|
|
|
|
fp.close()
|
|
return {'c_nom': c_nom,
|
|
'c_adr': c_adr,
|
|
'c_adr2': c_adr2,
|
|
'c_cp': c_cp,
|
|
'c_ville': c_ville,
|
|
'c_telp': c_telp,
|
|
'c_obs': c_obs,
|
|
'c_email': c_email,
|
|
'tx_trav': tx_trav,
|
|
'no_sinistre': no_sinistre,
|
|
}
|
|
|
|
def get_pdf_infos_domus(extracted_file):
|
|
# à partir du fichier texte du pdf de DOMUS
|
|
# parcourir les lignes pour retrouver les infos utiles
|
|
with open(extracted_file, encoding="utf-8") as fp:
|
|
cnt = 1
|
|
line = fp.readline()
|
|
# première ligne doit être :
|
|
if line[:-1] != 'ORDRE DE MISSION':
|
|
fp.close()
|
|
return {'c_nom': ''}
|
|
|
|
c_obs = ''
|
|
tx_trav = ''
|
|
c_telp = ''
|
|
c_email = ''
|
|
while line:
|
|
if line.find('SINISTRE N°') == 0:
|
|
line = fp.readline()
|
|
line = fp.readline()
|
|
no_sinistre = line[:-1]
|
|
if line.find('Adresse du sinistre :') == 0:
|
|
line = fp.readline()
|
|
c_nom = line[:-1]
|
|
line = fp.readline()
|
|
c_adr = line[:-1]
|
|
line = fp.readline()
|
|
c_adr2 = line[:-1]
|
|
# début 2ème ligne adr est un code postal ?
|
|
if to_int(c_adr2) > 0 :
|
|
# oui, mémoriser le code postal et la ville
|
|
c_cp = c_adr2
|
|
line = fp.readline()
|
|
c_ville = line[:-1]
|
|
c_adr2 = ''
|
|
else:
|
|
# non, le code postal et la ville se trouvent dans la 3è ligne
|
|
line = fp.readline()
|
|
c_cp = line[:-1]
|
|
c_ville = line[:-1]
|
|
line = fp.readline()
|
|
if 'Gsm : ' in line:
|
|
# les 10 derniers caratères
|
|
c_telp = line[-11:-1]
|
|
|
|
# lire ligne suivante
|
|
line = fp.readline()
|
|
cnt += 1
|
|
|
|
fp.close()
|
|
return {'c_nom': c_nom,
|
|
'c_adr': c_adr,
|
|
'c_adr2': c_adr2,
|
|
'c_cp': c_cp[:5],
|
|
'c_ville': c_ville,
|
|
'c_telp': c_telp,
|
|
'c_email': c_email,
|
|
'c_obs': c_obs,
|
|
'tx_trav': tx_trav,
|
|
'no_sinistre': no_sinistre,
|
|
}
|
|
|
|
def get_pdf_infos_axa(extracted_file):
|
|
# à partir du fichier texte du pdf de AXA
|
|
# parcourir les lignes pour retrouver les infos utiles
|
|
with open(extracted_file, encoding="utf-8") as fp:
|
|
|
|
cnt = 1
|
|
line = fp.readline()
|
|
# première ligne doit être :
|
|
if line[:-1] != 'Assurance et Banque':
|
|
fp.close()
|
|
return {'c_nom': ''}
|
|
|
|
c_telp = ''
|
|
c_email = ''
|
|
while line:
|
|
if line[:-1] == 'LibellØ':
|
|
# import pdb;pdb.set_trace()
|
|
line = fp.readline()
|
|
if 'PEINTURE' in line or 'PAPIER PEINT' in line or 'CARRELAGE' in line or 'CERAMIQUE' in line:
|
|
societe = 'PE'
|
|
elif 'MENUISERIE' in line or 'FERMETURE' in line or 'PARQUET' in line:
|
|
societe = 'ME'
|
|
else:
|
|
societe = ''
|
|
|
|
if line[:-1] == 'Contrat':
|
|
# import pdb;pdb.set_trace()
|
|
line = fp.readline()
|
|
no_police = line[:-1]
|
|
if line[:-1] == 'RØfØrence sinistre':
|
|
# import pdb;pdb.set_trace()
|
|
line = fp.readline()
|
|
no_sinistre = line[:-1]
|
|
if line[:-1] == 'Notre assurØ':
|
|
# import pdb;pdb.set_trace()
|
|
line = fp.readline()
|
|
c_nom = line[:-1]
|
|
line = fp.readline()
|
|
c_adr = line[:-1]
|
|
line = fp.readline()
|
|
c_adr2 = line[:-1]
|
|
line = fp.readline()
|
|
c_adr3 = line[:-1]
|
|
# début 3ème ligne adr est un code postal ?
|
|
if to_int(c_adr3[0:5]) > 0 :
|
|
# oui, mémoriser le code postal et la ville
|
|
c_cp = c_adr3[0:5]
|
|
c_ville = c_adr3[6:]
|
|
else:
|
|
# non, le code postal et la ville se trouvent dans la 2è ligne
|
|
c_cp = c_adr2[0:5]
|
|
c_ville = c_adr2[6:]
|
|
c_adr2 = ''
|
|
if 'portable ' in line:
|
|
# import pdb;pdb.set_trace()
|
|
# les 10 derniers caratères
|
|
c_telp = line[-11:-1]
|
|
line = fp.readline()
|
|
# séparer l'émail
|
|
# import pdb;pdb.set_trace()
|
|
elt = line[:-1].split(': ')
|
|
if len(elt) == 2:
|
|
c_email = elt[1]
|
|
# lire ligne suivante
|
|
line = fp.readline()
|
|
cnt += 1
|
|
|
|
fp.close()
|
|
# import pdb;pdb.set_trace()
|
|
|
|
return {'c_nom': c_nom,
|
|
'c_adr': c_adr,
|
|
'c_adr2': c_adr2,
|
|
'c_cp': c_cp[:5],
|
|
'c_ville': c_ville,
|
|
'c_telp': c_telp,
|
|
'c_email': c_email,
|
|
'no_police': no_police,
|
|
'no_sinistre': no_sinistre,
|
|
'societe': societe,
|
|
}
|
|
|
|
def resize_photos(image_file):
|
|
# using the Python Image Library (PIL) to resize an image
|
|
img_org = Image.open(image_file)
|
|
# get the size of the original image
|
|
width_org, height_org = img_org.size
|
|
# set the max width
|
|
width = 1366
|
|
height = int(height_org / width_org * width)
|
|
# best down-sizing filter
|
|
img_anti = img_org.resize((width, height), Image.ANTIALIAS)
|
|
# split image filename into name and extension
|
|
name, ext = os.path.splitext(image_file)
|
|
# create a new file name for saving the result
|
|
img_anti.save(image_file)
|
|
|
|
return
|
|
|
|
def photos_size(photos):
|
|
# calculer la taille totale des photos
|
|
sum_size = 0
|
|
for photo in photos :
|
|
# récupère la taille sans ' Ko'
|
|
sum_size += int(photo.taillefichier[:-3])
|
|
|
|
return '%s Mo' % round(sum_size / 1024, 1)
|
|
|
|
@view_config(route_name='upload_om', renderer='../templates/dossier/upload_om.pt', permission='view')
|
|
def upload_om(request):
|
|
"""
|
|
UPLOAD d'un ordre de mission en PDF
|
|
|
|
"""
|
|
def generer_mission(request, extracted_file, temp_file_path):
|
|
|
|
# extraire les infos de la demmande AXA
|
|
dem_info = get_pdf_infos_axa(extracted_file)
|
|
societe = dem_info['societe']
|
|
if societe == 'PE':
|
|
cd_cli = 15207
|
|
elif societe == 'ME':
|
|
cd_cli = 1190
|
|
else:
|
|
return "Descriptif de travaux non prevu par le programme. Prévenir M. CAO."
|
|
|
|
# extraction OK ? oui, créer une dem_devis et récupèrer son no_id
|
|
message = ''
|
|
if dem_info['c_nom'] != '':
|
|
# dem_devis n'existe pas, creer nouveau dossier
|
|
nochantier = insert_dossier(request, societe, cd_cli, dem_info['c_nom'], dem_info['c_adr'], dem_info['c_adr2'], dem_info['c_cp'], \
|
|
dem_info['c_ville'], dem_info['c_telp'], dem_info['c_email'], dem_info['no_sinistre'], dem_info['no_police'], '', '')
|
|
nodossier = "%s-%s" % (societe, nochantier)
|
|
# log de nuit
|
|
print('--> CREER DOSSIER sinistre %s <--' % nodossier)
|
|
|
|
# récupère le nom du fichier et ajouter le no de dossier
|
|
filename = os.path.basename(temp_file_path)
|
|
filename = '%s-DD%s-%s' % (societe, nochantier, filename)
|
|
tempFile2Dossier(request, societe, nochantier, '0', 'CLT', temp_file_path, filename, 'EMAIL')
|
|
|
|
return message
|
|
|
|
logged_in = request.authenticated_userid.upper()
|
|
|
|
url = request.route_url("upload_om")
|
|
message = ''
|
|
html_text = ''
|
|
|
|
if 'form.submitted' in request.params or 'form.previewed' in request.params:
|
|
# récupère le fichier download dans le dossier /tmp
|
|
input_file = request.POST['filename'].file
|
|
input_name = request.POST['filename'].filename
|
|
ext_allowed = ['pdf']
|
|
temp_file = downloadFile2Temp(input_file, input_name, ext_allowed)
|
|
if temp_file[:8] == 'ERREUR: ':
|
|
request.session.flash(temp_file, 'danger')
|
|
return HTTPFound(location=url)
|
|
|
|
# convertir le fichier pdf en texte
|
|
extracted_text, extracted_file = pdf_convert_to_txt(temp_file)
|
|
|
|
# prévisualiser le pdf
|
|
if 'form.previewed' in request.params:
|
|
# convert text to html
|
|
html_text = "<p>" + extracted_text.replace("\n", "<br>") + "</p>"
|
|
html_text = html_text.replace("Ø", "é")
|
|
|
|
# importer le pdf
|
|
if 'form.submitted' in request.params:
|
|
# origine du PDF = AXA ?
|
|
if 'AXA France GESTION SINISTRES' in extracted_text :
|
|
# PDF = ordre de mission AXA ?
|
|
if 'bon de commande pour les travaux' in extracted_text :
|
|
# genere le dossier d'après le fichier PDF
|
|
message = generer_mission(request, extracted_file, temp_file)
|
|
if message == '':
|
|
request.session.flash("Le fichier PDF a été importé avec succès.", 'success')
|
|
return HTTPFound(location=request.route_url('dossier_lookup'))
|
|
else:
|
|
message = "ERREUR : L'importation de ce document AXA n'est pas prévue."
|
|
else:
|
|
message = "ERREUR : L'importation de ce type de document n'est pas prévue."
|
|
|
|
return {
|
|
'page_title': 'Importer un ordre de mission AXA',
|
|
'url': url,
|
|
'message': message,
|
|
'html_text': html_text,
|
|
}
|
|
|
|
@view_config(route_name='dem_devis', renderer='../templates/dossier/dem_devis.pt', permission='view')
|
|
def dem_devis(request):
|
|
logged_in = request.authenticated_userid.upper()
|
|
goto_url = '/dossier_selected/dossier_view/%s/' % date.today().strftime('%Y-%m-%d')
|
|
url = request.route_url('dem_devis')
|
|
member = get_member_by_id(request, logged_in)
|
|
societe_defaut = member.societe
|
|
societe = societe_defaut
|
|
access_defaut = member.access
|
|
liste=[]
|
|
|
|
if 'form.submitted' in request.params:
|
|
societe = request.params['societe']
|
|
|
|
dossiers_traites = get_dossiers_traites(request, societe)
|
|
|
|
for item in dossiers_traites:
|
|
d = ('%s-%s' % (societe, item.numero),item.date.strftime('%d-%m-%Y'), item.nomcli, item.chantier, to_euro(item.montant),
|
|
item.nosin, item.libelle, item.usermaj)
|
|
liste.append(d)
|
|
|
|
order_option = 'desc'
|
|
|
|
return {
|
|
'page_title': 'Dossiers avec statut: "A TRAITER"',
|
|
'url': url,
|
|
'goto_url': goto_url,
|
|
'dt_data': json.dumps(liste),
|
|
'societe': societe,
|
|
'order_option': order_option,
|
|
}
|
|
|
|
@view_config(route_name='note_add', permission='view')
|
|
def note_add(request):
|
|
logged_in = request.authenticated_userid.upper()
|
|
nodossier = request.matchdict['nodossier']
|
|
insert_dem_note(request, nodossier, 'NOTE', logged_in)
|
|
|
|
return HTTPFound(request.route_url('dossier_view', nodossier=nodossier))
|
|
|
|
@view_config(route_name='note_edit', renderer='../templates/dossier/note_edit.pt', permission='view')
|
|
def note_edit(request):
|
|
nodossier = request.matchdict['nodossier']
|
|
noligne = request.matchdict['noligne']
|
|
url = request.route_url('note_edit', nodossier=nodossier, noligne=noligne)
|
|
|
|
message = ""
|
|
note = get_dem_notes(request, nodossier, noligne, 'NOTE')
|
|
|
|
if 'form.submitted' in request.params:
|
|
notes = request.params["notes"]
|
|
|
|
update_dem_note(request, nodossier, noligne, notes)
|
|
request.session.flash("'%s' a été modifiée avec succès." % note.libelle, 'success')
|
|
return HTTPFound(location=request.route_url('dossier_view', nodossier=nodossier))
|
|
|
|
if 'form.deleted' in request.params:
|
|
delete_dem_note(request, nodossier, noligne)
|
|
request.session.flash("'%s' a été supprimée avec succès." % note.libelle, 'success')
|
|
return HTTPFound(location=request.route_url('dossier_view', nodossier=nodossier))
|
|
|
|
return {
|
|
'page_title': note.libelle,
|
|
'url': url,
|
|
'message': message,
|
|
'nodossier': nodossier,
|
|
'note': note,
|
|
}
|
|
|
|
@view_config(route_name='croquis_edit', renderer='../templates/dossier/croquis_edit.pt', permission='view')
|
|
def croquis_edit(request):
|
|
|
|
return {
|
|
'page_title': 'NOUVEAU CROQUIS',
|
|
}
|
|
|
|
@view_config(route_name='dern_suivis', renderer='../templates/dossier/dern_suivis.pt', permission='view')
|
|
def dern_suivis(request):
|
|
# lire les derniers suivis créés par les attachés de clientèle
|
|
items = get_derniers_suivis(request)
|
|
|
|
return {
|
|
'page_title': 'Derniers suivis créés',
|
|
'items': items,
|
|
} |