From 0291022eddbbb6dc710fc65e97ea0fe6aac96f03 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Wed, 19 May 2021 14:50:24 +0200 Subject: [PATCH] Init --- README.md | 32 ++++ bindings.py | 486 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + stats.py | 72 +++++++ 4 files changed, 592 insertions(+) create mode 100644 bindings.py create mode 100644 requirements.txt create mode 100644 stats.py diff --git a/README.md b/README.md index fcaa6f0..c642d7d 100644 --- a/README.md +++ b/README.md @@ -1 +1,33 @@ # foodsoftstats + +More insight into the Foodsoft database. + +## Hacking + +Setup: + +``` +$ python3 -m venv .venv && source .venv/bin/activate +$ pip install -U pip setuptools -r requirements.txt +``` + +Import (ask on the chat for the database dump): + +``` +$ mysql foodsoft < 2021-05-19-foodsoft.sql +``` + +Run: + +``` +$ export DB_PASSWORD=mypassword +$ python stats.py +``` + +## Generate bindings + +You need to have access to a database which has the Foodsoft schema loaded. + +``` +$ sqlacodegen "mysql+pymysql://:@/" > bindings.py +``` diff --git a/bindings.py b/bindings.py new file mode 100644 index 0000000..5b26d02 --- /dev/null +++ b/bindings.py @@ -0,0 +1,486 @@ +"""Sqlacodegen generated Python bindings for the Foodsoft database schema.""" + +from sqlalchemy import ( + DECIMAL, + Column, + Date, + DateTime, + Float, + Index, + LargeBinary, + String, + Table, + Text, + text, +) +from sqlalchemy.dialects.mysql import INTEGER, TINYINT +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() +metadata = Base.metadata + + +class ArticleCategory(Base): + __tablename__ = "article_categories" + + id = Column(INTEGER(11), primary_key=True) + name = Column(String(255), nullable=False, unique=True, server_default=text("''")) + description = Column(String(255)) + + +class ArticlePrice(Base): + __tablename__ = "article_prices" + + id = Column(INTEGER(11), primary_key=True) + article_id = Column(INTEGER(11), index=True) + price = Column(DECIMAL(8, 2), nullable=False, server_default=text("0.00")) + tax = Column(DECIMAL(8, 2), nullable=False, server_default=text("0.00")) + deposit = Column(DECIMAL(8, 2), nullable=False, server_default=text("0.00")) + unit_quantity = Column(INTEGER(11)) + created_at = Column(DateTime) + + +class Article(Base): + __tablename__ = "articles" + __table_args__ = ( + Index("index_articles_on_name_and_supplier_id", "name", "supplier_id"), + ) + + id = Column(INTEGER(11), primary_key=True) + name = Column(String(255), nullable=False, server_default=text("''")) + supplier_id = Column( + INTEGER(11), nullable=False, index=True, server_default=text("0") + ) + article_category_id = Column( + INTEGER(11), nullable=False, index=True, server_default=text("0") + ) + unit = Column(String(255), nullable=False, server_default=text("''")) + note = Column(String(255)) + availability = Column(TINYINT(1), nullable=False, server_default=text("1")) + manufacturer = Column(String(255)) + origin = Column(String(255)) + shared_updated_on = Column(DateTime) + price = Column(DECIMAL(8, 2)) + tax = Column(Float) + deposit = Column(DECIMAL(8, 2), server_default=text("0.00")) + unit_quantity = Column(INTEGER(11), nullable=False, server_default=text("1")) + order_number = Column(String(255)) + created_at = Column(DateTime) + updated_at = Column(DateTime) + deleted_at = Column(DateTime) + type = Column(String(255), index=True) + quantity = Column(INTEGER(11), server_default=text("0")) + + +class Assignment(Base): + __tablename__ = "assignments" + __table_args__ = ( + Index( + "index_assignments_on_user_id_and_task_id", + "user_id", + "task_id", + unique=True, + ), + ) + + id = Column(INTEGER(11), primary_key=True) + user_id = Column(INTEGER(11), nullable=False, server_default=text("0")) + task_id = Column(INTEGER(11), nullable=False, server_default=text("0")) + accepted = Column(TINYINT(1), server_default=text("0")) + + +class Delivery(Base): + __tablename__ = "deliveries" + + id = Column(INTEGER(11), primary_key=True) + supplier_id = Column(INTEGER(11), index=True) + delivered_on = Column(Date) + created_at = Column(DateTime) + note = Column(Text) + invoice_id = Column(INTEGER(11)) + + +class Document(Base): + __tablename__ = "documents" + + id = Column(INTEGER(11), primary_key=True) + name = Column(String(255)) + mime = Column(String(255)) + data = Column(LargeBinary) + created_by_user_id = Column(INTEGER(11)) + created_at = Column(DateTime) + updated_at = Column(DateTime) + + +class FinancialLink(Base): + __tablename__ = "financial_link" + + id = Column(INTEGER(11), primary_key=True) + note = Column(Text) + + +class FinancialTransaction(Base): + __tablename__ = "financial_transactions" + + id = Column(INTEGER(11), primary_key=True) + ordergroup_id = Column( + INTEGER(11), nullable=False, index=True, server_default=text("0") + ) + amount = Column(DECIMAL(8, 2), nullable=False, server_default=text("0.00")) + note = Column(Text, nullable=False) + user_id = Column(INTEGER(11), nullable=False, server_default=text("0")) + created_on = Column(DateTime, nullable=False) + financial_link_id = Column(INTEGER(11)) + + +class GroupOrderArticleQuantity(Base): + __tablename__ = "group_order_article_quantities" + + id = Column(INTEGER(11), primary_key=True) + group_order_article_id = Column( + INTEGER(11), nullable=False, index=True, server_default=text("0") + ) + quantity = Column(INTEGER(11), server_default=text("0")) + tolerance = Column(INTEGER(11), server_default=text("0")) + created_on = Column(DateTime, nullable=False) + + +class GroupOrderArticle(Base): + __tablename__ = "group_order_articles" + __table_args__ = ( + Index("goa_index", "group_order_id", "order_article_id", unique=True), + ) + + id = Column(INTEGER(11), primary_key=True) + group_order_id = Column( + INTEGER(11), nullable=False, index=True, server_default=text("0") + ) + order_article_id = Column( + INTEGER(11), nullable=False, index=True, server_default=text("0") + ) + quantity = Column(INTEGER(11), nullable=False, server_default=text("0")) + tolerance = Column(INTEGER(11), nullable=False, server_default=text("0")) + updated_on = Column(DateTime, nullable=False) + result = Column(DECIMAL(8, 3)) + result_computed = Column(DECIMAL(8, 3)) + + +class GroupOrder(Base): + __tablename__ = "group_orders" + __table_args__ = ( + Index( + "index_group_orders_on_ordergroup_id_and_order_id", + "ordergroup_id", + "order_id", + unique=True, + ), + ) + + id = Column(INTEGER(11), primary_key=True) + ordergroup_id = Column(INTEGER(11), index=True) + order_id = Column(INTEGER(11), nullable=False, index=True, server_default=text("0")) + price = Column(DECIMAL(8, 2), nullable=False, server_default=text("0.00")) + lock_version = Column(INTEGER(11), nullable=False, server_default=text("0")) + updated_on = Column(DateTime, nullable=False) + updated_by_user_id = Column(INTEGER(11)) + + +class Group(Base): + __tablename__ = "groups" + + id = Column(INTEGER(11), primary_key=True) + type = Column(String(255), nullable=False, server_default=text("''")) + name = Column(String(255), nullable=False, unique=True, server_default=text("''")) + description = Column(String(255)) + account_balance = Column( + DECIMAL(12, 2), nullable=False, server_default=text("0.00") + ) + created_on = Column(DateTime, nullable=False) + role_admin = Column(TINYINT(1), nullable=False, server_default=text("0")) + role_suppliers = Column(TINYINT(1), nullable=False, server_default=text("0")) + role_article_meta = Column(TINYINT(1), nullable=False, server_default=text("0")) + role_finance = Column(TINYINT(1), nullable=False, server_default=text("0")) + role_orders = Column(TINYINT(1), nullable=False, server_default=text("0")) + deleted_at = Column(DateTime) + contact_person = Column(String(255)) + contact_phone = Column(String(255)) + contact_address = Column(String(255)) + stats = Column(Text) + next_weekly_tasks_number = Column(INTEGER(11), server_default=text("8")) + ignore_apple_restriction = Column(TINYINT(1), server_default=text("0")) + role_invoices = Column(TINYINT(1), nullable=False, server_default=text("0")) + break_start = Column(Date) + break_end = Column(Date) + + +class Invite(Base): + __tablename__ = "invites" + + id = Column(INTEGER(11), primary_key=True) + token = Column(String(255), nullable=False, index=True, server_default=text("''")) + expires_at = Column(DateTime, nullable=False) + group_id = Column(INTEGER(11), nullable=False, server_default=text("0")) + user_id = Column(INTEGER(11), nullable=False, server_default=text("0")) + email = Column(String(255), nullable=False, server_default=text("''")) + + +class Invoice(Base): + __tablename__ = "invoices" + + id = Column(INTEGER(11), primary_key=True) + supplier_id = Column(INTEGER(11), index=True) + number = Column(String(255)) + date = Column(Date) + paid_on = Column(Date) + note = Column(Text) + amount = Column(DECIMAL(8, 2), nullable=False, server_default=text("0.00")) + deposit = Column(DECIMAL(8, 2), nullable=False, server_default=text("0.00")) + deposit_credit = Column(DECIMAL(8, 2), nullable=False, server_default=text("0.00")) + created_at = Column(DateTime) + updated_at = Column(DateTime) + created_by_user_id = Column(INTEGER(11)) + attachment_mime = Column(String(255)) + attachment_data = Column(LargeBinary) + financial_link_id = Column(INTEGER(11)) + + +class MailDeliveryStatu(Base): + __tablename__ = "mail_delivery_status" + + id = Column(INTEGER(11), primary_key=True) + created_at = Column(DateTime) + email = Column(String(255), nullable=False, index=True) + message = Column(String(255), nullable=False) + attachment_mime = Column(String(255)) + attachment_data = Column(LargeBinary) + + +class Membership(Base): + __tablename__ = "memberships" + __table_args__ = ( + Index( + "index_memberships_on_user_id_and_group_id", + "user_id", + "group_id", + unique=True, + ), + ) + + id = Column(INTEGER(11), primary_key=True) + group_id = Column(INTEGER(11), nullable=False, server_default=text("0")) + user_id = Column(INTEGER(11), nullable=False, server_default=text("0")) + + +class Message(Base): + __tablename__ = "messages" + + id = Column(INTEGER(11), primary_key=True) + sender_id = Column(INTEGER(11)) + recipients_ids = Column(Text) + subject = Column(String(255), nullable=False) + body = Column(Text) + email_state = Column(INTEGER(11), nullable=False, server_default=text("0")) + private = Column(TINYINT(1), server_default=text("0")) + created_at = Column(DateTime) + reply_to = Column(INTEGER(11)) + group_id = Column(INTEGER(11)) + salt = Column(String(255)) + received_email = Column(LargeBinary) + + +class OrderArticle(Base): + __tablename__ = "order_articles" + __table_args__ = ( + Index( + "index_order_articles_on_order_id_and_article_id", + "order_id", + "article_id", + unique=True, + ), + ) + + id = Column(INTEGER(11), primary_key=True) + order_id = Column(INTEGER(11), nullable=False, index=True, server_default=text("0")) + article_id = Column(INTEGER(11), nullable=False, server_default=text("0")) + quantity = Column(INTEGER(11), nullable=False, server_default=text("0")) + tolerance = Column(INTEGER(11), nullable=False, server_default=text("0")) + units_to_order = Column(INTEGER(11), nullable=False, server_default=text("0")) + lock_version = Column(INTEGER(11), nullable=False, server_default=text("0")) + article_price_id = Column(INTEGER(11)) + units_billed = Column(INTEGER(11)) + units_received = Column(INTEGER(11)) + + +class OrderComment(Base): + __tablename__ = "order_comments" + + id = Column(INTEGER(11), primary_key=True) + order_id = Column(INTEGER(11), index=True) + user_id = Column(INTEGER(11)) + text = Column(Text) + created_at = Column(DateTime) + + +class Order(Base): + __tablename__ = "orders" + + id = Column(INTEGER(11), primary_key=True) + supplier_id = Column(INTEGER(11)) + note = Column(Text) + starts = Column(DateTime) + ends = Column(DateTime) + state = Column(String(255), index=True, server_default=text("'open'")) + lock_version = Column(INTEGER(11), nullable=False, server_default=text("0")) + updated_by_user_id = Column(INTEGER(11)) + foodcoop_result = Column(DECIMAL(8, 2)) + created_by_user_id = Column(INTEGER(11)) + boxfill = Column(DateTime) + pickup = Column(Date) + invoice_id = Column(INTEGER(11)) + last_sent_mail = Column(DateTime) + end_action = Column(INTEGER(11), nullable=False, server_default=text("0")) + + +class PageVersion(Base): + __tablename__ = "page_versions" + + id = Column(INTEGER(11), primary_key=True) + page_id = Column(INTEGER(11), index=True) + lock_version = Column(INTEGER(11)) + body = Column(Text) + updated_by = Column(INTEGER(11)) + redirect = Column(INTEGER(11)) + parent_id = Column(INTEGER(11)) + updated_at = Column(DateTime) + + +class Page(Base): + __tablename__ = "pages" + + id = Column(INTEGER(11), primary_key=True) + title = Column(String(255), index=True) + body = Column(Text) + permalink = Column(String(255), index=True) + lock_version = Column(INTEGER(11), server_default=text("0")) + updated_by = Column(INTEGER(11)) + redirect = Column(INTEGER(11)) + parent_id = Column(INTEGER(11)) + created_at = Column(DateTime) + updated_at = Column(DateTime) + + +class PeriodicTaskGroup(Base): + __tablename__ = "periodic_task_groups" + + id = Column(INTEGER(11), primary_key=True) + next_task_date = Column(Date) + created_at = Column(DateTime, nullable=False) + updated_at = Column(DateTime, nullable=False) + + +t_schema_migrations = Table( + "schema_migrations", + metadata, + Column("version", String(255), nullable=False, unique=True), +) + + +class Setting(Base): + __tablename__ = "settings" + __table_args__ = ( + Index( + "index_settings_on_thing_type_and_thing_id_and_var", + "thing_type", + "thing_id", + "var", + unique=True, + ), + ) + + id = Column(INTEGER(11), primary_key=True) + var = Column(String(255), nullable=False) + value = Column(Text) + thing_id = Column(INTEGER(11)) + thing_type = Column(String(30)) + created_at = Column(DateTime, nullable=False) + updated_at = Column(DateTime, nullable=False) + + +class StockChange(Base): + __tablename__ = "stock_changes" + + id = Column(INTEGER(11), primary_key=True) + delivery_id = Column(INTEGER(11), index=True) + order_id = Column(INTEGER(11)) + stock_article_id = Column(INTEGER(11), index=True) + quantity = Column(INTEGER(11), server_default=text("0")) + created_at = Column(DateTime) + stock_taking_id = Column(INTEGER(11), index=True) + + +class StockTaking(Base): + __tablename__ = "stock_takings" + + id = Column(INTEGER(11), primary_key=True) + date = Column(Date) + note = Column(Text) + created_at = Column(DateTime) + + +class Supplier(Base): + __tablename__ = "suppliers" + + id = Column(INTEGER(11), primary_key=True) + name = Column(String(255), nullable=False, unique=True, server_default=text("''")) + address = Column(String(255), nullable=False, server_default=text("''")) + phone = Column(String(255), nullable=False, server_default=text("''")) + phone2 = Column(String(255)) + fax = Column(String(255)) + email = Column(String(255)) + url = Column(String(255)) + contact_person = Column(String(255)) + customer_number = Column(String(255)) + delivery_days = Column(String(255)) + order_howto = Column(String(255)) + note = Column(String(255)) + shared_supplier_id = Column(INTEGER(11)) + min_order_quantity = Column(String(255)) + deleted_at = Column(DateTime) + shared_sync_method = Column(String(255)) + iban = Column(String(255)) + + +class Task(Base): + __tablename__ = "tasks" + + id = Column(INTEGER(11), primary_key=True) + name = Column(String(255), nullable=False, index=True, server_default=text("''")) + description = Column(String(255)) + due_date = Column(Date, index=True) + done = Column(TINYINT(1), server_default=text("0")) + workgroup_id = Column(INTEGER(11), index=True) + created_on = Column(DateTime, nullable=False) + updated_on = Column(DateTime, nullable=False) + required_users = Column(INTEGER(11), server_default=text("1")) + duration = Column(INTEGER(11), server_default=text("1")) + periodic_task_group_id = Column(INTEGER(11)) + + +class User(Base): + __tablename__ = "users" + + id = Column(INTEGER(11), primary_key=True) + nick = Column(String(255), unique=True) + password_hash = Column(String(255), nullable=False, server_default=text("''")) + password_salt = Column(String(255), nullable=False, server_default=text("''")) + first_name = Column(String(255), nullable=False, server_default=text("''")) + last_name = Column(String(255), nullable=False, server_default=text("''")) + email = Column(String(255), nullable=False, unique=True, server_default=text("''")) + phone = Column(String(255)) + created_on = Column(DateTime, nullable=False) + reset_password_token = Column(String(255)) + reset_password_expires = Column(DateTime) + last_login = Column(DateTime) + last_activity = Column(DateTime) + deleted_at = Column(DateTime) + iban = Column(String(255)) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..62c7053 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +sqlacodegen==2.3.0 +PyMySQL==1.0.2 diff --git a/stats.py b/stats.py new file mode 100644 index 0000000..78c0cf9 --- /dev/null +++ b/stats.py @@ -0,0 +1,72 @@ +from os import environ +from sys import exit + +from bindings import Article, GroupOrder, Order, OrderArticle, Supplier +from sqlalchemy import create_engine +from sqlalchemy.orm import Session + + +def never_ordered(session): + """List all articles never ordered by supplier.""" + + suppliers = session.query(Supplier) + order_articles = session.query(OrderArticle) + articles = session.query(Article) + + ordered_articles = [] + + for order in session.query(Order).all(): + supplier = suppliers.filter(Supplier.id == order.supplier_id).first() + + if not supplier: + continue + + all_order_articles = order_articles.filter( + OrderArticle.order_id == order.id + ).all() + + for order_article in all_order_articles: + if not order_article.units_received: + continue + + if order_article.article_id not in ordered_articles: + ordered_articles.append(order_article.article_id) + + never_ordered = articles.filter(Article.id.notin_(ordered_articles)).all() + + suppliers_articles = {} + for article in never_ordered: + supplier = suppliers.filter(Supplier.id == article.supplier_id).first() + + if supplier.name not in suppliers_articles: + suppliers_articles[supplier.name] = [] + + suppliers_articles[supplier.name].append(article) + + for supplier in suppliers_articles: + articles = suppliers_articles[supplier] + articles.sort(key=lambda a: a.created_at, reverse=True) + + if not articles: + continue + + print(f"Supplier: {supplier}, # articles never ordered: {len(articles)}") + + for artc in articles: + print(f"Article: {artc.name}, Added {artc.created_at}") + + print("") + + +DB_USER = environ.get("DB_USER", "root") +DB_PASSWORD = environ.get("DB_PASSWORD") +DB_HOST = environ.get("DB_HOST", "localhost") +DB_NAME = environ.get("DB_NAME", "foodsoft") + +if not DB_PASSWORD: + exit("Please set a DB_PASSWORD env var") + +connection = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}/{DB_NAME}" +session = Session(create_engine(connection)) + +never_ordered(session)