Compare commits

..
17 Commits
Author SHA1 Message Date
tanshu 72843feaac Version Bump v11.2.1 2023-08-10 13:32:47 +05:30
tanshu e072e77663 Fix: Recipe add was not working and removed unused rate input in the recipe detail component 2023-08-10 13:32:35 +05:30
tanshu 9a3bd413d6 Version Bump v11.2.0 2023-08-08 13:07:00 +05:30
tanshu 2fa346e084 Fix: Fingerprints should have the right constraint
Fix: Client list was not working
2023-08-08 13:06:42 +05:30
tanshu ecd3e45632 Version Bump v11.1.9 2023-08-07 12:48:02 +05:30
tanshu 48ec2df10d Fix: Fingerprint was ignoring the time aspect which is obviously very important 2023-08-07 12:47:57 +05:30
tanshu bc61eeacd3 Version Bump v11.1.8 2023-08-07 10:58:49 +05:30
tanshu a051071a1b Fix: Fingerprint upload was broken 2023-08-07 10:58:41 +05:30
tanshu a0b939ccd7 Version Bump v11.1.7 2023-08-07 09:20:37 +05:30
tanshu 0fc8fac5aa Fix: docker should upload to gondor now not beacon 2023-08-07 09:20:21 +05:30
tanshu 220c15b3fa Fix: Pydantic v2 is sending decimals as strings and fucking things up 2023-08-07 09:19:48 +05:30
tanshu a514c97409 Version Bump v11.1.6 2023-08-07 07:33:21 +05:30
tanshu 77e2411a88 Chore: Refactored the ansible playbook to use one .env file and vars
Chore: Moved to gondor
2023-08-07 07:32:28 +05:30
tanshu 5565e923ab Fix: Create account now working. Dataclasses need ALL the members in the default init created. 2023-08-07 07:27:53 +05:30
tanshu 45d5b658e8 Version Bump v11.1.5 2023-08-06 09:37:39 +05:30
tanshu f0cbe4a7de Chore: Started using gunicorn 2023-08-06 09:09:02 +05:30
tanshu 9ad411af65 Chore: Just made it look nicer 2023-08-05 08:05:14 +05:30
43 changed files with 329 additions and 175 deletions
@@ -0,0 +1,24 @@
"""FP
Revision ID: 48af31eb6f3f
Revises: 12262aadbc08
Create Date: 2023-08-07 13:01:05.401492
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '48af31eb6f3f'
down_revision = '12262aadbc08'
branch_labels = None
depends_on = None
def upgrade():
op.create_unique_constraint(op.f('uq_fingerprints_date'), 'fingerprints', ['date', 'employee_id'])
def downgrade():
op.drop_constraint(op.f('uq_fingerprints_date'), 'fingerprints', type_='unique')
+1 -1
View File
@@ -1,4 +1,4 @@
from brewman.main import init from .main import init
init() init()
+1 -1
View File
@@ -1 +1 @@
__version__ = "11.1.4" __version__ = "11.2.1"
+26
View File
@@ -1,3 +1,5 @@
import uuid
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from sqlalchemy.orm import Mapped, relationship from sqlalchemy.orm import Mapped, relationship
@@ -18,6 +20,30 @@ class Account(AccountBase):
"Product", primaryjoin="Account.id==Product.account_id", back_populates="account" "Product", primaryjoin="Account.id==Product.account_id", back_populates="account"
) )
def __init__(
self,
name: str,
type_id: int,
is_starred: bool,
is_active: bool,
is_reconcilable: bool,
cost_centre_id: uuid.UUID,
code: int | None = None,
id_: uuid.UUID | None = None,
is_fixture: bool = False,
) -> None:
if code is not None:
self.code = code
self.name = name
self.type_id = type_id
self.is_starred = is_starred
self.is_active = is_active
self.is_reconcilable = is_reconcilable
self.cost_centre_id = cost_centre_id
if id_ is not None:
self.id = id_
self.is_fixture = is_fixture
def can_delete(self, advanced_delete: bool) -> tuple[bool, str]: def can_delete(self, advanced_delete: bool) -> tuple[bool, str]:
if len(self.products) > 0: if len(self.products) > 0:
return False, "Account has products" return False, "Account has products"
-7
View File
@@ -70,13 +70,6 @@ class AccountBase:
self.id = id_ self.id = id_
self.is_fixture = is_fixture self.is_fixture = is_fixture
def create(self, db: Session) -> "AccountBase":
self.code = db.execute(
select(func.coalesce(func.max(AccountBase.code), 0) + 1).where(AccountBase.type_id == self.type_id)
).scalar_one()
db.add(self)
return self
def can_delete(self, advanced_delete: bool) -> tuple[bool, str]: def can_delete(self, advanced_delete: bool) -> tuple[bool, str]:
if self.is_fixture: if self.is_fixture:
return False, f"{self.name} is a fixture and cannot be edited or deleted." return False, f"{self.name} is a fixture and cannot be edited or deleted."
+2 -1
View File
@@ -3,7 +3,7 @@ import uuid
from datetime import datetime from datetime import datetime
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from sqlalchemy import DateTime, ForeignKey, Uuid from sqlalchemy import DateTime, ForeignKey, UniqueConstraint, Uuid
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db.base_class import reg from ..db.base_class import reg
@@ -16,6 +16,7 @@ if TYPE_CHECKING:
@reg.mapped_as_dataclass(unsafe_hash=True) @reg.mapped_as_dataclass(unsafe_hash=True)
class Fingerprint: class Fingerprint:
__tablename__ = "fingerprints" __tablename__ = "fingerprints"
__table_args__ = (UniqueConstraint("date", "employee_id"),)
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, insert_default=uuid.uuid4) id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, insert_default=uuid.uuid4)
employee_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("employees.id"), nullable=False) employee_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("employees.id"), nullable=False)
+27
View File
@@ -40,3 +40,30 @@ class Recipe:
tags: Mapped[list["Tag"]] = relationship( tags: Mapped[list["Tag"]] = relationship(
"Tag", secondary=RecipeTag.__table__, order_by="Tag.name", back_populates="recipes" "Tag", secondary=RecipeTag.__table__, order_by="Tag.name", back_populates="recipes"
) )
def __init__(
self,
date_: date,
source: str,
instructions: str,
garnishing: str,
plating: str,
notes: str,
recipe_yield: Decimal,
sku_id: uuid.UUID | None = None,
sku: "StockKeepingUnit" | None = None,
id_: uuid.UUID | None = None,
):
self.date_ = date_
self.source = source
self.instructions = instructions
self.garnishing = garnishing
self.plating = plating
self.notes = notes
self.recipe_yield = recipe_yield
if sku_id is not None:
self.sku_id = sku_id
if sku is not None:
self.sku = sku
if id_ is not None:
self.id = id_
+23
View File
@@ -29,3 +29,26 @@ class RecipeItem:
recipe: Mapped["Recipe"] = relationship("Recipe", back_populates="items") recipe: Mapped["Recipe"] = relationship("Recipe", back_populates="items")
product: Mapped["Product"] = relationship("Product") product: Mapped["Product"] = relationship("Product")
def __init__(
self,
quantity: Decimal,
description: str = "",
recipe_id: uuid.UUID | None = None,
product_id: uuid.UUID | None = None,
recipe: "Recipe" | None = None,
product: "Product" | None = None,
id_: uuid.UUID | None = None,
):
self.quantity = quantity
self.description = description
if recipe_id is not None:
self.recipe_id = recipe_id
if product_id is not None:
self.product_id = product_id
if recipe is not None:
self.recipe = recipe
if product is not None:
self.product = product
if id_ is not None:
self.id = id_
+2 -3
View File
@@ -35,15 +35,14 @@ def save(
with SessionFuture() as db: with SessionFuture() as db:
item = Account( item = Account(
name=data.name, name=data.name,
code=Account.get_code(data.type_, db),
type_id=data.type_, type_id=data.type_,
is_starred=data.is_starred, is_starred=data.is_starred,
is_active=data.is_active, is_active=data.is_active,
is_reconcilable=data.is_reconcilable, is_reconcilable=data.is_reconcilable,
cost_centre_id=data.cost_centre.id_, cost_centre_id=data.cost_centre.id_,
) )
item.code = db.execute(
select(func.coalesce(func.max(Account.code), 0) + 1).where(Account.type_id == item.type_id)
).scalar_one()
db.add(item) db.add(item)
db.commit() db.commit()
except SQLAlchemyError as e: except SQLAlchemyError as e:
+5 -14
View File
@@ -8,7 +8,7 @@ from io import StringIO
import brewman.schemas.fingerprint as schemas import brewman.schemas.fingerprint as schemas
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
from sqlalchemy import bindparam, select from sqlalchemy import select
from sqlalchemy.dialects.postgresql import insert as pg_insert from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -36,22 +36,13 @@ def upload_prints(
for id_, code in db.execute(select(Employee.id, Employee.code)).all(): for id_, code in db.execute(select(Employee.id, Employee.code)).all():
employees[code] = id_ employees[code] = id_
file_data = read_file(fingerprints) file_data = read_file(fingerprints)
prints = [d for d in fp(file_data, employees) if start <= d.date_.date() <= finish] prints = [d.model_dump() for d in fp(file_data, employees) if start <= d.date_.date() <= finish]
for p in prints:
p["id"] = p.pop("id_")
paged_data = [prints[i : i + 100] for i in range(0, len(prints), 100)] paged_data = [prints[i : i + 100] for i in range(0, len(prints), 100)]
for i, page in enumerate(paged_data): for i, page in enumerate(paged_data):
print(f"Processing page {i} of {len(paged_data)}") print(f"Processing page {i} of {len(paged_data)}")
db.execute( db.execute(pg_insert(Fingerprint).on_conflict_do_nothing(), page)
pg_insert(Fingerprint)
.values(
{
"id": bindparam("id"),
"employee_id": bindparam("employee_id"),
"date": bindparam("date"),
}
)
.on_conflict_do_nothing(),
[p.dict() for p in page],
)
db.commit() db.commit()
except SQLAlchemyError as e: except SQLAlchemyError as e:
raise HTTPException( raise HTTPException(
+3 -1
View File
@@ -58,6 +58,7 @@ def save(
instructions=data.instructions, instructions=data.instructions,
garnishing=data.garnishing, garnishing=data.garnishing,
plating=data.plating, plating=data.plating,
notes=data.notes,
sku=recipe_sku, sku=recipe_sku,
recipe_yield=round(data.recipe_yield, 2), recipe_yield=round(data.recipe_yield, 2),
) )
@@ -71,7 +72,8 @@ def save(
r_item.recipe_id = recipe.id r_item.recipe_id = recipe.id
db.add(r_item) db.add(r_item)
check_recursion(set([recipe_sku.product_id]), set(), recipe, db) # TODO: Check recursion
# check_recursion(set([recipe_sku.product_id]), set(), recipe, db)
db.commit() db.commit()
return recipe_info(recipe) return recipe_info(recipe)
except SQLAlchemyError as e: except SQLAlchemyError as e:
+5 -3
View File
@@ -44,11 +44,13 @@ class ClientList(Client):
@field_validator("last_date", mode="before") @field_validator("last_date", mode="before")
@classmethod @classmethod
def parse_last_date(cls, value: datetime | str) -> datetime | None: def parse_last_date(cls, value: datetime | str | None) -> datetime | None:
if value is None or value == "":
return None
if isinstance(value, datetime): if isinstance(value, datetime):
return value return value
return datetime.strptime(value, "%d-%b-%Y %H:%M") return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("last_date") @field_serializer("last_date")
def serialize_last_date(self, value: datetime, info: FieldSerializationInfo) -> str: def serialize_last_date(self, value: datetime | None, info: FieldSerializationInfo) -> str | None:
return value.strftime("%d-%b-%Y %H:%M") return None if value is None else value.strftime("%d-%b-%Y %H:%M")
+4 -4
View File
@@ -20,11 +20,11 @@ class Fingerprint(BaseModel):
@field_validator("date_", mode="before") @field_validator("date_", mode="before")
@classmethod @classmethod
def parse_date(cls, value: date | str) -> date: def parse_date(cls, value: datetime | str) -> datetime:
if isinstance(value, date): if isinstance(value, datetime):
return value return value
return datetime.strptime(value, "%d-%b-%Y").date() return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("date_") @field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str: def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y") return value.strftime("%d-%b-%Y %H:%M")
+68
View File
@@ -0,0 +1,68 @@
import json
import multiprocessing
import os
workers_per_core_str = os.getenv("WORKERS_PER_CORE", "1")
max_workers_str = os.getenv("MAX_WORKERS")
use_max_workers = None
if max_workers_str:
use_max_workers = int(max_workers_str)
web_concurrency_str = os.getenv("WEB_CONCURRENCY", None)
host = os.getenv("HOST", "0.0.0.0")
port = os.getenv("PORT", "9994")
bind_env = os.getenv("BIND", None)
use_loglevel = os.getenv("LOG_LEVEL", "info")
if bind_env:
use_bind = bind_env
else:
use_bind = f"{host}:{port}"
cores = multiprocessing.cpu_count()
workers_per_core = float(workers_per_core_str)
default_web_concurrency = workers_per_core * cores
if web_concurrency_str:
web_concurrency = int(web_concurrency_str)
assert web_concurrency > 0
else:
web_concurrency = max(int(default_web_concurrency), 2)
if use_max_workers:
web_concurrency = min(web_concurrency, use_max_workers)
accesslog_var = os.getenv("ACCESS_LOG", "-")
use_accesslog = accesslog_var or None
errorlog_var = os.getenv("ERROR_LOG", "-")
use_errorlog = errorlog_var or None
graceful_timeout_str = os.getenv("GRACEFUL_TIMEOUT", "120")
timeout_str = os.getenv("TIMEOUT", "120")
keepalive_str = os.getenv("KEEP_ALIVE", "5")
# Gunicorn config variables
loglevel = use_loglevel
workers = web_concurrency
bind = use_bind
errorlog = use_errorlog
worker_tmp_dir = "/dev/shm"
accesslog = use_accesslog
graceful_timeout = int(graceful_timeout_str)
timeout = int(timeout_str)
keepalive = int(keepalive_str)
# For debugging and testing
log_data = {
"loglevel": loglevel,
"workers": workers,
"bind": bind,
"graceful_timeout": graceful_timeout,
"timeout": timeout,
"keepalive": keepalive,
"errorlog": errorlog,
"accesslog": accesslog,
# Additional, non-gunicorn variables
"workers_per_core": workers_per_core,
"use_max_workers": use_max_workers,
"host": host,
"port": port,
}
print(json.dumps(log_data))
+53
View File
@@ -0,0 +1,53 @@
[loggers]
keys=root, gunicorn.error, gunicorn.access
[handlers]
keys=console, error, access
[formatters]
keys=generic, error, access
[logger_root]
level=INFO
handlers=console
qualname=root
[logger_gunicorn.error]
level=INFO
handlers=console
qualname=gunicorn.error
[logger_gunicorn.access]
level=INFO
handlers=access
qualname=gunicorn.access
[handler_console]
class=StreamHandler
formatter=generic
args=(sys.stdout, )
[handler_error]
class=StreamHandler
formatter=error
args=(sys.stdout, )
[handler_access]
class=StreamHandler
formatter=access
args=(sys.stdout, )
[formatter_generic]
format=%(asctime)s [%(name)s %(levelname)s %(process)d] %(message)s
datefmt=%Y-%m-%d %H:%M:%S %Z
class=logging.Formatter
[formatter_error]
format=%(asctime)s [%(name)s %(levelname)s %(process)d] %(message)s | %(funcName)s() | %(pathname)s L%(lineno)-4d
datefmt=%Y-%m-%d %H:%M:%S %Z
class=logging.Formatter
[formatter_access]
format=%(asctime)s [%(name)s %(levelname)s %(process)d] %(message)s
datefmt=%Y-%m-%d %H:%M:%S %Z
class=logging.Formatter
+8 -7
View File
@@ -1,27 +1,28 @@
[tool.poetry] [tool.poetry]
name = "brewman" name = "brewman"
version = "11.1.4" version = "11.2.1"
description = "Accounting plus inventory management for a restaurant." description = "Accounting plus inventory management for a restaurant."
authors = ["tanshu <git@tanshu.com>"] authors = ["tanshu <git@tanshu.com>"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.11" python = "^3.11"
uvicorn = {extras = ["standard"], version = "^0.21.1"} uvicorn = {extras = ["standard"], version = "^0.23.2"}
fastapi = {extras = ["all"], version = "^0.100.0"} fastapi = {extras = ["all"], version = "^0.101.0"}
python-jose = {extras = ["cryptography"], version = "^3.3.0"} python-jose = {extras = ["cryptography"], version = "^3.3.0"}
passlib = {extras = ["bcrypt"], version = "^1.7.4"} passlib = {extras = ["bcrypt"], version = "^1.7.4"}
psycopg2-binary = "^2.9.5" psycopg2-binary = "^2.9.7"
SQLAlchemy = "^2.0.7" SQLAlchemy = "^2.0.19"
python-multipart = "^0.0.6" python-multipart = "^0.0.6"
PyJWT = "^2.8.0" PyJWT = "^2.8.0"
alembic = "^1.11.1" alembic = "^1.11.2"
itsdangerous = "^2.1.2" itsdangerous = "^2.1.2"
python-dotenv = "^1.0.0" python-dotenv = "^1.0.0"
pydantic = {extras = ["dotenv"], version = "^2.0.3"} pydantic = {extras = ["dotenv"], version = "^2.1.1"}
starlette = "^0.27.0" starlette = "^0.27.0"
pandas = "^2.0.0" pandas = "^2.0.0"
arq = "^0.25.0" arq = "^0.25.0"
openpyxl = "^3.1.2" openpyxl = "^3.1.2"
gunicorn = "^21.2.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
flake8 = "^6.0.0" flake8 = "^6.0.0"
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
set -euo pipefail
gunicorn brewman.main:app --worker-class uvicorn.workers.UvicornWorker --config ./gunicorn.conf.py --log-config ./logging.conf
+1 -1
View File
@@ -32,5 +32,5 @@ else
echo "No version bump" echo "No version bump"
fi fi
cd "$parent_path/docker" || exit cd "$parent_path/docker" || exit
docker save brewman:latest | bzip2 | pv | ssh beacon 'bunzip2 | sudo docker load' docker save brewman:latest | bzip2 | pv | ssh gondor 'bunzip2 | sudo docker load'
ansible-playbook --inventory hosts playbook.yml ansible-playbook --inventory hosts playbook.yml
+1 -1
View File
@@ -50,4 +50,4 @@ RUN chmod 777 /app/docker-entrypoint.sh \
&& ln -s /app/docker-entrypoint.sh / && ln -s /app/docker-entrypoint.sh /
ENTRYPOINT ["docker-entrypoint.sh"] ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["poetry", "run", "python", "-m", "brewman"] CMD ["poetry", "run", "gunicorn", "brewman.main:app", "--worker-class", "uvicorn.workers.UvicornWorker", "--config", "/app/gunicorn.conf.py", "--log-config", "/app/logging.conf"]
@@ -2,15 +2,11 @@ HOST=0.0.0.0
PORT=80 PORT=80
LOG_LEVEL=WARN LOG_LEVEL=WARN
DEBUG=false DEBUG=false
SQLALCHEMY_DATABASE_URI=postgresql://postgres:123456@db:5432/brewman_hinchco SQLALCHEMY_DATABASE_URI=postgresql://postgres:123456@db:5432/brewman_{{ name }}
MODULE_NAME=brewman.main MODULE_NAME=brewman.main
PROJECT_NAME=brewman PROJECT_NAME=brewman
POSTGRES_SERVER=db SECRET_KEY={{ secret_key }}
POSTGRES_USER=postgres MIDDLEWARE_SECRET_KEY={{ middleware_key }}
POSTGRES_PASSWORD=123456
POSTGRES_DB=brewman_hinchco
SECRET_KEY=7b889cff76532fde8483304cf415243f70df200518ba9aee4d26c0709ad6fbd1
MIDDLEWARE_SECRET_KEY=1e36e7f678
ALGORITHM=HS256 ALGORITHM=HS256
JWT_TOKEN_EXPIRE_MINUTES=30 JWT_TOKEN_EXPIRE_MINUTES=30
ALEMBIC_LOG_LEVEL=INFO ALEMBIC_LOG_LEVEL=INFO
-17
View File
@@ -1,17 +0,0 @@
HOST=0.0.0.0
PORT=80
LOG_LEVEL=WARN
DEBUG=false
SQLALCHEMY_DATABASE_URI=postgresql://postgres:123456@db:5432/acc
MODULE_NAME=brewman.main
PROJECT_NAME=brewman
POSTGRES_SERVER=db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=123456
POSTGRES_DB=exp
SECRET_KEY=c9bee2d38676447c2f7a9ea715446e2fd09f16fbaa5b3f6a6f207ec18993987f
MIDDLEWARE_SECRET_KEY=cb71666b9c
ALGORITHM=HS256
JWT_TOKEN_EXPIRE_MINUTES=30
ALEMBIC_LOG_LEVEL=INFO
ALEMBIC_SQLALCHEMY_LOG_LEVEL=WARN
-17
View File
@@ -1,17 +0,0 @@
HOST=0.0.0.0
PORT=80
LOG_LEVEL=WARN
DEBUG=false
SQLALCHEMY_DATABASE_URI=postgresql://postgres:123456@db:5432/exp
MODULE_NAME=brewman.main
PROJECT_NAME=brewman
POSTGRES_SERVER=db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=123456
POSTGRES_DB=exp
SECRET_KEY=8546a61262dab7c05ccf2e26abe30bc10966904df6dfd29259ea85dd0844a8e7
MIDDLEWARE_SECRET_KEY=da6fcd999b
ALGORITHM=HS256
JWT_TOKEN_EXPIRE_MINUTES=30
ALEMBIC_LOG_LEVEL=INFO
ALEMBIC_SQLALCHEMY_LOG_LEVEL=WARN
-17
View File
@@ -1,17 +0,0 @@
HOST=0.0.0.0
PORT=80
LOG_LEVEL=WARN
DEBUG=false
SQLALCHEMY_DATABASE_URI=postgresql://postgres:123456@db:5432/hops
MODULE_NAME=brewman.main
PROJECT_NAME=brewman
POSTGRES_SERVER=db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=123456
POSTGRES_DB=exp
SECRET_KEY=cfb3be420c4e2b0ed423b2e4e238713d0461e2ba56198138ad6c4d82aef6295c
MIDDLEWARE_SECRET_KEY=9c2bdd24be
ALGORITHM=HS256
JWT_TOKEN_EXPIRE_MINUTES=30
ALEMBIC_LOG_LEVEL=INFO
ALEMBIC_SQLALCHEMY_LOG_LEVEL=WARN
-17
View File
@@ -1,17 +0,0 @@
HOST=0.0.0.0
PORT=80
LOG_LEVEL=WARN
DEBUG=false
SQLALCHEMY_DATABASE_URI=postgresql://postgres:123456@db:5432/mhl
MODULE_NAME=brewman.main
PROJECT_NAME=brewman
POSTGRES_SERVER=db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=123456
POSTGRES_DB=exp
SECRET_KEY=c9fd1b99931feb083f67470170650420b99eb35368d3de186427166c28d32c8b
MIDDLEWARE_SECRET_KEY=9183bdcfb0
ALGORITHM=HS256
JWT_TOKEN_EXPIRE_MINUTES=30
ALEMBIC_LOG_LEVEL=INFO
ALEMBIC_SQLALCHEMY_LOG_LEVEL=WARN
+5 -5
View File
@@ -5,11 +5,11 @@
# - A hostname/ip can be a member of multiple groups # - A hostname/ip can be a member of multiple groups
[brewman] [brewman]
acc ansible_host=beacon var_file=vars/acc.yml acc ansible_host=gondor var_file=vars/acc.yml
exp ansible_host=beacon var_file=vars/exp.yml exp ansible_host=gondor var_file=vars/exp.yml
hops ansible_host=beacon var_file=vars/hops.yml hops ansible_host=gondor var_file=vars/hops.yml
mhl ansible_host=beacon var_file=vars/mhl.yml mhl ansible_host=gondor var_file=vars/mhl.yml
hinchco ansible_host=beacon var_file=vars/hinchco.yml hinchco ansible_host=gondor var_file=vars/hinchco.yml
[all:vars] [all:vars]
ansible_python_interpreter=/usr/bin/python3 ansible_python_interpreter=/usr/bin/python3
+6 -3
View File
@@ -6,6 +6,7 @@
become: true become: true
vars_files: vars_files:
- "{{ var_file }}" - "{{ var_file }}"
- vars/default.yml
tasks: tasks:
- name: Copy dockerfile - name: Copy dockerfile
@@ -30,7 +31,7 @@
- name: Upload the .env file - name: Upload the .env file
template: template:
src: "{{ env_file }}" src: "files/.env"
dest: "/var/lib/{{ host_directory }}/.env" dest: "/var/lib/{{ host_directory }}/.env"
- name: Create brewman container - name: Create brewman container
@@ -40,8 +41,10 @@
state: started state: started
restart_policy: "unless-stopped" restart_policy: "unless-stopped"
env_file: "/var/lib/{{ host_directory }}/.env" env_file: "/var/lib/{{ host_directory }}/.env"
links: etc_hosts:
- "postgres:db" db : "{{ db_host }}"
# links:
# - "postgres:db"
published_ports: published_ports:
- "127.0.0.1:{{ host_port }}:80" - "127.0.0.1:{{ host_port }}:80"
volumes: volumes:
+5 -2
View File
@@ -1,6 +1,9 @@
--- ---
name: acc
http_host: "acc.hopsngrains.com" http_host: "acc.hopsngrains.com"
http_conf: "acc.hopsngrains.com.conf" http_conf: "acc.hopsngrains.com.conf"
host_port: "8659" host_port: "8659"
host_directory: "brewman-acc"
env_file: "files/.env-acc" secret_key: c9bee2d38676447c2f7a9ea715446e2fd09f16fbaa5b3f6a6f207ec18993987f
middleware_key: cb71666b9c
+4
View File
@@ -0,0 +1,4 @@
---
db_host: 172.26.12.67
host_directory: "brewman-{{ name }}"
db_name: "brewman_{{ name }}"
+5 -2
View File
@@ -1,6 +1,9 @@
--- ---
name: exp
http_host: "exp.tanshu.com" http_host: "exp.tanshu.com"
http_conf: "exp.tanshu.com.conf" http_conf: "exp.tanshu.com.conf"
host_port: "8656" host_port: "8656"
host_directory: "brewman-exp"
env_file: "files/.env-exp" secret_key: 8546a61262dab7c05ccf2e26abe30bc10966904df6dfd29259ea85dd0844a8e7
middleware_key: da6fcd999b
+5 -2
View File
@@ -1,6 +1,9 @@
--- ---
name: hinchco
http_host: "acc.hinchco.in" http_host: "acc.hinchco.in"
http_conf: "acc.hinchco.in.conf" http_conf: "acc.hinchco.in.conf"
host_port: "8655" host_port: "8655"
host_directory: "brewman-hinchco"
env_file: "files/.env-hinchco" secret_key: 7b889cff76532fde8483304cf415243f70df200518ba9aee4d26c0709ad6fbd1
middleware_key: 1e36e7f678
+5 -2
View File
@@ -1,6 +1,9 @@
--- ---
name: hops
http_host: "hops.hopsngrains.com" http_host: "hops.hopsngrains.com"
http_conf: "hops.hopsngrains.com.conf" http_conf: "hops.hopsngrains.com.conf"
host_port: "8658" host_port: "8658"
host_directory: "brewman-hops"
env_file: "files/.env-hops" secret_key: cfb3be420c4e2b0ed423b2e4e238713d0461e2ba56198138ad6c4d82aef6295c
middleware_key: 9c2bdd24be
+5 -2
View File
@@ -1,6 +1,9 @@
--- ---
name: mhl
http_host: "mhl.hopsngrains.com" http_host: "mhl.hopsngrains.com"
http_conf: "mhl.hopsngrains.com.conf" http_conf: "mhl.hopsngrains.com.conf"
host_port: "8657" host_port: "8657"
host_directory: "brewman-mhl"
env_file: "files/.env-mhl" secret_key: c9fd1b99931feb083f67470170650420b99eb35368d3de186427166c28d32c8b
middleware_key: 9183bdcfb0
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "overlord", "name": "overlord",
"version": "11.1.4", "version": "11.2.1",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
@@ -148,8 +148,8 @@ export class EmployeeBenefitsComponent implements OnInit, AfterViewInit {
if (formValue === undefined) { if (formValue === undefined) {
return; return;
} }
const grossSalary = +(formValue.grossSalary ?? '0'); const grossSalary = Number(formValue.grossSalary);
const daysWorked = +(formValue.daysWorked ?? '0'); const daysWorked = Number(formValue.daysWorked);
const date = this.form.value.date ?? new Date(); const date = this.form.value.date ?? new Date();
const daysInMonth = moment(date).daysInMonth(); const daysInMonth = moment(date).daysInMonth();
const esi = EmployeeBenefitsComponent.getEsi(grossSalary, daysWorked, daysInMonth); const esi = EmployeeBenefitsComponent.getEsi(grossSalary, daysWorked, daysInMonth);
@@ -129,8 +129,8 @@ export class EmployeeDetailComponent implements OnInit, AfterViewInit {
const formValue = this.form.value; const formValue = this.form.value;
this.item.name = formValue.name ?? ''; this.item.name = formValue.name ?? '';
this.item.designation = formValue.designation ?? ''; this.item.designation = formValue.designation ?? '';
this.item.salary = +(formValue.salary ?? '0'); this.item.salary = Number(formValue.salary);
this.item.points = +(formValue.points ?? '0'); this.item.points = Number(formValue.points);
this.item.isActive = formValue.isActive ?? true; this.item.isActive = formValue.isActive ?? true;
this.item.costCentre.id = formValue.costCentre ?? ''; this.item.costCentre.id = formValue.costCentre ?? '';
this.item.joiningDate = moment(formValue.joiningDate).format('DD-MMM-YYYY'); this.item.joiningDate = moment(formValue.joiningDate).format('DD-MMM-YYYY');
+4 -4
View File
@@ -193,10 +193,10 @@ export class IssueComponent implements OnInit, AfterViewInit, OnDestroy {
this.voucher.inventories.push( this.voucher.inventories.push(
new Inventory({ new Inventory({
quantity, quantity,
rate: this.batch.rate, rate: +this.batch.rate,
tax: this.batch.tax, tax: +this.batch.tax,
discount: this.batch.discount, discount: +this.batch.discount,
amount: quantity * this.batch.rate * (1 + this.batch.tax) * (1 - this.batch.discount), amount: quantity * +this.batch.rate * (1 + +this.batch.tax) * (1 - +this.batch.discount),
batch: this.batch, batch: this.batch,
}), }),
); );
@@ -62,13 +62,13 @@ export class ProductLedgerDataSource extends DataSource<ProductLedgerItem> {
case 'date': case 'date':
return compare(a.date, b.date, isAsc); return compare(a.date, b.date, isAsc);
case 'debitQuantity': case 'debitQuantity':
return compare(Number(a.debitQuantity ?? '0'), Number(b.debitQuantity ?? '0'), isAsc); return compare(Number(a.debitQuantity), Number(b.debitQuantity), isAsc);
case 'debitAmount': case 'debitAmount':
return compare(Number(a.debitAmount ?? '0'), Number(b.debitAmount ?? '0'), isAsc); return compare(Number(a.debitAmount), Number(b.debitAmount), isAsc);
case 'creditQuantity': case 'creditQuantity':
return compare(Number(a.creditQuantity ?? '0'), Number(b.creditQuantity ?? '0'), isAsc); return compare(Number(a.creditQuantity), Number(b.creditQuantity), isAsc);
case 'creditAmount': case 'creditAmount':
return compare(Number(a.creditAmount ?? '0'), Number(b.creditAmount ?? '0'), isAsc); return compare(Number(a.creditAmount), Number(b.creditAmount), isAsc);
default: default:
return 0; return 0;
} }
@@ -113,12 +113,12 @@ export class ProductLedgerComponent implements OnInit, AfterViewInit {
this.runningAmount = 0; this.runningAmount = 0;
this.info.body.forEach((item) => { this.info.body.forEach((item) => {
if (item.type !== 'Opening Balance') { if (item.type !== 'Opening Balance') {
this.debitAmount += Number(item.debitAmount ?? '0'); this.debitAmount += Number(item.debitAmount);
this.creditQuantity += Number(item.creditQuantity ?? '0'); this.creditQuantity += Number(item.creditQuantity);
this.creditAmount += Number(item.creditAmount ?? '0'); this.creditAmount += Number(item.creditAmount);
} }
this.runningQuantity += Number(item.debitQuantity ?? '0') - Number(item.creditQuantity ?? '0'); this.runningQuantity += Number(item.debitQuantity) - Number(item.creditQuantity);
this.runningAmount += Number(item.debitAmount ?? '0') - Number(item.creditAmount ?? '0'); this.runningAmount += Number(item.debitAmount) - Number(item.creditAmount);
item.runningQuantity = this.runningQuantity; item.runningQuantity = this.runningQuantity;
item.runningAmount = this.runningAmount; item.runningAmount = this.runningAmount;
}); });
@@ -113,22 +113,22 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
if (formValue === undefined) { if (formValue === undefined) {
return; return;
} }
const fraction = +(formValue.fraction ?? '0'); const fraction = Number(formValue.fraction);
if (fraction < 1) { if (fraction < 1) {
this.toaster.show('Danger', 'Fraction has to be >= 1'); this.toaster.show('Danger', 'Fraction has to be >= 1');
return; return;
} }
const productYield = +(formValue.productYield ?? '0'); const productYield = Number(formValue.productYield);
if (productYield < 0 || productYield > 1) { if (productYield < 0 || productYield > 1) {
this.toaster.show('Danger', 'Product Yield has to be > 0 and <= 1'); this.toaster.show('Danger', 'Product Yield has to be > 0 and <= 1');
return; return;
} }
const costPrice = +(formValue.costPrice ?? '0'); const costPrice = Number(formValue.costPrice);
if (costPrice < 0) { if (costPrice < 0) {
this.toaster.show('Danger', 'Price has to be >= 0'); this.toaster.show('Danger', 'Price has to be >= 0');
return; return;
} }
const salePrice = +(formValue.salePrice ?? '0'); const salePrice = Number(formValue.salePrice);
if (salePrice < 0) { if (salePrice < 0) {
this.toaster.show('Danger', 'Sale Price has to be >= 0'); this.toaster.show('Danger', 'Sale Price has to be >= 0');
return; return;
@@ -24,7 +24,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div class="flex flex-row justify-around content-start items-start sm:max-lg:flex-col"> <div class="flex flex-row justify-around content-start items-start sm:max-lg:flex-col">
<mat-form-field class="flex-auto basis-3/5 mr-5"> <mat-form-field class="flex-auto basis-4/5 mr-5">
<mat-label>Product</mat-label> <mat-label>Product</mat-label>
<input <input
type="text" type="text"
@@ -43,7 +43,7 @@
<mat-option *ngFor="let product of products | async" [value]="product">{{ product.name }}</mat-option> <mat-option *ngFor="let product of products | async" [value]="product">{{ product.name }}</mat-option>
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </mat-form-field>
<mat-form-field class="flex-auto basis-1/10 mr-5"> <mat-form-field class="flex-auto basis-1/5">
<mat-label>Yield</mat-label> <mat-label>Yield</mat-label>
<input type="text" matInput formControlName="recipeYield" autocomplete="off" /> <input type="text" matInput formControlName="recipeYield" autocomplete="off" />
</mat-form-field> </mat-form-field>
@@ -74,17 +74,12 @@
<mat-label>Quantity</mat-label> <mat-label>Quantity</mat-label>
<input type="text" matInput formControlName="quantity" autocomplete="off" /> <input type="text" matInput formControlName="quantity" autocomplete="off" />
</mat-form-field> </mat-form-field>
<mat-form-field class="flex-auto basis-1/5 mr-5"> <mat-form-field class="flex-auto basis-[30%] mr-5">
<mat-label>Description</mat-label> <mat-label>Description</mat-label>
<input type="text" matInput formControlName="description" autocomplete="off" /> <input type="text" matInput formControlName="description" autocomplete="off" />
</mat-form-field> </mat-form-field>
<mat-form-field class="flex-auto basis-1/10 mr-5">
<mat-label>Rate</mat-label>
<input type="text" matInput formControlName="rate" autocomplete="off" />
<span matTextPrefix>&nbsp;</span>
</mat-form-field>
<button mat-raised-button color="primary" (click)="addRow()" class="flex-auto basis-1/10">Add</button> <button mat-raised-button color="primary" (click)="addRow()" class="flex-auto basis-[10%]">Add</button>
</div> </div>
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements"> <mat-table #table [dataSource]="dataSource" matSort aria-label="Elements">
<!-- Ingredient Column --> <!-- Ingredient Column -->
@@ -39,7 +39,6 @@ export class RecipeDetailComponent implements OnInit, AfterViewInit {
ingredient: FormControl<string | null>; ingredient: FormControl<string | null>;
quantity: FormControl<string>; quantity: FormControl<string>;
description: FormControl<string>; description: FormControl<string>;
rate: FormControl<string>;
}>; }>;
instructions: FormControl<string>; instructions: FormControl<string>;
garnishing: FormControl<string>; garnishing: FormControl<string>;
@@ -75,7 +74,6 @@ export class RecipeDetailComponent implements OnInit, AfterViewInit {
ingredient: new FormControl(''), ingredient: new FormControl(''),
quantity: new FormControl('', { nonNullable: true }), quantity: new FormControl('', { nonNullable: true }),
description: new FormControl('', { nonNullable: true }), description: new FormControl('', { nonNullable: true }),
rate: new FormControl('', { nonNullable: true }),
}), }),
instructions: new FormControl('', { nonNullable: true }), instructions: new FormControl('', { nonNullable: true }),
garnishing: new FormControl('', { nonNullable: true }), garnishing: new FormControl('', { nonNullable: true }),
@@ -118,7 +116,6 @@ export class RecipeDetailComponent implements OnInit, AfterViewInit {
ingredient: null, ingredient: null,
quantity: '', quantity: '',
description: '', description: '',
rate: '',
}, },
instructions: item.instructions, instructions: item.instructions,
garnishing: item.garnishing, garnishing: item.garnishing,
@@ -160,8 +157,7 @@ export class RecipeDetailComponent implements OnInit, AfterViewInit {
return; return;
} }
const quantity = this.math.parseAmount(formValue.quantity, 2); const quantity = this.math.parseAmount(formValue.quantity, 2);
const rate = this.math.parseAmount(formValue.rate, 2); if (this.ingredient === null || quantity <= 0) {
if (this.ingredient === null || quantity <= 0 || rate <= 0) {
return; return;
} }
const oldFiltered = this.item.items.filter((x) => x.product.id === (this.ingredient as ProductSku).id); const oldFiltered = this.item.items.filter((x) => x.product.id === (this.ingredient as ProductSku).id);
@@ -2,5 +2,5 @@ export const environment = {
production: true, production: true,
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
ACCESS_TOKEN_REFRESH_MINUTES: 10, // refresh token 10 minutes before expiry ACCESS_TOKEN_REFRESH_MINUTES: 10, // refresh token 10 minutes before expiry
version: '11.1.4', version: '11.2.1',
}; };
+1 -1
View File
@@ -6,7 +6,7 @@ export const environment = {
production: false, production: false,
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
ACCESS_TOKEN_REFRESH_MINUTES: 10, // refresh token 10 minutes before expiry ACCESS_TOKEN_REFRESH_MINUTES: 10, // refresh token 10 minutes before expiry
version: '11.1.4', version: '11.2.1',
}; };
/* /*