Commit 7765f316 authored by Majerti - Tjebbes's avatar Majerti - Tjebbes

Merge branch 'jd-fix-691-cooordonnees-enseignes-pdf' into 'master'

Fix #691 Utilise les coordonnées des enseignes dans l'en-tête par défaut des PDF

Closes #691

See merge request endi/endi!1141
parents 84bc72a2 e961a0df
Pipeline #125808 passed with stage
in 4 minutes and 32 seconds
html{
font-size: 11px;
}
.default-header {
border-bottom: 0.01cm solid #cdcdcd;
width: 100%;
height: 3.8cm;
}
.default-header .logo-col {
text-align: right;
}
.addressblock{
position:relative;
padding-left: 9.3cm; /* + margin 1cm */
......
......@@ -2,6 +2,15 @@
html{
font-size: $base_fontsize !important;
}
.default-header {
border-bottom: 0.01cm solid #cdcdcd;
width: 100%;
height: 20vw; /* 1/5 ratio, as image baneers */
}
.default-header .logo-col {
text-align: right;
}
.addressblock{
position:relative;
padding-left: 50%; /* + margin 1cm */
......
"""5.0 Add Company address fields
Revision ID: 48f2b841d4fc
Revises: 226992705175
Create Date: 2019-03-27 23:18:14.873094
"""
# revision identifiers, used by Alembic.
revision = '48f2b841d4fc'
down_revision = '226992705175'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
def update_database_structure():
op.add_column('company', sa.Column('address', sa.String(length=255), nullable=True))
op.add_column('company', sa.Column('city', sa.String(length=255), nullable=True))
op.add_column('company', sa.Column('country', sa.String(length=150), nullable=True))
op.add_column('company', sa.Column('zip_code', sa.String(length=20), nullable=True))
def migrate_datas():
from endi_base.models.base import DBSESSION
session = DBSESSION()
from alembic.context import get_bind
conn = get_bind()
def upgrade():
update_database_structure()
migrate_datas()
def downgrade():
op.drop_column('company', 'zip_code')
op.drop_column('company', 'country')
op.drop_column('company', 'city')
op.drop_column('company', 'address')
"""5.0 Revision merge
Revision ID: 3fa5e47992bf
Revises: ('3be3fcec8d38', '48f2b841d4fc')
Create Date: 2019-03-28 21:40:55.996486
"""
# revision identifiers, used by Alembic.
revision = '3fa5e47992bf'
down_revision = ('3be3fcec8d38', '48f2b841d4fc')
from alembic import op
import sqlalchemy as sa
def update_database_structure():
pass
def migrate_datas():
from endi_base.models.base import DBSESSION
session = DBSESSION()
from alembic.context import get_bind
conn = get_bind()
def upgrade():
update_database_structure()
migrate_datas()
def downgrade():
pass
......@@ -51,7 +51,20 @@ def deferred_upload_header_widget(node, kw):
HEADER_RESIZER.complete,
]
)
return files.CustomFileUploadWidget(tmpstore)
return files.CustomFileUploadWidget(
tmpstore,
show_delete_control=True,
)
@colander.deferred
def deferred_upload_logo_widget(node, kw):
request = kw['request']
tmpstore = files.SessionDBFileUploadTempStore(request)
return files.CustomFileUploadWidget(
tmpstore,
show_delete_control=True,
)
@colander.deferred
......@@ -111,49 +124,93 @@ class CompanySchema(colander.MappingSchema):
name = colander.SchemaNode(
colander.String(),
widget=deferred_edit_adminonly_widget,
title=u'Nom'
title=u'Nom',
section=u'Informations publiques',
)
goal = colander.SchemaNode(
colander.String(),
title=u"Descriptif de l'activité")
title=u"Descriptif de l'activité",
section=u'Informations publiques',
)
activities = CompanyActivitySchema(
title=u"Domaines d'activité",
missing=colander.drop,
section=u'Informations publiques',
)
email = forms.mail_node(missing=u'')
email = forms.mail_node(
missing=u'',
section=u'Informations publiques',
)
phone = colander.SchemaNode(
colander.String(),
title=u'Téléphone',
missing=u'')
colander.String(),
title=u'Téléphone',
section=u'Informations publiques',
missing=u'',
)
mobile = colander.SchemaNode(
colander.String(),
title=u'Téléphone portable',
missing=u'')
colander.String(),
title=u'Téléphone portable',
section=u'Informations publiques',
missing=u'',
)
address = colander.SchemaNode(
colander.String(),
title=u"Adresse",
section=u'Informations publiques',
missing=u'',
)
zip_code = colander.SchemaNode(
colander.String(),
title=u"Code postal",
section=u'Informations publiques',
missing=u'',
)
city = colander.SchemaNode(
colander.String(),
title=u"Ville",
section=u'Informations publiques',
missing=u'',
)
country = colander.SchemaNode(
colander.String(),
title=u"Pays",
section=u'Informations publiques',
missing=u'France',
)
logo = files.ImageNode(
widget=deferred_upload_logo_widget,
title="Choisir un logo",
section=u"Informations publiques",
missing=colander.drop,
)
header = files.ImageNode(
widget=deferred_upload_header_widget,
title=u'Entête des fichiers PDF',
section=u"Personalisation des documents",
missing=colander.drop,
description=u"Le fichier est idéalement au format 5/1 (par exemple \
1000px x 200 px)",
1000px x 200 px). Remplace l'en-tête par défaut qui utilise les Informations publiques. ",
)
# Fields specific to the treasury
code_compta = colander.SchemaNode(
colander.String(),
title=u"Compte analytique",
description=u"Compte analytique utilisé dans le logiciel de \
colander.String(),
title=u"Compte analytique",
section='Paramètres techniques (compta, gestion)',
description=u"Compte analytique utilisé dans le logiciel de \
comptabilité",
missing="")
missing="",
)
# general_customer_account = colander.SchemaNode(
# colander.String(),
......@@ -209,36 +266,42 @@ comptabilité",
# )
contribution = colander.SchemaNode(
QuantityType(),
widget=deform.widget.TextInputWidget(
input_append="%",
css_class="col-md-1"
),
validator=colander.Range(
min=0,
max=100,
min_err=u"Veuillez fournir un nombre supérieur à 0",
max_err=u"Veuillez fournir un nombre inférieur à 100"),
title=u"Contribution à la CAE",
missing=colander.drop,
description=u"Pourcentage que cette entreprise contribue à la CAE",
QuantityType(),
widget=deform.widget.TextInputWidget(
input_append="%",
css_class="col-md-1"
),
validator=colander.Range(
min=0,
max=100,
min_err=u"Veuillez fournir un nombre supérieur à 0",
max_err=u"Veuillez fournir un nombre inférieur à 100"),
title=u"Contribution à la CAE",
section='Paramètres techniques (compta, gestion)',
missing=colander.drop,
description=u"Pourcentage que cette entreprise contribue à la CAE",
)
cgv = forms.textarea_node(
title=u"Conditions générales complémentaires",
section=u"Personalisation des documents",
richwidget=True,
missing="",
)
RIB = colander.SchemaNode(
colander.String(),
title=u'RIB',
missing=u'')
colander.String(),
title=u'RIB',
section='Paramètres techniques (compta, gestion)',
missing=u'',
)
IBAN = colander.SchemaNode(
colander.String(),
title=u'IBAN',
missing=u'')
colander.String(),
title=u'IBAN',
section='Paramètres techniques (compta, gestion)',
missing=u'',
)
come_from = forms.come_from_node()
......
......@@ -25,19 +25,45 @@ from endi.forms.validators import validate_image_mime
class CustomFileUploadWidget(deform.widget.FileUploadWidget):
"""
file upload widget that handles filters when deserializing
File upload widget that handles:
- filters when deserializing
- file deletion (via a checkbox)
filters
An optionnal list (or simple filter) that will be fired on the datas
(for example in order to reduce image sizes)
show_delete_control (default :False)
Display a checkbox to allow deleting the file from form ("clearing"
file field).
"""
template = "fileupload.pt"
def __init__(self, *args, **kwargs):
self.show_delete_control = kwargs.pop('show_delete_control', False)
super(CustomFileUploadWidget, self).__init__(*args, **kwargs)
@property
def _pstruct_schema(self):
# Overrides a private attribute form FileUploadWidget
pstruct_schema = deform.widget.FileUploadWidget._pstruct_schema.clone()
delete_field_node = colander.SchemaNode(
colander.String(allow_empty=True),
name="delete",
missing=None,
)
pstruct_schema.add(delete_field_node)
return pstruct_schema
def deserialize(self, field, pstruct):
data = deform.widget.FileUploadWidget.deserialize(self, field, pstruct)
# We're returning the datas in the appstruct dict, we format the file if
# needed
uid = self._pstruct_schema.deserialize(pstruct).get('uid')
if hasattr(data, 'has_key') and 'fp' in data:
data['fp'].seek(0)
......@@ -49,6 +75,9 @@ class CustomFileUploadWidget(deform.widget.FileUploadWidget):
data['size'] = len(data['fp'].read())
data['fp'].seek(0)
data['name'] = data['filename']
elif pstruct.get('delete') and uid and self.show_delete_control:
data = {'delete': True}
return data
......@@ -104,13 +133,14 @@ class FileNode(colander.SchemaNode):
"""
request = self.bindings['request']
max_filesize = self._get_max_allowed_file_size(request)
file_obj = value.get('fp')
if file_obj:
file_obj.seek(0)
size = len(file_obj.read())
if size > max_filesize:
message = u"Ce fichier est trop volumineux"
raise colander.Invalid(node, message)
if value:
file_obj = value.get('fp')
if file_obj:
file_obj.seek(0)
size = len(file_obj.read())
if size > max_filesize:
message = u"Ce fichier est trop volumineux"
raise colander.Invalid(node, message)
def _get_max_allowed_file_size(self, request):
"""
......
......@@ -11,7 +11,7 @@ def validate_image_mime(node, value):
"""
Validate mime types for image files
"""
if value.get('mimetype'):
if value and value.get('mimetype'):
if not value['mimetype'].startswith('image/'):
message = u"Veuillez télécharger un fichier de type jpg, png, \
bmp ou gif"
......
......@@ -105,6 +105,58 @@ class Company(DBBASE, PersistentACLMixin):
),
group='edit'
)
address = deferred(
Column(
"address",
String(255),
info={
'colanderalchemy': {
'title': u'Adresse',
}
},
default="",
),
group='edit'
)
zip_code = deferred(
Column(
"zip_code",
String(20),
info={
'colanderalchemy': {
'title': u'Code postal',
},
},
default="",
),
group='edit',
)
city = deferred(
Column(
"city",
String(255),
info={
'colanderalchemy': {
'title': u'Ville',
}
},
default="",
),
group='edit',
)
country = deferred(
Column(
"country",
String(150),
info={
'colanderalchemy': {
'title': u'Pays'
},
},
default=u'France',
),
group='edit',
)
comments = deferred(
Column(
"comments",
......@@ -368,21 +420,24 @@ class Company(DBBASE, PersistentACLMixin):
@header.setter
def header(self, appstruct):
if self.header_file is None:
from endi.models.files import File
self.header_file = File()
if self.header_file is not None and appstruct.get('delete'):
DBSESSION().delete(self.header_file)
else:
if self.header_file is None:
from endi.models.files import File
self.header_file = File()
appstruct['filename'] = appstruct['name'] = 'header.png'
appstruct['mimetype'] = 'image/png'
appstruct['filename'] = appstruct['name'] = 'header.png'
appstruct['mimetype'] = 'image/png'
data = appstruct.pop('data', None)
for key, value in appstruct.items():
setattr(self.header_file, key, value)
data = appstruct.pop('data', None)
for key, value in appstruct.items():
setattr(self.header_file, key, value)
if data is not None:
self.header_file.data = data
if data is not None:
self.header_file.data = data
self.header_file.description = 'Header'
self.header_file.description = 'Header'
@property
def logo(self):
......@@ -390,14 +445,17 @@ class Company(DBBASE, PersistentACLMixin):
@logo.setter
def logo(self, appstruct):
if self.logo_file is None:
from endi.models.files import File
self.logo_file = File()
self.logo_file.name = appstruct.get('name', 'logo.png')
for key, value in appstruct.items():
setattr(self.logo_file, key, value)
self.logo_file.description = 'Logo'
if self.logo_file is not None and appstruct.get('delete'):
DBSESSION().delete(self.logo_file)
else:
if self.logo_file is None:
from endi.models.files import File
self.logo_file = File()
self.logo_file.name = appstruct.get('name', 'logo.png')
for key, value in appstruct.items():
setattr(self.logo_file, key, value)
self.logo_file.description = 'Logo'
@classmethod
def query(cls, keys=None, active=True):
......
......@@ -22,6 +22,11 @@ from endi.scripts.utils import (
)
def set_if_present(obj, field_name, value):
if getattr(obj, field_name):
setattr(obj, field_name, value)
class Anonymizer(object):
def __init__(self, logger):
try:
......@@ -146,10 +151,16 @@ class Anonymizer(object):
comp.phone = self.faker.phone_number()
comp.mobile = self.faker.phone_number()
comp.email = self.faker.ascii_safe_email()
header = build_header(
u"{0}\n {1} - {2}".format(comp.name, comp.phone, comp.email)
)
comp.header = {'name': 'header.png', 'data': header}
set_if_present(comp, 'address', self.faker.street_address())
set_if_present(comp, 'city', self.faker.city())
set_if_present(comp, 'zip_code', self._zipcode())
if comp.country and comp.country != 'France':
comp.country = self.faker.country()
if comp.header:
header = build_header(
u"{0}\n {1} - {2}".format(comp.name, comp.phone, comp.email)
)
comp.header = {'name': 'header.png', 'data': header}
self.session.execute(u"update company set cgv=''")
def _an_competence(self):
......
html {
font-size: 11px; }
.default-header {
border-bottom: 0.01cm solid #cdcdcd;
width: 100%;
height: 3.8cm; }
.default-header .logo-col {
text-align: right; }
.addressblock {
position: relative;
padding-left: 9.3cm;
......
html {
font-size: 16px !important; }
.default-header {
border-bottom: 0.01cm solid #cdcdcd;
width: 100%;
height: 20vw; }
.default-header .logo-col {
text-align: right; }
.addressblock {
position: relative;
padding-left: 50%;
......
<%def name='esc(datas)'><%text>${</%text>${datas}<%text>}</%text>\
</%def>
<%def name='address(datas, _type)'>
%if _type == 'customer' or _type == 'supplier':
<address>
<b>${datas.label}</b><br />
${datas.address}<br />
${datas.zip_code} ${datas.city}
% if datas.country and datas.country!= 'France':
<br />${datas.country}
% endif
</address>
%elif _type == 'company':
<address>
<b>${datas.name}</b><br />
</address>
%endif
</%def>
<%def name="format_text(data, breaklines=True)">
<%doc>
Replace \n with br for html output
......@@ -139,6 +123,21 @@
</a>
</li>
</%def>
<%def name="format_address(obj, multiline=False)">
<% separator = '<br />' if multiline else ', ' %>
% if obj.address:
${obj.address}${separator if obj.city else '' | n}
${obj.zip_code} ${obj.city.upper()}
% endif
% if obj.country and obj.country != 'France':
% if multiline:
${separator | n}${obj.country}
% else:
(${obj.country})
% endif
% endif
</%def>
<%def name="format_company(company)">
<h3>
<a href="${request.route_path('company', id=company.id)}">Entreprise ${company.name}</a>
......@@ -160,6 +159,22 @@
<dd>${format_phone(getattr(company, attr))}</dd>
% endif
% endfor
% if company.address or company.country or company.city:
<dt>Adresse</dt>
<dd>
${format_address(company, multiline=False)}
</dd>
% endif
% if company.activities:
<dt>Domaine(s) d'activité</dt>
<dd>
<ul>
% for activity in company.activities:
<li>${activity.label}</li>
% endfor
</ul>
</dd>
% endif
% if request.has_permission('admin_treasury'):
<dt>Code comptable</dt>
<dd>${company.code_compta or u"Non renseigné"}</dd>
......@@ -187,17 +202,13 @@
Non renseigné
% endif
</dd>
<dt>RIB / IBAN</dt>
<dd>${company.RIB or u'Non renseigné'} / ${company.IBAN or u'Non renseigné'}</dd>
% endif
% if company.activities:
<dt>Domaine(s) d'activité</dt>
<dd>
<ul>
% for activity in company.activities:
<li>${activity.label}</li>
% endfor
</ul>
</dd>
% endif
<dt>CGV complémentaires</dt>
<dd>${company.cgv and u'Renseignées' or u"Non renseignées"}</dd>
<dt>En-tête des documents</dt>
<dd>${company.cgv and u'Personalisé (image)' or u"Par défaut"}</dd>
</dl>
</%def>
<%def name="format_filelist(parent_node, delete=False)">
......@@ -305,4 +316,4 @@
% for tag in tags:
<span class="label label-default">${tag.label}</span>
% endfor
</%def>
\ No newline at end of file
</%def>
......@@ -12,6 +12,10 @@
<span tal:content="cstruct.get('filename')"
id="${oid}-filename"/>
<label tal:condition="field.widget.show_delete_control">
<input type="checkbox" name="delete" id="${oid}-delete"/>
Supprimer
</label>
</div>
<input type="file" name="upload"
......
<%doc>
Task content rendering panel
Only renders the content of the page
Task content rendering panel
Only renders the content of the page
</%doc>
<%namespace file="/base/utils.mako" import="format_text" />
<%namespace file="/base/utils.mako" import="format_address" />
<%def name="table(title, datas, css='')">
<div class="title ${css}">
${title}
......@@ -13,7 +14,33 @@
</%def>
<div id='content'>
<div class='header'>
<img src='${api.img_url(company.header_file)}' alt='${company.name}'/>
% if company.header_file:
<img src='${api.img_url(company.header_file)}' alt='${company.name}'/>
% else:
<table class="default-header">
<tr>
<td class="info-col">
<p class="cae-details">
<em>${config.get('cae_business_name', '')}</em>
</p>
<div class="company-details">
<address>
<strong>${company.name}</strong><br />
${format_address(company, multiline=True)}
</address>
<p>
${company.email}<br />
${company.phone}<br />
${company.mobile}<br />
</p>
</div>
</td>
<td class="logo-col">
<img src='${api.img_url(company.logo_file)}' alt=''/>
</td>
</tr>
</table>
% endif
</div>
<div class='row'>
<div class='addressblock'>
......
......@@ -7,6 +7,8 @@ DATAS = {
"goal": u"Be the best",
"contribution": "80",
"submit": "submit",
"logo": {},
"header": {},
}
......
......@@ -119,7 +119,7 @@ def build_header(text, size=(1000, 250)):
:param str text: The text to write
:returns: The header image
:rtype: StrinIO instance populated with the image datas in PNG Format
:rtype: StringIO instance populated with the image datas in PNG Format
"""