parent
2505589212
commit
0291022edd
@ -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://<user>:<password>@<host>/<name>" > 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)) |
@ -0,0 +1,2 @@ |
||||
sqlacodegen==2.3.0 |
||||
PyMySQL==1.0.2 |
@ -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) |
Loading…
Reference in new issue