[javascript] FulfillmentDialog.vue

Viewer

copydownloadembedprintName: FulfillmentDialog.vue
  1. <template>
  2.   <v-layout row>
  3.     <v-flex grow>
  4.       <!-- VOUCHER POPUP ERROR -->
  5.       <v-dialog max-width="450px" v-if="voucherErrorDialogShow" v-model="voucherErrorDialogShow">
  6.           <v-layout justify-center align-center>
  7.             <v-card width="100%">
  8.               <v-card-title><span class="subheading font-weight-bold">Voucher Details</span></v-card-title>
  9.               <v-card-text>
  10.                 Voucher <span style="color:red;">Invalid</span> because:
  11.                 <span v-html="voucherErrorDialogMessage">{{voucherErrorDialogMessage}}</span>
  12.               </v-card-text>
  13.               <v-card-actions>
  14.                 <v-spacer></v-spacer>
  15.                   <v-btn flat @click="closeVoucherErrorMessageDialog()" style="color: red;">
  16.                     Close
  17.                   </v-btn>
  18.               </v-card-actions>
  19.             </v-card>
  20.           </v-layout>
  21.       </v-dialog>
  22.       <!-- /VOUCHER POPUP ERROR -->
  23.       <v-dialog v-model="dialogFulfill" persistent fullscreen hide-overlay transition="dialog-bottom-transition">
  24.         <v-card>
  25.           <v-toolbar dark color="primary">
  26.             <v-btn icon dark @click="closeFulfillment">
  27.               <v-icon color="#FFFFFF">close</v-icon>
  28.             </v-btn>
  29.             <v-toolbar-title>ADJUST FULFILLMENT</v-toolbar-title>
  30.             <v-spacer></v-spacer>
  31.             <v-btn color="amber darken-2" depressed @click="submitFulfillment">Confirm Fulfillment</v-btn>
  32.           </v-toolbar>
  33.           <v-container grid-list-md fluid>
  34.             <v-layout row wrap>
  35.               <!-- LEFT -->
  36.               <v-flex xs12 sm9>
  37.                 <v-layout column>
  38.                   <v-flex class="mb-3">
  39.                     <h1 class="title font-weight-bold">{{ mainTableTitle }}</h1>
  40.                   </v-flex>
  41.                   <v-flex>
  42.                     <!-- BULK MODE -->
  43.                     <v-layout column>
  44.                       <v-flex class="mb-2">
  45.                         <v-card>
  46.                           <v-divider></v-divider>
  47.                           <v-container fluid class="mt-0 mx-0 pa-0">
  48.                             <v-data-table
  49.                               :loading="loadingSOList"
  50.                               :headers="headersFulfillment"
  51.                               :items="fulfilled"
  52.                               :hide-actions="true"
  53.                               class="elevation-1"
  54.                             >
  55.                               <template v-slot:progress>
  56.                                 <v-layout row mt-2>
  57.                                   <v-flex>
  58.                                     <v-progress-circular indeterminate color="primary"></v-progress-circular>
  59.                                   </v-flex>
  60.                                 </v-layout>
  61.                               </template>
  62.                               <template v-slot:items="props">
  63.                                 <tr :style="props.item.payment_type !== 'cash' && type !== 'st' ? 'background-color: #deffdf' : ''">
  64.                                   <td style="white-space: nowrap">
  65.                                     <div class="d-flex justify-space-between align-center">
  66.                                       <v-icon @click="removeSalesOrder(props.item)" color="error" left>remove_circle</v-icon>
  67.                                       <span
  68.                                         class="text-uppercase"
  69.                                         :class="{
  70.                                           'green--text' : props.item.success,
  71.                                           'red--text' : !props.item.success
  72.                                         }"
  73.                                       >
  74.                                         {{ props.item.success ? 'Success' : 'Failed' }}
  75.                                       </span>
  76.                                     </div>
  77.                                   </td>
  78.                                   <td>{{ props.item.id }}</td>
  79.                                   <td>{{ props.item.store_name }}</td>
  80.                                   <td>
  81.                                     <table class="products-table">
  82.                                         <tr>
  83.                                           <th>Product Name</th>
  84.                                           <th>Unit</th>
  85.                                           <th align="right">Fulfilled</th>
  86.                                           <th align="right">Request</th>
  87.                                         </tr>
  88.                                         <template v-for="item in props.item.products">
  89.                                           <tr v-for="unit in item.units" :key="`${item.product_id}-${unit.unit_name}`">
  90.                                             <td>{{ item.name || '-' }}</td>
  91.                                             <td>{{ unit.unit_name ? _(unit.unit_name).capitalize() : '-' }}</td>
  92.                                             <td>
  93.                                               <v-icon color="red"
  94.                                                       v-if="isFulfilledStatusValid(unit.fulfilled_quantity,unit.quantity, props.item.id, unit.product_id) === false">warning
  95.                                               </v-icon>
  96.                                               {{ (unit.fulfilled_quantity || 0) | thousandSeparator }}
  97.                                             </td>
  98.                                             <td>{{ (unit.quantity || 0) | thousandSeparator }}</td>
  99.                                           </tr>
  100.                                         </template>
  101.                                     </table>
  102.                                   </td>
  103.                                   <td>
  104.                                     <v-btn
  105.                                       v-if="type === 'st' || props.item.payment_type === 'cash'"
  106.                                       small
  107.                                       depressed
  108.                                       color="warning"
  109.                                       style="height: 28px;"
  110.                                       @click="editFulfill(props.item)"
  111.                                     >Fulfill
  112.                                     </v-btn>
  113.                                   </td>
  114.                                 </tr>
  115.                                 <tr v-if="orders[props.item.id] && orders[props.item.id].voucher && isVoucherDetailLoaded === true" style="size:14px; border: 0px solid black;" class="text-xs-right">
  116.                                   <td colspan="6">
  117.                                     <template v-if="isVoucherStatusValid(props.item.products, props.item.id) === true">
  118.                                       <span class="success--text body-2">
  119.                                         Voucher Valid
  120.                                       </span>
  121.                                     </template>
  122.                                     <template v-else>
  123.                                       <@click="showVoucherErrorDialog(props.item.products, props.item.id)" id="voucher-status-tooltip" class="error--text body-2" title="See details">
  124.                                         <v-icon color="red" depressed size="16px">info</v-icon>
  125.                                         Voucher Invalid
  126.                                       </a>
  127.                                     </template>
  128.                                   </td>
  129.                                 </tr>
  130.                               </template>
  131.                             </v-data-table>
  132.                           </v-container>
  133.                         </v-card>
  134.                       </v-flex>
  135.                     </v-layout>
  136.                   </v-flex>
  137.                   <!-- SALES EXCHANGES -->
  138.                   <v-flex v-if="fulfilledSalesExchange.length > 0">
  139.                     <!-- BULK MODE -->
  140.                     <v-layout column>
  141.                       <v-flex class="mb-2">
  142.                         <v-card>
  143.                           <v-card-title>
  144.                             <span class="subheading black--text">SALES EXCHANGES</span>
  145.                           </v-card-title>
  146.                           <v-divider></v-divider>
  147.                           <v-container fluid class="mt-0 mx-0 pa-0">
  148.                             <v-data-table
  149.                               :loading="loadingSOList"
  150.                               :headers="headersFulfillment"
  151.                               :items="fulfilledSalesExchange"
  152.                               :hide-actions="true"
  153.                               class="elevation-1"
  154.                             >
  155.                               <template v-slot:progress>
  156.                                 <v-layout row mt-2>
  157.                                   <v-flex>
  158.                                     <v-progress-circular indeterminate color="primary"></v-progress-circular>
  159.                                   </v-flex>
  160.                                 </v-layout>
  161.                               </template>
  162.                               <template v-slot:items="props">
  163.                                 <td>
  164.                                   <strong
  165.                                     class="text-uppercase"
  166.                                     :class="{
  167.                                       'green--text' : props.item.success,
  168.                                       'red--text' : !props.item.success
  169.                                     }"
  170.                                   >
  171.                                     {{ props.item.success ? 'Success' : 'Failed' }}
  172.                                   </strong>
  173.                                 </td>
  174.                                 <td>{{ props.item.id }}</td>
  175.                                 <td>{{ props.item.store_name }}</td>
  176.                                 <td>
  177.                                   <table class="products-table">
  178.                                       <tr>
  179.                                         <th>Product Name</th>
  180.                                         <th>Unit</th>
  181.                                         <th align="right">Fulfilled</th>
  182.                                         <th align="right">Request</th>
  183.                                       </tr>
  184.                                       <template v-for="item in props.item.products">
  185.                                         <tr v-for="unit in item.units" :key="`${unit.product_id}${unit.unit_name}`">
  186.                                           <td>{{ item.name || '-' }}</td>
  187.                                           <td>{{ unit.unit_name ? _(unit.unit_name).capitalize() : '-' }}</td>
  188.                                           <td>
  189.                                             <v-icon color="red"
  190.                                                     v-if="isFulfilledStatusValid(unit.fulfilled_quantity,unit.quantity, props.item.id, unit.product_id) === false">warning
  191.                                             </v-icon>
  192.                                             {{ (unit.fulfilled_quantity || 0) | thousandSeparator }}
  193.                                           </td>
  194.                                           <td>{{ (unit.quantity || 0) | thousandSeparator }}</td>
  195.                                         </tr>
  196.                                       </template>
  197.                                   </table>
  198.                                 </td>
  199.                                 <td>
  200.                                   <v-btn
  201.                                     depressed
  202.                                     color="warning"
  203.                                     @click="editFulfill(props.item)"
  204.                                   >Fulfill
  205.                                   </v-btn>
  206.                                 </td>
  207.                               </template>
  208.                             </v-data-table>
  209.                           </v-container>
  210.                         </v-card>
  211.                       </v-flex>
  212.                     </v-layout>
  213.                   </v-flex>
  214.                 </v-layout>
  215.               </v-flex>
  216.               <!-- RIGHT -->
  217.               <v-flex xs12 sm3>
  218.                 <v-layout column>
  219.                   <v-flex class="mb-3">
  220.                     <h2 class="title font-weight-bold">Total Product</h2>
  221.                   </v-flex>
  222.                   <v-flex>
  223.                     <v-data-table
  224.                       :headers="headersTotalProduct"
  225.                       :items="totalFulfilledProducts"
  226.                       hide-actions
  227.                       class="elevation-1"
  228.                     >
  229.                       <template v-slot:items="props">
  230.                         <td style="padding-right: 0px">{{ props.item.name || '-' }}</td>
  231.                         <td colspan="2" style="padding-right: 0px">
  232.                           <table class="summary-products-table">
  233.                             <tr v-for="(unit, indexUnit) in props.item.units" :key="indexUnit">
  234.                               <td>{{ unit.unit_name || '-' }}</td>
  235.                               <td>{{ (unit.fulfilled_quantity || 0) | thousandSeparator }}</td>
  236.                             </tr>
  237.                           </table>
  238.                         </td>
  239.                       </template>
  240.                     </v-data-table>
  241.                   </v-flex>
  242.                 </v-layout>
  243.               </v-flex>
  244.             </v-layout>
  245.           </v-container>
  246.         </v-card>
  247.       </v-dialog>
  248.       <!-- DIALOG MANUAL FULFILL -->
  249.       <v-dialog v-model="editFulfillment.dialog" v-if="editFulfillment.dialog" max-width="700px">
  250.         <v-layout column>
  251.           <v-flex>
  252.             <v-card>
  253.               <v-card-title>
  254.                 <span class="title black--text font-weight-bold">{{ editFulfillment.selectedFulfill.id }} - {{ editFulfillment.selectedFulfill.store_name }}</span>
  255.               </v-card-title>
  256.               <v-divider></v-divider>
  257.               <v-container fluid class="ma-0 pa-0">
  258.                 <v-data-table
  259.                   :headers="headersEditFulfill"
  260.                   :items="editFulfillmentProducts"
  261.                   :hide-actions="true"
  262.                   class="elevation-1"
  263.                 >
  264.                   <template slot="items" slot-scope="props">
  265.                     <tr v-for="unit in props.item.units" :key="`${props.item.product_id}-${unit.unit_name}`">
  266.                       <td>{{ props.item.name || '-' }}</td>
  267.                       <td class="text-capitalize">{{ unit.unit_name || '-' }}</td>
  268.                       <td>
  269.                         <v-layout row wrap align-center>
  270.                           <v-flex xs3 sm3 md3>
  271.                             <v-currency-field
  272.                               :decimal-length="0"
  273.                               :value-as-integer="true"
  274.                               :value="unit.fulfilled_quantity"
  275.                               type="number"
  276.                               v-model.number="unit.fulfilled_quantity"
  277.                               :min="0"
  278.                               single-line
  279.                               v-validate="{ 'required': true, 'numeric': true, 'between': { 'min': 0, 'max': Math.min(tempTotalStockAvailable[props.item.product_id].unit_stock_available_obj[unit.unit_name].stock_available + unit.fulfilled_quantity, parseInt(unit.quantity)) } }"
  280.                               :error-messages="errors.first('edit-fulfillment.fulfill-' + props.item.product_id + unit.unit_name)"
  281.                               data-vv-scope="edit-fulfillment"
  282.                               data-vv-as="Quantity"
  283.                               :data-vv-name="'edit-fulfillment.fulfill-' + props.item.product_id + unit.unit_name"
  284.                             >
  285.                             </v-currency-field>
  286.                           </v-flex>
  287.                           <v-flex xs3 sm3 md3 class="subheading">
  288.                             / {{ unit.quantity | thousandSeparator }}
  289.                           </v-flex>
  290.                         </v-layout>
  291.                       </td>
  292.                       <td>{{ tempTotalStockAvailable[props.item.product_id].unit_stock_available_obj[unit.unit_name].stock_available | thousandSeparator }}</td>
  293.                     </tr>
  294.                   </template>
  295.                 </v-data-table>
  296.               </v-container>
  297.               <v-card-actions>
  298.                 <v-spacer></v-spacer>
  299.                 <v-btn flat color="dark" @click="editFulfillment.dialog = false">Cancel</v-btn>
  300.                 <v-btn flat color="primary" @click="submitEditFulfillment">Confirm</v-btn>
  301.               </v-card-actions>
  302.             </v-card>
  303.           </v-flex>
  304.         </v-layout>
  305.       </v-dialog>
  306.       <!-- SNACKBAR !-->
  307.       <v-snackbar top color="red" :timeout="7000" v-model="snackbar">
  308.         <span style="color:white;">{{ snackbarText }}</span>
  309.       </v-snackbar>
  310.       <!-- /Dialog Fullfill V2 Putaway -->
  311.       <v-dialog
  312.         v-model="confirmDialog"
  313.         persistent
  314.         max-width="400px"
  315.       >
  316.         <v-form v-model="updateFormValid" ref="confirmUpdateToOnLoading">
  317.           <v-card>
  318.             <v-card-title class="title font-weight-bold">Process SO</v-card-title>
  319.             <v-divider/>
  320.             <v-card-text v-if="failedOrders.length > 0">
  321.               <v-layout row wrap>
  322.                 <v-flex xs12 md12 sm12 lg12 class="pb-2" v-if="failedOrders.length > 0">
  323.                   <span style="font-weight:bold; font-size:16px;">
  324.                     <v-icon color="warning" class="pr-1">warning</v-icon>
  325.                     Warning there
  326.                     <span v-if="failedOrders.length > 0 && failedOrders.length <= 1">is </span>
  327.                     <span v-else>are</span>
  328.                     <span style="color:red;">FAILED</span>
  329.                     <span>Order</span><span v-if="failedOrders.length > 1">s</span>
  330.                   </span>
  331.                 </v-flex>
  332.                 <v-flex xs12 md12 sm12 lg12 v-if="failedOrders.length > 0">
  333.                   <v-layout row wrap v-for="(failedOrder, index) in failedOrders" :key="index">
  334.                     <v-flex lg12 md12 sm12 xs12 class="pb-1">
  335.                       <span v-if="!failedOrder.success">{{failedOrder.id}} - <span style="color:red; font-weight:bold;">FAILED</span> </span>
  336.                     </v-flex>
  337.                   </v-layout>
  338.                 </v-flex>
  339.               </v-layout>
  340.             </v-card-text>
  341.             <v-divider v-if="failedOrders.length > 0"></v-divider>
  342.             <v-card-text>
  343.               Are you sure to process {{ totalSuccessOrders }} selected SO?
  344.             </v-card-text>
  345.             <v-divider></v-divider>
  346.             <v-card-text>
  347.               <v-layout column>
  348.                 <v-flex>
  349.                   <label for="routing">Routing Process<span class="red--text">*</span></label>
  350.                 </v-flex>
  351.                 <v-flex>
  352.                   <v-radio-group v-model="routingMethod" color="primary" row>
  353.                     <v-radio label="Manual" value="manual" color="primary"></v-radio>
  354.                     <v-radio label="Rudolve" value="rudolve" color="primary" :disabled="type === 'st'"></v-radio>
  355.                   </v-radio-group>
  356.                 </v-flex>
  357.                 <template v-if="routingMethod === 'manual'">
  358.                   <v-flex>
  359.                     <label for="delivery_date">Delivery Date</label>
  360.                   </v-flex>
  361.                   <v-flex>
  362.                     <v-layout row wrap>
  363.                       <v-flex xs6>
  364.                         <databoard-date-picker
  365.                           v-model="deliveryDate.selected"
  366.                           :min="deliveryDate.min"
  367.                           :outline="false"
  368.                           required
  369.                           solo
  370.                           class="solo-outline"
  371.                           :rules="[ x => !!x || 'Please choose delivery date' ]"
  372.                         ></databoard-date-picker>
  373.                       </v-flex>
  374.                     </v-layout>
  375.                   </v-flex>
  376.                 </template>
  377.               </v-layout>
  378.             </v-card-text>
  379.             <v-divider></v-divider>
  380.             <v-divider/>
  381.             <v-card-actions>
  382.               <v-spacer></v-spacer>
  383.               <v-btn color="grey darken-2" flat @click="closeConfirmDialog()">Cancel</v-btn>
  384.               <v-btn flat color="green darken-1" :disabled="updateFormValid === false" :loading="fullscreenLoading" @click="confirmFulfillment" class="white--text">Yes</v-btn>
  385.             </v-card-actions>
  386.           </v-card>
  387.         </v-form>
  388.       </v-dialog>
  389.       <!-- LOADING -->
  390.       <v-dialog v-if="fullscreenLoading" v-model="fullscreenLoading" fullscreen full-width>
  391.         <v-container fluid fill-height style="background-color: rgba(255, 255, 255, 0.5);">
  392.           <v-layout justify-center align-center>
  393.             <v-progress-circular
  394.               indeterminate
  395.               color="primary">
  396.             </v-progress-circular>
  397.           </v-layout>
  398.         </v-container>
  399.       </v-dialog>
  400.       <!-- END LOADING -->
  401.     </v-flex>
  402.   </v-layout>
  403. </template>
  404.  
  405. <script>
  406. import _ from 'underscore'
  407. import moment from 'moment'
  408. import DataboardDatePicker from '@/components/FieldComponents/DataboardDatePicker'
  409. import { validateDistributionCentreSalesOrders } from './scripts/fulfillment'
  410. const FulfillmentFunctions = require('./scripts/fulfillment')
  411.  
  412. export default {
  413.   props: {
  414.     orders: {type: Object, required: false},
  415.     value: {type: Boolean, required: true},
  416.     type: {type: String, required: true}
  417.   },
  418.   components: {
  419.     DataboardDatePicker
  420.   },
  421.   data () {
  422.     return {
  423.       isVoucherDetailLoaded: false,
  424.       invalidReasons: {},
  425.       ruleProductDetails: {},
  426.       totalFulfilledPrice: {},
  427.       totalFulfilledPricePercent: {},
  428.       totalFulfilledQtyPercent: {},
  429.       checkerList: [],
  430.       selectedChecker: '',
  431.       updateFormValid: false,
  432.       snackbar: false,
  433.       snackbarText: '',
  434.       fullscreenLoading: true,
  435.       loadingSOList: false,
  436.       loadingPickingList: false,
  437.       headersFulfillment: [
  438.         { text: 'STATUS', value: 'status', sortable: false },
  439.         { text: 'ORDER ID ', value: 'value.id', sortable: false },
  440.         { text: 'MERCHANT', value: 'value.merchant.store_name', sortable: false },
  441.         { text: 'PRODUCTS', value: 'value.products_fulfilled', sortable: false },
  442.         { text: '', value: 'action', sortable: false }
  443.       ],
  444.       headersTotalProduct: [
  445.         { text: 'PRODUCTS', value: 'name', width: '50%', sortable: false },
  446.         { text: 'UNIT', value: 'unit_name', sortable: false },
  447.         { text: '', value: 'quantity', sortable: false }
  448.       ],
  449.       headersEditFulfill: [
  450.         { text: 'PRODUCTS', value: 'name', sortable: false },
  451.         { text: 'UNIT', value: 'unit_name', sortable: false },
  452.         { text: 'FULFILLED/REQUEST', value: 'fulfilled_request', sortable: false },
  453.         { text: 'STOCK AVAILABLE', value: 'available', sortable: false }
  454.       ],
  455.       fulfillmentResult: {},
  456.       distributionCentre: '',
  457.       editFulfillment: {
  458.         dialog: false,
  459.         selectedFulfill: {},
  460.         originalSelectedFulfill: {}
  461.       },
  462.       voucherErrorDialogShow: false,
  463.       voucherErrorDialogMessage: '',
  464.       editPickingLocationData: {
  465.         dialog: false,
  466.         selectedProduct: {},
  467.         alert: {
  468.           status: false,
  469.           message: '',
  470.           type: ''
  471.         }
  472.       },
  473.       // Confirm dialog
  474.       confirmDialog: false,
  475.       deliveryDate: {
  476.         selected: moment().format('YYYY-MM-DD'),
  477.         min: moment().format('YYYY-MM-DD')
  478.       },
  479.       productDetails: [],
  480.       // Default
  481.       routingMethod: 'manual'
  482.     }
  483.   },
  484.   async beforeMount () {
  485.     // this.isVoucherDetailLoaded = false
  486.     // if (this.type === 'so') {
  487.     //   await this.queryProductDetails()
  488.     //   await this.queryVoucherDetails()
  489.     // }
  490.     // this.isVoucherDetailLoaded = true
  491.   },
  492.   created () {
  493.     this.initialFulfillment()
  494.     this.getDistributionCentre()
  495.     // this.getCheckers()
  496.   },
  497.   computed: {
  498.     mainTableTitle () {
  499.       return this.type === 'st' ? 'Stock Transfer' : 'Sales Order'
  500.     },
  501.     failedOrders () {
  502.       if (!this.fulfillmentResult.fulfilled) return []
  503.       let salesOrders = _.toArray(this.fulfillmentResult.fulfilled.sales_orders)
  504.       let salesExchanges = _.toArray(this.fulfillmentResult.fulfilled.sales_exchanges)
  505.       let stockTransfers = _.toArray(this.fulfillmentResult.fulfilled.stock_transfers)
  506.       let failedArr = []
  507.  
  508.       salesOrders.forEach(salesOrder => {
  509.         if (!salesOrder.success) failedArr.push({id: salesOrder.id, success: salesOrder.success})
  510.       })
  511.       salesExchanges.forEach(salesExchange => {
  512.         if (!salesExchange.success && !salesExchange.showAsSalesExchange) failedArr.push({id: salesExchange.id, success: salesExchange.success})
  513.       })
  514.       stockTransfers.forEach(stockTransfer => {
  515.         if (!stockTransfer.success) failedArr.push({id: stockTransfer.id, success: stockTransfer.success})
  516.       })
  517.       return failedArr
  518.     },
  519.     successOrders () {
  520.       if (!this.fulfillmentResult.fulfilled) return []
  521.       let successFulfillment = {}
  522.  
  523.       if (this.type === 'so') {
  524.         let salesOrders = { ...this.fulfillmentResult.fulfilled.sales_orders }
  525.         _.keys(salesOrders).forEach(salesOrderId => {
  526.           if (!salesOrders[salesOrderId].success) delete (salesOrders[salesOrderId])
  527.         })
  528.         if (!_.isEmpty(salesOrders)) successFulfillment.salesOrders = salesOrders
  529.  
  530.         let salesExchanges = { ...this.fulfillmentResult.fulfilled.sales_exchanges }
  531.         _.keys(salesExchanges).forEach(salesExchangeId => {
  532.           if (!salesExchanges[salesExchangeId].success) delete (salesExchanges[salesExchangeId])
  533.         })
  534.         if (!_.isEmpty(salesExchanges)) successFulfillment.salesExchanges = salesExchanges
  535.       } else if (this.type === 'st') {
  536.         let stockTransfers = { ...this.fulfillmentResult.fulfilled.stock_transfers }
  537.         _.keys(stockTransfers).forEach(stockTransferId => {
  538.           if (!stockTransfers[stockTransferId].success) delete (stockTransfers[stockTransferId])
  539.         })
  540.         if (!_.isEmpty(stockTransfers)) successFulfillment.stockTransfers = stockTransfers
  541.       }
  542.       return successFulfillment
  543.     },
  544.     totalSuccessOrders () {
  545.       if (_.isEmpty(this.successOrders)) return 0
  546.       if (this.type === 'so') {
  547.         let totalSO = 0
  548.         let totalSE = 0
  549.         if (this.successOrders.salesOrders) totalSO += Object.keys(this.successOrders.salesOrders).length
  550.         if (this.successOrders.salesExchanges) totalSE += Object.keys(this.successOrders.salesExchanges).length
  551.         return totalSO + totalSE
  552.       } else if (this.type === 'st') {
  553.         let totalST = 0
  554.         if (this.successOrders.stockTransfers) totalST += Object.keys(this.successOrders.stockTransfers).length
  555.         return totalST
  556.       }
  557.     },
  558.     totalFulfilledProducts () {
  559.       const summary = {}
  560.       for (const key in this.fulfillmentResult.fulfilled) {
  561.         // `key` is either `sales_orders`, `sales_exchanges`, or `stock_transfers`
  562.         const fulfilled = JSON.parse(JSON.stringify(this.fulfillmentResult.fulfilled[key]))
  563.         Object.keys(fulfilled).forEach(id => {
  564.           Object.keys(fulfilled[id].products).forEach(productId => {
  565.             if (!summary[productId]) {
  566.               summary[productId] = {
  567.                 name: fulfilled[id].products[productId].name,
  568.                 id: productId,
  569.                 units: JSON.parse(JSON.stringify(fulfilled[id].products[productId].units))
  570.               }
  571.             } else {
  572.               summary[productId].units.forEach(existingUnit => {
  573.                 fulfilled[id].products[productId].units.forEach(unit => {
  574.                   if (unit.unit_name === existingUnit.unit_name) {
  575.                     existingUnit.fulfilled_quantity += unit.fulfilled_quantity
  576.                   }
  577.                 })
  578.               })
  579.             }
  580.           })
  581.         })
  582.       }
  583.       return Object.values(summary)
  584.         .map((summaryItem) => ({
  585.           name: summaryItem.name,
  586.           id: summaryItem.id,
  587.           units: summaryItem.units.filter(unit => unit.fulfilled_quantity > 0)
  588.         }))
  589.         .filter(summaryItem => summaryItem.units.length > 0)
  590.         .sort((a, b) => {
  591.           if (a.name < b.name) return -1
  592.           else if (a.name > b.name) return 1
  593.           else return 0
  594.         })
  595.     },
  596.     // Main dialog
  597.     dialogFulfill: {
  598.       get () {
  599.         return this.value
  600.       },
  601.       set (value) {
  602.         this.$emit('input', value)
  603.       }
  604.     },
  605.     // To Array Fulfillment Result
  606.     fulfilled () {
  607.       if (!this.fulfillmentResult.fulfilled) return []
  608.       else {
  609.         if (this.type === 'so') {
  610.           if (this.fulfillmentResult.fulfilled.sales_orders) {
  611.             let orders = _.toArray(this.fulfillmentResult.fulfilled.sales_orders)
  612.             orders.forEach(order => {
  613.               Object.keys(order.products).forEach(id => {
  614.                 order.products[id].units.forEach(unit => {
  615.                   unit.product_id = id
  616.                 })
  617.               })
  618.             })
  619.             //  add sorting by payment type
  620.             // orders = _(orders).chain().sortBy(order => { return order.payment_type !== 'cash' }).value().reverse()
  621.             return orders
  622.           } else return []
  623.         } else if (this.type === 'st') return this.fulfillmentResult.fulfilled.stock_transfers ? _.toArray(this.fulfillmentResult.fulfilled.stock_transfers) : []
  624.       }
  625.     },
  626.     fulfilledSalesExchange () {
  627.       if (!this.fulfillmentResult.fulfilled) return []
  628.       else return this.fulfillmentResult ? _.toArray(this.fulfillmentResult.fulfilled.sales_exchanges) : []
  629.     },
  630.     pickingLocations () {
  631.       return this.fulfillmentResult.locations ? _.toArray(this.fulfillmentResult.locations) : []
  632.     },
  633.     // Edit fulfillment
  634.     editFulfillmentProducts () {
  635.       return this.editFulfillment.selectedFulfill.products ? _.toArray(this.editFulfillment.selectedFulfill.products) : []
  636.     },
  637.     tempTotalStockAvailable () {
  638.       let products = JSON.parse(JSON.stringify(this.products))
  639.       Object.keys(products).forEach(productId => {
  640.         products[productId].unit_stock_available_obj = {}
  641.         products[productId].unit_stock_available.forEach(unit => {
  642.           products[productId].unit_stock_available_obj[unit.name] = {...unit}
  643.         })
  644.       })
  645.       if (this.editFulfillment.selectedFulfill) {
  646.         Object.keys(products).forEach(productId => {
  647.           Object.keys(this.editFulfillment.selectedFulfill.products).forEach(editedProductId => {
  648.             if (productId === editedProductId) {
  649.               let totalConvertedQty = 0
  650.               this.editFulfillment.selectedFulfill.products[productId].units.forEach(unit => {
  651.                 let convertedQty = unit.fulfilled_quantity * products[productId].unit_stock_available_obj[unit.unit_name].unit_quantity
  652.                 totalConvertedQty += convertedQty
  653.                 products[productId].total_stock_available -= convertedQty
  654.               })
  655.               this.editFulfillment.selectedFulfill.products[productId].qty = totalConvertedQty
  656.             }
  657.           })
  658.           Object.keys(this.editFulfillment.originalSelectedFulfill.products).forEach(editedProductId => {
  659.             if (productId === editedProductId) {
  660.               this.editFulfillment.originalSelectedFulfill.products[productId].units.forEach(unit => {
  661.                 let convertedQty = unit.fulfilled_quantity * products[productId].unit_stock_available_obj[unit.unit_name].unit_quantity
  662.                 products[productId].total_stock_available += convertedQty
  663.               })
  664.             }
  665.           })
  666.           Object.values(products[productId].unit_stock_available_obj).forEach(unit => {
  667.             unit.stock_available = Math.floor(products[productId].total_stock_available / unit.unit_quantity)
  668.           })
  669.         })
  670.       }
  671.       return products
  672.     },
  673.     // Utility
  674.     products () {
  675.       return this.fulfillmentResult.products
  676.     }
  677.   },
  678.   methods: {
  679.     originalRequestedQuantity (productId, unitName) {
  680.       if (productId !== undefined && unitName !== undefined) {
  681.         let quantity = this.editFulfillment.originalSelectedFulfill.products[productId].units.filter(unit => unit.unit_name === unitName)
  682.         if (quantity.length > 0) return quantity[0].fulfilled_quantity
  683.         else return 0
  684.       } else {
  685.         return 0
  686.       }
  687.     },
  688.     unitStockAvailable (productId, unitName) {
  689.       if (productId !== undefined && unitName !== undefined) {
  690.         let quantity = this.products[productId].unit_stock_available.filter(unit => unit.name === unitName)
  691.         if (quantity.length > 0) return quantity[0].stock_available
  692.         else return 0
  693.       } else {
  694.         return 0
  695.       }
  696.     },
  697.     async queryProductDetails () {
  698.       const salesOrderProductIds = []
  699.       for (const orderKey in this.orders) {
  700.         const salesOrder = this.orders[orderKey]
  701.         const product = salesOrder.products_requested
  702.         const productIds = Object.keys(product)
  703.         salesOrderProductIds.push(...productIds)
  704.       }
  705.       const filteredDuplicatedProductIds = salesOrderProductIds.filter((item, pos, self) => self.indexOf(item) === pos)
  706.       const payload = {
  707.         products: filteredDuplicatedProductIds
  708.       }
  709.       const queryResult = await this.$store.dispatch('purchases/fetchProductsDetails', payload)
  710.       this.productDetails = queryResult
  711.     },
  712.     async queryVoucherDetails () {
  713.       for (const orderKey in this.orders) {
  714.         if (!this.orders[orderKey] || !this.orders[orderKey].voucher) continue
  715.         if (this.orders.rules) continue
  716.         const payload = {
  717.           salesOrder: this.orders[orderKey]
  718.         }
  719.         const salesOrder = await this.$store.dispatch('purchases/fetchPOVoucherDetails', payload)
  720.         this.orders[orderKey].voucher = salesOrder.voucher
  721.       }
  722.     },
  723.     showVoucherErrorDialog (products, salesOrderId) {
  724.       if (typeof products === 'object') {
  725.         this.voucherErrorDialogMessage = 'Internal server error'
  726.       }
  727.       const invalidReasons = this.invalidReasons[salesOrderId]
  728.       if (invalidReasons.length !== 0) {
  729.         this.voucherErrorDialogMessage = `<ol>${invalidReasons.map((value, index) => `<li>${value}</li>`).join('')}</ol>`
  730.       }
  731.       this.voucherErrorDialogShow = true
  732.     },
  733.     closeConfirmDialog () {
  734.       this.confirmDialog = false
  735.       this.selectedChecker = ''
  736.       this.$refs.confirmUpdateToOnLoading.resetValidation()
  737.     },
  738.     // AUTO FULFILLMENT
  739.     async initialFulfillment () {
  740.       this.isVoucherDetailLoaded = false
  741.       this.loadingSOList = true
  742.       this.loadingPickingList = true
  743.       this.fullscreenLoading = true
  744.       let params
  745.       if (this.type === 'so') {
  746.         params = { sales_orders: this.orders }
  747.       } else {
  748.         let stockTransfers = (await this.$store.dispatch('stockTransfer/getStockTransfer', _.keys(this.orders)[0])).data
  749.         params = { stock_transfers: { [`${stockTransfers.id}`]: stockTransfers } }
  750.       }
  751.       if (this.type === 'so') {
  752.         await this.queryProductDetails()
  753.         await this.queryVoucherDetails()
  754.       }
  755.       await this.$store.dispatch('sales/autoFulfill', params).then(result => {
  756.         let data = result.data
  757.         Object.keys(data.products).forEach(productId => {
  758.           data.products[productId]['unit_stock_available'] = data.products[productId].unit_conversions.map(unit => ({
  759.             ...unit,
  760.             stock_available: Number(0)
  761.           }))
  762.           let TOTAL_STOCK_LEFT = data.products[productId].total_stock_available
  763.           data.products[productId].unit_stock_available.forEach(unit => {
  764.             unit.stock_available = Math.floor(TOTAL_STOCK_LEFT / unit.unit_quantity)
  765.           })
  766.         })
  767.         this.$set(this, 'fulfillmentResult', data)
  768.         this.loadingSOList = false
  769.         this.loadingPickingList = false
  770.         this.fullscreenLoading = false
  771.         this.isVoucherDetailLoaded = true
  772.       })
  773.     },
  774.     callSnackbar (message) {
  775.       this.snackbar = true
  776.       this.snackbarText = message
  777.     },
  778.     getDistributionCentre () {
  779.       if (this.type === 'st') {
  780.         this.distributionCentre = this.orders[Object.keys(this.orders)[0]].dc_sender.formatted_name
  781.       } else {
  782.         // assumed 'so'
  783.         if (validateDistributionCentreSalesOrders(this.orders)) {
  784.           this.distributionCentre = this.orders[_.keys(this.orders)[0]].merchant.distribution_centre || 'bsd_taman_tekno'
  785.         }
  786.       }
  787.     },
  788.     // EDIT FULFILLMENT
  789.     editFulfill (item) {
  790.       this.$set(this.editFulfillment, 'originalSelectedFulfill', item)
  791.       this.$set(this.editFulfillment, 'selectedFulfill', JSON.parse(JSON.stringify(item)))
  792.       this.checkSalesExchangeRelation()
  793.       this.$set(this.editFulfillment, 'dialog', true)
  794.     },
  795.     checkSalesExchangeRelation () {
  796.       //  check for SE that have relation to SO
  797.       if (this.editFulfillment.originalSelectedFulfill.sales_order_id && this.type === 'so') {
  798.         let salesOrders = this.fulfillmentResult.fulfilled.sales_orders
  799.         Object.keys(salesOrders).forEach(salesOrderId => {
  800.           let salesExchange = this.editFulfillment.originalSelectedFulfill
  801.           if (salesExchange.sales_order_id === salesOrderId && !salesOrders[salesOrderId].success) {
  802.             this.callSnackbar('SO ' + salesExchange.sales_order_id + ' must be SUCCESS to fulfill this SE')
  803.             this.$set(this.editFulfillment, 'dialog', false)
  804.           }
  805.         })
  806.       }
  807.     },
  808.     async submitEditFulfillment () {
  809.       // Check negative fulfillment and overload fulfillment
  810.       let valid = await this.$validator.validateAll('edit-fulfillment')
  811.  
  812.       if (!valid) return
  813.  
  814.       // Update success status
  815.       let id = this.editFulfillment.originalSelectedFulfill.id
  816.       let success = false
  817.       _.keys(this.editFulfillment.selectedFulfill.products).forEach(productId => {
  818.         this.editFulfillment.selectedFulfill.products[productId].units.forEach(unit => {
  819.           if (unit.fulfilled_quantity > 0) success = true
  820.         })
  821.       })
  822.       this.editFulfillment.selectedFulfill.success = success
  823.  
  824.       if (this.type === 'so') { // condition for sales exchange
  825.         if (!this.editFulfillment.originalSelectedFulfill.sales_order_id) this.fulfillmentResult.fulfilled.sales_orders[id] = this.editFulfillment.selectedFulfill
  826.         else this.fulfillmentResult.fulfilled.sales_exchanges[id] = this.editFulfillment.selectedFulfill
  827.       } else if (this.type === 'st') {
  828.         this.fulfillmentResult.fulfilled.stock_transfers[id] = this.editFulfillment.selectedFulfill
  829.       }
  830.  
  831.       FulfillmentFunctions.returnStockToProducts(
  832.         this.fulfillmentResult.products,
  833.         this.editFulfillment.originalSelectedFulfill.products,
  834.         this.editFulfillment.selectedFulfill.products
  835.       )
  836.       //  check for SO that have relation to SE
  837.       if (this.type === 'so') {
  838.         let salesExchanges = this.fulfillmentResult.fulfilled.sales_exchanges
  839.         Object.keys(salesExchanges).forEach(salesExchangeId => {
  840.           if (id === salesExchanges[salesExchangeId].sales_order_id) {
  841.             this.changeStatusSalesExchange(salesExchanges[salesExchangeId], success)
  842.           }
  843.         })
  844.       }
  845.       this.$set(this.editFulfillment, 'dialog', false)
  846.       await this.recalculateTotalPrice()
  847.     },
  848.     changeStatusSalesExchange (item, success) {
  849.       const id = item.id
  850.  
  851.       const originalSelectedFulfill = item
  852.       const selectedFulfill = JSON.parse(JSON.stringify(item))
  853.  
  854.       _.keys(selectedFulfill.products).forEach(productId => {
  855.         if (!success) {
  856.           selectedFulfill.success = success
  857.           selectedFulfill.products[productId].qty = 0
  858.           for (const unit of selectedFulfill.products[productId].units) {
  859.             unit.fulfilled_quantity = 0
  860.           }
  861.         }
  862.       })
  863.  
  864.       this.fulfillmentResult.fulfilled.sales_exchanges[id] = selectedFulfill
  865.  
  866.       FulfillmentFunctions.returnStockToProducts(
  867.         this.fulfillmentResult.products,
  868.         originalSelectedFulfill.products,
  869.         selectedFulfill.products
  870.       )
  871.     },
  872.     updateSalesOrder (id) {
  873.       this.$set(
  874.         this.fulfillmentResult.updated_sales_orders,
  875.         id,
  876.         FulfillmentFunctions.updateSalesOrderInFulfillment(
  877.           this.orders[id],
  878.           this.fulfillmentResult.fulfilled.sales_orders[id].products
  879.         ).sales_order
  880.       )
  881.     },
  882.     // REMOVE SALES ORDER
  883.     removeSalesOrder (item) {
  884.       const id = item.id
  885.       if (this.type === 'so') {
  886.         FulfillmentFunctions.returnStockToProducts(this.fulfillmentResult.products, this.fulfillmentResult.fulfilled.sales_orders[id].products)
  887.         this.$delete(this.fulfillmentResult.fulfilled.sales_orders, id)
  888.         if (this.fulfillmentResult.fulfilled.sales_exchanges) {
  889.           let salesExchanges = this.fulfillmentResult.fulfilled.sales_exchanges
  890.           Object.keys(salesExchanges).forEach(salesExchangeId => {
  891.             if (id === salesExchanges[salesExchangeId].sales_order_id) {
  892.               FulfillmentFunctions.returnStockToProducts(this.fulfillmentResult.products, salesExchanges[salesExchangeId].products)
  893.               this.$delete(salesExchanges, salesExchangeId)
  894.             }
  895.           })
  896.         }
  897.       } else if (this.type === 'st') {
  898.         FulfillmentFunctions.returnStockToProducts(this.fulfillmentResult.products, this.fulfillmentResult.fulfilled.stock_transfers[id].products)
  899.         this.$delete(this.fulfillmentResult.fulfilled.stock_transfers, id)
  900.       }
  901.  
  902.       this.$emit('remove-sales-order', id)
  903.     },
  904.     // FULFILLMENT TITLE BAR
  905.     closeFulfillment () {
  906.       this.$emit('close-dialog')
  907.     },
  908.     submitFulfillment () {
  909.       this.confirmDialog = true
  910.     },
  911.     confirmFulfillment () {
  912.       // CHECK FOR ONE SUCCESS ORDERS
  913.       if (_.isEmpty(this.successOrders)) {
  914.         this.callSnackbar('AT LEAST 1 ORDER STATUS MUST BE SUCCESS')
  915.         return
  916.       }
  917.       this.fullscreenLoading = true
  918.       this.$store.dispatch('sales/fulfillment', {
  919.         // TO DO: research about voucher implementation in fulfillment, ask SUDONO
  920.         routing_type: this.routingMethod,
  921.         orders: this.orders,
  922.         orders_total_price: this.totalFulfilledPrice,
  923.         orders_total_price_percent: this.totalFulfilledPricePercent,
  924.         orders_total_qty_percent: this.totalFulfilledQtyPercent,
  925.         fulfilled: this.fulfillmentResult.fulfilled,
  926.         deliveryDate: this.deliveryDate.selected,
  927.         order_type: (this.type === 'st') ? 'stock_transfers' : 'sales_orders',
  928.         voucher_validity: Object.keys(this.orders)
  929.                               .filter(salesOrderId => this.orders[salesOrderId].voucher)
  930.                               .reduce((a, b) => {
  931.                                 a[b] = this.isVoucherStatusValid(this.fulfillmentResult.fulfilled.sales_orders[b].products, b)
  932.                                 return a
  933.                               }, {})
  934.       })
  935.       .then(result => {
  936.         let param = { success: false }
  937.         if (result.status === 201 || result.status === 200) {
  938.           param.success = true
  939.           if (this.routingMethod === 'manual') {
  940.             param.delivery_order_id = result.data.delivery_order_id
  941.           } else {
  942.             param.invoices = result.data.sales_orders
  943.           }
  944.         } else {
  945.           param.success = false
  946.           param.error = 'Error Code ' + result.status + ': ' + result.message
  947.         }
  948.         setTimeout(() => {
  949.           this.fullscreenLoading = false
  950.           this.$emit('finish-fulfillment', param)
  951.         }, 1500)
  952.       })
  953.     },
  954.     addInvalidRuleText (salesOrderId, value) {
  955.       const invalidReasons = this.invalidReasons[salesOrderId]
  956.       if (!Array.isArray(invalidReasons)) {
  957.         this.invalidReasons[salesOrderId] = []
  958.       }
  959.       const index = invalidReasons.indexOf(value)
  960.       if (index === -1) {
  961.         invalidReasons.push(value)
  962.       }
  963.     },
  964.     removeInvalidRuleText (salesOrderId, value) {
  965.       const invalidReasons = this.invalidReasons[salesOrderId]
  966.       if (!Array.isArray(invalidReasons)) {
  967.         this.invalidReasons[salesOrderId] = []
  968.       }
  969.       const index = invalidReasons.indexOf(value)
  970.       if (index > -1) {
  971.         invalidReasons.splice(index, 1)
  972.       }
  973.     },
  974.     // checkProductGeneralRule (salesOrderId, productDetails, soProducts, invalidText) {
  975.     //   for (const productDetail of productDetails) {
  976.     //     const product = soProducts[productDetail.id]
  977.     //     if (!product) {
  978.     //       this.addInvalidRuleText(salesOrderId, invalidText)
  979.     //       continue
  980.     //     }
  981.     //     const productQty = Number.parseInt(product.qty)
  982.     //     if (productQty <= 0) {
  983.     //       this.addInvalidRuleText(salesOrderId, invalidText)
  984.     //       continue
  985.     //     } else {
  986.     //       this.removeInvalidRuleText(salesOrderId, invalidText)
  987.     //       continue
  988.     //     }
  989.     //   }
  990.     // },
  991.     // processInvalidSo (valueName, invalidText, salesOrderId, soProducts, productDetails) {
  992.     //   const invalidTextComplete = invalidText + valueName
  993.     //   this.addInvalidRuleText(salesOrderId, invalidTextComplete)
  994.     //   this.checkProductGeneralRule(salesOrderId, productDetails, soProducts, invalidTextComplete)
  995.     // },
  996.     isVoucherStatusValid (soProducts, salesOrderId) {
  997.       if (this.type === 'st') {
  998.         return true
  999.       }
  1000.       this.invalidReasons[salesOrderId] = []
  1001.       const salesOrder = this.orders[salesOrderId]
  1002.       const voucherSO = salesOrder.voucher
  1003.       const soProductIds = Object.keys(soProducts)
  1004.  
  1005.       const soProductDetails = this.productDetails.filter(value => soProductIds.indexOf(value.id) > -1)
  1006.       soProductDetails.forEach(productDetail => { productDetail.combined_category_id = `${productDetail.department_id}${productDetail.category_id}})
  1007.       if (
  1008.         !soProducts ||
  1009.         !voucherSO ||
  1010.         !voucherSO.rules ||
  1011.         !voucherSO.rules.order
  1012.       ) {
  1013.         return true
  1014.       }
  1015.       // Check benefit products
  1016.       const benefit = voucherSO.benefit || {}
  1017.       const discount = benefit.discount || {}
  1018.       if (discount.scope === 'product') {
  1019.         // Check if all products in scope are not fulfilled then the voucher is invalid
  1020.         const orderedDiscountProduct = discount.products.filter(product => soProducts[product.id])
  1021.  
  1022.         const isDiscountProductFulfilled = orderedDiscountProduct.reduce((isValid, discountProduct) => {
  1023.           const productId = discountProduct.id
  1024.           return isValid || soProducts[productId].qty > 0
  1025.         }, false)
  1026.  
  1027.         if (!isDiscountProductFulfilled) {
  1028.           const orderedDiscountProductText = `<ol>${orderedDiscountProduct.map(discountProduct => `<li>${discountProduct.name}</li>`).join('')}</ol>`
  1029.           const reason = `The discounted products not fulfilled: <br/>${orderedDiscountProductText}`
  1030.           this.addInvalidRuleText(salesOrderId, reason)
  1031.         }
  1032.       }
  1033.  
  1034.       // Check rules
  1035.       const rulesOrder = voucherSO.rules.order
  1036.       const scope = rulesOrder.scope
  1037.       const orderType = rulesOrder.type
  1038.  
  1039.       const ruleMinimumAmount = Number.parseFloat(rulesOrder.minimum_amount || 0)
  1040.       const minimumAmount = (80 / 100) * ruleMinimumAmount
  1041.       const productExclusions = scope === 'product' && orderType === 'all' ? (rulesOrder.metadata.product_exclusion || []) : []
  1042.       const excludedTotalPrice = productExclusions.reduce((sumExcluded, excludedProduct) => {
  1043.         const product = soProducts[excludedProduct.id]
  1044.         const totalProductPrice = product ? product.units.reduce((sum, unit) => {
  1045.           const fulfilledQuantity = unit.fulfilled_quantity
  1046.           const price = unit.price
  1047.           return (fulfilledQuantity * price) + sum
  1048.         }, 0) : 0
  1049.         return sumExcluded + totalProductPrice
  1050.       }, 0)
  1051.       const fulfilledTotalPrice = Number.parseFloat(this.totalFulfilledPrice[salesOrderId]) - Number.parseFloat(excludedTotalPrice)
  1052.       const isTotalAndMinimumValid = !Number.isNaN(minimumAmount) && !Number.isNaN(fulfilledTotalPrice)
  1053.       const isFulfilledTotalReach80Percent = (isTotalAndMinimumValid === true) ? (minimumAmount <= fulfilledTotalPrice) : true
  1054.  
  1055.       // Validate minimum total fulfilled amount of 80%
  1056.       if (!isFulfilledTotalReach80Percent) {
  1057.         const excludedProductsText = `<ol>${productExclusions.map(excludedProduct => `<li>${excludedProduct.name}</li>`).join('')}</ol>`
  1058.         const reason = `Total fulfilled < 80%${productExclusions.length > 0 ? `, excluded:<br/>${excludedProductsText}: ''}`
  1059.         this.addInvalidRuleText(salesOrderId, reason)
  1060.       }
  1061.  
  1062.       // Validate there is at least 1 product fulfilled (except excluded products)
  1063.       if (fulfilledTotalPrice <= 0) {
  1064.         const reason = `There is no product fulfilled except the excluded ones`
  1065.         this.addInvalidRuleText(salesOrderId, reason)
  1066.       }
  1067.  
  1068.       // included product
  1069.       const productCondition = rulesOrder.metadata.logic
  1070.       if (scope === 'category' || scope === 'brand') {
  1071.         const scopeKey = (scope === 'category') ? 'combined_category_id' : 'brand_id'
  1072.         const metadataValues = voucherSO.rules.order.metadata.values.map(value => ({
  1073.           id: scope === 'category' ? `${value.department_id}${value.id}: value.id,
  1074.           name: value.name
  1075.         }))
  1076.         const metadataValuesIds = metadataValues.map(value => value.id)
  1077.         const fulfilledProductDetails = soProductDetails.filter(productDetail => soProducts[productDetail.id].qty > 0)
  1078.         const soProductsValuesIds = fulfilledProductDetails.map(value => value[scopeKey])
  1079.         const metadataValuesNotExist = metadataValuesIds.filter(
  1080.           value => soProductsValuesIds.indexOf(value) === -1)
  1081.         const fulfilledScopedProductDetails = fulfilledProductDetails.filter(
  1082.           value => metadataValuesIds.indexOf(value[scopeKey]) > -1)
  1083.         const productDetailsValuesIds = fulfilledScopedProductDetails.map(value => value[scopeKey])
  1084.         const isAllValuesExist = (productDetailsValuesIds.length === metadataValuesIds.length)
  1085.         const isAllValuesNotExist = metadataValuesIds.length === metadataValuesNotExist.length
  1086.  
  1087.         if (productCondition === 'and' && isAllValuesExist === false) {
  1088.           const missingMetadataValues = metadataValues.filter(value => metadataValuesNotExist.includes(value.id))
  1089.           const reason = `These ${scope === 'category' ? 'categories' : 'brands'} not fulfilled:<br/><ol>${missingMetadataValues.map(value => `<li>${value.name}</li>`).join('')}</ol>`
  1090.           this.addInvalidRuleText(salesOrderId, reason)
  1091.         } else if (productCondition === 'or' && isAllValuesNotExist === true) {
  1092.           const reason = `Fulfilled at least one of these ${scope === 'category' ? 'categories' : 'brands'}:<br/><ol>${metadataValues.map(value => `<li>${value.name}</li>`).join('')}</ol>`
  1093.           this.addInvalidRuleText(salesOrderId, reason)
  1094.         }
  1095.       }
  1096.  
  1097.       // if (scope === 'product' && orderType === 'all') {
  1098.       //   const productExclusion = rulesOrder.metadata.product_exclusion ? rulesOrder.metadata.product_exclusion : []
  1099.       //   const productIdsNotFound = productExclusion.filter(value => soProductIds.indexOf(value.id) === -1)
  1100.       //   if (productIdsNotFound.length > 0) {
  1101.       //     this.invalidReasons[salesOrderId].push(...productIdsNotFound.map(value => value.name))
  1102.       //   }
  1103.       // }
  1104.  
  1105.       if (scope === 'product' && orderType === 'include') {
  1106.         const minimumSelection = rulesOrder.metadata.minimum_selection
  1107.  
  1108.         if (minimumSelection === 'total') {
  1109.           const minimumTotalValue = Number.parseInt(rulesOrder.metadata.minimum_total_value)
  1110.           const metadataValues = voucherSO.rules.order.metadata.values
  1111.           const soProductsValue = Object.values(soProducts).filter(product => metadataValues.find(value => product.product_id === value.id))
  1112.           const totalValue = soProductsValue.reduce(
  1113.             (total, value) => value.units.reduce(
  1114.               (uTotal, uValue) => (Number.parseInt(uValue.fulfilled_quantity) * Number.parseInt(uValue.price)) + uTotal
  1115.               , total)
  1116.             , 0)
  1117.  
  1118.           if (totalValue < (0.8 * minimumTotalValue)) {
  1119.             // Voucher invalid in this state
  1120.             const reason = `Total fulfilled of these products is not enough (< 80% of minimum voucher condition): <br/><ol>${soProductsValue.map(value => `<li>${value.name}</li>`).join('')}</ol>`
  1121.             this.invalidReasons[salesOrderId].push(reason)
  1122.           }
  1123.  
  1124.           const notFulfilledProducts = soProductsValue.filter(product => product.qty <= 0)
  1125.           if (notFulfilledProducts.length > 0) {
  1126.             const reason = `These products is not fulfilled at least 1: <br/><ol>${notFulfilledProducts.map(value => `<li>${value.name}</li>`).join('')}</ol>`
  1127.             this.invalidReasons[salesOrderId].push(reason)
  1128.           }
  1129.         }
  1130.  
  1131.         if (minimumSelection === 'individual') {
  1132.           const metadataValues = voucherSO.rules.order.metadata.values
  1133.           const validProducts = []
  1134.           const invalidProducts = []
  1135.           for (const value of metadataValues) {
  1136.             const fulfilled = soProducts[value.id]
  1137.             // This is for the "or" type where the product can be not ordered
  1138.             if (!fulfilled) {
  1139.               // If it is not ordered, skip this
  1140.               // invalidProducts.push(value)
  1141.               continue
  1142.             }
  1143.             const totalUnitPrice = fulfilled.units.reduce(
  1144.               (uTotal, uValue) => (Number.parseInt(uValue.fulfilled_quantity) * Number.parseInt(uValue.price)) + uTotal
  1145.             , 0)
  1146.             const isTotalPriceReachMinimum = totalUnitPrice >= (0.8 * Number.parseInt(value.minimum_value))
  1147.  
  1148.             if (isTotalPriceReachMinimum) {
  1149.               validProducts.push(value)
  1150.             } else {
  1151.               invalidProducts.push(value)
  1152.             }
  1153.           }
  1154.  
  1155.           // "OR" is invalid when there is no valid products
  1156.           if (productCondition === 'or' && validProducts.length <= 0) {
  1157.             const thousandSeparator = this.$options.filters.thousandSeparator
  1158.             const reason = `Any of these conditions is not fulfilled: <br/><ol>${invalidProducts.map(value => `<li>${value.name} - minimum Rp ${thousandSeparator(value.minimum_value)}</li>`).join('')}</ol>`
  1159.             this.addInvalidRuleText(salesOrderId, reason)
  1160.           } else if (productCondition === 'and' && invalidProducts.length > 0) {
  1161.             const thousandSeparator = this.$options.filters.thousandSeparator
  1162.             const reason = `All of these conditions are not fulfilled: <br/><ol>${invalidProducts.map(value => `<li>${value.name} - minimum Rp ${thousandSeparator(value.minimum_value)}</li>`).join('')}</ol>`
  1163.             this.addInvalidRuleText(salesOrderId, reason)
  1164.           }
  1165.         }
  1166.  
  1167.         if (minimumSelection === 'none') {
  1168.           const metadataValues = voucherSO.rules.order.metadata.values
  1169.           const validProducts = []
  1170.           const invalidProducts = []
  1171.           for (const value of metadataValues) {
  1172.             const fulfilled = soProducts[value.id]
  1173.             // This is for the "or" type where the product can be not ordered
  1174.             if (!fulfilled) {
  1175.               // If it is not ordered, skip this
  1176.               // invalidProducts.push(value)
  1177.               continue
  1178.             }
  1179.             const isFulfilled = fulfilled.qty > 0
  1180.  
  1181.             if (isFulfilled) {
  1182.               validProducts.push(value)
  1183.             } else {
  1184.               invalidProducts.push(value)
  1185.             }
  1186.           }
  1187.           if (productCondition === 'or' && validProducts.length <= 0) {
  1188.             const reason = `Any of these products is not fulfilled: <br/><ol>${invalidProducts.map(value => `<li>${value.name}</li>`).join('')}</ol>`
  1189.             this.addInvalidRuleText(salesOrderId, reason)
  1190.           } else if (productCondition === 'and' && invalidProducts.length > 0) {
  1191.             const reason = `All of these products are not fulfilled: <br/><ol>${invalidProducts.map(value => `<li>${value.name}</li>`).join('')}</ol>`
  1192.             this.addInvalidRuleText(salesOrderId, reason)
  1193.           }
  1194.         }
  1195.       }
  1196.  
  1197.       // Voucher Product Condition: "Doesn't Contains Products"
  1198.       if (scope === 'product' && orderType === 'exclude') {
  1199.         // Do nothing.
  1200.         // Should be already handled in create order
  1201.       }
  1202.  
  1203.       // let isIncludedProductsExist = true
  1204.       // const isIncludedProductRuleExist = voucherSO.rules.order.products && Array.isArray(voucherSO.rules.order.products.included)
  1205.       // if (isIncludedProductRuleExist) {
  1206.       //   const ruleIncludedProducts = voucherSO.rules.order.products.included
  1207.       //   for (const ruleIncludedProduct of ruleIncludedProducts) {
  1208.       //     const productId = ruleIncludedProduct.id
  1209.       //     const product = soProducts[productId]
  1210.       //     if (!product) {
  1211.       //       this.invalidReasons[salesOrderId].push(ruleIncludedProduct.name)
  1212.       //       isIncludedProductsExist = false
  1213.       //       continue
  1214.       //     }
  1215.       //     const productQty = Number.parseInt(product.qty)
  1216.       //     if (productQty <= 0) {
  1217.       //       this.invalidReasons[salesOrderId].push(ruleIncludedProduct.name)
  1218.       //       isIncludedProductsExist = false
  1219.       //       continue
  1220.       //     }
  1221.       //   }
  1222.       // }
  1223.  
  1224.       if (this.invalidReasons[salesOrderId].length !== 0) {
  1225.         return false
  1226.       }
  1227.       return true
  1228.     },
  1229.     isFulfilledQtyValid (fulfilled, request, soId, productId) {
  1230.       const fulfilledValue = parseInt(fulfilled)
  1231.       const requestValue = parseInt(request)
  1232.       const fulfilledPercentage = (fulfilledValue / requestValue) * 100
  1233.       if (!this.totalFulfilledQtyPercent[soId]) {
  1234.         this.totalFulfilledQtyPercent[soId] = {}
  1235.       }
  1236.       this.totalFulfilledQtyPercent[soId][productId] = fulfilledPercentage
  1237.       return fulfilledPercentage >= 80
  1238.     },
  1239.     isFulfilledPriceValid (fulfilled, request, soId, productId) {
  1240.       const fulfilledValue = parseInt(fulfilled)
  1241.       const requestValue = parseInt(request)
  1242.       let itemPrice = 0
  1243.       if (this.orders[soId].products_requested && this.orders[soId].products_requested[productId]) {
  1244.         itemPrice = parseInt(this.orders[soId].products_requested[productId].selling_price) | 0
  1245.       }
  1246.       const fulfilledPrice = itemPrice * fulfilledValue
  1247.       const requestPrice = itemPrice * requestValue
  1248.       const pricePercentage = (fulfilledPrice / requestPrice) * 100
  1249.       if (pricePercentage < 80) {
  1250.         return false
  1251.       }
  1252.       return true
  1253.     },
  1254.     isFulfilledStatusValid (fulfilled, request, soId, productId) {
  1255.       if (!this.totalFulfilledPrice[soId]) {
  1256.         this.recalculateTotalPrice()
  1257.       }
  1258.       return request === fulfilled
  1259.     },
  1260.     recalculateTotalPrice () {
  1261.       const salesOrders = this.fulfillmentResult.fulfilled.sales_orders
  1262.       for (const salesOrderKey in salesOrders) {
  1263.         this.totalFulfilledPrice[salesOrderKey] = 0
  1264.         const products = salesOrders[salesOrderKey].products
  1265.         for (const productKey in products) {
  1266.           const totalProductPrice = products[productKey].units.reduce((sum, unit) => {
  1267.             const fulfilledQuantity = unit.fulfilled_quantity
  1268.             const price = unit.price
  1269.             return (fulfilledQuantity * price) + sum
  1270.           }, 0)
  1271.           this.totalFulfilledPrice[salesOrderKey] += totalProductPrice
  1272.         }
  1273.       }
  1274.     },
  1275.     closeVoucherErrorMessageDialog () {
  1276.       this.voucherErrorDialogShow = false
  1277.       this.dialogFulfill = true
  1278.     }
  1279.   }
  1280. }
  1281. </script>
  1282.  
  1283. <style scoped>
  1284. table {
  1285.   min-width: 100%;
  1286. }
  1287. table.products-table,
  1288. table.summary-products-table {
  1289.   border-collapse: collapse;
  1290. }
  1291. table.products-table th:first-child,
  1292. table.products-table td:first-child {
  1293.   padding-left: 0px;
  1294.   min-width: 170px;
  1295. }
  1296. table th {
  1297.   text-align: left;
  1298.   text-transform: uppercase;
  1299.   border-bottom: 1px solid;
  1300.   padding: 5px;
  1301.   border-color: #eaeaea;
  1302. }
  1303. table.summary-products-table th:first-child,
  1304. table.summary-products-table td:first-child {
  1305.   padding: 4px 24px 4px 0;
  1306. }
  1307. table.summary-products-table th:last-child,
  1308. table.summary-products-table td:last-child {
  1309.   padding-left: 0px;
  1310.   padding-right: 0px;
  1311. }
  1312. table.summary-products-table td,
  1313. table.summary-products-table th {
  1314.   height: 36px;
  1315. }
  1316. table.summary-products-table td {
  1317.   min-width: 90px;
  1318. }
  1319. table.summary-products-table tr {
  1320.   border-bottom: none !important;
  1321. }
  1322. >>>.solo-outline .v-text-field__slot {
  1323.   padding: 4px;
  1324. }
  1325. /* table td {
  1326.   padding: 5px;
  1327. } */
  1328. </style>
  1329.  

Editor

You can edit this paste and save as new: