diff --git a/tailbone/templates/customers/pending/view.mako b/tailbone/templates/customers/pending/view.mako new file mode 100644 index 00000000..e9e54c99 --- /dev/null +++ b/tailbone/templates/customers/pending/view.mako @@ -0,0 +1,143 @@ +## -*- coding: utf-8; -*- +<%inherit file="/master/view.mako" /> +## <%namespace file="/util.mako" import="view_profiles_helper" /> + +<%def name="object_helpers()"> + ${parent.object_helpers()} + + % if instance.custorder_records: + + % endif + + ## % if instance.status_code == enum.PENDING_CUSTOMER_STATUS_PENDING and master.has_any_perm('resolve_person', 'resolve_customer'): + % if instance.status_code == enum.PENDING_CUSTOMER_STATUS_PENDING and master.has_perm('resolve_person'): + + + + + + % endif + + +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + + + +${parent.body()} diff --git a/tailbone/templates/custorders/create.mako b/tailbone/templates/custorders/create.mako index ff41f765..db9af7ec 100644 --- a/tailbone/templates/custorders/create.mako +++ b/tailbone/templates/custorders/create.mako @@ -12,18 +12,6 @@ % endif -<%def name="render_instance_header_buttons()"> - ${parent.render_instance_header_buttons()} - % if use_buefy and master.configurable and master.has_perm('configure'): -
- - -
- % endif - - <%def name="page_content()">
% if use_buefy: @@ -1968,6 +1956,9 @@ % if product_price_may_be_questionable: this.productPriceNeedsConfirmation = false % endif + + this.itemDialogTabIndex = 1 + }, response => { this.clearProduct() }) diff --git a/tailbone/templates/products/pending/view.mako b/tailbone/templates/products/pending/view.mako new file mode 100644 index 00000000..90d9c687 --- /dev/null +++ b/tailbone/templates/products/pending/view.mako @@ -0,0 +1,130 @@ +## -*- coding: utf-8; -*- +<%inherit file="/master/view.mako" /> + +<%def name="object_helpers()"> + ${parent.object_helpers()} + % if instance.custorder_item_records: + + % endif + + % if instance.status_code == enum.PENDING_PRODUCT_STATUS_PENDING and master.has_perm('resolve_product'): + + + + + + % endif + + +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + + + + +${parent.body()} diff --git a/tailbone/views/customers.py b/tailbone/views/customers.py index ba05f475..bf8284c0 100644 --- a/tailbone/views/customers.py +++ b/tailbone/views/customers.py @@ -500,6 +500,9 @@ class PendingCustomerView(MasterView): super(PendingCustomerView, self).configure_grid(g) g.set_enum('status_code', self.enum.PENDING_CUSTOMER_STATUS) + g.filters['status_code'].default_active = True + g.filters['status_code'].default_verb = 'not_equal' + g.filters['status_code'].default_value = six.text_type(self.enum.PENDING_CUSTOMER_STATUS_RESOLVED) g.set_sort_defaults('display_name') g.set_link('id') @@ -523,6 +526,51 @@ class PendingCustomerView(MasterView): f.set_readonly('user') f.set_renderer('user', self.render_user) + def editable_instance(self, pending): + if pending.status_code == self.enum.PENDING_CUSTOMER_STATUS_RESOLVED: + return False + return True + + def resolve_person(self): + model = self.model + pending = self.get_instance() + redirect = self.redirect(self.get_action_url('view', pending)) + + uuid = self.request.POST['person_uuid'] + person = self.Session.query(model.Person).get(uuid) + if not person: + self.request.session.flash("Person not found!", 'error') + return redirect + + app = self.get_rattail_app() + people_handler = app.get_people_handler() + people_handler.resolve_person(pending, person, self.request.user) + self.Session.flush() + return redirect + + @classmethod + def defaults(cls, config): + cls._defaults(config) + cls._pending_customer_defaults(config) + + @classmethod + def _pending_customer_defaults(cls, config): + route_prefix = cls.get_route_prefix() + instance_url_prefix = cls.get_instance_url_prefix() + permission_prefix = cls.get_permission_prefix() + model_title = cls.get_model_title() + + # resolve person + config.add_tailbone_permission(permission_prefix, + '{}.resolve_person'.format(permission_prefix), + "Resolve a {} as a Person".format(model_title)) + config.add_route('{}.resolve_person'.format(route_prefix), + '{}/resolve-person'.format(instance_url_prefix), + request_method='POST') + config.add_view(cls, attr='resolve_person', + route_name='{}.resolve_person'.format(route_prefix), + permission='{}.resolve_person'.format(permission_prefix)) + # # TODO: this is referenced by some custom apps, but should be moved?? # def unique_id(value, field): diff --git a/tailbone/views/custorders/items.py b/tailbone/views/custorders/items.py index 4d62e505..823130ed 100644 --- a/tailbone/views/custorders/items.py +++ b/tailbone/views/custorders/items.py @@ -52,6 +52,7 @@ class CustomerOrderItemView(MasterView): deletable = False labels = { + 'order': "Customer Order", 'order_id': "Order ID", 'order_uom': "Order UOM", 'status_code': "Status", @@ -172,21 +173,37 @@ class CustomerOrderItemView(MasterView): def configure_form(self, f): super(CustomerOrderItemView, self).configure_form(f) use_buefy = self.get_use_buefy() + item = f.model_instance # order f.set_renderer('order', self.render_order) - # product + # (pending) product f.set_renderer('product', self.render_product) - - # pending_product f.set_renderer('pending_product', self.render_pending_product) + if self.viewing: + if item.product and not item.pending_product: + f.remove('pending_product') + elif item.pending_product and not item.product: + f.remove('product') # product uom f.set_enum('product_unit_of_measure', self.enum.UNIT_OF_MEASURE) + # highlight pending fields + f.set_renderer('product_brand', self.highlight_pending_field) + f.set_renderer('product_description', self.highlight_pending_field) + f.set_renderer('product_size', self.highlight_pending_field) + f.set_renderer('case_quantity', self.highlight_pending_field_quantity) + + 'unit_price', + 'total_price', + 'price_needs_confirmation', + 'paid_amount', + 'status_code', + 'notes', + # quantity fields - f.set_type('case_quantity', 'quantity') f.set_type('cases_ordered', 'quantity') f.set_type('units_ordered', 'quantity') f.set_type('order_quantity', 'quantity') @@ -210,10 +227,27 @@ class CustomerOrderItemView(MasterView): else: f.remove('notes') + def highlight_pending_field(self, item, field, value=None): + if value is None: + value = getattr(item, field) + if not item.product_uuid and item.pending_product_uuid: + return HTML.tag('span', c=[value], + class_='has-text-success') + return value + + def highlight_pending_field_quantity(self, item, field): + app = self.get_rattail_app() + value = getattr(item, field) + value = app.render_quantity(value) + return self.highlight_pending_field(item, field, value) + def render_price_with_confirmation(self, item, field): price = getattr(item, field) app = self.get_rattail_app() text = app.render_currency(price) + if not item.product_uuid and item.pending_product_uuid: + text = HTML.tag('span', c=[text], + class_='has-text-success') if item.price_needs_confirmation: return HTML.tag('span', class_='has-background-warning', c=[text]) diff --git a/tailbone/views/custorders/orders.py b/tailbone/views/custorders/orders.py index a97b9978..c60e859e 100644 --- a/tailbone/views/custorders/orders.py +++ b/tailbone/views/custorders/orders.py @@ -108,20 +108,23 @@ class CustomerOrderView(MasterView): def configure_grid(self, g): super(CustomerOrderView, self).configure_grid(g) - g.set_joiner('customer', lambda q: q.outerjoin(model.Customer)) - g.set_joiner('person', lambda q: q.outerjoin(model.Person)) - - g.filters['customer'] = g.make_filter('customer', model.Customer.name, - label="Customer Name", - default_active=True, - default_verb='contains') - g.filters['person'] = g.make_filter('person', model.Person.display_name, - label="Person Name", - default_active=True, - default_verb='contains') - - g.set_sorter('customer', model.Customer.name) - g.set_sorter('person', model.Person.display_name) + # customer or person + if self.batch_handler.new_order_requires_customer(): + g.remove('person') + g.set_joiner('customer', lambda q: q.outerjoin(model.Customer)) + g.set_sorter('customer', model.Customer.name) + g.filters['customer'] = g.make_filter('customer', model.Customer.name, + label="Customer Name", + default_active=True, + default_verb='contains') + else: + g.remove('customer') + g.set_joiner('person', lambda q: q.outerjoin(model.Person)) + g.set_sorter('person', model.Person.display_name) + g.filters['person'] = g.make_filter('person', model.Person.display_name, + label="Person Name", + default_active=True, + default_verb='contains') g.set_enum('status_code', self.enum.CUSTORDER_STATUS) @@ -133,13 +136,33 @@ class CustomerOrderView(MasterView): def configure_form(self, f): super(CustomerOrderView, self).configure_form(f) + order = f.model_instance f.set_readonly('id') f.set_renderer('store', self.render_store) + + # (pending) customer f.set_renderer('customer', self.render_customer) f.set_renderer('person', self.render_person) f.set_renderer('pending_customer', self.render_pending_customer) + if self.viewing: + if self.batch_handler.new_order_requires_customer(): + f.remove('person') + if order.customer and not order.pending_customer: + f.remove('pending_customer') + elif order.pending_customer and not order.customer: + f.remove('customer') + else: + f.remove('customer') + if order.person and not order.pending_customer: + f.remove('pending_customer') + elif order.pending_customer and not order.person: + f.remove('person') + + # contact info + f.set_renderer('phone_number', self.highlight_pending_field) + f.set_renderer('email_address', self.highlight_pending_field) f.set_type('total_price', 'currency') @@ -150,6 +173,20 @@ class CustomerOrderView(MasterView): f.set_readonly('created_by') f.set_renderer('created_by', self.render_user) + def highlight_pending_field(self, order, field): + value = getattr(order, field) + pending = False + if self.batch_handler.new_order_requires_customer(): + if not order.customer_uuid and order.pending_customer_uuid: + pending = True + else: + if not order.person_uuid and order.pending_customer_uuid: + pending = True + if pending: + return HTML.tag('span', c=[value], + class_='has-text-success') + return value + def render_person(self, order, field): person = order.person if not person: @@ -164,7 +201,8 @@ class CustomerOrderView(MasterView): return text = six.text_type(pending) url = self.request.route_url('pending_customers.view', uuid=pending.uuid) - return tags.link_to(text, url) + return tags.link_to(text, url, + class_='has-background-warning') def get_row_data(self, order): return self.Session.query(model.CustomerOrderItem)\ @@ -218,6 +256,10 @@ class CustomerOrderView(MasterView): g.set_link('product_brand') g.set_link('product_description') + def row_grid_extra_class(self, item, i): + if not item.product_uuid and item.pending_product_uuid: + return 'has-text-success' + def render_price_with_confirmation(self, item, field): price = getattr(item, field) app = self.get_rattail_app() diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 75996653..f83ec52e 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -290,6 +290,12 @@ class MasterView(View): return self.request.has_perm('{}.{}'.format( self.get_permission_prefix(), name)) + def has_any_perm(self, *names): + for name in names: + if self.has_perm(name): + return True + return False + @classmethod def get_config_url(cls): if hasattr(cls, 'config_url'): @@ -801,7 +807,8 @@ class MasterView(View): return text = six.text_type(pending) url = self.request.route_url('pending_products.view', uuid=pending.uuid) - return tags.link_to(text, url) + return tags.link_to(text, url, + class_='has-background-warning') def render_vendor(self, obj, field): vendor = getattr(obj, field) diff --git a/tailbone/views/products.py b/tailbone/views/products.py index 7f40e1e3..30e5fe5f 100644 --- a/tailbone/views/products.py +++ b/tailbone/views/products.py @@ -2035,6 +2035,9 @@ class PendingProductView(MasterView): super(PendingProductView, self).configure_grid(g) g.set_enum('status_code', self.enum.PENDING_PRODUCT_STATUS) + g.filters['status_code'].default_active = True + g.filters['status_code'].default_verb = 'not_equal' + g.filters['status_code'].default_value = six.text_type(self.enum.PENDING_PRODUCT_STATUS_RESOLVED) g.set_sort_defaults('created', 'desc') @@ -2137,6 +2140,11 @@ class PendingProductView(MasterView): # f.set_readonly('status_code') f.set_enum('status_code', self.enum.PENDING_PRODUCT_STATUS) + def editable_instance(self, pending): + if pending.status_code == self.enum.PENDING_PRODUCT_STATUS_RESOLVED: + return False + return True + def objectify(self, form, data=None): if data is None: data = form.validated @@ -2182,6 +2190,45 @@ class PendingProductView(MasterView): 'error') return self.redirect(self.get_action_url('view', pending)) + def resolve_product(self): + model = self.model + pending = self.get_instance() + redirect = self.redirect(self.get_action_url('view', pending)) + + uuid = self.request.POST['product_uuid'] + product = self.Session.query(model.Product).get(uuid) + if not product: + self.request.session.flash("Product not found!", 'error') + return redirect + + app = self.get_rattail_app() + products_handler = app.get_products_handler() + products_handler.resolve_product(pending, product, self.request.user) + return redirect + + @classmethod + def defaults(cls, config): + cls._defaults(config) + cls._pending_product_defaults(config) + + @classmethod + def _pending_product_defaults(cls, config): + route_prefix = cls.get_route_prefix() + instance_url_prefix = cls.get_instance_url_prefix() + permission_prefix = cls.get_permission_prefix() + model_title = cls.get_model_title() + + # resolve product + config.add_tailbone_permission(permission_prefix, + '{}.resolve_product'.format(permission_prefix), + "Resolve a {} as a Product".format(model_title)) + config.add_route('{}.resolve_product'.format(route_prefix), + '{}/resolve-product'.format(instance_url_prefix), + request_method='POST') + config.add_view(cls, attr='resolve_product', + route_name='{}.resolve_product'.format(route_prefix), + permission='{}.resolve_product'.format(permission_prefix)) + def print_labels(request): profile = request.params.get('profile')