commit c66dccfaaa5e05d3a3430c61e2e94f076e41140a Author: Markus Zimmerberger Date: Tue Feb 13 15:50:44 2024 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5d6580e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules +payment_slips +sust +env +.vscode +__pycache__ \ No newline at end of file diff --git a/Sportunion quer 4c.jpg b/Sportunion quer 4c.jpg new file mode 100644 index 0000000..d0180a7 Binary files /dev/null and b/Sportunion quer 4c.jpg differ diff --git a/Sportunion quer 4c.png b/Sportunion quer 4c.png new file mode 100644 index 0000000..db280e7 Binary files /dev/null and b/Sportunion quer 4c.png differ diff --git a/Sportunion quer 4c_old.png b/Sportunion quer 4c_old.png new file mode 100644 index 0000000..88ffe99 Binary files /dev/null and b/Sportunion quer 4c_old.png differ diff --git a/coordinates_class.py b/coordinates_class.py new file mode 100644 index 0000000..6fa24cc --- /dev/null +++ b/coordinates_class.py @@ -0,0 +1,32 @@ +from sqlalchemy import func +from sqlalchemy.types import UserDefinedType, Float + +class EasyGeometry(UserDefinedType): + + def get_col_spec(self): + return "GEOMETRY" + + def bind_expression(self, bindvalue): + return func.ST_GeomFromText(bindvalue, type_=self) + + def column_expression(self, col): + return func.ST_AsText(col, type_=self) + + def bind_processor(self, dialect): + def process(value): + if value is None: + return None + assert isinstance(value, tuple) + lat, lng = value + return "POINT(%s %s)" % (lng, lat) + return process + + def result_processor(self, dialect, coltype): + def process(value): + if value is None: + return None + #m = re.match(r'^POINT\((\S+) (\S+)\)$', value) + #lng, lat = m.groups() + lng, lat = value[6:-1].split() # 'POINT(135.00 35.00)' => ('135.00', '35.00') + return (float(lat), float(lng)) + return process \ No newline at end of file diff --git a/create_payment_slip.py b/create_payment_slip.py new file mode 100644 index 0000000..e392976 --- /dev/null +++ b/create_payment_slip.py @@ -0,0 +1,106 @@ +import models +import locale + + +from reportlab.lib.pagesizes import A4 +from reportlab.pdfgen import canvas +from segno import helpers + + +import datetime +import locale + +def print_multiple_lines(lines: int, text): + cnt = 1 + while cnt < lines: + text.textLines("") + cnt += 1 + +def create_payment_slip(member: models.Members): + locale.setlocale(locale.LC_ALL, '') + + w, h = A4 + + # create epc qr-code + qrcode = helpers.make_epc_qr(name='Sportunion St. Thomas', + iban='AT493443700000113407', + amount=15*member.member_count, + reference=f'2024-4732-{member.id:03}-{member.member_count}') + qrcode.save("epc_qr.png", scale=2) + + subfolder = "mail/" if member.member_email else "" + c = canvas.Canvas(f"payment_slips/{subfolder}{member.member_zip}_payment_slip_member{member.id}.pdf", pagesize=A4) + #img = qrcode.to_pil(scale=4, dark='darkred', data_dark='darkorange', + # data_light='yellow') + #qrcodeba = bytearray(img) + + c.drawImage(image="epc_qr.png", x=500, y=40) # epc qr + c.drawImage(image="Sportunion quer 4c.png", x=(w-180), y=(h-100), width=150, height=50, mask='auto') + + c.setFont("Helvetica", 9) + text = c.beginText(60, h - 150) + if member.member_count > 1: + text.textLine(f"{member.member_firstname_spouse} u. {member.member_firstname} {member.member_surname}") + else: + text.textLine(f"{member.member_firstname} {member.member_surname} {member.member_suffix}") + text.textLine(f"{member.member_street} {member.member_streetnumber}") + text.textLine(f"{member.member_zip} {member.member_city}") + print_multiple_lines(5, text) + if member.member_count > 1: + salutation = "eure" + salutation2 = "euer" + salutation3 = "euch" + salutation4 = "Solltet ihr" + salutation5 = "sendet" + text.textLine(f"Liebe {member.member_firstname_spouse} und lieber {member.member_firstname}!") + else: + salutation = "Deine" + salutation2 = "Dein" + salutation3 = "Dich" + salutation4 = "Solltest Du" + salutation5 = "sende" + if member.member_gender == "female": + salutation_gender = "liebe" + else: + salutation_gender = "lieber" + text.textLine(f"{salutation_gender.capitalize()} {member.member_firstname}!") + + c.drawString(w - 150, h - 225, f'St. Thomas, {datetime.date.today().strftime('%x')}') + print_multiple_lines(5, text) + text.textLines(f"Vielen Dank für {salutation} Treue!\n\n{salutation2.capitalize()} Beitrag ist gut angelegt, denn der Sportverein St. Thomas bietet für jede(n) etwas:") + print_multiple_lines(2, text) + text.textLines("* Skigymnastik\n* Radfahren\n* Stockschiessen\n* Pilates\n* Tagesskifahrt\n* uvam") + print_multiple_lines(5, text) + text.textLines(f"Wir möchten das Einheben des Mitgliedsbeitrags zuküntig einfacher machen, daher bitten wir {salutation3},\nuns eine Mail mit dem Betreff \"Zahlschein oder Abbucher\" an unsere Mail-Adresse sv.stthomas@gmx.at zu senden!") + print_multiple_lines(3, text) + text.textLines(f"{salutation2.capitalize()} Sportverein-Team!") + c.drawText(text) + + text = c.beginText(60, h - 500) + c.setFont("Helvetica", 7) + text.textLines(f"Auf Grund der DSGVO müssen wir darauf hinweisen, dass wir außer Name, Adresse, Mail-Adresse\nzum Zwecke der Einebung des Mitgliedsbeitrags, keine persönlichen Daten unserer Mitglieder führen!\nDiese Daten werden von uns nicht an Dritte weitergegeben!\nWeiters informieren wir darüber, dass Fotos, die bei Veranstaltungen oder Aktivitäten entstehen,\nauf unserer Homepage oder in Zeitungen veröffentlicht werden können.\n{salutation4} gegen die Speicherung {salutation}r Daten oder die Veröffentlichung diverser Foto sein, {salutation5} uns bitte eine Mail.") + c.drawText(text) + + # payment slip details + text = c.beginText(11, 75) + c.setFont("Helvetica", 8) + text.textLine(f"Beitrag gesamt: {locale.format_string('%.2f', 15 * member.member_count)}") + text.textLine(f"Mitgliedsnummer: 4732-{member.id:03}") + c.drawText(text) + + text = c.beginText(188, 103) + c.setFont("Helvetica", 8) + text.textLine(f"Bei Online-Zahlung bitte die Zahlungsreferenz 2024-4732-{member.id:03}-{member.member_count} eintragen") + print_multiple_lines(1, text) + if member.member_count > 1: + text.textLine(f"Mitgliedsnummer: 4732-{member.id:03} {member.member_firstname_spouse} {member.member_suffix} {member.member_firstname} {member.member_surname}") + else: + text.textLine(f"Mitgliedsnummer: 4732-{member.id:03} {member.member_firstname} {member.member_surname}") + c.drawText(text) + + c.setFont("Helvetica", 10) + c.drawString(188, h - 696, f'2024-4732-{member.id:03}-{member.member_count}') # Zahlungsreferenz Zahlungsanweisung + c.drawString(11, 120, f'2024-4732-{member.id:03}-{member.member_count}') # Zahlungsreferenz Auftragsbestätigung + c.drawString(130, h - 694, (locale.format_string('%.2f', 15 * member.member_count))) # Betrag Auftragsbestätigung + c.drawString(w - 30, 170, (locale.format_string('%.2f', 15 * member.member_count))) # Betrag Zahlungsanweisung + c.save() \ No newline at end of file diff --git a/database.py b/database.py new file mode 100644 index 0000000..86b8392 --- /dev/null +++ b/database.py @@ -0,0 +1,11 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.ext.declarative import declarative_base + +URL_DATABASE = 'postgresql://sust:sust@192.168.1.202:5432/sust' + +engine = create_engine(URL_DATABASE) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() \ No newline at end of file diff --git a/epc_qr.png b/epc_qr.png new file mode 100644 index 0000000..a1b7f43 Binary files /dev/null and b/epc_qr.png differ diff --git a/helper_scripts.sql b/helper_scripts.sql new file mode 100644 index 0000000..543c6c3 --- /dev/null +++ b/helper_scripts.sql @@ -0,0 +1,25 @@ +update members m set member_geo_latitude = null, member_geo_longitude = null +where id in (110,107,45,21,19,108,36,24,22,18,35,162,90,3,142,122,121,115,9,128,79,78,50,186,183,180,176,157,148,98); + +select * from members m where m.member_geo_latitude is null; + +select * from members m where id in (110,107,45,21,19,108,36,24,22,18,35,162,90,3,142,122,121,115,9,128,79,78,50,186,183,180,176,157,148,98); + +update members m set member_geo_latitude = null, member_geo_longitude = null + +ALTER TABLE sust.deliveries ADD CONSTRAINT deliveries_member_id_delivered_by_fkey FOREIGN KEY (delivered_by_id) REFERENCES members(id) + +update deliveries set delivered_by_id = 186 + +update members set member_city = 'Sankt Thomas' where member_city = 'St. Thomas' + +update members set member_city = trim(member_city) + +insert into deliveries (delivery_date, member_id, delivered_by_id, delivery_year) +values (now(), 78, 186, '2024-01-01'), + (now(), 79, 186, '2024-01-01'), + (now(), 3, 186, '2024-01-01'), + (now(), 4, 186, '2024-01-01'), + (now(), 9, 186, '2024-01-01') + +update deliveries set delivery_year = '2024-01-01' \ No newline at end of file diff --git a/launch_venv.ps1 b/launch_venv.ps1 new file mode 100644 index 0000000..6b9c2ec --- /dev/null +++ b/launch_venv.ps1 @@ -0,0 +1 @@ +.\.venv\Scripts\Activate \ No newline at end of file diff --git a/logging_config.py b/logging_config.py new file mode 100644 index 0000000..8de7279 --- /dev/null +++ b/logging_config.py @@ -0,0 +1,24 @@ +import logging.config +from pythonjsonlogger import jsonlogger + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "json": { + "format": "%(asctime)s %(levelname)s %(message)s", + "class": "pythonjsonlogger.jsonlogger.JsonFormatter", + } + }, + "handlers": { + "stdout": { + "class": "logging.StreamHandler", + "stream": "ext://sys.stdout", + "formatter": "json", + } + }, + "loggers": {"": {"handlers": ["stdout"], "level": "DEBUG"}}, +} + + +logging.config.dictConfig(LOGGING) \ No newline at end of file diff --git a/mail_payment_slips.py b/mail_payment_slips.py new file mode 100644 index 0000000..9388a55 --- /dev/null +++ b/mail_payment_slips.py @@ -0,0 +1,76 @@ +from sqlalchemy import extract +from database import engine, SessionLocal +import models +import logging, sys +import smtplib +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.mime.application import MIMEApplication +from os.path import exists +import datetime +from create_payment_slip import create_payment_slip + + +logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) + +def payment_slip_delivered(member: models.Members, year: int): + return db.query(models.Deliveries.delivery_date)\ + .filter(models.Deliveries.member_id == member.id, extract('year', models.Deliveries.delivery_date) == year)\ + .count() + +def send_mail(member: models.Members): + delivered = payment_slip_delivered(member, 2024) + if delivered: + logging.debug(f'Member: {member.member_surname} already mailed or delivered!') + return + + smtp_server = 'mail.gmx.com' + smtp_port = 587 + smtp_username = 'zisco@gmx.at' + smtp_password = '57spunk07' + + from_email = 'sv.stthomas@gmx.at' + to_email = member.member_email + subject = 'Mitgliedsbeitrag 2024' + body = f'Liebes Mitglied,\n\nDein Zahlschein für 2024 liegt bei.\nDu kannst gerne mittels beiliegendem, aufgedrucktem QR-Code überweisen.\nDanke sagt Dein Team vom Sportverein!' + + msg = MIMEMultipart() + msg['From'] = from_email + msg['To'] = to_email + msg['Subject'] = subject + msg.attach(MIMEText(body)) + + # es kann sein, dass die Mail-Adresse erst später ins System kam + path_to_file = f'payment_slips\\mail\\{member.member_zip}_payment_slip_member{member.id}.pdf' + if not exists(path_to_file): + create_payment_slip(member=member) + + with open(path_to_file, 'rb') as f: + attachment = MIMEApplication(f.read(), _subtype='pdf') + attachment.add_header('Content-Disposition', 'attachment', filename=f'{member.member_zip}_payment_slip_member{member.id}.pdf') + msg.attach(attachment) + + with smtplib.SMTP(smtp_server, smtp_port) as smtp: + smtp.starttls() + smtp.login(smtp_username, smtp_password) + smtp.send_message(msg) + + newDelivery = models.Deliveries(delivery_date = datetime.datetime.now(), + member_id = member.id, + delivered_by_id = 186, + delivery_year = datetime.date.today()) + + db.add(newDelivery) + db.commit() + + logging.debug(f'Sent mail to {member.member_email}') + +db = SessionLocal() +models.Base.metadata.create_all(bind=engine) +#members = db.query(models.Members).filter(models.Members.member_email != None, ~models.Members.id.in_([183,51,200,49])).all() +#members = db.query(models.Members).filter(models.Members.member_active == True).all() +#members = db.query(models.Members).filter(models.Members.id.in_([2,3,9,199]), models.Members.member_active == True, models.Members.member_printed == False).all() +members = db.query(models.Members).filter(models.Members.id.in_([89,149,132,102,106])).all() + +for member in members: + send_mail(member=member) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..e56b84c --- /dev/null +++ b/main.py @@ -0,0 +1,130 @@ +from fastapi import FastAPI, HTTPException, Depends +from fastapi.security import OAuth2PasswordBearer +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from typing import List, Annotated, Optional +import models +from database import engine, SessionLocal +from sqlalchemy.orm import Session +from datetime import datetime + +#http://127.0.0.1:8000/docs -> swagger like ui +#ngrok http http://localhost:8000 + +app = FastAPI() +#oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") +models.Base.metadata.create_all(bind=engine) + +origins = [ + "http://localhost.tiangolo.com", + "https://localhost.tiangolo.com", + "http://localhost", + "http://localhost:8080", + "http://localhost:3000", +] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + +class DeliveryBase(BaseModel): + delivery_date: datetime + delivered_by: int + +class PaymentBase(BaseModel): + payment_date: datetime + +class MemberBase(BaseModel): + member_count: int + member_firstname: str + member_firstname_spouse: str | None = None + member_surname: str + member_suffix: str | None = None + member_street: str + member_streetnumber: str | None = None + member_email: str | None = None + member_zip: int + member_city: str + member_active: bool + member_gender: str | None = None + member_connected: int | None = None + member_printed: bool + #member_coordinates: + #payments: Optional[List[PaymentBase]] + member_deliveries: Optional[List[DeliveryBase]] + #deliveries: Mapped[List["DeliveryBase"]] = relationship() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +db_dependency = Annotated[Session, Depends(get_db)] + +@app.get("/members/", response_model=List[MemberBase]) +async def read_members(db: db_dependency, skip: int = 0, limit: int = 100 ): + members = db.query(models.Members).offset(skip).limit(limit).all() + if not members: + raise HTTPException(status_code=400, detail='No members') + return members + +@app.get("/member/{member_id}") +async def read_member(member_id: int, db: db_dependency): #, token: Annotated[str, Depends(oauth2_scheme)]): + result = db.query(models.Members).filter(models.Members.id == member_id).first() + if not result: + raise HTTPException(status_code=400, detail='Member does not exist') + return result + +@app.get("/payments/{member_id}") +async def read_payments(member_id: int, db: db_dependency): + result = db.query(models.Members).filter(models.Members.id == member_id).all() + if not result: + raise HTTPException(status_code=400, detail=f'No payments found for member {member_id}') + return result + +@app.post("/members/") +async def create_member(member: MemberBase, db: db_dependency): + #db_member = models.Members(**member.model_dump()) + db_member = models.Members(member_firstname=member.member_firstname, + member_surname=member.member_surname, + member_active=member.member_active) + db.add(db_member) + db.commit() + +@app.post("/fullmembers/") +async def create_members(members: List[MemberBase], db: db_dependency): + for member in members: + db_member = models.Members(member_count=member.member_count, + member_firstname=member.member_firstname, + member_firstname_spouse=member.member_firstname_spouse, + member_surname=member.member_surname, + member_suffix=member.member_suffix, + member_street=member.member_street, + member_streetnumber=member.member_streetnumber, + member_email=member.member_email, + member_zip=member.member_zip, + member_city=member.member_city, + member_active=member.member_active, + member_gender=member.member_gender, + member_connected=member.member_connected) + db.add(db_member) + #db.commit() + #db.refresh(db_member) + if not member.payments is None: + for payment in member.payments: + db_payment = models.Payments(payment_date=payment.payment_date, + member_id=db_member.id) + db.add(db_payment) + db.commit() \ No newline at end of file diff --git a/map.html b/map.html new file mode 100644 index 0000000..fd31ca7 --- /dev/null +++ b/map.html @@ -0,0 +1,5902 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/member_map.py b/member_map.py new file mode 100644 index 0000000..9b53b34 --- /dev/null +++ b/member_map.py @@ -0,0 +1,116 @@ +# import the library and its Marker clusterization service +import folium +from database import SessionLocal +from folium.plugins import MarkerCluster +import models +import logging +from geopy.geocoders import Bing +from sqlalchemy import * +import logging +import webbrowser +import re + + +def payment_slip_delivered(member: models.Members, year: int): + return db.query(models.Deliveries.delivery_date)\ + .filter(models.Deliveries.member_id == member.id, extract('year', models.Deliveries.delivery_date) == year)\ + .count() + +def update_missing_locs(): + members = db.query(models.Members).filter(models.Members.member_geo_latitude == None).all() + + for member in members: + address = f'{member.member_street} {member.member_streetnumber}, {member.member_zip} {member.member_city}, Austria' + location = geolocator.geocode(address) + + if location != None: + #member.member_coordinates = [location.latitude, location.longitude] + member.member_geo_latitude = location.latitude + member.member_geo_longitude = location.longitude + + logger.info(f'Member: {member.id} updated with geo-coordinates') + else: + logger.info(f'Member: {member.id} address couldn\'t be found') + + db.commit() + +def checkEmail(email): + regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b' + + if email is None: return False + + # pass the regular expression + # and the string into the fullmatch() method + if(re.fullmatch(regex, email)): + return True + + else: + return False + +logger = logging.getLogger(__name__) +logger.info("An info") +logger.warning("A warning") + +#geolocator = Nominatim(user_agent="sust", timeout=10) +geolocator = Bing(api_key='ApbTfEJ024nQCu6DC7TPIlqAcRMfZT8lPjAyMnVRcmo9rqxde5x-IS4fW-nGu0FI') + +db = SessionLocal() +#models.Base.metadata.create_all(bind=engine) + +update_missing_locs() + +#members = db.query(models.Members).filter(models.Members.id.in_([2,4,5,6])).all() + +#TODO try +""" members = Members.query.all() +for member in members: + for delivery in members.deliveries: + print(f'{delivery.delivery_date}') """ + +members = db.query(models.Members.member_geo_latitude, + models.Members.member_geo_longitude, + models.Members.member_firstname, + models.Members.member_surname, + models.Members.id, + models.Members.member_email)\ + .filter(models.Members.member_geo_latitude != None, models.Members.member_count != 0)\ + .all() + +# .join(models.Members.member_deliveries)\ +# Create a map object and center it to the avarage coordinates to m +coords = [48.284793, 13.880550] +map = folium.Map(location=coords, zoom_start = 13) + +# if the points are too close to each other, cluster them, create a cluster overlay with MarkerCluster, add to m +marker_cluster = MarkerCluster().add_to(map) + +# draw the markers and assign popup and hover texts +# add the markers the the cluster layers so that they are automatically clustered +#folium.Marker([lat, lon], popup=str(name)+': '+color+'-'+str(clname), icon=folium.Icon(color='black',icon_color='#FFFF00')).add_to(feature_group) +for member in members: + if member.member_geo_latitude != None and member.member_geo_latitude != None: + delivered = payment_slip_delivered(member, 2024) + + iconColor = "blue" + hasMail = checkEmail(member.member_email) + if hasMail: + iconColor = "orange" + elif delivered: + iconColor = "green" + + if hasMail and delivered: + iconColor = "darkgreen" + + #delivered = member.member_deliveries + location = (member.member_geo_latitude, member.member_geo_longitude) + folium.Marker(location=location, + popup=f'{member.member_firstname} {member.member_surname}', + tooltip=f'\nZugestellt' if delivered else 'Nicht zugestellt', + icon=folium.Icon(color=iconColor))\ + .add_to(marker_cluster) + +#Display the map +map.save("map.html") +webbrowser.open("map.html") + +#func.count(models.Members.member_deliveries).label("count"))\ \ No newline at end of file diff --git a/models.py b/models.py new file mode 100644 index 0000000..d92fc51 --- /dev/null +++ b/models.py @@ -0,0 +1,80 @@ +from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, DateTime, Float +from sqlalchemy.types import TypeDecorator, VARCHAR, Integer +from sqlalchemy_utils import EmailType +from sqlalchemy.orm import relationship, Mapped +from geoalchemy2 import Geometry +from database import Base +import datetime +from coordinates_class import EasyGeometry + +class NullCatchingString(TypeDecorator): + impl = VARCHAR + + def process_result_value(self, value, dialect): + if value is None: + return '' + return value + +class NullCatchingInteger(TypeDecorator): + impl = Integer + + def process_result_value(self, value, dialect): + if value is None: + return '' + return value + +class Members(Base): + __tablename__ = 'members' + + id = Column(Integer, primary_key=True, index=True) + member_count = Column(Integer) + member_firstname = Column(String, index=True) + member_firstname_spouse = Column(NullCatchingString()) + member_surname = Column(String, index=True) + member_suffix = Column(NullCatchingString(), nullable=True) + member_street = Column(NullCatchingString(), index=True) + member_streetnumber = Column(NullCatchingString(), nullable=True) + member_email = Column(EmailType, index=True) + member_zip = Column(NullCatchingInteger(), index=True) + member_city = Column(NullCatchingString(), index=True) + member_active = Column(Boolean, default=True) + member_gender = Column(String, nullable=True) + member_connected = Column(Integer, ForeignKey("members.id"), nullable=True) + created_date = Column(DateTime, default=datetime.datetime.utcnow) + updated_date = Column(DateTime, default=datetime.datetime.utcnow) + member_died = Column(DateTime) + member_printed = Column(Boolean, default=False) + #member_coordinates = Column(Geometry(geometry_type='POINT')) + member_geo_latitude = Column(Float, nullable=True) + member_geo_longitude = Column(Float, nullable=True) + member_deliveries = relationship('Deliveries', backref='members') + #member_deliveries: Mapped[List["Deliveries"]] = relationship() + + def __repr__(self): + return f'' + +class Payments(Base): + __tablename__ = 'payments' + + id = Column(Integer, primary_key=True, index=True) + payment_date = Column(String, index=True) + member_id = Column(Integer, ForeignKey("members.id")) + + def __repr__(self): + return f'' + +class Deliveries(Base): + __tablename__ = 'deliveries' + + id = Column(Integer, primary_key=True, index=True) + delivery_date = Column(DateTime, index=True) + member_id = Column(Integer, ForeignKey("members.id")) + delivered_by_id = Column(Integer) + delivery_year = Column(DateTime, index=True) + + def __repr__(self): + return f'' + +""" delivered_by_id = Column(Integer, ForeignKey("members.id" )) + member = relationship("Members", foreign_keys=[member_id]) + delivering_member = relationship("Members", foreign_keys=[member_id]) """ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c4fbf82 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,102 @@ +{ + "name": "fastAPI-PostgreSQL", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "axios": "^1.6.2" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a4232df --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "axios": "^1.6.2" + } +} diff --git a/print_payment_slips.py b/print_payment_slips.py new file mode 100644 index 0000000..eb96f67 --- /dev/null +++ b/print_payment_slips.py @@ -0,0 +1,23 @@ +from reportlab.lib.pagesizes import A4 +from create_payment_slip import create_payment_slip +from database import engine, SessionLocal +from pydantic import BaseModel +from sqlalchemy.orm import Session +import models +from reportlab.lib.utils import ImageReader +import logging, sys + +logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) + +db = SessionLocal() +models.Base.metadata.create_all(bind=engine) +#members = db.query(models.Members).filter(models.Members.member_email == None).all() +#members = db.query(models.Members).filter(models.Members.member_active == True).all() +#members = db.query(models.Members).filter(models.Members.id.in_([2,3,9,199]), models.Members.member_active == True, models.Members.member_printed == False).all() +members = db.query(models.Members).filter(models.Members.id.in_([200])).all() + +for member in members: + create_payment_slip(member=member) + + logging.debug(f'Member: {member.member_surname}') + #logging.info('We processed %d records', len(processed_records)) diff --git a/whatsapp_payment_slips.py b/whatsapp_payment_slips.py new file mode 100644 index 0000000..8c8bdd5 --- /dev/null +++ b/whatsapp_payment_slips.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +from heyoo import WhatsApp + +#import pywhatkit + +from database import engine, SessionLocal +import models +import logging, sys +import smtplib +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.mime.application import MIMEApplication +from os.path import exists +import datetime + + +logging.basicConfig(stream=sys.stderr, level=logging.INFO) + +def send_whatsapp(member: models.Members): + messenger = WhatsApp('EAAj1p1TLuUsBOy3849bBRZC8fEAZCc12440OW6L4BXDiiviWNWtAOLK3iyOKy3KnoRL3FOzCbiCoU0JKcoIAVzHLeBKUITaYWZBBl616wGyuqvIeQTyMFRuHiXM1MQ2ivnz74Bg6VD6GXMEpF0wzU68RdyyTwaQzSRmaXMW0R9IN0I9BUnSgFUJDqO1P66sngthaLuOxS3LGjvi', + phone_number_id='250992074758193') + + #For sending a Text messages + messenger.send_message('Hello I am WhatsApp Cloud API (on behalf of your husband)', '4367761802110') + + #For sending an Image + # messenger.send_image( + # image="https://i.imgur.com/YSJayCb.jpeg", + # recipient_id="91989155xxxx", + # ) + + # subject = 'Mitgliedsbeitrag 2024' + # body = f'Liebes Mitglied,\n\nDein Zahlschein für 2024 liegt bei.\nDu kannst gerne mittels beiliegendem, aufgedrucktem QR-Code überweisen.\nDanke sagt Dein Team vom Sportverein!' + + # # es kann sein, dass die Mail-Adresse erst später ins System kam + # path_to_file = f'payment_slips\\mail\\{member.member_zip}_payment_slip_member{member.id}.pdf' + # if exists(path_to_file): + # with open(path_to_file, 'rb') as f: + # attachment = MIMEApplication(f.read(), _subtype='pdf') + # attachment.add_header('Content-Disposition', 'attachment', filename=f'{member.member_zip}_payment_slip_member{member.id}.pdf') + # msg.attach(attachment) + + # with smtplib.SMTP(smtp_server, smtp_port) as smtp: + # smtp.starttls() + # smtp.login(smtp_username, smtp_password) + # smtp.send_message(msg) + + # newDelivery = models.Deliveries(delivery_date = datetime.datetime.now(), + # member_id = member.id, + # delivered_by_id = 186, + # delivery_year = datetime.date.today()) + + # db.add(newDelivery) + # db.commit() + + # logging.debug(f'Sent mail to {member.member_email}') + +db = SessionLocal() +models.Base.metadata.create_all(bind=engine) +#members = db.query(models.Members).filter(models.Members.member_email != None, ~models.Members.id.in_([183,51,200,49])).all() +#members = db.query(models.Members).filter(models.Members.member_active == True).all() +#members = db.query(models.Members).filter(models.Members.id.in_([2,3,9,199]), models.Members.member_active == True, models.Members.member_printed == False).all() +members = db.query(models.Members).filter(models.Members.id.in_([186])).all() + +for member in members: + send_whatsapp(member=member) + + logging.debug(f'Member: {member.member_surname}') \ No newline at end of file