Add basic "pending product" support for new custorder batch

This commit is contained in:
Lance Edgar 2021-12-22 12:06:00 -06:00
parent 408bffb775
commit c0db03bc28
13 changed files with 844 additions and 234 deletions

View file

@ -0,0 +1,72 @@
## -*- coding: utf-8; -*-
<%inherit file="/configure.mako" />
<%def name="form_content()">
<h3 class="block is-size-3">Customer Handling</h3>
<div class="block" style="padding-left: 2rem;">
<b-field message="If not set, only a Person is required.">
<b-checkbox name="rattail.custorders.new_order_requires_customer"
v-model="simpleSettings['rattail.custorders.new_order_requires_customer']"
@input="settingsNeedSaved = true">
Require a Customer account
</b-checkbox>
</b-field>
<b-field message="If not set, default contact info is always assumed.">
<b-checkbox name="rattail.custorders.new_orders.allow_contact_info_choice"
v-model="simpleSettings['rattail.custorders.new_orders.allow_contact_info_choice']"
@input="settingsNeedSaved = true">
Allow user to choose contact info
</b-checkbox>
</b-field>
<b-field message="Only applies if user is allowed to choose contact info.">
<b-checkbox name="rattail.custorders.new_orders.allow_contact_info_create"
v-model="simpleSettings['rattail.custorders.new_orders.allow_contact_info_create']"
@input="settingsNeedSaved = true">
Allow user to enter new contact info
</b-checkbox>
</b-field>
<p class="block">
If you allow users to enter new contact info, the default action
when the order is submitted, is to send email with details of
the new contact info.&nbsp; Settings for these are at:
</p>
<ul class="list">
<li class="list-item">
${h.link_to("New Phone Request", url('emailprofiles.view', key='new_phone_requested'))}
</li>
<li class="list-item">
${h.link_to("New Email Request", url('emailprofiles.view', key='new_email_requested'))}
</li>
</ul>
</div>
<h3 class="block is-size-3">Product Handling</h3>
<div class="block" style="padding-left: 2rem;">
<b-field message="If set, user can enter details of an arbitrary new &quot;pending&quot; product.">
<b-checkbox name="rattail.custorders.allow_unknown_product"
v-model="simpleSettings['rattail.custorders.allow_unknown_product']"
@input="settingsNeedSaved = true">
Allow creating orders for "unknown" products
</b-checkbox>
</b-field>
<b-field>
<b-checkbox name="rattail.custorders.product_price_may_be_questionable"
v-model="simpleSettings['rattail.custorders.product_price_may_be_questionable']"
@input="settingsNeedSaved = true">
Allow prices to be flagged as "questionable"
</b-checkbox>
</b-field>
</div>
</%def>
${parent.body()}

View file

@ -12,6 +12,18 @@
% endif
</%def>
<%def name="render_instance_header_buttons()">
${parent.render_instance_header_buttons()}
% if use_buefy and master.configurable and master.has_perm('configure'):
<div class="level-item">
<once-button tag="a" href="${url('{}.configure'.format(route_prefix))}"
icon-left="cog"
text="Configure">
</once-button>
</div>
% endif
</%def>
<%def name="page_content()">
<br />
% if use_buefy:
@ -155,11 +167,11 @@
<div class="level-left">
<div class="level-item">
<div v-if="orderPhoneNumber">
<p>
<p :class="addOtherPhoneNumber ? 'has-text-success': null">
{{ orderPhoneNumber }}
</p>
<p v-if="addOtherPhoneNumber"
class="is-size-7 is-italic">
class="is-size-7 is-italic has-text-success">
will be added to customer record
</p>
</div>
@ -170,7 +182,7 @@
</div>
% if allow_contact_info_choice:
<div class="level-item"
% if restrict_contact_info:
% if not allow_contact_info_create:
v-if="contactPhones.length &gt; 1"
% endif
>
@ -203,7 +215,7 @@
</b-radio>
</b-field>
% if not restrict_contact_info:
% if allow_contact_info_create:
<b-field>
<b-radio v-model="existingPhoneUUID"
:native-value="null">
@ -249,11 +261,11 @@
<div class="level-left">
<div class="level-item">
<div v-if="orderEmailAddress">
<p>
<p :class="addOtherEmailAddress ? 'has-text-success' : null">
{{ orderEmailAddress }}
</p>
<p v-if="addOtherEmailAddress"
class="is-size-7 is-italic">
class="is-size-7 is-italic has-text-success">
will be added to customer record
</p>
</div>
@ -264,7 +276,7 @@
</div>
% if allow_contact_info_choice:
<div class="level-item"
% if restrict_contact_info:
% if not allow_contact_info_create:
v-if="contactEmails.length &gt; 1"
% endif
>
@ -296,7 +308,7 @@
</b-radio>
</b-field>
% if not restrict_contact_info:
% if allow_contact_info_create:
<b-field>
<b-radio v-model="existingEmailUUID"
:native-value="null">
@ -556,7 +568,13 @@
<span>{{ productCaseQuantity }}</span>
</b-field>
<b-field label="Unit Price">
<b-field label="Reg. Price"
v-if="productSalePriceDisplay">
<span>{{ productUnitRegularPriceDisplay }}</span>
</b-field>
<b-field label="Unit Price"
v-if="!productSalePriceDisplay">
<span
% if product_price_may_be_questionable:
:class="productPriceNeedsConfirmation ? 'has-background-warning' : ''"
@ -599,70 +617,172 @@
<br />
<div class="field">
<b-radio v-model="productIsKnown" disabled
<b-radio v-model="productIsKnown"
% if not allow_unknown_product:
disabled
% endif
:native-value="false">
Product is not yet in the system.
</b-radio>
</div>
<div v-show="!productIsKnown"
style="padding-left: 5rem;">
<b-field grouped>
<b-field label="Brand">
<b-input v-model="pendingProduct.brand_name">
</b-input>
</b-field>
<b-field label="Description"
:type="pendingProduct.description ? null : 'is-danger'">
<b-input v-model="pendingProduct.description">
</b-input>
</b-field>
<b-field label="Unit Size">
<b-input v-model="pendingProduct.size">
</b-input>
</b-field>
</b-field>
<b-field grouped>
<b-field :label="productKeyLabel">
<b-input v-model="pendingProduct[productKeyField]">
</b-input>
</b-field>
<b-field label="Department">
<b-select v-model="pendingProduct.department_uuid">
<option :value="null">(not known)</option>
<option v-for="option in departmentOptions"
:key="option.value"
:value="option.value">
{{ option.label }}
</option>
</b-select>
</b-field>
<b-field label="Unit Reg. Price">
<b-input v-model="pendingProduct.regular_price_amount"
type="number" step="0.01">
</b-input>
</b-field>
</b-field>
<b-field grouped>
<b-field label="Vendor">
<b-input v-model="pendingProduct.vendor_name">
</b-input>
</b-field>
<b-field label="Vendor Item Code">
<b-input v-model="pendingProduct.vendor_item_code">
</b-input>
</b-field>
<b-field label="Unit Cost">
<b-input v-model="pendingProduct.unit_cost"
type="number" step="0.01"
style="width: 10rem;">
</b-input>
</b-field>
<b-field label="Case Size">
<b-input v-model="pendingProduct.case_size"
type="number" step="0.01"
style="width: 7rem;">
</b-input>
</b-field>
</b-field>
<b-field label="Notes">
<b-input v-model="pendingProduct.notes"
type="textarea">
</b-input>
</b-field>
</div>
</b-tab-item>
<b-tab-item label="Quantity">
<div class="is-pulled-right has-text-centered">
<img :src="productImageURL"
style="height: 150px; width: 150px; "/>
## <p>{{ productKey }}</p>
</div>
<b-field grouped>
<b-field label="Product" horizontal>
<span>{{ productDisplay }}</span>
<span :class="productIsKnown ? null : 'has-text-success'">
{{ productIsKnown ? productDisplay : pendingProduct.brand_name + ' ' + pendingProduct.description + ' ' + pendingProduct.size }}
</span>
</b-field>
</b-field>
<b-field grouped>
<b-field label="Unit Size">
<span>{{ productSize }}</span>
<span :class="productIsKnown ? null : 'has-text-success'">
{{ productIsKnown ? productSize : pendingProduct.size }}
</span>
</b-field>
<b-field label="Unit Price">
<span
% if product_price_may_be_questionable:
:class="productPriceNeedsConfirmation ? 'has-background-warning' : ''"
% endif
>
{{ productUnitPriceDisplay }}
<b-field label="Reg. Price"
v-if="productSalePriceDisplay">
<span>
{{ productUnitRegularPriceDisplay }}
</span>
</b-field>
<b-field label="Unit Price"
v-if="!productSalePriceDisplay">
<span :class="productIsKnown ? null : 'has-text-success'"
% if product_price_may_be_questionable:
:class="productPriceNeedsConfirmation ? 'has-background-warning' : ''"
% endif
>
{{ productIsKnown ? productUnitPriceDisplay : '$' + pendingProduct.regular_price_amount }}
</span>
</b-field>
<b-field label="Sale Price"
v-if="productSalePriceDisplay">
<span class="has-background-warning">
<span class="has-background-warning"
:class="productIsKnown ? null : 'has-text-success'">
{{ productSalePriceDisplay }}
</span>
</b-field>
<b-field label="Sale Ends"
v-if="productSaleEndsDisplay">
<span class="has-background-warning">
<span class="has-background-warning"
:class="productIsKnown ? null : 'has-text-success'">
{{ productSaleEndsDisplay }}
</span>
</b-field>
<b-field label="Case Size">
<span>{{ productCaseQuantity }}</span>
<span :class="productIsKnown ? null : 'has-text-success'">
{{ productIsKnown ? productCaseQuantity : pendingProduct.case_size }}
</span>
</b-field>
<b-field label="Case Price">
<span
% if product_price_may_be_questionable:
:class="(productPriceNeedsConfirmation || productSalePriceDisplay) ? 'has-background-warning' : ''"
% else:
:class="productSalePriceDisplay ? 'has-background-warning' : ''"
% endif
>
{{ productCasePriceDisplay }}
% if product_price_may_be_questionable:
:class="{'has-text-success': !productIsKnown, 'has-background-warning': productPriceNeedsConfirmation || productSalePriceDisplay}"
% else:
:class="{'has-text-success': !productIsKnown, 'has-background-warning': !!productSalePriceDisplay}"
% endif
>
{{ getCasePriceDisplay() }}
</span>
</b-field>
@ -671,7 +791,9 @@
<b-field grouped>
<b-field label="Quantity" horizontal>
<b-input v-model="productQuantity"></b-input>
<b-input v-model="productQuantity"
type="number" step="0.01">
</b-input>
</b-field>
<b-select v-model="productUOM">
@ -684,6 +806,14 @@
</b-field>
<b-field grouped>
<b-field label="Total Price">
<span :class="productSalePriceDisplay ? 'has-background-warning': null">
{{ getItemTotalPriceDisplay() }}
</span>
</b-field>
</b-field>
</b-tab-item>
</b-tabs>
@ -692,9 +822,10 @@
Cancel
</b-button>
<b-button type="is-primary"
@click="itemDialogSave()"
:disabled="itemDialogSaveDisabled"
icon-pack="fas"
icon-left="fas fa-save"
@click="itemDialogSave()">
icon-left="save">
{{ itemDialogSaveButtonText }}
</b-button>
</div>
@ -807,60 +938,65 @@
</b-modal>
<b-table v-if="items.length"
:data="items">
:data="items"
:row-class="(row, i) => row.product_uuid ? null : 'has-text-success'">
<template slot-scope="props">
<b-table-column field="product_upc_pretty" label="UPC">
{{ props.row.product_upc_pretty }}
<b-table-column :label="productKeyLabel">
{{ props.row.product_key }}
</b-table-column>
<b-table-column field="product_brand" label="Brand">
<b-table-column label="Brand">
{{ props.row.product_brand }}
</b-table-column>
<b-table-column field="product_description" label="Description">
<b-table-column label="Description">
{{ props.row.product_description }}
</b-table-column>
<b-table-column field="product_size" label="Size">
<b-table-column label="Size">
{{ props.row.product_size }}
</b-table-column>
<b-table-column field="department_display" label="Department">
<b-table-column label="Department">
{{ props.row.department_display }}
</b-table-column>
<b-table-column field="order_quantity_display" label="Quantity">
<b-table-column label="Quantity">
<span v-html="props.row.order_quantity_display"></span>
</b-table-column>
<b-table-column field="unit_price_display" label="Unit Price">
<b-table-column label="Unit Price">
<span
% if product_price_may_be_questionable:
:class="props.row.price_needs_confirmation ? 'has-background-warning' : ''"
% else:
:class="props.row.pricing_reflects_sale ? 'has-background-warning' : null"
% endif
>
{{ props.row.unit_price_display }}
</span>
</b-table-column>
<b-table-column field="total_price_display" label="Total">
<b-table-column label="Total">
<span
% if product_price_may_be_questionable:
:class="props.row.price_needs_confirmation ? 'has-background-warning' : ''"
% else:
:class="props.row.pricing_reflects_sale ? 'has-background-warning' : null"
% endif
>
{{ props.row.total_price_display }}
</span>
</b-table-column>
<b-table-column field="vendor_display" label="Vendor">
<b-table-column label="Vendor">
{{ props.row.vendor_display }}
</b-table-column>
<b-table-column field="actions" label="Actions">
<a href="#" class="grid-action"
@click.prevent="showEditItemDialog(props.index)">
@click.prevent="showEditItemDialog(props.row)">
<i class="fas fa-edit"></i>
Edit
</a>
@ -974,11 +1110,16 @@
productDisplay: null,
productUPC: null,
productKey: null,
productKeyField: ${json.dumps(product_key_field)|n},
productKeyLabel: ${json.dumps(product_key_label)|n},
productSize: null,
productCaseQuantity: null,
productUnitPrice: null,
productUnitPriceDisplay: null,
productUnitRegularPriceDisplay: null,
productCasePrice: null,
productCasePriceDisplay: null,
productSalePrice: null,
productSalePriceDisplay: null,
productSaleEndsDisplay: null,
productURL: null,
@ -994,6 +1135,9 @@
productPriceNeedsConfirmation: false,
% endif
pendingProduct: {},
departmentOptions: ${json.dumps(department_options)|n},
## TODO: should find a better way to handle CSRF token
csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n},
@ -1171,6 +1315,19 @@
return text
},
itemDialogSaveDisabled() {
if (this.productIsKnown) {
if (!this.productUUID) {
return true
}
} else {
if (!this.pendingProduct.description) {
return true
}
}
return false
},
itemDialogSaveButtonText() {
return this.editingItem ? "Update Item" : "Add Item"
},
@ -1522,6 +1679,71 @@
},
getCasePriceDisplay() {
if (this.productIsKnown) {
return this.productCasePriceDisplay
}
let casePrice = this.getItemCasePrice()
if (casePrice) {
return "$" + casePrice
}
},
getItemUnitPrice() {
if (this.productIsKnown) {
return this.productSalePrice || this.productUnitPrice
}
return this.pendingProduct.regular_price_amount
},
getItemCasePrice() {
if (this.productIsKnown) {
return this.productCasePrice
}
if (this.pendingProduct.regular_price_amount) {
if (this.pendingProduct.case_size) {
let casePrice = this.pendingProduct.regular_price_amount * this.pendingProduct.case_size
casePrice = casePrice.toFixed(2)
return casePrice
}
}
},
getItemTotalPriceDisplay() {
let basePrice = null
if (this.productUOM == '${enum.UNIT_OF_MEASURE_CASE}') {
basePrice = this.getItemCasePrice()
} else {
basePrice = this.getItemUnitPrice()
}
if (basePrice) {
let totalPrice = basePrice * this.productQuantity
if (totalPrice) {
totalPrice = totalPrice.toFixed(2)
return "$" + totalPrice
}
}
},
copyPendingProductAttrs(from, to) {
to.upc = from.upc
to.item_id = from.item_id
to.scancode = from.scancode
to.brand_name = from.brand_name
to.description = from.description
to.size = from.size
to.department_uuid = from.department_uuid
to.regular_price_amount = from.regular_price_amount
to.vendor_name = from.vendor_name
to.vendor_item_code = from.vendor_item_code
to.unit_cost = from.unit_cost
to.case_size = from.case_size
to.notes = from.notes
},
showAddItemDialog() {
this.customerPanelOpen = false
this.editingItem = null
@ -1532,11 +1754,18 @@
this.productKey = null
this.productSize = null
this.productCaseQuantity = null
this.productUnitPrice = null
this.productUnitPriceDisplay = null
this.productUnitRegularPriceDisplay = null
this.productCasePrice = null
this.productCasePriceDisplay = null
this.productSalePrice = null
this.productSalePriceDisplay = null
this.productSaleEndsDisplay = null
this.productImageURL = '${request.static_url('tailbone:static/img/product.png')}'
this.pendingProduct = {}
this.productQuantity = 1
this.productUnitChoices = this.defaultUnitChoices
this.productUOM = this.defaultUOM
@ -1581,8 +1810,12 @@
this.productKey = selected.key
this.productSize = selected.size
this.productCaseQuantity = selected.case_quantity
this.productUnitPrice = selected.unit_price
this.productUnitPriceDisplay = selected.unit_price_display
this.productUnitRegularPriceDisplay = selected.unit_price_display
this.productCasePrice = selected.case_price
this.productCasePriceDisplay = selected.case_price_display
this.productSalePrice = selected.sale_price
this.productSalePriceDisplay = selected.sale_price_display
this.productSaleEndsDisplay = selected.sale_ends_display
this.productImageURL = selected.image_url
@ -1600,30 +1833,41 @@
this.showingItemDialog = true
},
showEditItemDialog(index) {
row = this.items[index]
showEditItemDialog(row) {
this.editingItem = row
this.productIsKnown = true // TODO
this.productIsKnown = !!row.product_uuid
this.productUUID = row.product_uuid
this.pendingProduct = {}
if (row.pending_product) {
this.copyPendingProductAttrs(row.pending_product,
this.pendingProduct)
}
this.productDisplay = row.product_full_description
this.productUPC = row.product_upc_pretty || row.product_upc
this.productKey = row.product_key
this.productSize = row.product_size
this.productCaseQuantity = row.case_quantity
this.productURL = row.product_url
this.productUnitPrice = row.unit_price
this.productUnitPriceDisplay = row.unit_price_display
this.productUnitRegularPriceDisplay = row.unit_regular_price_display
this.productCasePrice = row.case_price
this.productCasePriceDisplay = row.case_price_display
this.productSalePriceDisplay = row.sale_price_display
this.productSalePrice = row.sale_price
this.productSalePriceDisplay = row.unit_sale_price_display
this.productSaleEndsDisplay = row.sale_ends_display
this.productImageURL = row.product_image_url
this.productQuantity = row.order_quantity
this.productUnitChoices = row.order_uom_choices
this.productUOM = row.order_uom
this.productImageURL = row.product_image_url || '${request.static_url('tailbone:static/img/product.png')}'
% if product_price_may_be_questionable:
this.productPriceNeedsConfirmation = row.price_needs_confirmation
% endif
this.productQuantity = row.order_quantity
this.productUnitChoices = row.order_uom_choices
this.productUOM = row.order_uom
this.itemDialogTabIndex = 1
this.showingItemDialog = true
},
@ -1658,8 +1902,12 @@
this.productKey = null
this.productSize = null
this.productCaseQuantity = null
this.productUnitPrice = null
this.productUnitPriceDisplay = null
this.productUnitRegularPriceDisplay = null
this.productCasePrice = null
this.productCasePriceDisplay = null
this.productSalePrice = null
this.productSalePriceDisplay = null
this.productSaleEndsDisplay = null
this.productURL = null
@ -1705,8 +1953,12 @@
this.productDisplay = response.data.full_description
this.productSize = response.data.size
this.productCaseQuantity = response.data.case_quantity
this.productUnitPrice = response.data.unit_price
this.productUnitPriceDisplay = response.data.unit_price_display
this.productUnitRegularPriceDisplay = response.data.unit_price_display
this.productCasePrice = response.data.case_price
this.productCasePriceDisplay = response.data.case_price_display
this.productSalePrice = response.data.sale_price
this.productSalePriceDisplay = response.data.sale_price_display
this.productSaleEndsDisplay = response.data.sale_ends_display
this.productURL = response.data.url
@ -1728,13 +1980,17 @@
let params = {
product_is_known: this.productIsKnown,
product_uuid: this.productUUID,
% if product_price_may_be_questionable:
price_needs_confirmation: this.productPriceNeedsConfirmation,
% endif
order_quantity: this.productQuantity,
order_uom: this.productUOM,
}
% if product_price_may_be_questionable:
price_needs_confirmation: this.productPriceNeedsConfirmation,
% endif
if (this.productIsKnown) {
params.product_uuid = this.productUUID
} else {
params.pending_product = this.pendingProduct
}
if (this.editingItem) {