fix: expose config for new order w/ pending product
- whether or not this feature should be allowed at all - user must have permission also - UI should honor both of the above - also, which fields are required for new pending product
This commit is contained in:
		
							parent
							
								
									e677cd5d8c
								
							
						
					
					
						commit
						ebd22fe6ee
					
				
					 5 changed files with 177 additions and 28 deletions
				
			
		| 
						 | 
				
			
			@ -44,6 +44,14 @@ class NewOrderBatchHandler(BatchHandler):
 | 
			
		|||
    """
 | 
			
		||||
    model_class = NewOrderBatch
 | 
			
		||||
 | 
			
		||||
    def allow_unknown_product(self):
 | 
			
		||||
        """
 | 
			
		||||
        Returns a boolean indicating whether "unknown" (pending)
 | 
			
		||||
        products are allowed when creating a new order.
 | 
			
		||||
        """
 | 
			
		||||
        return self.config.get_bool('sideshow.orders.allow_unknown_product',
 | 
			
		||||
                                    default=True)
 | 
			
		||||
 | 
			
		||||
    def set_pending_customer(self, batch, data):
 | 
			
		||||
        """
 | 
			
		||||
        Set (add or update) pending customer info for the batch.
 | 
			
		||||
| 
						 | 
				
			
			@ -122,6 +130,9 @@ class NewOrderBatchHandler(BatchHandler):
 | 
			
		|||
           :class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
 | 
			
		||||
           which was added to the batch.
 | 
			
		||||
        """
 | 
			
		||||
        if not self.allow_unknown_product():
 | 
			
		||||
            raise TypeError("unknown/pending product not allowed for new orders")
 | 
			
		||||
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        enum = self.app.enum
 | 
			
		||||
        session = self.app.get_session(batch)
 | 
			
		||||
| 
						 | 
				
			
			@ -167,6 +178,9 @@ class NewOrderBatchHandler(BatchHandler):
 | 
			
		|||
        :param data: Dict of field data for the
 | 
			
		||||
           :class:`~sideshow.db.model.products.PendingProduct` record.
 | 
			
		||||
        """
 | 
			
		||||
        if not self.allow_unknown_product():
 | 
			
		||||
            raise TypeError("unknown/pending product not allowed for new orders")
 | 
			
		||||
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        enum = self.app.enum
 | 
			
		||||
        session = self.app.get_session(row)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										41
									
								
								src/sideshow/web/templates/orders/configure.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/sideshow/web/templates/orders/configure.mako
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,41 @@
 | 
			
		|||
## -*- coding: utf-8; -*-
 | 
			
		||||
<%inherit file="/configure.mako" />
 | 
			
		||||
 | 
			
		||||
<%def name="form_content()">
 | 
			
		||||
 | 
			
		||||
  <h3 class="block is-size-3">Products</h3>
 | 
			
		||||
  <div class="block" style="padding-left: 2rem;">
 | 
			
		||||
 | 
			
		||||
    <b-field message="If set, user can enter details of an arbitrary new "pending" product.">
 | 
			
		||||
      <b-checkbox name="sideshow.orders.allow_unknown_product"
 | 
			
		||||
                  v-model="simpleSettings['sideshow.orders.allow_unknown_product']"
 | 
			
		||||
                  native-value="true"
 | 
			
		||||
                  @input="settingsNeedSaved = true">
 | 
			
		||||
        Allow creating orders for "unknown" products
 | 
			
		||||
      </b-checkbox>
 | 
			
		||||
    </b-field>
 | 
			
		||||
 | 
			
		||||
    <div v-show="simpleSettings['sideshow.orders.allow_unknown_product']"
 | 
			
		||||
         style="padding-left: 2rem;">
 | 
			
		||||
 | 
			
		||||
      <p class="block">
 | 
			
		||||
        Require these fields for new product:
 | 
			
		||||
      </p>
 | 
			
		||||
 | 
			
		||||
      <div class="block"
 | 
			
		||||
           style="margin-left: 2rem;">
 | 
			
		||||
        % for field in pending_product_fields:
 | 
			
		||||
            <b-field>
 | 
			
		||||
              <b-checkbox name="sideshow.orders.unknown_product.fields.${field}.required"
 | 
			
		||||
                          v-model="simpleSettings['sideshow.orders.unknown_product.fields.${field}.required']"
 | 
			
		||||
                          native-value="true"
 | 
			
		||||
                          @input="settingsNeedSaved = true">
 | 
			
		||||
                ${field}
 | 
			
		||||
              </b-checkbox>
 | 
			
		||||
            </b-field>
 | 
			
		||||
        % endfor
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</%def>
 | 
			
		||||
| 
						 | 
				
			
			@ -402,8 +402,8 @@
 | 
			
		|||
                          <div v-if="productID">
 | 
			
		||||
 | 
			
		||||
                            <b-field grouped>
 | 
			
		||||
                              <b-field :label="productKeyLabel">
 | 
			
		||||
                                <span>{{ productKey }}</span>
 | 
			
		||||
                              <b-field label="Scancode">
 | 
			
		||||
                                <span>{{ productScancode }}</span>
 | 
			
		||||
                              </b-field>
 | 
			
		||||
 | 
			
		||||
                              <b-field label="Unit Size">
 | 
			
		||||
| 
						 | 
				
			
			@ -493,12 +493,12 @@
 | 
			
		|||
 | 
			
		||||
                        <div style="display: flex; gap: 1rem;">
 | 
			
		||||
 | 
			
		||||
                          <b-field :label="productKeyLabel"
 | 
			
		||||
                                   % if 'key' in pending_product_required_fields:
 | 
			
		||||
                                   :type="pendingProduct[productKeyField] ? null : 'is-danger'"
 | 
			
		||||
                          <b-field label="Scancode"
 | 
			
		||||
                                   % if 'scancode' in pending_product_required_fields:
 | 
			
		||||
                                   :type="pendingProduct.scancode ? null : 'is-danger'"
 | 
			
		||||
                                   % endif
 | 
			
		||||
                                   style="width: 100%;">
 | 
			
		||||
                            <b-input v-model="pendingProduct[productKeyField]" />
 | 
			
		||||
                            <b-input v-model="pendingProduct.scancode" />
 | 
			
		||||
                          </b-field>
 | 
			
		||||
 | 
			
		||||
                          <b-field label="Department"
 | 
			
		||||
| 
						 | 
				
			
			@ -725,9 +725,9 @@
 | 
			
		|||
                     :data="items"
 | 
			
		||||
                     :row-class="(row, i) => row.product_id ? null : 'has-text-success'">
 | 
			
		||||
 | 
			
		||||
              <${b}-table-column :label="productKeyLabel"
 | 
			
		||||
              <${b}-table-column label="Scancode"
 | 
			
		||||
                              v-slot="props">
 | 
			
		||||
                {{ props.row.product_key }}
 | 
			
		||||
                {{ props.row.product_scancode }}
 | 
			
		||||
              </${b}-table-column>
 | 
			
		||||
 | 
			
		||||
              <${b}-table-column label="Brand"
 | 
			
		||||
| 
						 | 
				
			
			@ -876,16 +876,11 @@
 | 
			
		|||
                    itemDialogTabIndex: 0,
 | 
			
		||||
                % endif
 | 
			
		||||
 | 
			
		||||
                ## TODO
 | 
			
		||||
                productIsKnown: false,
 | 
			
		||||
 | 
			
		||||
                productIsKnown: true,
 | 
			
		||||
                selectedProduct: null,
 | 
			
		||||
                productID: null,
 | 
			
		||||
                productDisplay: null,
 | 
			
		||||
                ## TODO
 | 
			
		||||
                productKey: null,
 | 
			
		||||
                productKeyField: 'scancode',
 | 
			
		||||
                productKeyLabel: "Scancode",
 | 
			
		||||
                productScancode: null,
 | 
			
		||||
                productSize: null,
 | 
			
		||||
                productCaseQuantity: null,
 | 
			
		||||
                productUnitPrice: null,
 | 
			
		||||
| 
						 | 
				
			
			@ -1341,13 +1336,11 @@
 | 
			
		|||
            showAddItemDialog() {
 | 
			
		||||
                this.customerPanelOpen = false
 | 
			
		||||
                this.editingItem = null
 | 
			
		||||
                // TODO
 | 
			
		||||
                // this.productIsKnown = true
 | 
			
		||||
                this.productIsKnown = false
 | 
			
		||||
                this.productIsKnown = true
 | 
			
		||||
                ## this.selectedProduct = null
 | 
			
		||||
                this.productID = null
 | 
			
		||||
                this.productDisplay = null
 | 
			
		||||
                ## this.productKey = null
 | 
			
		||||
                this.productScancode = null
 | 
			
		||||
                this.productSize = null
 | 
			
		||||
                this.productCaseQuantity = null
 | 
			
		||||
                this.productUnitPrice = null
 | 
			
		||||
| 
						 | 
				
			
			@ -1402,7 +1395,7 @@
 | 
			
		|||
                this.pendingProduct = pending
 | 
			
		||||
 | 
			
		||||
                this.productDisplay = row.product_full_description
 | 
			
		||||
                this.productKey = row.product_key
 | 
			
		||||
                this.productScancode = row.product_scancode
 | 
			
		||||
                this.productSize = row.product_size
 | 
			
		||||
                this.productCaseQuantity = row.case_quantity
 | 
			
		||||
                this.productURL = row.product_url
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -58,6 +58,7 @@ class OrderView(MasterView):
 | 
			
		|||
    """
 | 
			
		||||
    model_class = Order
 | 
			
		||||
    editable = False
 | 
			
		||||
    configurable = True
 | 
			
		||||
 | 
			
		||||
    labels = {
 | 
			
		||||
        'order_id': "Order ID",
 | 
			
		||||
| 
						 | 
				
			
			@ -122,15 +123,14 @@ class OrderView(MasterView):
 | 
			
		|||
 | 
			
		||||
    PENDING_PRODUCT_ENTRY_FIELDS = [
 | 
			
		||||
        'scancode',
 | 
			
		||||
        'department_id',
 | 
			
		||||
        'department_name',
 | 
			
		||||
        'brand_name',
 | 
			
		||||
        'description',
 | 
			
		||||
        'size',
 | 
			
		||||
        'department_name',
 | 
			
		||||
        'vendor_name',
 | 
			
		||||
        'vendor_item_code',
 | 
			
		||||
        'unit_cost',
 | 
			
		||||
        'case_size',
 | 
			
		||||
        'unit_cost',
 | 
			
		||||
        'unit_price_reg',
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -213,10 +213,10 @@ class OrderView(MasterView):
 | 
			
		|||
            'normalized_batch': self.normalize_batch(batch),
 | 
			
		||||
            'order_items': [self.normalize_row(row)
 | 
			
		||||
                            for row in batch.rows],
 | 
			
		||||
 | 
			
		||||
            'allow_unknown_product': True, # TODO
 | 
			
		||||
            'default_uom_choices': self.get_default_uom_choices(),
 | 
			
		||||
            'default_uom': None, # TODO?
 | 
			
		||||
            'allow_unknown_product': (self.batch_handler.allow_unknown_product()
 | 
			
		||||
                                      and self.has_perm('create_unknown_product')),
 | 
			
		||||
            'pending_product_required_fields': self.get_pending_product_required_fields(),
 | 
			
		||||
        })
 | 
			
		||||
        return self.render_to_response('create', context)
 | 
			
		||||
| 
						 | 
				
			
			@ -567,9 +567,6 @@ class OrderView(MasterView):
 | 
			
		|||
                'special_order': pending.special_order,
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        # TODO: remove this
 | 
			
		||||
        data['product_key'] = row.product_scancode
 | 
			
		||||
 | 
			
		||||
        # display text for order qty/uom
 | 
			
		||||
        if row.order_uom == enum.ORDER_UOM_CASE:
 | 
			
		||||
            if row.case_size is None:
 | 
			
		||||
| 
						 | 
				
			
			@ -670,6 +667,55 @@ class OrderView(MasterView):
 | 
			
		|||
        """ """
 | 
			
		||||
        return self.request.route_url('order_items.view', uuid=item.uuid)
 | 
			
		||||
 | 
			
		||||
    def configure_get_simple_settings(self):
 | 
			
		||||
        """ """
 | 
			
		||||
        settings = [
 | 
			
		||||
 | 
			
		||||
            # products
 | 
			
		||||
            {'name': 'sideshow.orders.allow_unknown_product',
 | 
			
		||||
             'type': bool,
 | 
			
		||||
             'default': True},
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        # required fields for new product entry
 | 
			
		||||
        for field in self.PENDING_PRODUCT_ENTRY_FIELDS:
 | 
			
		||||
            setting = {'name': f'sideshow.orders.unknown_product.fields.{field}.required',
 | 
			
		||||
                       'type': bool}
 | 
			
		||||
            if field == 'description':
 | 
			
		||||
                setting['default'] = True
 | 
			
		||||
            settings.append(setting)
 | 
			
		||||
 | 
			
		||||
        return settings
 | 
			
		||||
 | 
			
		||||
    def configure_get_context(self, **kwargs):
 | 
			
		||||
        """ """
 | 
			
		||||
        context = super().configure_get_context(**kwargs)
 | 
			
		||||
 | 
			
		||||
        context['pending_product_fields'] = self.PENDING_PRODUCT_ENTRY_FIELDS
 | 
			
		||||
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def defaults(cls, config):
 | 
			
		||||
        cls._order_defaults(config)
 | 
			
		||||
        cls._defaults(config)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def _order_defaults(cls, config):
 | 
			
		||||
        permission_prefix = cls.get_permission_prefix()
 | 
			
		||||
        model_title = cls.get_model_title()
 | 
			
		||||
        model_title_plural = cls.get_model_title_plural()
 | 
			
		||||
 | 
			
		||||
        # fix perm group
 | 
			
		||||
        config.add_wutta_permission_group(permission_prefix,
 | 
			
		||||
                                          model_title_plural,
 | 
			
		||||
                                          overwrite=False)
 | 
			
		||||
 | 
			
		||||
        # extra perm required to create order with unknown/pending product
 | 
			
		||||
        config.add_wutta_permission(permission_prefix,
 | 
			
		||||
                                    f'{permission_prefix}.create_unknown_product',
 | 
			
		||||
                                    f"Create new {model_title} for unknown/pending product")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OrderItemView(MasterView):
 | 
			
		||||
    """
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
# -*- coding: utf-8; -*-
 | 
			
		||||
 | 
			
		||||
import datetime
 | 
			
		||||
import decimal
 | 
			
		||||
 | 
			
		||||
from wuttjamaican.testing import DataTestCase
 | 
			
		||||
| 
						 | 
				
			
			@ -18,6 +19,16 @@ class TestNewOrderBatchHandler(DataTestCase):
 | 
			
		|||
    def make_handler(self):
 | 
			
		||||
        return mod.NewOrderBatchHandler(self.config)
 | 
			
		||||
 | 
			
		||||
    def tets_allow_unknown_product(self):
 | 
			
		||||
        handler = self.make_handler()
 | 
			
		||||
 | 
			
		||||
        # true by default
 | 
			
		||||
        self.assertTrue(handler.allow_unknown_product())
 | 
			
		||||
 | 
			
		||||
        # config can disable
 | 
			
		||||
        config.setdefault('sideshow.orders.allow_unknown_product', 'false')
 | 
			
		||||
        self.assertFalse(handler.allow_unknown_product())
 | 
			
		||||
 | 
			
		||||
    def test_set_pending_customer(self):
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        handler = self.make_handler()
 | 
			
		||||
| 
						 | 
				
			
			@ -119,6 +130,10 @@ class TestNewOrderBatchHandler(DataTestCase):
 | 
			
		|||
        self.assertEqual(product.unit_price_reg, decimal.Decimal('5.99'))
 | 
			
		||||
        self.assertIs(product.created_by, user)
 | 
			
		||||
 | 
			
		||||
        # error if unknown products not allowed
 | 
			
		||||
        self.config.setdefault('sideshow.orders.allow_unknown_product', 'false')
 | 
			
		||||
        self.assertRaises(TypeError, handler.add_pending_product, batch, kw, 1, enum.ORDER_UOM_UNIT)
 | 
			
		||||
 | 
			
		||||
    def test_set_pending_product(self):
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        enum = self.app.enum
 | 
			
		||||
| 
						 | 
				
			
			@ -210,6 +225,15 @@ class TestNewOrderBatchHandler(DataTestCase):
 | 
			
		|||
        self.assertEqual(product.unit_price_reg, decimal.Decimal('3.59'))
 | 
			
		||||
        self.assertIs(product.created_by, user)
 | 
			
		||||
 | 
			
		||||
        # error if unknown products not allowed
 | 
			
		||||
        self.config.setdefault('sideshow.orders.allow_unknown_product', 'false')
 | 
			
		||||
        self.assertRaises(TypeError, handler.set_pending_product, row, dict(
 | 
			
		||||
            scancode='07430500116',
 | 
			
		||||
            size='16oz',
 | 
			
		||||
            unit_cost=decimal.Decimal('2.19'),
 | 
			
		||||
            unit_price_reg=decimal.Decimal('3.59'),
 | 
			
		||||
        ))
 | 
			
		||||
 | 
			
		||||
    def test_refresh_row(self):
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        enum = self.app.enum
 | 
			
		||||
| 
						 | 
				
			
			@ -310,6 +334,37 @@ class TestNewOrderBatchHandler(DataTestCase):
 | 
			
		|||
        self.assertEqual(row.case_price_quoted, decimal.Decimal('71.88'))
 | 
			
		||||
        self.assertEqual(row.total_price, decimal.Decimal('143.76'))
 | 
			
		||||
 | 
			
		||||
        # refreshed from pending product (sale price)
 | 
			
		||||
        product = model.PendingProduct(scancode='07430500132',
 | 
			
		||||
                                       brand_name='Bragg',
 | 
			
		||||
                                       description='Vinegar',
 | 
			
		||||
                                       size='32oz',
 | 
			
		||||
                                       case_size=12,
 | 
			
		||||
                                       unit_cost=decimal.Decimal('3.99'),
 | 
			
		||||
                                       unit_price_reg=decimal.Decimal('5.99'),
 | 
			
		||||
                                       created_by=user,
 | 
			
		||||
                                       status=enum.PendingProductStatus.PENDING)
 | 
			
		||||
        row = handler.make_row(pending_product=product, order_qty=2, order_uom=enum.ORDER_UOM_CASE,
 | 
			
		||||
                               unit_price_sale=decimal.Decimal('5.19'),
 | 
			
		||||
                               sale_ends=datetime.datetime(2099, 1, 1))
 | 
			
		||||
        self.assertIsNone(row.status_code)
 | 
			
		||||
        handler.add_row(batch, row)
 | 
			
		||||
        self.assertEqual(row.status_code, row.STATUS_OK)
 | 
			
		||||
        self.assertIsNone(row.product_id)
 | 
			
		||||
        self.assertIs(row.pending_product, product)
 | 
			
		||||
        self.assertEqual(row.product_scancode, '07430500132')
 | 
			
		||||
        self.assertEqual(row.product_brand, 'Bragg')
 | 
			
		||||
        self.assertEqual(row.product_description, 'Vinegar')
 | 
			
		||||
        self.assertEqual(row.product_size, '32oz')
 | 
			
		||||
        self.assertEqual(row.case_size, 12)
 | 
			
		||||
        self.assertEqual(row.unit_cost, decimal.Decimal('3.99'))
 | 
			
		||||
        self.assertEqual(row.unit_price_reg, decimal.Decimal('5.99'))
 | 
			
		||||
        self.assertEqual(row.unit_price_sale, decimal.Decimal('5.19'))
 | 
			
		||||
        self.assertEqual(row.sale_ends, datetime.datetime(2099, 1, 1))
 | 
			
		||||
        self.assertEqual(row.unit_price_quoted, decimal.Decimal('5.19'))
 | 
			
		||||
        self.assertEqual(row.case_price_quoted, decimal.Decimal('62.28'))
 | 
			
		||||
        self.assertEqual(row.total_price, decimal.Decimal('124.56'))
 | 
			
		||||
 | 
			
		||||
    def test_remove_row(self):
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        enum = self.app.enum
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue