html to pdf for ticket

This commit is contained in:
Jonas 12t 2021-10-31 13:56:57 +04:00
parent 41cd94e072
commit 8c768cfd5a
15 changed files with 1040 additions and 56 deletions

View File

@ -0,0 +1,149 @@
@font-face {
font-family: Libre Barcode;
src: url(librebarcode128-regular.ttf);
}
@font-face {
font-family: Barlow Condensed;
src: url(barlowcondensed-regular.otf);
}
@font-face {
font-family: Barlow Condensed;
font-weight: 300;
src: url(barlowcondensed-light.otf);
}
@font-face {
font-family: Barlow Condensed;
font-weight: 700;
src: url(barlowcondensed-bold.otf);
}
@page {
margin: 0;
size: landscape;
}
html {
align-content: center;
align-items: center;
background: #eef1f5;
display: flex;
font-family: Barlow Condensed, sans-serif;
height: 100%;
justify-content: center;
}
body {
background: #fff;
box-sizing: border-box;
color: #2A3239;
display: flex;
flex-wrap: wrap;
height: 8cm;
justify-content: space-between;
margin: 0;
width: 25cm;
}
section {
box-sizing: border-box;
}
dl {
columns: 4;
text-align: center;
}
dt {
font-size: 9pt;
font-weight: 700;
text-transform: uppercase;
}
dd {
margin-left: 0;
}
ul {
align-items: center;
display: flex;
list-style: none;
margin: 0;
padding-left: 0;
}
li {
font-weight: 700;
text-transform: uppercase;
}
#informations {
flex: 1;
padding: 0;
position: relative;
}
#informations h1 {
display: inline-block;
font-size: 25pt;
font-weight: 300;
text-transform: uppercase;
}
#informations #name {
margin-left: 1cm;
}
#informations #destination {
position: absolute;
right: 1cm;
}
#informations dl {
background: #2A3239;
color: #fff;
margin: 0;
padding: 1cm 0;
}
#informations dd {
border-left: 1pt solid #fff;
font-size: 35pt;
}
#informations dd:first-of-type {
border-left: 0;
}
#informations ul {
margin-left: 1cm;
}
#informations li {
font-weight: 300;
padding: 0.15cm;
}
#informations li:first-of-type {
background: #2A3239;
border-radius: 4pt;
color: #fff;
}
#informations li:last-of-type {
font-family: Libre Barcode, cursive;
font-size: 25pt;
margin-left: auto;
padding-right: 1cm;
padding-top: 0.5cm;
}
#ticket {
border-left: 1pt dashed #2A3239;
display: flex;
flex-direction: column;
height: 8cm;
justify-content: space-around;
padding: 0 1cm;
}
#ticket h2 {
font-weight: 300;
margin: 0;
text-transform: uppercase;
}
#ticket p {
font-family: Libre Barcode, cursive;
font-size: 25pt;
margin: 0;
text-align: center;
}
#ticket dl {
margin: 0;
}
#ticket li {
margin: 0 0.25cm;
}

View File

@ -0,0 +1,611 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="fr">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
<!-- [ if !mso]> <!-->
<meta content="IE=edge" http-equiv="X-UA-Compatible"/>
<!-- <![endif] -->
<meta content="telephone=no" name="format-detection"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Vos billets</title>
<style type="text/css">
.ExternalClass {
width: 100%;
}
.ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div, .ExternalClass b, .ExternalClass br, .ExternalClass img {
line-height: 100% !important;
}
/* iOS BLUE LINKS */
.appleBody a {
color: #212121;
text-decoration: none;
}
.appleFooter a {
color: #212121 !important;
text-decoration: none !important;
}
/* END iOS BLUE LINKS */
img {
color: #ffffff;
text-align: center;
font-family: Open Sans, Helvetica, Arial, sans-serif;
display: block;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100% !important;
-ms-text-size-adjust: 100% !important;
font-family: 'Open Sans', Helvetica, Arial, sans-serif !important;
}
body, #body_style {
background: #fffffe;
}
table td {
border-collapse: collapse;
border-spacing: 0 !important;
}
table tr {
border-collapse: collapse;
border-spacing: 0 !important;
}
table tbody {
border-collapse: collapse;
border-spacing: 0 !important;
}
table {
border-collapse: collapse;
border-spacing: 0 !important;
}
span.yshortcuts, a span.yshortcuts {
color: #000001;
background-color: none;
border: none;
}
span.yshortcuts:hover,
span.yshortcuts:active,
span.yshortcuts:focus {
color: #000001;
background-color: none;
border: none;
}
img {
-ms-interpolation-mode:: bicubic;
}
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/**** My desktop styles ****/
@media only screen and (min-width: 600px) {
.noDesk {
display: none !important;
}
.td-padding {
padding-left: 15px !important;
padding-right: 15px !important;
}
.padding-container {
padding: 0px 15px 0px 15px !important;
mso-padding-alt: 0px 15px 0px 15px !important;
}
.mobile-column-left-padding {
padding: 0px 0px 0px 0px !important;
mso-alt-padding: 0px 0px 0px 0px !important;
}
.mobile-column-right-padding {
padding: 0px 0px 0px 0px !important;
mso-alt-padding: 0px 0px 0px 0px !important;
}
.mobile {
display: none !important
}
}
/**** My mobile styles ****/
@media only screen and (max-width: 599px) and (-webkit-min-device-pixel-ratio: 1) {
*[class].wrapper {
width: 100% !important;
}
*[class].container {
width: 100% !important;
}
*[class].mobile {
width: 100% !important;
display: block !important;
}
*[class].image {
width: 100% !important;
height: auto;
}
*[class].center {
margin: 0 auto !important;
text-align: center !important;
}
*[class="mobileOff"] {
width: 0px !important;
display: none !important;
}
*[class*="mobileOn"] {
display: block !important;
max-height: none !important;
}
p[class="mobile-padding"] {
padding-left: 0px !important;
padding-top: 10px;
}
.padding-container {
padding: 0px 15px 0px 15px !important;
mso-padding-alt: 0px 15px 0px 15px !important;
}
.hund {
width: 100% !important;
height: auto !important;
}
.td-padding {
padding-left: 15px !important;
padding-right: 15px !important;
}
.mobile-column-left-padding {
padding: 18px 0px 18px 0px !important;
mso-alt-padding: 18px 0px 18px 0px !important;
}
.mobile-column-right-padding {
padding: 18px 0px 0px 0px !important;
mso-alt-padding: 18px 0px 0px 0px !important;
}
.stack {
width: 100% !important;
}
img {
width: 100% !important;
height: auto !important;
}
*[class="hide"] {
display: none !important
}
*[class="Gmail"] {
display: none !important
}
.Gmail {
display: none !important
}
.bottom-padding-fix {
padding: 0px 0px 18px 0px !important;
mso-alt-padding: 0px 0px 18px 0px;
}
}
.social, .social:active {
opacity: 1 !important;
transform: scale(1);
transition: all .2s !important;
}
.social:hover {
opacity: 0.8 !important;
transform: scale(1.1);
transition: all .2s !important;
}
.button.raised {
transition: box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1);
transition: all .2s;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26);
}
.button.raised:hover {
box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2);
transition: all .2s;
-webkit-box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2);
transition: all .2s;
-moz-box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2);
transition: all .2s;
}
.card-1 {
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .16), 0 2px 10px 0 rgba(0, 0, 0, .12);
-webkit-box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .16), 0 2px 10px 0 rgba(0, 0, 0, .12);
-moz-box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .16), 0 2px 10px 0 rgba(0, 0, 0, .12);
transition: box-shadow .45s;
}
.card-1:hover {
box-shadow: 0 8px 17px 0 rgba(0, 0, 0, .2), 0 6px 20px 0 rgba(0, 0, 0, .19);
-webkit-box-shadow: 0 8px 17px 0 rgba(0, 0, 0, .2), 0 6px 20px 0 rgba(0, 0, 0, .19);
-moz-box-shadow: 0 8px 17px 0 rgba(0, 0, 0, .2), 0 6px 20px 0 rgba(0, 0, 0, .19);
transition: box-shadow .45s;
}
.ripplelink {
display: block
color: #fff;
text-decoration: none;
position: relative;
overflow: hidden;
-webkit-transition: all 0.2s ease;
-moz-transition: all 0.2s ease;
-o-transition: all 0.2s ease;
transition: all 0.2s ease;
z-index: 0;
}
.ripplelink:hover {
z-index: 1000;
}
.ink {
display: block;
position: absolute;
background: rgba(255, 255, 255, 0.3);
border-radius: 100%;
-webkit-transform: scale(0);
-moz-transform: scale(0);
-o-transform: scale(0);
transform: scale(0);
}
.animate {
-webkit-animation: ripple 0.65s linear;
-moz-animation: ripple 0.65s linear;
-ms-animation: ripple 0.65s linear;
-o-animation: ripple 0.65s linear;
animation: ripple 0.65s linear;
}
@-webkit-keyframes ripple {
100% {
opacity: 0;
-webkit-transform: scale(2.5);
}
}
@-moz-keyframes ripple {
100% {
opacity: 0;
-moz-transform: scale(2.5);
}
}
@-o-keyframes ripple {
100% {
opacity: 0;
-o-transform: scale(2.5);
}
}
@keyframes ripple {
100% {
opacity: 0;
transform: scale(2.5);
}
}
</style>
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
</head>
<body style="margin:0; padding:0; background-color: #eeeeee;" bgcolor="#eeeeee">
<!--[if mso]>
<style type="text/css">
body, table, td {
font-family: Arial, Helvetica, sans-serif !important;
}
</style>
<![endif]-->
<!-- START EMAIL -->
<table width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#eeeeee">
<div class="Gmail"
style="height: 1px !important; margin-top: -1px !important; max-width: 600px !important; min-width: 600px !important; width: 600px !important;"></div>
<div style="display: none; max-height: 0px; overflow: hidden;">
Vos billets pour {{ config.organisation }}
</div>
<!-- Insert &zwnj;&nbsp; hack after hidden preview text -->
<div style="display: none; max-height: 0px; overflow: hidden;">
&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
</div>
<!-- START LOGO -->
<tr>
<td width="100%" valign="top" align="center" class="padding-container"
style="padding: 18px 0px 18px 0px!important; mso-padding-alt: 18px 0px 18px 0px;">
<table width="600" cellpadding="0" cellspacing="0" border="0" align="center" class="wrapper">
<tr>
<td align="center">
<table cellpadding="0" cellspacing="0" border="0">
<tr>
<td width="100%" valign="top" align="center">
<table width="600" cellpadding="0" cellspacing="0" border="0" align="center"
class="wrapper" bgcolor="#eeeeee">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" border="0"
class="container" align="center">
<!-- START HEADER IMAGE -->
<tr>
<td align="center" class="hund" width="600">
<a href="www.tibillet.re">
<img src="https://wiki.tibillet.re/images/c/ca/Logo_Tibillet_Noir_Ombre.png"
width="200" alt="Logo" border="0"
style="max-width: 200px; display:block; ">
</a>
</td>
</tr>
<!-- END HEADER IMAGE -->
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- END LOGO -->
<!-- START CARD 1 -->
<tr>
<td width="100%" valign="top" align="center" class="padding-container"
style="padding-top: 0px!important; padding-bottom: 18px!important; mso-padding-alt: 0px 0px 18px 0px;">
<table width="600" cellpadding="0" cellspacing="0" border="0" align="center" class="wrapper">
<tr>
<td>
<table cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="border-radius: 3px; border-bottom: 2px solid #d4d4d4;" class="card-1"
width="100%" valign="top" align="center">
<table style="border-radius: 3px;" width="600" cellpadding="0" cellspacing="0"
border="0" align="center" class="wrapper" bgcolor="#ffffff">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" border="0"
class="container">
<!-- START HEADER IMAGE -->
<tr>
<td align="center" class="hund ripplelink" width="600">
<img align="center" width="600"
style="border-radius: 3px 3px 0px 0px; width: 100%; max-width: 600px!important"
class="hund"
src="{{ config.img.med }}">
</td>
</tr>
<!-- END HEADER IMAGE -->
<!-- START BODY COPY -->
<tr>
<td class="td-padding" align="left"
style="font-family: 'Roboto Mono', monospace; color: #212121!important; font-size: 24px; line-height: 30px; padding-top: 18px; padding-left: 18px!important; padding-right: 18px!important; padding-bottom: 0px!important; mso-line-height-rule: exactly; mso-padding-alt: 18px 18px 0px 13px;">
{{ config.organisation }}
</td>
</tr>
<tr>
<td class="td-padding" align="left"
style="font-family: 'Roboto Mono', monospace; color: #212121!important; font-size: 16px; line-height: 24px; padding-top: 18px; padding-left: 18px!important; padding-right: 18px!important; padding-bottom: 0px!important; mso-line-height-rule: exactly; mso-padding-alt: 18px 18px 0px 18px;">
Bonjour !
<br><br>
Nous vous confirmons votre achat
d'un total de {{ reservation.total_paid | floatformat:2 }}€.
<strong>Grand merci pour votre reservation !</strong>
<br><br>
{% if reservation.tickets %}
Vous trouverez vos tickets en pièce jointe.
{% else %}
Aucun ticket pour un concert ? Etrange etrange
{% endif %}
</td>
</tr>
<!-- END BODY COPY -->
<!-- BUTTON -->
<tr>
<td align="left"
style="padding: 18px 18px 18px 18px; mso-alt-padding: 18px 18px 18px 18px!important;">
<table width="100%" border="0" cellspacing="0"
cellpadding="0">
<tr>
<td>
<table border="0" cellspacing="0"
cellpadding="0">
<tr>
<td align="left"
style="border-radius: 3px;"
bgcolor="#17bef7">
<a class="button raised"
href="{{ config.domaine_cashless }}/rapport/invoice/{{ adhesion.commande }}"
target="_blank"
style="font-size: 14px; line-height: 14px; font-weight: 500; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; border-radius: 3px; padding: 10px 25px; border: 1px solid #17bef7; display: inline-block;">
TELECHARGER REÇU</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- END BUTTON -->
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- END CARD 1 -->
<!-- START CARD 2 -->
<tr>
<td width="100%" valign="top" align="center" class="padding-container"
style="padding-top: 0px!important; padding-bottom: 18px!important; mso-padding-alt: 0px 0px 18px 0px;">
<table width="600" cellpadding="0" cellspacing="0" border="0" align="center" class="wrapper">
<tr>
<td>
<table cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="border-radius: 3px; border-bottom: 2px solid #d4d4d4;" class="card-1"
width="100%" valign="top" align="center">
<table style="border-radius: 3px;" width="600" cellpadding="0" cellspacing="0"
border="0" align="center" class="wrapper" bgcolor="#ffffff">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" border="0"
class="container">
<!-- START HEADER IMAGE -->
<tr>
<td align="center" class="hund ripplelink" width="600">
<img align="center" width="600"
style="border-radius: 3px 3px 0px 0px; width: 100%; max-width: 600px!important"
class="hund"
src="https://wiki.tibillet.re/images/0/0d/Logo_Tibillet_Noir_Ombre_700px.png">
</td>
</tr>
<!-- END HEADER IMAGE -->
<!-- START BODY COPY -->
<tr>
<td class="td-padding" align="left"
style="font-family: 'Roboto Mono', monospace; color: #212121!important; font-size: 24px; line-height: 30px; padding-top: 18px; padding-left: 18px!important; padding-right: 18px!important; padding-bottom: 0px!important; mso-line-height-rule: exactly; mso-padding-alt: 18px 18px 0px 13px;">
Vous entrez maintenant dans le réseau TiBillet !
</td>
</tr>
<tr>
<td class="td-padding" align="left"
style="font-family: 'Roboto Mono', monospace; color: #212121!important; font-size: 16px; line-height: 24px; padding-top: 18px; padding-left: 18px!important; padding-right: 18px!important; padding-bottom: 0px!important; mso-line-height-rule: exactly; mso-padding-alt: 18px 18px 0px 18px;">
<a href="https://www.tibillet.re">TiBillet</a> est une
solution coopérative et open-source de gestion associative,
de cashless et de billetterie orientée économie
sociale et solidaire.
<br><br>
Vous pouvez acheter des billets dans tout le reseau avec
votre
carte cashless, recharger cette dernière en ligne ou sur
place,
et en profiter dans tout les lieux partenaires !
<br><br>
Vous êtes artiste ? organisateur ? Vous pouvez créer votre
espace
TiBillet en quelques clics et démarcher dans tout le reseau
coopératif !
<br><br>
Si vous voulez en savoir plus, n'hésitez pas à visiter notre
<a href="https://wiki.tibillet.re">wiki</a> !
<br><br>
</td>
</tr>
<!-- END BODY COPY -->
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- END CARD 2 -->
<!-- SPACER -->
<!--[if gte mso 9]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600" height="18">
<![endif]-->
<tr>
<td height="18px"></td>
</tr>
<!--[if gte mso 9]>
</td>
</tr>
</table>
<![endif]-->
<!-- END SPACER -->
<!-- SPACER -->
<!--[if gte mso 9]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600" height="36">
<![endif]-->
<tr>
<td height="36px"></td>
</tr>
<!--[if gte mso 9]>
</td>
</tr>
</table>
<![endif]-->
<!-- END SPACER -->
</table>
<!-- END EMAIL -->
<div style="display:none; white-space:nowrap; font:15px courier; line-height:0;">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</div>
</body>
</html>

View File

@ -0,0 +1,53 @@
<html>
<head>
{% load static %}
<meta charset="utf-8">
<link rel="stylesheet" href="{% static 'ticket/ticket.css' %}"/>
<title>Boarding ticket</title>
<meta name="description" content="Boarding ticket">
</head>
<body>
<section id="informations">
<h1 id="name">Théodore Marcelin</h1>
<h1 id="destination">CDG ✈ LFLL</h1>
<dl>
<dt>Flight</dt>
<dd>DL31</dd>
<dt>Gate</dt>
<dd>29</dd>
<dt>Seat</dt>
<dd>26E</dd>
<dt>Zone</dt>
<dd>4</dd>
</dl>
<ul>
<li>5:10pm</li>
<li>Dec 15, 2018</li>
<li>Coach</li>
<li>1257797706706</li>
</ul>
</section>
<section id="ticket">
<p>1257797706706</p>
<h2>Théodore Marcelin</h2>
<dl>
<dt>Flight</dt>
<dd>DL31</dd>
<dt>Gate</dt>
<dd>29</dd>
<dt>Seat</dt>
<dd>26E</dd>
<dt>Zone</dt>
<dd>4</dd>
</dl>
<ul>
<li>CDG ✈ LFLL</li>
<li>5:10pm</li>
</ul>
</section>
</body>
</html>

Binary file not shown.

View File

@ -0,0 +1,98 @@
import os
import threading
from django.core.mail import send_mail, EmailMessage, EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils import timezone
from weasyprint import HTML
from BaseBillet.models import Configuration, Reservation, Ticket
import logging
logger = logging.getLogger(__name__)
'''
from ApiBillet.thread_mailer import ThreadMaileur
config = Configuration.get_solo()
context = {'config': config, }
mail = ThreadMaileur('jturbeaux@pm.me', "Vos Billets", template='mails/ticket.html', context=context)
mail.send_with_tread()
'''
class ThreadMaileur():
def __init__(self, email, title, text=None, html=None, template=None, context=None):
self.title = title
self.email = email
self.text = text
self.html = html
self.config = Configuration.get_solo()
self.context = None
if template and context :
self.html = render_to_string(template, context=context)
self.context = context
self.attached_file = self._attached_file()
def _attached_file(self):
attached_file = []
if self.context :
if self.context.get('reservation'):
reservation: Reservation = self.context.get('reservation')
tickets = reservation.tickets.filter(status=Ticket.NOT_SCANNED)
if len(tickets) > 0:
for ticket in tickets :
attached_file.append(render_to_string('ticket/ticket.html', context={'context': 'context'}))
return attached_file
def config_valid(self):
EMAIL_HOST = os.environ.get('EMAIL_HOST')
EMAIL_PORT = os.environ.get('EMAIL_PORT')
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
if EMAIL_HOST and EMAIL_PORT and EMAIL_HOST_USER and EMAIL_HOST_PASSWORD and self.config.email :
return True
else:
return False
def send(self):
if self.html and self.config_valid() :
logger.info(f' send_mail')
mail = EmailMultiAlternatives(
self.title,
self.text,
self.config.email,
[self.email,],
)
mail.attach_alternative(self.html, "text/html")
# msg = EmailMessage(subject, html_content, from_email, [to])
# msg.content_subtype = "html" # Main content is now text/html
# msg.send()
# import ipdb; ipdb.set_trace()
i=1
for file in self.attached_file:
html_before_pdf = HTML(string=file)
mail.attach(f'ticket_{i}.pdf', html_before_pdf.write_pdf(), 'application/pdf')
i += 1
mail_return = mail.send(fail_silently=False)
if mail_return == 1 :
logger.info(f' mail envoyé : {mail_return} - {self.email}')
else :
logger.error(f' mail non envoyé : {mail_return} - {self.email}')
return mail
else :
logger.error(f'Pas de contenu HTML ou de configuration email valide')
raise ValueError('Pas de contenu HTML ou de configuration email valide')
def send_with_tread(self):
self.send()
# logger.info(f'{timezone.now()} on lance le thread email {self.email}')
# thread_email = threading.Thread(target=self.send)
# thread_email.start()
# logger.info(f'{timezone.now()} Thread email lancé')

View File

@ -4,6 +4,8 @@ from django.urls import include, path, re_path
from ApiBillet import views as api_view from ApiBillet import views as api_view
from rest_framework import routers from rest_framework import routers
from ApiBillet.views import TicketPdf
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register(r'events', api_view.EventsViewSet, basename='event') router.register(r'events', api_view.EventsViewSet, basename='event')
router.register(r'products', api_view.ProductViewSet, basename='product') router.register(r'products', api_view.ProductViewSet, basename='product')
@ -13,4 +15,6 @@ router.register(r'reservations', api_view.ReservationViewset, basename='reservat
urlpatterns = [ urlpatterns = [
path('', include(router.urls)), path('', include(router.urls)),
path('ticket/<uuid:pk_uuid>', TicketPdf.as_view()),
] ]

View File

@ -1,17 +1,25 @@
from datetime import datetime
from django.http import Http404
from django.shortcuts import render from django.shortcuts import render
# Create your views here. # Create your views here.
from django.utils import timezone
from django_weasyprint import WeasyTemplateView
from rest_framework.generics import get_object_or_404 from rest_framework.generics import get_object_or_404
from rest_framework.permissions import AllowAny
from rest_framework.response import Response from rest_framework.response import Response
from ApiBillet.serializers import EventSerializer, PriceSerializer, ProductSerializer, ReservationSerializer, \ from ApiBillet.serializers import EventSerializer, PriceSerializer, ProductSerializer, ReservationSerializer, \
ReservationValidator ReservationValidator
from AuthBillet.models import TenantAdminPermission from AuthBillet.models import TenantAdminPermission
from Customers.models import Client, Domain from Customers.models import Client, Domain
from BaseBillet.models import Event, Price, Product, Reservation from BaseBillet.models import Event, Price, Product, Reservation, Configuration, Ticket
from rest_framework import viewsets, permissions, status from rest_framework import viewsets, permissions, status
import os import os
import logging
logger = logging.getLogger(__name__)
def new_tenants(schema_name): def new_tenants(schema_name):
@ -130,7 +138,28 @@ class ReservationViewset(viewsets.ViewSet):
return Response(validator.data, status=status.HTTP_201_CREATED) return Response(validator.data, status=status.HTTP_201_CREATED)
return Response(validator.errors, status=status.HTTP_400_BAD_REQUEST) return Response(validator.errors, status=status.HTTP_400_BAD_REQUEST)
def get_permissions(self): def get_permissions(self):
permission_classes = [TenantAdminPermission] permission_classes = [TenantAdminPermission]
return [permission() for permission in permission_classes] return [permission() for permission in permission_classes]
class TicketPdf(WeasyTemplateView):
permission_classes = [AllowAny]
template_name = 'ticket/ticket.html'
def get_context_data(self, pk_uuid, **kwargs):
logger.info(f"{timezone.now()} création de pdf demandé. uuid : {pk_uuid}")
self.config = Configuration.get_solo()
ticket: Ticket = get_object_or_404(Ticket, uuid=pk_uuid)
kwargs['ticket'] = ticket
kwargs['config'] = self.config
self.nom_prenom = f"{ticket.first_name.upper()}_{ticket.last_name.capitalize()}"
return kwargs
def get_pdf_filename(self, **kwargs):
nom_prenom = self.nom_prenom
return f"Ticket_{nom_prenom}.pdf"
#

View File

@ -3,6 +3,7 @@ import uuid
import requests import requests
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import models from django.db import models
from django.db.models.aggregates import Sum
# Create your models here. # Create your models here.
from django.db.models import Q from django.db.models import Q
@ -314,8 +315,6 @@ class Event(models.Model):
verbose_name_plural = _('Evenements') verbose_name_plural = _('Evenements')
class Reservation(models.Model): class Reservation(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True, db_index=True) uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True, db_index=True)
datetime = models.DateTimeField(auto_now=True) datetime = models.DateTimeField(auto_now=True)
@ -337,6 +336,7 @@ class Reservation(models.Model):
status = models.CharField(max_length=3, choices=TYPE_CHOICES, default=UNPAID, status = models.CharField(max_length=3, choices=TYPE_CHOICES, default=UNPAID,
verbose_name=_("Status de la réservation")) verbose_name=_("Status de la réservation"))
mail_send = models.BooleanField(default=False)
# paiement = models.OneToOneField(Paiement_stripe, on_delete=models.PROTECT, blank=True, null=True, # paiement = models.OneToOneField(Paiement_stripe, on_delete=models.PROTECT, blank=True, null=True,
# related_name='reservation') # related_name='reservation')
@ -348,10 +348,30 @@ class Reservation(models.Model):
def user_mail(self): def user_mail(self):
return self.user_commande.email return self.user_commande.email
def paiements_paid(self):
return self.paiements.filter(
Q(status=Paiement_stripe.PAID) | Q(status=Paiement_stripe.VALID)
)
def articles_paid(self):
articles_paid = []
for paiement in self.paiements_paid():
for ligne in paiement.lignearticle_set.filter(
Q(status=LigneArticle.PAID) | Q(status=LigneArticle.VALID)
):
articles_paid.append(ligne)
return articles_paid
def total_paid(self):
total_paid = 0
for article in self.articles_paid():
article: LigneArticle
total_paid += article.price.prix * article.qty
return total_paid
def __str__(self): def __str__(self):
return f"{str(self.uuid).partition('-')[0]} - {self.user_commande.email}" return f"{str(self.uuid).partition('-')[0]} - {self.user_commande.email}"
# def total_billet(self): # def total_billet(self):
# total = 0 # total = 0
# for ligne in self.paiements.all(): # for ligne in self.paiements.all():
@ -372,8 +392,6 @@ class Reservation(models.Model):
# #
class Ticket(models.Model): class Ticket(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True, db_index=True) uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True, db_index=True)
@ -410,7 +428,6 @@ class Ticket(models.Model):
ordering = ('-datetime',) ordering = ('-datetime',)
class Paiement_stripe(models.Model): class Paiement_stripe(models.Model):
""" """
La commande La commande
@ -437,7 +454,8 @@ class Paiement_stripe(models.Model):
(CANCELED, 'Annulée'), (CANCELED, 'Annulée'),
) )
reservation = models.ForeignKey(Reservation, on_delete=models.PROTECT, blank=True, null=True) reservation = models.ForeignKey(Reservation, on_delete=models.PROTECT, blank=True, null=True,
related_name="paiements")
status = models.CharField(max_length=1, choices=STATUT_CHOICES, default=NON, verbose_name="Statut de la commande") status = models.CharField(max_length=1, choices=STATUT_CHOICES, default=NON, verbose_name="Statut de la commande")
@ -446,7 +464,8 @@ class Paiement_stripe(models.Model):
(QRCODE, _('Depuis scan QR-Code')), (QRCODE, _('Depuis scan QR-Code')),
(API_BILLETTERIE, _('Depuis billetterie')), (API_BILLETTERIE, _('Depuis billetterie')),
) )
source = models.CharField(max_length=1, choices=SOURCE_CHOICES, default=API_BILLETTERIE, verbose_name="Source de la commande") source = models.CharField(max_length=1, choices=SOURCE_CHOICES, default=API_BILLETTERIE,
verbose_name="Source de la commande")
total = models.FloatField(default=0) total = models.FloatField(default=0)
@ -461,8 +480,6 @@ class Paiement_stripe(models.Model):
[f"{ligne.product.name} {ligne.qty * ligne.product.prix}" for ligne in self.lignearticle_set.all()]) [f"{ligne.product.name} {ligne.qty * ligne.product.prix}" for ligne in self.lignearticle_set.all()])
class LigneArticle(models.Model): class LigneArticle(models.Model):
uuid = models.UUIDField(primary_key=True, db_index=True, default=uuid.uuid4) uuid = models.UUIDField(primary_key=True, db_index=True, default=uuid.uuid4)
datetime = models.DateTimeField(auto_now=True) datetime = models.DateTimeField(auto_now=True)
@ -494,4 +511,3 @@ class LigneArticle(models.Model):
return self.paiement_stripe.status return self.paiement_stripe.status
else: else:
return _('no stripe send') return _('no stripe send')

View File

@ -1,7 +1,11 @@
import requests import requests
from django.db import connection
from django.db.models import Q
from django.db.models.signals import post_save, pre_save from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone
from ApiBillet.thread_mailer import ThreadMaileur
from BaseBillet.models import Reservation, LigneArticle, Ticket, Product, Configuration, Paiement_stripe from BaseBillet.models import Reservation, LigneArticle, Ticket, Product, Configuration, Paiement_stripe
import logging import logging
@ -20,21 +24,6 @@ def trigger_reservation(sender, instance: Reservation, created, **kwargs):
ticket.save() ticket.save()
@receiver(pre_save, sender=LigneArticle)
def trigger_LigneArticle(sender, instance: LigneArticle, update_fields=None, **kwargs):
# if not created
if not instance._state.adding:
old_instance = sender.objects.get(pk=instance.pk)
new_instance = pre_save_signal_status(old_instance, instance)
@receiver(pre_save, sender=Paiement_stripe)
def trigger_paiement_stripe(sender, instance: Paiement_stripe, update_fields=None, **kwargs):
# if not create
if not instance._state.adding:
old_instance = sender.objects.get(pk=instance.pk)
new_instance = pre_save_signal_status(old_instance, instance)
######################################################################## ########################################################################
######################## SIGNAL PRE & POST SAVE ######################## ######################## SIGNAL PRE & POST SAVE ########################
@ -136,6 +125,8 @@ def expire_paiement_stripe(old_instance, new_instance):
def valide_stripe_paiement(old_instance, new_instance): def valide_stripe_paiement(old_instance, new_instance):
logger.info(f" TRIGGER PAIEMENT STRIPE valide_stripe_paiement {old_instance.status} to {new_instance.status}") logger.info(f" TRIGGER PAIEMENT STRIPE valide_stripe_paiement {old_instance.status} to {new_instance.status}")
pass pass
@ -143,8 +134,28 @@ def valide_stripe_paiement(old_instance, new_instance):
def send_billet_to_mail(old_instance, new_instance): def send_billet_to_mail(old_instance, new_instance):
logger.info(f" TRIGGER RESERVATION send_billet_to_mail {old_instance.status} to {new_instance.status}") if not new_instance.mail_send :
pass logger.info(f" TRIGGER RESERVATION send_billet_to_mail {old_instance.status} to {new_instance.status}")
new_instance : Reservation
config = Configuration.get_solo()
if new_instance.user_commande.email:
try:
mail = ThreadMaileur(
new_instance.user_commande.email,
f"Votre reservation pour {config.organisation}",
template='mails/buy_confirmation.html',
context={
'config': config,
'reservation': new_instance,
},
)
# import ipdb; ipdb.set_trace()
mail.send_with_tread()
except Exception as e :
logger.error(f"{timezone.now()} Erreur envoie de mail pour reservation {new_instance} : {e}")
else :
logger.info(f" TRIGGER RESERVATION mail déja envoyé {new_instance} : {new_instance.mail_send} - status : {old_instance.status} to {new_instance.status}")
######################## MOTEUR TRIGGER ######################## ######################## MOTEUR TRIGGER ########################
@ -165,7 +176,8 @@ TRANSITIONS = {
Reservation.PAID: send_billet_to_mail Reservation.PAID: send_billet_to_mail
}, },
Reservation.PAID: { Reservation.PAID: {
LigneArticle.PAID: send_billet_to_mail, Reservation.VALID: send_billet_to_mail,
Reservation.PAID: send_billet_to_mail,
'_else_': error_regression, '_else_': error_regression,
}, },
Reservation.VALID: { Reservation.VALID: {
@ -202,21 +214,26 @@ TRANSITIONS = {
}, },
} }
@receiver(pre_save)
def pre_save_signal_status(sender, instance, **kwargs):
# if not create
if not instance._state.adding:
sender_str = sender.__name__.upper()
dict_transition = TRANSITIONS.get(sender_str)
if dict_transition:
old_instance = sender.objects.get(pk=instance.pk)
new_instance = instance
def pre_save_signal_status(old_instance, new_instance): logger.info(f"dict_transition {sender_str} {new_instance} : {old_instance.status} to {new_instance.status}")
sender_str = old_instance.__class__.__name__.upper() transitions = dict_transition.get(old_instance.status, None)
dict_transition = TRANSITIONS.get(sender_str) if transitions:
if dict_transition: # Par ordre de préférence :
logger.info(f"dict_transition {sender_str} {new_instance} : {old_instance.status} to {new_instance.status}") trigger_function = transitions.get('_all_', (
transitions = dict_transition.get(old_instance.status, None) transitions.get(new_instance.status, (
if transitions: transitions.get('_else_', None)
# Par ordre de préférence : ))))
trigger_function = transitions.get('_all_', (
transitions.get(new_instance.status, (
transitions.get('_else_', None)
))))
if trigger_function: if trigger_function:
if not callable(trigger_function): if not callable(trigger_function):
raise Exception(f'Fonction {trigger_function} is not callable. Disdonc !?') raise Exception(f'Fonction {trigger_function} is not callable. Disdonc !?')
trigger_function(old_instance, new_instance) trigger_function(old_instance, new_instance)

View File

@ -229,11 +229,12 @@ class retour_stripe(View):
return HttpResponseRedirect(f"/qr/{ligne_article.carte.uuid}#erreurpaiement") return HttpResponseRedirect(f"/qr/{ligne_article.carte.uuid}#erreurpaiement")
elif paiement_stripe.source == Paiement_stripe.API_BILLETTERIE : elif paiement_stripe.source == Paiement_stripe.API_BILLETTERIE :
return HttpResponse( if paiement_stripe.status == Paiement_stripe.VALID :
'Coucou') return HttpResponse(
'Coucou')
else :
raise Http404('paiement_stripe.source ?') raise Http404(f'{paiement_stripe.status}')
''' '''

View File

@ -61,11 +61,17 @@ RUN pip install django-stdimage
RUN pip install stripe RUN pip install stripe
RUN apt-get install -y fonts-font-awesome
RUN apt-get install -y libffi-dev
RUN apt-get install -y libgdk-pixbuf2.0-0
RUN apt-get install -y libpango1.0-0
RUN apt-get install -y python-dev
# RUN apt-get install -y python-lxml
RUN apt-get install -y shared-mime-info
RUN apt-get install -y libcairo2
RUN pip install django-weasyprint
RUN apt-get -y clean
RUN python --version RUN python --version
RUN django-admin --version RUN django-admin --version