diff --git a/tailbone/templates/products/view.mako b/tailbone/templates/products/view.mako index 8f27fb02..e401e880 100644 --- a/tailbone/templates/products/view.mako +++ b/tailbone/templates/products/view.mako @@ -1,10 +1,41 @@ ## -*- coding: utf-8; -*- <%inherit file="/master/view.mako" /> +<%def name="extra_javascript()"> + ${parent.extra_javascript()} + % if not use_buefy and request.rattail_config.versioning_enabled() and master.has_perm('versions'): + + % endif +%def> + <%def name="extra_styles()"> ${parent.extra_styles()} + + % else: + #srp-history-dialog .grid { + color: black; + } % endif + %def> <%def name="render_main_fields(form)"> @@ -341,6 +376,12 @@ + + % if request.rattail_config.versioning_enabled() and master.has_perm('versions'): +
+ % endif % endif % if buttons: diff --git a/tailbone/views/products.py b/tailbone/views/products.py index 99f2106f..c95ec47a 100644 --- a/tailbone/views/products.py +++ b/tailbone/views/products.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2018 Lance Edgar +# Copyright © 2010-2020 Lance Edgar # # This file is part of Rattail. # @@ -30,16 +30,19 @@ import re import logging import six +import humanize import sqlalchemy as sa from sqlalchemy import orm +import sqlalchemy_continuum as continuum from rattail import enum, pod, sil from rattail.db import model, api, auth, Session as RattailSession from rattail.gpc import GPC from rattail.threads import Thread from rattail.exceptions import LabelPrintingError -from rattail.util import load_object, pretty_quantity +from rattail.util import load_object, pretty_quantity, OrderedDict from rattail.batch import get_batch_handler +from rattail.time import localtime, make_utc import colander from deform import widget as dfwidget @@ -383,7 +386,7 @@ class ProductsView(MasterView): f.remove_field('suggested_price') else: f.set_readonly('suggested_price') - f.set_renderer('suggested_price', self.render_price) + f.set_renderer('suggested_price', self.render_suggested_price) # regular_price if self.creating: @@ -445,11 +448,11 @@ class ProductsView(MasterView): def render_cost(self, product, field): cost = getattr(product, field) - if cost: - if cost.unit_cost: - return "$ {:0.2f}".format(cost.unit_cost) - else: - return "TODO: does this item have a cost?" + if not cost: + return "" + if cost.unit_cost is None: + return "" + return "${:0.2f}".format(cost.unit_cost) def render_price(self, product, column): price = product[column] @@ -470,13 +473,31 @@ class ProductsView(MasterView): return "$ {:0.2f} / {}".format(price.pack_price, price.pack_multiple) return "" - def render_cost(self, product, column): - cost = product.cost - if not cost: - return "" - if cost.unit_cost is None: - return "" - return "${:0.2f}".format(cost.unit_cost) + def add_srp_history_link(self, text): + if not self.rattail_config.versioning_enabled(): + return text + if not self.has_perm('versions'): + return text + + history = tags.link_to("(view history)", '#', + id='view-srp-history') + if not text: + return history + + text = HTML.tag('span', c=text) + br = HTML.tag('br') + return HTML.tag('div', c=[text, br, history]) + + def render_suggested_price(self, product, column): + text = self.render_price(product, column) + + if text and self.rattail_config.versioning_enabled(): + history = self.get_srp_history(product) + if history: + date = localtime(self.rattail_config, history[0]['changed'], from_utc=True).date() + text = "{} (as of {})".format(text, date) + + return self.add_srp_history_link(text) def render_true_cost(self, product, field): if not product.volatile: @@ -906,12 +927,96 @@ class ProductsView(MasterView): if not kwargs.get('image_url'): kwargs['image_url'] = self.request.static_url('tailbone:static/img/product.png') + # add SRP history, if user has access + if self.rattail_config.versioning_enabled() and self.has_perm('versions'): + data = self.get_srp_history(product) + grid = grids.Grid('products.srp_history', data, + request=self.request, + columns=[ + 'price', + 'since', + 'changed', + 'changed_by', + ]) + grid.set_type('price', 'currency') + grid.set_type('changed', 'datetime') + kwargs['srp_history_grid'] = grid + kwargs['costs_label_preferred'] = "Pref." kwargs['costs_label_vendor'] = "Vendor" kwargs['costs_label_code'] = "Order Code" kwargs['costs_label_case_size'] = "Case Size" return kwargs + def get_srp_history(self, product): + """ + Returns a sequence of "records" which corresponds to the given + product's SRP history. + """ + Transaction = continuum.transaction_class(model.Product) + ProductVersion = continuum.version_class(model.Product) + ProductPriceVersion = continuum.version_class(model.ProductPrice) + now = make_utc() + history = [] + + # first we find all relevant ProductVersion records + versions = self.Session.query(ProductVersion)\ + .join(Transaction, + Transaction.id == ProductVersion.transaction_id)\ + .filter(ProductVersion.uuid == product.uuid)\ + .order_by(Transaction.issued_at, + Transaction.id)\ + .all() + + last_uuid = None + for version in versions: + if version.suggested_price_uuid != last_uuid: + changed = version.transaction.issued_at + if version.suggested_price: + assert isinstance(version.suggested_price, ProductPriceVersion) + price = version.suggested_price.price + else: + price = None + history.append({ + 'transaction_id': version.transaction.id, + 'price': price, + 'since': humanize.naturaltime(now - changed), + 'changed': changed, + 'changed_by': version.transaction.user, + }) + last_uuid = version.suggested_price_uuid + + # next we find all relevant ProductPriceVersion records + versions = self.Session.query(ProductPriceVersion)\ + .join(Transaction, + Transaction.id == ProductPriceVersion.transaction_id)\ + .filter(ProductPriceVersion.product_uuid == product.uuid)\ + .filter(ProductPriceVersion.type == self.enum.PRICE_TYPE_MFR_SUGGESTED)\ + .order_by(Transaction.issued_at, + Transaction.id)\ + .all() + + last_price = None + for version in versions: + if version.price != last_price: + changed = version.transaction.issued_at + price = version.price + history.append({ + 'transaction_id': version.transaction.id, + 'price': version.price, + 'since': humanize.naturaltime(now - changed), + 'changed': changed, + 'changed_by': version.transaction.user, + }) + last_price = version.price + + final_history = OrderedDict() + for hist in reversed(history): + if hist['transaction_id'] not in final_history: + final_history[hist['transaction_id']] = hist + + return list(final_history.values()) + def edit(self): # TODO: Should add some more/better hooks, so don't have to duplicate # so much code here.