mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
182 lines
6 KiB
Python
182 lines
6 KiB
Python
from decimal import Decimal
|
|
from functools import partial
|
|
from pathlib import Path
|
|
from typing import List, Tuple
|
|
|
|
import yaml
|
|
from pint import Quantity
|
|
|
|
from wrolpi import before_startup
|
|
from wrolpi.common import logger, Base
|
|
from wrolpi.db import get_db_session
|
|
from wrolpi.errors import NoInventories, InventoriesVersionMismatch
|
|
from wrolpi.vars import CONFIG_DIR
|
|
from .inventory import unit_registry, get_items, get_inventories, increment_inventories_version, \
|
|
get_inventories_version
|
|
from .models import Inventory, Item
|
|
|
|
MY_DIR: Path = Path(__file__).parent
|
|
|
|
logger = logger.getChild(__name__)
|
|
|
|
|
|
def sum_by_key(items: List[Base], key: callable):
|
|
"""
|
|
Sum the total size of each item by the provided key function. Returns a dict containing the key, and the total_size
|
|
for that key. Combine total of like units. This means ounces and pounds will be in the same total.
|
|
"""
|
|
summed = dict()
|
|
for item in items:
|
|
k = key(item)
|
|
item_size, count, unit = item.item_size, item.count, unit_registry(item.unit)
|
|
key_dim = (k, unit.dimensionality)
|
|
|
|
total_size = item_size * count * unit
|
|
|
|
try:
|
|
summed[key_dim] += total_size
|
|
except KeyError:
|
|
summed[key_dim] = total_size
|
|
|
|
summed = {k: compact_unit(v) for k, v in summed.items()}
|
|
return summed
|
|
|
|
|
|
def get_inventory_by_keys(keys: Tuple, inventory_id: int):
|
|
items = get_items(inventory_id)
|
|
|
|
summed = sum_by_key(items, lambda i: tuple(getattr(i, k) or '' for k in keys))
|
|
|
|
inventory = []
|
|
for key, quantity in sorted(summed.items(), key=lambda i: i[0][0]):
|
|
quantity = cleanup_quantity(quantity)
|
|
total_size, units = quantity.to_tuple()
|
|
unit = units[0][0]
|
|
|
|
d = dict(total_size=total_size, unit=unit)
|
|
d.update(dict(zip(keys, key[0])))
|
|
inventory.append(d)
|
|
|
|
return inventory
|
|
|
|
|
|
get_inventory_by_category = partial(get_inventory_by_keys, ('category',))
|
|
get_inventory_by_subcategory = partial(get_inventory_by_keys, ('category', 'subcategory'))
|
|
get_inventory_by_name = partial(get_inventory_by_keys, ('brand', 'name'))
|
|
INVENTORY_UNITS = {
|
|
('ounce', 1): (16, unit_registry.pound),
|
|
('pound', 1): (2000, unit_registry.ton),
|
|
}
|
|
UNIT_PRECISION = 5
|
|
|
|
|
|
def compact_unit(quantity: unit_registry.Quantity) -> unit_registry.Quantity:
|
|
"""
|
|
Convert a Quantity to it's more readable format. Such as 2000 pounds to 1 ton.
|
|
"""
|
|
number, (units,) = quantity.to_tuple()
|
|
next_unit = INVENTORY_UNITS.get(units)
|
|
if not next_unit:
|
|
# No units after this one, return as is.
|
|
return round(quantity, UNIT_PRECISION)
|
|
|
|
max_quantity, next_unit = next_unit
|
|
if number >= max_quantity:
|
|
# Number is too high for this unit, move to the next unit.
|
|
return compact_unit(quantity.to(next_unit))
|
|
|
|
# No units were necessary, return as is.
|
|
quantity = round(quantity, UNIT_PRECISION)
|
|
return quantity
|
|
|
|
|
|
def quantity_to_tuple(quantity: unit_registry.Quantity) -> Tuple[Decimal, Quantity]:
|
|
decimal, (units,) = quantity.to_tuple()
|
|
unit, _ = units
|
|
quantity, unit = round(decimal, UNIT_PRECISION), unit_registry(unit)
|
|
return quantity, unit
|
|
|
|
|
|
def cleanup_quantity(quantity: Quantity) -> Quantity:
|
|
"""
|
|
Remove trailing zeros from a Quantity.
|
|
"""
|
|
num, unit = quantity_to_tuple(quantity)
|
|
num = round(num, UNIT_PRECISION)
|
|
num = str(num).rstrip('0').rstrip('.')
|
|
return Decimal(num) * unit
|
|
|
|
|
|
DEFAULT_SAVE_PATH = CONFIG_DIR / 'inventories.yaml'
|
|
|
|
|
|
def save_inventories_file(path: str = None):
|
|
"""
|
|
Write all inventories and their respective items to a YAML file.
|
|
"""
|
|
path: Path = Path(path) if path else DEFAULT_SAVE_PATH
|
|
|
|
inventories = []
|
|
for inventory in get_inventories():
|
|
inventories.append(inventory.dict())
|
|
|
|
if not inventories:
|
|
raise NoInventories('No Inventories are in the database!')
|
|
|
|
with increment_inventories_version() as version:
|
|
if path.is_file():
|
|
# Check that we aren't overwriting our inventories with empty inventories.
|
|
with open(path, 'rt') as fh:
|
|
old = yaml.load(fh, Loader=yaml.Loader)
|
|
if old and not inventories:
|
|
raise FileExistsError(f'Refusing to overwrite non-empty inventories.yaml with empty inventories.'
|
|
f' {path}')
|
|
|
|
if old and old.get('version') > version:
|
|
raise InventoriesVersionMismatch(
|
|
f'Inventories config version is {old["version"]} but DB version is {get_inventories_version()}')
|
|
|
|
with open(path, 'wt') as fh:
|
|
contents = dict(
|
|
version=version,
|
|
inventories=inventories,
|
|
)
|
|
|
|
yaml.dump(contents, fh)
|
|
|
|
|
|
@before_startup
|
|
def import_inventories_file(path: str = None):
|
|
path: Path = Path(path) if path else DEFAULT_SAVE_PATH
|
|
|
|
if not path.is_file():
|
|
logger.warning(f'No inventories config file at {path}')
|
|
return
|
|
|
|
with open(path, 'rt') as fh:
|
|
contents = yaml.load(fh, Loader=yaml.Loader)
|
|
|
|
if not contents or 'inventories' not in contents:
|
|
raise ValueError('Inventories file does not contain the expected "inventories" list.')
|
|
|
|
inventories = get_inventories()
|
|
inventories_names = {i.name for i in inventories}
|
|
new_inventories = [i for i in contents['inventories'] if i['name'] not in inventories_names]
|
|
with get_db_session(commit=True) as session:
|
|
for inventory in new_inventories:
|
|
items = inventory['items']
|
|
inventory = Inventory(
|
|
name=inventory['name'],
|
|
created_at=inventory['created_at'],
|
|
deleted_at=inventory['deleted_at'],
|
|
)
|
|
session.add(inventory)
|
|
# Get the Inventory from the DB so we can use it's ID.
|
|
session.flush()
|
|
session.refresh(inventory)
|
|
|
|
for item in items:
|
|
del item['inventory_id']
|
|
item = Item(inventory_id=inventory.id, **item)
|
|
item.inventory_id = inventory.id
|
|
session.add(item)
|