Итак, это начинается с того, что я составляю электронную таблицу более 10 лет назад, чтобы человек мог отслеживать свою единственную медицинскую практику. Это было до того, как я получил степень MBA и сдал экзамен CPA. Эта таблица ломается несколько раз в год, а затем я склеиваю ее скотчем. Электронная таблица действительно проста и наиболее примечательна тем, чего в ней нет - двойным бухгалтерским учетом, разделением между платежами (например, платежами Paypal), созданием счетов или квитанций и, что наиболее важно, отдельным шифрованием личной информации в соответствии с HIPAA. Теперь он технически совместим с HIPAA, потому что он сохраняется с паролем, и только один человек обычно имеет к нему доступ. Однако технически соответствие и хорошее - это разные вещи. Первое меня обычно не устраивает.
Итак, я пишу веб-приложение, чтобы заменить электронную таблицу. Для меня это в основном обучающее упражнение. Я мог бы превратить это в бизнес, и я сделал несколько дизайнерских решений, чтобы сделать это проще. Однако основная цель веб-приложения - заменить электронную таблицу, которую использует один человек.
Стек, который я использую, довольно прост - Python, Flask, SQLAlchemy, Bootstrap. Сначала я буду использовать sqlite. Все будет работать на ноутбуке, так что sqlite достаточно хорош. И последнее: я не создавал схему в БД. Сначала я использовал инструмент для проектирования, а затем написал все модели SQLAlchemy. Это было отлажено для создания фактических записей в каждой таблице.
Вся схема:

Это не так уж и полезно, но я хотел выпустить всю энчиладу.
Мы разберем это на подразделы.
Во-первых, сущность:

Entity - это главное дизайнерское решение, которое позволило мне сделать это приложение для нескольких предприятий. Таким образом, у нас может быть несколько клиентов в одной базе данных. Конечно, для начала у нас будет только один покупатель. Пройдемся по полям:
- id: это первичный ключ
- uuid: мы собираемся сгенерировать uuid для каждой строки в каждой таблице. Это может показаться странным, но это дешево и упростит поиск таких вещей, как lastEdited. Это также позволит упростить аудит безопасности, если в этом возникнет необходимость. Я делаю это еще и потому, что это поможет мне привыкнуть к тому, как я должен думать об IPv6.
- name: Название сущности, да.
- id_Sogeum: это внешний ключ к случайным солям. Мы собираемся использовать это при шифровании PII. Посмотрите на таблицу Sogeum, и вы увидите, что каждая из них также получает UUID и статус. Мы собираемся произвести ротацию и вывод на пенсию, поэтому нам нужно отслеживать ключевой статус, а это значит, что нам нужно отслеживать соленый статус. Когда ключ удален, мы фактически удалим его из БД. Хотя, возможно, мы внесем это в журналы. Обратите внимание, что это не FK обратно в Entity на Sogeum. Если бы вам удалось получить копию этой таблицы, вы не смогли бы сказать, какие соли принадлежат какому объекту. Администраторам будет очень сложно получить доступ к PII - ключи шифрования на самом деле никогда не хранятся. Они генерируются во время хранения и доступа. Итак, допустим, если ваш администратор баз данных захочет прочитать его, ему придется написать код для генерации ключа
- ein: идентификационный номер работодателя. Это то же самое, что и номер социального страхования, но для бизнеса.
- id_Address: мы нормализуем все адреса в этой таблице. Это немного усложняет код создания Entity, поскольку нам нужно сначала вставить адрес, а затем вернуться и обновить его поле id_Entity после создания Entity.
- id_PhoneNumber: точно так же, как адрес.
- id_EntityType: есть несколько таблиц, в которых у вас есть тип. Вместо того, чтобы иметь отдельную таблицу для каждой, которая «очень статична», у нас есть две таблицы: TypesUniversal и TypesForEntity. Таким образом, универсальные средства для всего приложения, в то время как сущность означает специфические для сущности. Затем у нас есть таблица TypeFlavor. Например, будет аромат Entity. Затем будут заявки на LLC, Sole Proprietorship, S Corp, LLP и т. Д.
- createDate: у нас будет таблица журнала, в которой мы будем регистрировать каждое действие CRUD. Но эта таблица будет периодически усекаться, чтобы содержать только одну запись для каждого UUID. Итак, в самих строках таблицы мы указываем созданную дату. Кажется, это самый эффективный способ его отслеживать.
После того, как сущность является пользователем:

- id: Первичный ключ
- createDate: как и в случае с Entity.
- uuid: каждый объект получает UUID.
- id_Person: Каждый объект, которому нужен человек, нормализуется в таблице Person. Его поля легко понять. Если строка конкретного человека является клиентом (он же пациент), эти данные будут фактически зашифрованы. Так что если кому-то удастся получить дамп таблицы Person, большинство строк будет зашифровано разными ключами. Да, повеселитесь, если вам удастся получить дамп со стола. Еще одна вещь, на которую следует обратить внимание, заключается в том, что вся эта нормализация значительно упрощает соблюдение правил конфиденциальности. У нас нет личных данных, скрывающихся в случайных местах, поэтому мы можем легко контролировать доступ к ним.
- emailAddress: именно то, что вы ожидали
- id_Address: нормализовано для хранения всех адресов вместе.
- id_PhoneNumber: точно так же, как id_Address.
- имя пользователя: это может быть то же самое, что и адрес электронной почты, или что-то другое. Все зависит от пользователя.
- pwHash: на самом деле мы не храним пароли, мы храним хеши. Мы их перемешаем с Argon2 и, конечно же, посолим. Эти хэши не будут иметь ничего общего с таблицей Sogeum. Прежде чем засолить хэши, мы проверим их на соответствие наиболее популярным паролям и запретим их.
- id_Entity: каждый пользователь должен быть привязан к сущности.
Далее идет Клиент:

- id: первичный ключ. Он автоинкремент.
- createDate: как и все остальные. Записываем это при создании строки.
- uuid: Ага, еще один. Я не думаю, что это нужно шифровать.
- emailAddress: Именно то, что написано. Мы воспользуемся регулярным выражением, чтобы проверить его действительность. Он будет зашифрован, поэтому при прямом выборе вы увидите тарабарщину.
- id_Person, id_Address, id_PhoneNumber: как и все остальное, они нормированы в отдельные таблицы. Человек будет зашифрован.
- rate1, rate2, rate3, rate4: пользователь может установить ряд ставок по умолчанию. Есть прейскурант, но он не запрещен. Мы будем использовать это в пользовательском интерфейсе, чтобы сделать его крутым, но это не нормализованные поля.
- id_Entity: Да, каждая запись клиента привязана к объекту.
Давайте сделаем ставки сейчас, поскольку мы только что упомянули об этом.

- id: первичный ключ.
- createDate: выполняется при создании.
- uuid: Ага, получит. Позже все это обретет смысл.
- имя: Очевидно.
- количество: Очевидно.
- Комментарий: Очевидно.
- id_Entity: Да, каждая строка специфична для объекта. Но мы не собираемся здесь ничего шифровать.
Теперь будет интересно. Давайте посмотрим на ClientJournal.

Прежде чем мы перейдем к полям, нам нужен контекст. HIPAA ясно дает понять, что данные биллинга и журналы лечения являются конфиденциальными данными, позволяющими установить личность. Таким образом, идея состоит в том, чтобы хранить все данные о лечении отдельно от всех других финансовых транзакций. Вот почему это существует.
Затем я поигрался с идеей объединенной таблицы журнала / бухгалтерской книги. Этот путь является более элегантным с технической точки зрения. Но в итоге мы получим действительно забавные данные, потому что системы бухгалтерского учета являются периодическими по своей природе. Вы периодически очищаете транзакции, но сохраняете сальдо в бухгалтерской книге. Мы можем сделать это на комбинированном столе, но это будет очень неудобно для того, кто действительно разбирается в бухгалтерском учете. Итак, мы имеем ситуацию, когда ожидание домена не соответствует технической ортодоксальности. Я выбираю ожидание домена.
Итак, давайте поговорим о полях в ClientJournal:
- id: первичный ключ.
- createDate: вот оно снова. В конце прикреплю код модели. Тогда вы увидите, что я не набирала это снова и снова.
- uuid: Это немного отличается от других таблиц. Мы собираемся использовать этот UUID в качестве идентификатора транзакции. Транзакции часто имеют несколько строк. Например, если кто-то платит через Paypal, мы будем кредитовать доход, дебетовать ваш счет Paypal (актив) и снимать комиссию Paypall (расходы). Итак, это будут две строки, каждая из которых будет иметь одинаковый UUID.
- id_Client: Конечно, каждая запись здесь будет специфичной для клиента. Данные здесь - это просто число, но почти все данные в таблице Client будут зашифрованы. Поскольку мы нормализовали данные, нам не нужно шифровать эту таблицу.
- id_TransType: пользователь сможет указать, что это за транзакция.
- id_Diagnosis: Специфично для медицины. Есть редактируемая таблица диагнозов. Каждая запись ClientJournal может быть привязана только к одной.
- date: Дата транзакции. Обычно это дата клиентского сеанса или платежа.
- creditAmt: Сумма кредитного счета.
- debitAmt: сумма для дебетового счета. Обратите внимание, что суммы creditAmt и debitAmt должны равняться друг другу в рамках транзакции. Мы не проверяем это с помощью БД, но такая проверка ошибок является ключом к бухгалтерскому учету.
- id_ChartOfAccounts_Debit, id_ChartOfAccounts_Credit: возврат FK в План счетов. Мы поговорим об этом позже.
- идентификатор: у большинства платежей будет идентификатор. Для чеков это номер чека. Это может быть номер транзакции. Когда мы говорим о PaymentType, это будет иметь смысл.
- note, desc: Да, пользователь может добавить примечание или описание
- id_Entity: Да, все зависит от сущности.
Далее идет ClientLedger:

Таким образом, ClientLedger - это то место, где мы действительно увидим, сколько денег поступает, и когда мы выставляем счет, большая часть данных будет поступать отсюда. Во-первых, пара вещей. Каждый клиент - это, по сути, субсчет в строке в Плане счетов, Доход клиента. Поэтому, когда вы создаете нового клиента, мы также создадим запись в ClientJournal, а также в ClientLedger. Когда у этого клиента есть сеанс или какая-либо другая услуга и он немедленно платит наличными, запись в журнале будет CR для пациента и DR для наличных (или наличных для депозита). Соответствующая запись ClientLedger будет такой же, но мы обновим баланс как для наличных, так и для пациента. Мы также сделаем записи в Главной книге, но вместо аварийного восстановления учетной записи клиента она будет привязана к суперсчету ClientRevenue. С другими платежами все немного сложнее, но принципы те же. Пошли в поля:
- id: первичный ключ.
- createDate: вот оно снова. В конце прикреплю код модели. Тогда вы увидите, что я не набирала это снова и снова.
- uuid: это будет точно соответствовать записи в ClientJournal. Я не уверен, всегда ли это правда, поэтому я воздерживаюсь от отношений с ФК. Посмотрим, как все пройдет при тестировании. Из-за разбиения несколько строк часто имеют одинаковый UUID.
- id_Client: почти каждая транзакция будет иметь один из них. Исключениями будут такие вещи, как начальный баланс, дальнейший баланс, корректирующие записи и т. Д.
- creditAmt, debitAmt: Очевидно, для чего это нужно.
- id_ChartOfAccounts_Debit: типы платежей автоматизируют многие из этих записей.
- id_ChartOfAccounts_Credit: здесь будет некоторое разнообразие записей, но большинство из них будет ClientRevenue.
- debitBalance, creditBalance: они очень важны, поэтому необходимо проявлять особую осторожность, чтобы убедиться, что они верны. Чтобы рассчитать его, вам нужно взять последнюю запись и затем добавить новую транзакцию. Поэтому нам нужно как минимум двумя способами проверить, является ли то, что вы берете, самым последним.
- примечание, описание: Они здесь. Посмотрим, сколько они используются.
- id_EntryType: будет таксономия типов записей. Мне придется просмотреть некоторые из моих бухгалтерских текстов, чтобы придумать это.
- id_Entity: конечно.
- date: Я действительно забыл об этом, и мне пришлось пересмотреть схему, чтобы получить это там. Программное обеспечение!
Теперь займемся общим журналом:

Это очень похоже на ClientJournal, поэтому я не буду вдаваться в подробности.
- Здесь нет привязки к клиенту. Вместо этого у нас есть TransEntity. Допустим, вы оплачиваете счет за электричество. Что ж, вы можете сохранить их адрес и номер телефона, если хотите. Или, скажем, это ваша страховка от врачебной ошибки. Вы также можете указать контактное лицо.
- Для такого рода бизнеса это должен быть достаточно легкий стол. Мы не дублируем записи ClientJournal, поэтому это будут обычные бизнес-операции, такие как аренда, страхование, коммунальные услуги ...
В GeneralLedger:

Еще раз, этот очень похож на ClientLedger. Так что я не буду идти по полю.
- Важно то, что здесь также будут размещаться записи ClientLedger. По сути, у них будет удалена PII, просто оставив финансовые данные.
- uuid относится к транзакции. Таким образом, вы увидите один и тот же uuid в ClientJournal, ClientLedger и GeneralLedger.
Давайте посмотрим на ChartOfAccounts:

План счетов занимает центральное место в современном бухгалтерском учете. Я не хочу вести бухгалтерский курс, поэтому не буду разбираться во всех областях.
- parent_id: это позволяет нам делать субаккаунты. Большинство из них будет производиться пользователями. Например, я предварительно укажу Утилиты как счет расходов. Пользователь создаст субсчет электрической компании.
- id_Entity: У каждой сущности будет свой план счетов.
- ScheduleCField: идея состоит в том, что каждая учетная запись связана со строкой Schedule C (если вы не знаете, что это такое, то вам повезло). Затем вы можете нажать кнопку в апреле и подготовить все для уплаты налогов.
Контрольный журнал очень важен в бухгалтерском учете. Итак, у меня это встроено в схему:

Так что это в основном структурированный журнал. Каждый раз, когда мы вносим изменения, мы вставляем здесь строку. Теперь вы понимаете, почему у меня повсюду uuid - это упрощает задачу. Мы могли бы усилить это и поставить скандал каждый раз, когда кто-то на что-то смотрит. Даже для меня это перебор. Мы просто занесем это в журналы.
Но зачем мы это делаем? Я не хотел загромождать каждую таблицу такими полями, как lastEditedBy, lastEditDate, lastAction. Кроме того, это означает, что у нас есть только последняя запись. Вы не можете показать историю. Благодаря этому я могу отображать историю и по-прежнему использовать тот же ORM. Я просто делаю простой запрос с uuid. Если пространство становится проблемой, мы можем справиться с этим, усекая эту таблицу.
Я написал это в основном как документацию для себя. Если вы его видите, вы можете комментировать и вносить предложения. Если из-за этого ты хочешь дать мне работу, ну, ты можешь придумать, как меня найти. Я всегда прислушиваюсь к запросам, хотя не всегда оказываюсь на связи.
На этом пока все. Вот код модели (Medium будет выглядеть плохо, потому что я кодирую шириной 132, ну да ладно):
import uuid
import sys
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.ext.declarative import declared_attr, declarative_base
Base = declarative_base()
db = SQLAlchemy()
class BaseModel(db.Model):
# This is an abstract for our models that we will subclass for the actual tables. Every table will have these fields so
# this is the right way to do it.
__abstract__ = True
id = db.Column(db.Integer, primary_key=True)
createDate = db.Column(db.DateTime)
uuid = db.Column(db.String(32))
class EntityMixin(object):
# This mixin will create an id_Entity column with an FK to the Entity model.
@declared_attr
def id_Entity(cls):
return db.Column(db.Integer, db.ForeignKey('Entity.id'))
@declared_attr
def entity(cls):
return db.relationship('Entity', primaryjoin='%s.id_Entity==Entity.id' % cls.__name__, remote_side='Entity.id')
class NameMixin(object):
# This is a mixin to make the table name match the model name exactly.
@declared_attr
def __tablename__(cls):
return cls.__name__
class AuditTrail(NameMixin, db.Model):
# You could think of this as the application's "master journal" or temporary log
# So we throw data in here every time we modify or create an object.
# The table will be truncated periodically. That's why we have a create date in models for every object.
# The create date may not actually be available here. But having this table will actually allow us to
# build a UI where you can look at some history.
id = db.Column(db.Integer, primary_key=True)
objUUID = db.Column(db.String(32), nullable=False)
model = db.Column(db.String(100), nullable=False) #This will usually be the table name for the object
id_User = db.Column(db.Integer, db.ForeignKey('User.id'), index=True)
user = db.relationship("User", foreign_keys=[id_User])
uuid_User = db.Column(db.String(32), db.ForeignKey('User.uuid'))
userUUID = db.relationship("User", foreign_keys=[uuid_User])
id_Entity = db.Column(db.Integer, db.ForeignKey('Entity.id'), index=True)
entity = db.relationship("Entity", foreign_keys=[id_Entity])
uuid_Entity = db.Column(db.String(32), db.ForeignKey('Entity.uuid'))
entityUUID = db.relationship("Entity", foreign_keys=[uuid_Entity])
action = db.Column(db.String(25), nullable=False)
timestamp = db.Column(db.DateTime)
comment = db.Column(db.String(255))
class Sogeum(NameMixin, BaseModel):
# ipso factotum, learn Korean and you'll know what this model is about
sogeum = db.Column(db.String(128), nullable=False)
status = db.Column(db.Integer, nullable=False)
class Entity(NameMixin, db.Model):
# This model makes SAAS possible. Without it, we have to create a new DB for each subscriber company
id = db.Column(db.Integer, primary_key=True)
uuid = db.Column(db.String(32))
name = db.Column(db.String(255), index=True, nullable=False)
id_Sogeum = db.Column(db.Integer, db.ForeignKey('Sogeum.id'))
sogeum = db.relationship("Sogeum", foreign_keys=[id_Sogeum])
ein = db.Column(db.String(10))
id_Address = db.Column(db.Integer, db.ForeignKey('Address.id'))
address = db.relationship("Address", foreign_keys=[id_Address])
id_PhoneNumber = db.Column(db.Integer, db.ForeignKey('PhoneNumber.id'))
phoneNumber = db.relationship("PhoneNumber", foreign_keys=[id_PhoneNumber])
id_EntityType = db.Column(db.Integer, db.ForeignKey('TypesUniversal.id'), nullable=False)
entityType = db.relationship("TypesUniversal", foreign_keys=[id_EntityType])
createDate = db.Column(db.DateTime)
class Client(NameMixin, EntityMixin, BaseModel):
# In general accounting, this would be customer. But this app is custom to psychiatrists and similar practices
#entity = db.relationship("Entity", foreign_keys=[id_Entity])
emailAddress = db.Column(db.String(254), index=True)
id_Person = db.Column(db.Integer, db.ForeignKey('Person.id'), nullable=False)
person = db.relationship("Person", foreign_keys=[id_Person])
id_Address = db.Column(db.Integer, db.ForeignKey('Address.id'))
address = db.relationship("Address", foreign_keys=[id_Address])
id_PhoneNumber = db.Column(db.Integer, db.ForeignKey('PhoneNumber.id'))
phoneNumber = db.relationship("PhoneNumber", foreign_keys=[id_PhoneNumber])
rate1 = db.Column(db.Float)
rate2 = db.Column(db.Float)
rate3 = db.Column(db.Float)
rate4 = db.Column(db.Float)
class ChartOfAccounts(NameMixin, EntityMixin, BaseModel):
# Pretty straight forward. This will be prepopulated with a bunch and then the entity will have to add a lot
code = db.Column(db.String(5), nullable=False, index=True)
name = db.Column(db.String(50), nullable=False)
desc = db.Column(db.String(255))
parent_id = db.Column(db.Integer)
id_AccountTypes = db.Column(db.Integer, db.ForeignKey('TypesUniversal.id'))
accountType = db.relationship("TypesUniversal", foreign_keys=[id_AccountTypes])
id_ScheduleCField = db.Column(db.Integer, db.ForeignKey('ScheduleCField.id'))
scheduleCField = db.relationship("ScheduleCField", foreign_keys=[id_ScheduleCField])
class ClientJournal(NameMixin, EntityMixin, BaseModel):
# This holds the journal for all the practitioner's client sessions. It's separate from the GL because it must be HIPAA compliant
# uuid is going to be per transaction and not per row
id_Client = db.Column(db.Integer, db.ForeignKey('Client.id'))
client = db.relationship("Client", foreign_keys=[id_Client])
id_TransType = db.Column(db.Integer, db.ForeignKey('TypesForEntity.id'))
transType = db.relationship("TypesForEntity", foreign_keys=[id_TransType])
id_Diagnosis = db.Column(db.Integer, db.ForeignKey('Diagnosis.id'))
diagnosis = db.relationship("Diagnosis", foreign_keys=[id_Diagnosis])
date = db.Column(db.DateTime, nullable=False)
creditAmt = db.Column(db.Float)
debitAmt = db.Column(db.Float)
id_ChartOfAccounts_Debit = db.Column(db.Integer, db.ForeignKey('ChartOfAccounts.id'))
acctDebit = db.relationship("ChartOfAccounts", foreign_keys=[id_ChartOfAccounts_Debit])
id_ChartOfAccounts_Credit = db.Column(db.Integer, db.ForeignKey('ChartOfAccounts.id'))
acctCredit = db.relationship("ChartOfAccounts", foreign_keys=[id_ChartOfAccounts_Credit])
identifier = db.Column(db.String(255))
note = db.Column(db.String(255))
desc = db.Column(db.String(255))
class ClientLedger(NameMixin, EntityMixin, BaseModel):
# We break out the ledger from the journal to make maintenance easier.
# In the ledger, we're focusing on account balances. There is more detail on the transaction in the journals.
# We could put it all together, but closing out periods would be trickier.
# To check balances for clients, we'll have to come to this table.
# uuid will usually not be generated specifically for this table, but will come from the journal.
id_Client = db.Column(db.Integer, db.ForeignKey('Client.id'))
client = db.relationship("Client", foreign_keys=[id_Client])
creditAmt = db.Column(db.Float)
debitAmt = db.Column(db.Float)
id_ChartOfAccounts_Debit = db.Column(db.Integer, db.ForeignKey('ChartOfAccounts.id'))
acctDebit = db.relationship("ChartOfAccounts", foreign_keys=[id_ChartOfAccounts_Debit])
id_ChartOfAccounts_Credit = db.Column(db.Integer, db.ForeignKey('ChartOfAccounts.id'))
acctCredit = db.relationship("ChartOfAccounts", foreign_keys=[id_ChartOfAccounts_Credit])
debitBalance = db.Column(db.Integer, nullable=False)
creditBalance = db.Column(db.Integer, nullable=False)
note = db.Column(db.String(255))
desc = db.Column(db.String(255))
id_EntryType = db.Column(db.Integer, db.ForeignKey('TypesUniversal.id'))
entryType = db.relationship("TypesUniversal", foreign_keys=[id_EntryType])
date = db.Column(db.DateTime, index=True)
class User(NameMixin, EntityMixin, BaseModel):
# Pretty obvious what this is for
id_Person = db.Column(db.Integer, db.ForeignKey('Person.id'), nullable=False)
person = db.relationship("Person", foreign_keys=[id_Person])
emailAddress = db.Column(db.String(254), index=True)
id_Address = db.Column(db.Integer, db.ForeignKey('Address.id'))
address = db.relationship("Address", foreign_keys=[id_Address])
id_PhoneNumber = db.Column(db.Integer, db.ForeignKey('PhoneNumber.id'))
phoneNumber = db.relationship("PhoneNumber", foreign_keys=[id_PhoneNumber])
username = db.Column(
db.String(254), nullable=False,
index=True) # you can duplicate your email address as the username so this can be 254 characters
pwHash = db.Column(db.String(128), nullable=False) # This is likely larger than we need, but so what
roles = db.relationship("UserRoleJunction")
class Person(NameMixin, EntityMixin, BaseModel):
# This is essentially a normalization table. Most of the entries will be encrypted as they are clients
firstName = db.Column(db.String(100))
middleName = db.Column(db.String(100))
lastName = db.Column(db.String(100), index=True, nullable=False)
suffix = db.Column(db.String(50))
class Address(NameMixin, EntityMixin, BaseModel):
# Another normalization table to put addresses all in one place; addresses are not PII so we don't need to encrypt them
addrLine1 = db.Column(db.String(100), index=True, nullable=False)
addrLine2 = db.Column(db.String(100), default=None)
city = db.Column(db.String(100), nullable=False, index=True)
stateOrProvince = db.Column(db.String(100), nullable=False, index=True)
country = db.Column(db.String(100), nullable=False, default='USA')
class PhoneNumber(NameMixin, EntityMixin, BaseModel):
# Another normalization table. Instead of integers we use strings to support alphanumerics
countryCode = db.Column(db.String(4), default='01', nullable=False)
areaOrCityCode = db.Column(db.String(4))
number = db.Column(db.String(12), nullable=False)
class Rates(NameMixin, EntityMixin, BaseModel):
# This is a simple model to hold default rates. It's not FK'ed to to the client model so it's easy to override.
# In other words, this will will exclusively benefit the UI
name = db.Column(db.String(50), nullable=False)
amount = db.Column(db.Float, nullable=False)
comment = db.Column(db.String(255), default=None)
class GeneralJournal(NameMixin, EntityMixin, BaseModel):
'''
After a lot of thinking, I've decided to do actual double entry.
Double entry is going to make closing accounting periods much easier.
Also, it will correspond more directly to accounting as it is taught so all this will be easier to maintain.
Transactions are first put into the journal table. Then (probably simultaneously), balances will be calculated in
the General Ledger.
uuid is per transaction and will be shared across the ledger and journal.
'''
date = db.Column(db.DateTime, nullable=False, index=True)
debitAmt = db.Column(db.Float) # This can be null because of splits
id_ChartOfAccounts_Debit = db.Column(db.Integer, db.ForeignKey('ChartOfAccounts.id'))
acctDebit = db.relationship("ChartOfAccounts", foreign_keys=[id_ChartOfAccounts_Debit])
creditAmt = db.Column(db.Float) # This can be null because of splits
id_ChartOfAccounts_Credit = db.Column(db.Integer, db.ForeignKey('ChartOfAccounts.id'))
acctCredit = db.relationship("ChartOfAccounts", foreign_keys=[id_ChartOfAccounts_Credit])
transName = db.Column(db.String(100))
note = db.Column(db.String(255))
desc = db.Column(db.String(255))
id_TransEntity = db.Column(db.Integer, db.ForeignKey('TransEntity.id'))
transEntity = db.relationship("TransEntity", foreign_keys=[id_TransEntity])
id_TransType = db.Column(db.Integer, db.ForeignKey('TypesForEntity.id'))
transType = db.relationship("TypesForEntity", foreign_keys=[id_TransType])
class GeneralLedger(NameMixin, EntityMixin, BaseModel):
# This one is for tracking balances. We're going to
debitBalance = db.Column(db.Integer, nullable=False)
creditBalance = db.Column(db.Integer, nullable=False)
debitAmt = db.Column(db.Float) # This can be null because of splits
id_ChartOfAccounts_Debit = db.Column(db.Integer, db.ForeignKey('ChartOfAccounts.id'))
acctDebit = db.relationship("ChartOfAccounts", foreign_keys=[id_ChartOfAccounts_Debit])
creditAmt = db.Column(db.Float) # This can be null because of splits
id_ChartOfAccounts_Credit = db.Column(db.Integer, db.ForeignKey('ChartOfAccounts.id'))
acctCredit = db.relationship("ChartOfAccounts", foreign_keys=[id_ChartOfAccounts_Credit])
note = db.Column(db.String(255))
desc = db.Column(db.String(255))
id_EntryType = db.Column(db.Integer, db.ForeignKey('TypesUniversal.id'))
entryType = db.relationship("TypesUniversal", foreign_keys=[id_EntryType])
class TransEntity(NameMixin, EntityMixin, BaseModel):
# Our customers will do business with other entities like the phone company, insurance companies, etc.
# This model is to hold the metadata for these counterparties.
name = db.Column(db.String(100), nullable=False, index=True)
id_Person = db.Column(db.Integer, db.ForeignKey('Person.id'), nullable=False) # A contact person
person = db.relationship("Person", foreign_keys=[id_Person])
id_Address = db.Column(db.Integer, db.ForeignKey('Address.id'))
address = db.relationship("Address", foreign_keys=[id_Address])
id_PhoneNumber = db.Column(db.Integer, db.ForeignKey('PhoneNumber.id'))
phoneNumber = db.relationship("PhoneNumber", foreign_keys=[id_PhoneNumber])
emailAddress = db.Column(db.String(254))
class PaymentTypeAccountJunction(NameMixin, EntityMixin, BaseModel):
# This is the junction table between payment type and sessing ledger
# In the UI, the user will pick a type and based on what's here, the ledger will end up with the correct splits
id_PaymentType = db.Column(db.Integer, db.ForeignKey('TypesForEntity.id'), nullable=False, primary_key=True)
paymentType = db.relationship("TypesForEntity", foreign_keys=[id_PaymentType])
id_ChartOfAccounts_Credit = db.Column(db.Integer, db.ForeignKey('ChartOfAccounts.id'))
acctCredit = db.relationship("ChartOfAccounts", foreign_keys=[id_ChartOfAccounts_Credit])
percentCredit = db.Column(db.Float)
id_ChartOfAccounts_Debit = db.Column(db.Integer, db.ForeignKey('ChartOfAccounts.id'))
acctDebit = db.relationship("ChartOfAccounts", foreign_keys=[id_ChartOfAccounts_Debit])
percentDebit = db.Column(db.Float)
class TypesForEntity(NameMixin, EntityMixin, BaseModel):
# We were going to have a bunch of different type models.
# But, it seems more extensible if we have a generic type model with flavors from another model
# Then we can add new ones later without a schema change
# This one is for types specific to entities
name = db.Column(db.String(100), nullable=False, index=True)
desc = db.Column(db.String(254), default=None)
identifierType = db.Column(
db.String(100)) # this field allows the user to designate an identifier like check number
id_TypeFlavor = db.Column(db.Integer, db.ForeignKey('TypeFlavor.id'), nullable=False)
typeFlavor = db.relationship("TypeFlavor", foreign_keys=[id_TypeFlavor])
class TypesUniversal(NameMixin, BaseModel):
# This is like TypesForEntity, but these are universal
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, index=True)
desc = db.Column(db.String(254), default=None)
identifierType = db.Column(
db.String(100)) # this field allows the user to designate an identifier like check number
id_TypeFlavor = db.Column(db.Integer, db.ForeignKey('TypeFlavor.id'), nullable=False)
flavor = db.relationship("TypeFlavor", foreign_keys=[id_TypeFlavor])
class TypeFlavor(NameMixin, BaseModel):
# See comment on Types. Initial flavors include payment, session, account, entity
# This model uses the same data application wide so there is no entity field
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, index=True)
desc = db.Column(db.String(254), default=None)
class ScheduleCField(NameMixin, BaseModel):
# This one will go application wide, too, so no entity field
# It will be the source of an FK relatioship into Chart of Accounts
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, index=True)
desc = db.Column(db.String(254), default=None)
class Diagnosis(NameMixin, EntityMixin, BaseModel):
# This name should be generalized so that we can service more than health care providers
name = db.Column(db.String(100), nullable=False, index=True)
desc = db.Column(db.String(254), default=None)
class Role(NameMixin, BaseModel):
# We have to do authorizations. So there will be roles. Users can have more than one role so we'll also have a junction table
# We are not going to make the roles configurable by end users just yet.
name = db.Column(db.String(100), nullable=False)
desc = db.Column(db.String(255), default=None)
class UserRoleJunction(NameMixin, EntityMixin, BaseModel):
# In later versions, we may add fields to this so we can have scope
id_User = db.Column(db.Integer, db.ForeignKey('User.id'), primary_key=True)
user = db.relationship("User", foreign_keys=[id_User])
id_Role = db.Column(db.Integer, db.ForeignKey('Role.id'), primary_key=True)
role = db.relationship("Role", foreign_keys=[id_Role])