【愚公系列】2023年03月 WMS智能仓储系统-017.仓内作业(库存冻结、库存调整、库存盘点)

举报
愚公搬代码 发表于 2023/03/31 22:58:44 2023/03/31
【摘要】 前言这节主要分为两个模块:• 库存冻结:库存冻结的作用是为了防止过度的库存积压,从而避免库存的浪费。• 库存调整:库存调整的作用是提高库存利用率,改善库存管理,降低库存成本,改善企业的运作效率。• 库存盘点:库存盘点的目的是对现有库存量进行实际核对,以确定物料的实际数量、状况和位置,并核实它们是否与账面记载的数量一致。这样可以有效地发现任何库存记录错误,以及及时补充不足的库存。一、仓内作业1...


前言

这节主要分为两个模块:

• 库存冻结:库存冻结的作用是为了防止过度的库存积压,从而避免库存的浪费。

• 库存调整:库存调整的作用是提高库存利用率,改善库存管理,降低库存成本,改善企业的运作效率。

• 库存盘点:库存盘点的目的是对现有库存量进行实际核对,以确定物料的实际数量、状况和位置,并核实它们是否与账面记载的数量一致。这样可以有效地发现任何库存记录错误,以及及时补充不足的库存。


一、仓内作业

1.库存冻结


1.1 页面代码

<!-- Warehouse Freeze -->
<template>
<div class="container">
<div>
<!-- Main Content -->
<v-card class="mt-5">
<v-card-text>
<v-window v-model="data.activeTab">
<v-window-item>
<div class="operateArea">
<v-row no-gutters>
<!-- Operate Btn -->
<v-col cols="3" class="col">
<tooltip-btn
icon="mdi-lock-open-outline"
:tooltip-text="$t('wms.warehouseWorking.warehouseFreeze.freeze')"
@click="method.add(FREEZE_JOB_FREEZE)"
></tooltip-btn>
<tooltip-btn
icon="mdi-lock-open-variant-outline"
:tooltip-text="$t('wms.warehouseWorking.warehouseFreeze.unfreeze')"
@click="method.add(FREEZE_JOB_UNFREEZE)"
></tooltip-btn>
<tooltip-btn icon="mdi-refresh" :tooltip-text="$t('system.page.refresh')" @click="method.refresh"></tooltip-btn>
<tooltip-btn icon="mdi-export-variant" :tooltip-text="$t('system.page.export')" @click="method.exportTable"> </tooltip-btn>
</v-col>

<!-- Search Input -->
<v-col cols="9">
<v-row no-gutters @keyup.enter="method.sureSearch">
<v-col cols="4"></v-col>
<v-col cols="4"></v-col>
<v-col cols="4">
<v-text-field
v-model="data.searchForm.job_code"
clearable
hide-details
density="comfortable"
class="searchInput ml-5 mt-1"
:label="$t('wms.warehouseWorking.warehouseFreeze.job_code')"
variant="solo"
>
</v-text-field>
</v-col>
</v-row>
</v-col>
</v-row>
</div>

<!-- Table -->
<div
class="mt-5"
:style="{
height: cardHeight
}"
>
<vxe-table ref="xTable" :column-config="{ minWidth: '100px' }" :data="data.tableData" :height="tableHeight" align="center">
<template #empty>
{{ i18n.global.t('system.page.noData') }}
</template>
<vxe-column type="seq" width="60"></vxe-column>
<vxe-column type="checkbox" width="50"></vxe-column>
<vxe-column field="job_code" width="150px" :title="$t('wms.warehouseWorking.warehouseFreeze.job_code')"></vxe-column>
<vxe-column field="job_type" width="150px" :title="$t('wms.warehouseWorking.warehouseFreeze.job_type')">
<template #default="{ row, column }">
<span>{{ formatFreezeJobType(row[column.property]) }}</span>
</template>
</vxe-column>
<vxe-column field="warehouse_name" width="150px" :title="$t('wms.warehouseWorking.warehouseFreeze.warehouse')"></vxe-column>
<vxe-column field="location_name" width="150px" :title="$t('wms.warehouseWorking.warehouseFreeze.location_name')"></vxe-column>
<vxe-column field="spu_code" width="150px" :title="$t('base.commodityManagement.spu_code')"></vxe-column>
<vxe-column field="spu_name" width="150px" :title="$t('base.commodityManagement.spu_name')"></vxe-column>
<vxe-column field="sku_code" width="150px" :title="$t('base.commodityManagement.sku_code')"></vxe-column>
<vxe-column field="handler" width="150px" :title="$t('wms.warehouseWorking.warehouseFreeze.handler')"></vxe-column>
<vxe-column field="handle_time" width="170px" :title="$t('wms.warehouseWorking.warehouseFreeze.handle_time')">
<template #default="{ row, column }">
<span>{{ formatDate(row[column.property]) }}</span>
</template>
</vxe-column>
<!-- <vxe-column field="creator" :title="$t('wms.warehouseWorking.warehouseFreeze.creator')"></vxe-column>
<vxe-column
field="create_time"
width="170px"
:title="$t('wms.warehouseWorking.warehouseFreeze.create_time')"
></vxe-column> -->
<vxe-column field="operate" :title="$t('system.page.operate')" width="100" :resizable="false" show-overflow>
<template #default="{ row }">
<tooltip-btn
:flat="true"
icon="mdi-eye-outline"
:tooltip-text="$t('system.page.view')"
@click="method.viewRow(row)"
></tooltip-btn>
<!-- <tooltip-btn
:flat="true"
icon="mdi-book-open-outline"
:tooltip-text="$t('wms.warehouseWorking.warehouseMove.confirmMove')"
@click="method.confirmMove(row)"
></tooltip-btn> -->
<!-- <tooltip-btn
:flat="true"
icon="mdi-delete-outline"
:tooltip-text="$t('system.page.delete')"
:icon-color="errorColor"
@click="method.deleteRow(row)"
></tooltip-btn> -->
</template>
</vxe-column>
</vxe-table>
<custom-pager
:current-page="data.tablePage.pageIndex"
:page-size="data.tablePage.pageSize"
perfect
:total="data.tablePage.total"
:page-sizes="PAGE_SIZE"
:layouts="PAGE_LAYOUT"
@page-change="method.handlePageChange"
>
</custom-pager>
</div>
</v-window-item>
</v-window>
</v-card-text>
</v-card>
<addOrUpdateDialog
:show-dialog="data.showDialog"
:form="data.dialogForm"
:freeze-type="data.freezeType"
@close="method.closeDialog"
@saveSuccess="method.saveSuccess"
/>
</div>
</div>
</template>

<script lang="ts" setup>
import { computed, ref, reactive, onActivated, watch, nextTick } from 'vue'
import { VxePagerEvents } from 'vxe-table'
import { computedCardHeight, computedTableHeight } from '@/constant/style'
import { WarehouseFreezeVO } from '@/types/WarehouseWorking/WarehouseFreeze'
import { PAGE_SIZE, PAGE_LAYOUT, DEFAULT_PAGE_SIZE } from '@/constant/vxeTable'
import { FREEZE_JOB_FREEZE, FREEZE_JOB_UNFREEZE } from '@/constant/warehouseWorking'
import { hookComponent } from '@/components/system'
import { formatFreezeJobType } from '@/utils/format/formatWarehouseWorking'
import { getStockFreezeList, getStockFreezeOne } from '@/api/wms/warehouseFreeze'
import { DEBOUNCE_TIME } from '@/constant/system'
import { setSearchObject } from '@/utils/common'
import { SearchObject } from '@/types/System/Form'
import { formatDate } from '@/utils/format/formatSystem'
import tooltipBtn from '@/components/tooltip-btn.vue'
import addOrUpdateDialog from './add-or-update-freeze.vue'
import i18n from '@/languages/i18n'
import customPager from '@/components/custom-pager.vue'
import { exportData } from '@/utils/exportTable'

const xTable = ref()

const data = reactive({
showDialog: false,
freezeType: FREEZE_JOB_FREEZE,
timer: ref<any>(null),
activeTab: null,
searchForm: {
job_code: ''
},
tableData: ref<WarehouseFreezeVO[]>([]),
dialogForm: {
id: 0,
job_code: '',
job_type: FREEZE_JOB_FREEZE,
sku_id: 0,
goods_owner_id: 0,
goods_location_id: 0,
handler: '',
handle_time: '',
last_update_time: '',
tenant_id: 0,
warehouse_name: '',
location_name: '',
spu_code: '',
spu_name: '',
sku_code: '',
creator: '',
create_time: ''
},
tablePage: reactive({
total: 0,
pageIndex: 1,
pageSize: DEFAULT_PAGE_SIZE,
searchObjects: ref<Array<SearchObject>>([])
})
})

const method = reactive({
// Open a dialog to add
add: (jobType: boolean) => {
data.freezeType = jobType
data.dialogForm = {
id: 0,
job_code: '',
job_type: FREEZE_JOB_FREEZE,
sku_id: 0,
goods_owner_id: 0,
goods_location_id: 0,
handler: '',
handle_time: '',
last_update_time: '',
tenant_id: 0,
warehouse_name: '',
location_name: '',
spu_code: '',
spu_name: '',
sku_code: '',
creator: '',
create_time: ''
}
nextTick(() => {
data.showDialog = true
})
},

// After add or update success.
saveSuccess: () => {
method.refresh()
method.closeDialog()
},

// Refresh data
refresh: () => {
method.getStockProcessList()
},

// Shut add or update dialog
closeDialog: () => {
data.showDialog = false
},

getStockProcessList: async () => {
const { data: res } = await getStockFreezeList(data.tablePage)
if (!res.isSuccess) {
hookComponent.$message({
type: 'error',
content: res.errorMessage
})
return
}
data.tableData = res.data.rows
data.tablePage.total = res.data.totals
},

viewRow: async (row: WarehouseFreezeVO) => {
await method.getOne(row.id)
nextTick(() => {
data.showDialog = true
})
},

getOne: async (id: number) => {
const { data: res } = await getStockFreezeOne(id)
if (!res.isSuccess) {
hookComponent.$message({
type: 'error',
content: res.errorMessage
})
return
}

data.dialogForm = res.data
},

// deleteRow(row: WarehouseFreezeVO) {
// hookComponent.$dialog({
// content: i18n.global.t('system.tips.beforeDeleteMessage'),
// handleConfirm: async () => {
// if (row.id) {
// const { data: res } = await deleteStockFreeze(row.id)
// if (!res.isSuccess) {
// hookComponent.$message({
// type: 'error',
// content: res.errorMessage
// })
// return
// }

// hookComponent.$message({
// type: 'success',
// content: `${ i18n.global.t('system.page.delete') }${ i18n.global.t('system.tips.success') }`
// })
// method.refresh()
// }
// }
// })
// },

handlePageChange: ref<VxePagerEvents.PageChange>(({ currentPage, pageSize }) => {
data.tablePage.pageIndex = currentPage
data.tablePage.pageSize = pageSize
method.refresh()
}),

exportTable: () => {
const $table = xTable.value
exportData({
table: $table,
filename: i18n.global.t('router.sideBar.warehouseFreeze'),
columnFilterMethod({ column }: any) {
return !['checkbox'].includes(column?.type) && !['operate'].includes(column?.field)
}
})
},

sureSearch: () => {
data.tablePage.searchObjects = setSearchObject(data.searchForm)
method.refresh()
}
})

onActivated(() => {
method.refresh()
})

const cardHeight = computed(() => computedCardHeight({ hasTab: false }))
const tableHeight = computed(() => computedTableHeight({ hasTab: false }))

watch(
() => data.searchForm,
() => {
// debounce
if (data.timer) {
clearTimeout(data.timer)
}
data.timer = setTimeout(() => {
data.timer = null
method.sureSearch()
}, DEBOUNCE_TIME)
},
{
deep: true
}
)
</script>

<style scoped lang="less">
.operateArea {
width: 100%;
min-width: 760px;
display: flex;
align-items: center;
border-radius: 10px;
padding: 0 10px;
}

.col {
display: flex;
align-items: center;
}
</style>


<!-- Warehouse Freeze Operate Dialog -->
<template>
<v-dialog v-model="isShow" :width="'30%'" transition="dialog-top-transition" :persistent="true">
<template #default>
<v-card>
<v-toolbar class="" color="white" :title="`${$t('router.sideBar.warehouseFreeze')}(${jobTypeComp})`"></v-toolbar>
<v-card-text>
<v-form ref="formRef">
<v-text-field
v-model="data.form.spu_code"
:label="$t('base.commodityManagement.spu_code')"
:rules="data.rules.spu_code"
variant="outlined"
readonly
clearable
@click="method.openCommoditySelect"
@click:clear="method.clearCommodity"
></v-text-field>
<v-text-field
v-model="data.form.spu_name"
:label="$t('base.commodityManagement.spu_name')"
:rules="data.rules.spu_name"
variant="outlined"
disabled
></v-text-field>
<v-text-field
v-model="data.form.sku_code"
:label="$t('base.commodityManagement.sku_code')"
:rules="data.rules.sku_code"
variant="outlined"
disabled
></v-text-field>
<v-text-field
v-model="data.form.location_name"
:label="$t('wms.warehouseWorking.warehouseFreeze.location_name')"
:rules="data.rules.location_name"
variant="outlined"
disabled
></v-text-field>
<v-text-field
v-model="data.form.warehouse_name"
:label="$t('wms.warehouseWorking.warehouseFreeze.warehouse')"
:rules="data.rules.warehouse_name"
variant="outlined"
disabled
></v-text-field>
</v-form>
</v-card-text>
<v-card-actions class="justify-end">
<v-btn variant="text" @click="method.closeDialog">{{ $t('system.page.close') }}</v-btn>
<v-btn color="primary" variant="text" :disabled="operateDisabled" @click="method.submit">{{ $t('system.page.submit') }}</v-btn>
</v-card-actions>
</v-card>
</template>
</v-dialog>

<commodity-select
:show-dialog="data.showCommodityDialogSelect"
:sql-title="commoditySqlTitle"
@close="method.closeCommodityDialogSelect"
@sureSelect="method.sureSelectCommodity"
/>
<!-- <location-select :show-dialog="data.showLocationDialogSelect" @close="method.closeLocationDialogSelect" @sureSelect="method.sureSelectLocation" /> -->
</template>

<script lang="ts" setup>
import { reactive, computed, ref, watch } from 'vue'
import { FREEZE_JOB_FREEZE, FREEZE_JOB_UNFREEZE } from '@/constant/warehouseWorking'
import { hookComponent } from '@/components/system/index'
import { addStockFreeze } from '@/api/wms/warehouseFreeze'
import { WarehouseFreezeVO } from '@/types/WarehouseWorking/WarehouseFreeze'
import { removeObjectNull } from '@/utils/common'
import commoditySelect from '@/components/select/commodity-select.vue'
import i18n from '@/languages/i18n'

const formRef = ref()
const emit = defineEmits(['close', 'saveSuccess'])
const isUpdate = computed(() => props.form.id && props.form.id > 0)
const operateDisabled = computed(() => !!isUpdate.value)

const props = defineProps<{
showDialog: boolean
form: WarehouseFreezeVO
freezeType: boolean
}>()

const isShow = computed(() => props.showDialog)
const jobTypeComp = computed(() => (data.form.job_type === FREEZE_JOB_FREEZE
? i18n.global.t('wms.warehouseWorking.warehouseFreeze.freeze')
: i18n.global.t('wms.warehouseWorking.warehouseFreeze.unfreeze')))

// Unfreeze should be filter freezed.
const commoditySqlTitle = computed(() => (data.form.job_type === FREEZE_JOB_UNFREEZE ? 'frozen' : ''))

const data = reactive({
showCommodityDialogSelect: false,
showLocationDialogSelect: false,

form: ref<WarehouseFreezeVO>({
id: 0,
job_code: '',
job_type: FREEZE_JOB_FREEZE,
sku_id: 0,
goods_owner_id: 0,
goods_location_id: 0,
handler: '',
handle_time: '',
last_update_time: '',
tenant_id: 0,
warehouse_name: '',
location_name: '',
spu_code: '',
spu_name: '',
sku_code: '',
creator: '',
create_time: ''
}),
rules: {
warehouse_name: [],
location_name: [],
spu_code: [(val: string) => !!val || `${ i18n.global.t('system.checkText.mustInput') }${ i18n.global.t('base.commodityManagement.spu_code') }!`],
spu_name: [(val: string) => !!val || `${ i18n.global.t('system.checkText.mustInput') }${ i18n.global.t('base.commodityManagement.spu_name') }!`],
sku_code: [(val: string) => !!val || `${ i18n.global.t('system.checkText.mustInput') }${ i18n.global.t('base.commodityManagement.sku_code') }!`]
}
})

const method = reactive({
closeDialog: () => {
emit('close')
},
initForm: () => {
data.form = props.form
data.form.job_type = props.freezeType
},

openCommoditySelect: () => {
// Open select modal after UI rendered.
setTimeout(() => {
data.showCommodityDialogSelect = true
}, 100)
},

closeCommodityDialogSelect: () => {
data.showCommodityDialogSelect = false
},

openLocationSelect: () => {
setTimeout(() => {
data.showLocationDialogSelect = true
}, 100)
},

closeLocationDialogSelect: () => {
data.showLocationDialogSelect = false
},

sureSelectCommodity: (selectRecords: any) => {
try {
data.form.sku_id = selectRecords[0].sku_id
data.form.spu_code = selectRecords[0].spu_code
data.form.spu_name = selectRecords[0].spu_name
data.form.sku_code = selectRecords[0].sku_code

data.form.goods_owner_id = selectRecords[0].goods_owner_id
data.form.goods_location_id = selectRecords[0].goods_location_id
data.form.warehouse_name = selectRecords[0].warehouse_name
data.form.location_name = selectRecords[0].location_name
} catch (error) {
console.error(error)
}
},

clearCommodity: () => {
data.form.sku_id = 0
data.form.spu_code = ''
data.form.spu_name = ''
data.form.sku_code = ''
},

clearLocation: () => {
data.form.goods_location_id = 0
data.form.warehouse_name = ''
data.form.location_name = ''
},

submit: async () => {
const { valid } = await formRef.value.validate()

const form = method.constructFormBody()

if (valid) {
const { data: res } = await addStockFreeze(form)
if (!res.isSuccess) {
hookComponent.$message({
type: 'error',
content: res.errorMessage
})
return
}
hookComponent.$message({
type: 'success',
content: `${ i18n.global.t('system.page.submit') }${ i18n.global.t('system.tips.success') }`
})
emit('saveSuccess')
} else {
hookComponent.$message({
type: 'error',
content: i18n.global.t('system.checkText.checkFormFail')
})
}
},

constructFormBody: () => {
let form = { ...data.form }
form = removeObjectNull(form)

return form
}
})

watch(
() => isShow.value,
(val) => {
if (val) {
method.initForm()
}
}
)
</script>

<style scoped lang="less">
.v-form {
div {
margin-bottom: 7px;
}
}
</style>


1.2 接口代码

[Route("stockfreeze")]
[ApiController]
[ApiExplorerSettings(GroupName = "WMS")]
public class StockfreezeController : BaseController
{
#region Args

/// <summary>
/// stockfreeze Service
/// </summary>
private readonly IStockfreezeService _stockfreezeService;

/// <summary>
/// Localizer Service
/// </summary>
private readonly IStringLocalizer<ModernWMS.Core.MultiLanguage> _stringLocalizer;
#endregion

#region constructor
/// <summary>
/// constructor
/// </summary>
/// <param name="stockfreezeService">stockfreeze Service</param>
/// <param name="stringLocalizer">Localizer</param>
public StockfreezeController(
IStockfreezeService stockfreezeService
, IStringLocalizer<ModernWMS.Core.MultiLanguage> stringLocalizer
)
{
this._stockfreezeService = stockfreezeService;
this._stringLocalizer= stringLocalizer;
}
#endregion

#region Api
/// <summary>
/// page search
/// </summary>
/// <param name="pageSearch">args</param>
/// <returns></returns>
[HttpPost("list")]
public async Task<ResultModel<PageData<StockfreezeViewModel>>> PageAsync(PageSearch pageSearch)
{
var (data, totals) = await _stockfreezeService.PageAsync(pageSearch, CurrentUser);

return ResultModel<PageData<StockfreezeViewModel>>.Success(new PageData<StockfreezeViewModel>
{
Rows = data,
Totals = totals
});
}

/// <summary>
/// get all records
/// </summary>
/// <returns>args</returns>
[HttpGet("all")]
public async Task<ResultModel<List<StockfreezeViewModel>>> GetAllAsync()
{
var data = await _stockfreezeService.GetAllAsync(CurrentUser);
if (data.Any())
{
return ResultModel<List<StockfreezeViewModel>>.Success(data);
}
else
{
return ResultModel<List<StockfreezeViewModel>>.Success(new List<StockfreezeViewModel>());
}
}

/// <summary>
/// get a record by id
/// </summary>
/// <returns>args</returns>
[HttpGet]
public async Task<ResultModel<StockfreezeViewModel>> GetAsync(int id)
{
var data = await _stockfreezeService.GetAsync(id);
if (data!=null)
{
return ResultModel<StockfreezeViewModel>.Success(data);
}
else
{
return ResultModel<StockfreezeViewModel>.Error(_stringLocalizer["not_exists_entity"]);
}
}
/// <summary>
/// add a new record
/// </summary>
/// <param name="viewModel">args</param>
/// <returns></returns>
[HttpPost]
public async Task<ResultModel<int>> AddAsync(StockfreezeViewModel viewModel)
{
var (id, msg) = await _stockfreezeService.AddAsync(viewModel,CurrentUser);
if (id > 0)
{
return ResultModel<int>.Success(id);
}
else
{
return ResultModel<int>.Error(msg);
}
}

/// <summary>
/// update a record
/// </summary>
/// <param name="viewModel">args</param>
/// <returns></returns>
[HttpPut]
public async Task<ResultModel<bool>> UpdateAsync(StockfreezeViewModel viewModel)
{
var (flag, msg) = await _stockfreezeService.UpdateAsync(viewModel);
if (flag)
{
return ResultModel<bool>.Success(flag);
}
else
{
return ResultModel<bool>.Error(msg, 400, flag);
}
}

/// <summary>
/// delete a record
/// </summary>
/// <param name="id">id</param>
/// <returns></returns>
[HttpDelete]
public async Task<ResultModel<string>> DeleteAsync(int id)
{
var (flag, msg) = await _stockfreezeService.DeleteAsync(id);
if (flag)
{
return ResultModel<string>.Success(msg);
}
else
{
return ResultModel<string>.Error(msg);
}
}
#endregion

}


2.库存调整


库存调整主要是查询,数据来源与仓内加工和盘点

2.1 页面代码

<!-- Warehouse Adjust -->
<template>
<div class="container">
<div>
<!-- Main Content -->
<v-card class="mt-5">
<v-card-text>
<v-window v-model="data.activeTab">
<v-window-item>
<div class="operateArea">
<v-row no-gutters>
<!-- Operate Btn -->
<v-col cols="3" class="col">
<!-- <tooltip-btn icon="mdi-plus" :tooltip-text="$t('system.page.add')" @click="method.add()"></tooltip-btn> -->
<tooltip-btn icon="mdi-refresh" :tooltip-text="$t('system.page.refresh')" @click="method.refresh"></tooltip-btn>
<tooltip-btn icon="mdi-export-variant" :tooltip-text="$t('system.page.export')" @click="method.exportTable"> </tooltip-btn>
</v-col>

<!-- Search Input -->
<v-col cols="9">
<v-row no-gutters @keyup.enter="method.sureSearch">
<v-col cols="4"></v-col>
<v-col cols="4"></v-col>
<v-col cols="4">
<v-text-field
v-model="data.searchForm.job_code"
clearable
hide-details
density="comfortable"
class="searchInput ml-5 mt-1"
:label="$t('wms.warehouseWorking.warehouseAdjust.job_code')"
variant="solo"
>
</v-text-field>
</v-col>
</v-row>
</v-col>
</v-row>
</div>

<!-- Table -->
<div
class="mt-5"
:style="{
height: cardHeight
}"
>
<vxe-table ref="xTable" :column-config="{ minWidth: '100px' }" :data="data.tableData" :height="tableHeight" align="center">
<template #empty>
{{ i18n.global.t('system.page.noData') }}
</template>
<vxe-column type="seq" width="60"></vxe-column>
<vxe-column type="checkbox" width="50"></vxe-column>
<vxe-column field="job_code" width="150px" :title="$t('wms.warehouseWorking.warehouseAdjust.job_code')"></vxe-column>
<vxe-column field="job_type" width="150px" :title="$t('wms.warehouseWorking.warehouseAdjust.job_type')">
<template #default="{ row, column }">
<span>{{ formatAdjustJobType(row[column.property]) }}</span>
</template>
</vxe-column>
<vxe-column field="spu_code" width="150px" :title="$t('base.commodityManagement.spu_code')"></vxe-column>
<vxe-column field="spu_name" width="150px" :title="$t('base.commodityManagement.spu_name')"></vxe-column>
<vxe-column field="sku_code" width="150px" :title="$t('base.commodityManagement.sku_code')"></vxe-column>
<vxe-column field="qty" width="150px" :title="$t('wms.warehouseWorking.warehouseAdjust.qty')"></vxe-column>
<vxe-column field="warehouse_name" width="150px" :title="$t('wms.warehouseWorking.warehouseAdjust.warehouse')"></vxe-column>
<vxe-column field="location_name" width="150px" :title="$t('wms.warehouseWorking.warehouseAdjust.location_name')"></vxe-column>
<!-- <vxe-column field="handler" width="150px" :title="$t('wms.warehouseWorking.warehouseAdjust.handler')"></vxe-column>
<vxe-column
field="handle_time"
width="170px"
:title="$t('wms.warehouseWorking.warehouseAdjust.handle_time')"
></vxe-column> -->
<vxe-column field="creator" :title="$t('wms.warehouseWorking.warehouseAdjust.creator')"></vxe-column>
<vxe-column field="create_time" width="170px" :title="$t('wms.warehouseWorking.warehouseAdjust.create_time')"></vxe-column>
<!-- <vxe-column field="operate" :title="$t('system.page.operate')" width="200" :resizable="false" show-overflow>
<template #default="{ row }">
<tooltip-btn
:flat="true"
icon="mdi-eye-outline"
:tooltip-text="$t('system.page.view')"
@click="method.viewRow(row)"
></tooltip-btn>
<tooltip-btn
:flat="true"
icon="mdi-book-open-outline"
:tooltip-text="$t('wms.warehouseWorking.warehouseAdjust.confirmAdjust')"
:disabled="method.confirmAdjustBtnDisabled(row)"
@click="method.confirmAdjust(row)"
></tooltip-btn>
<tooltip-btn
:flat="true"
icon="mdi-delete-outline"
:tooltip-text="$t('system.page.delete')"
:icon-color="errorColor"
:disabled="method.confirmAdjustBtnDisabled(row)"
@click="method.deleteRow(row)"
></tooltip-btn>
</template>
</vxe-column> -->
</vxe-table>
<custom-pager
:current-page="data.tablePage.pageIndex"
:page-size="data.tablePage.pageSize"
perfect
:total="data.tablePage.total"
:page-sizes="PAGE_SIZE"
:layouts="PAGE_LAYOUT"
@page-change="method.handlePageChange"
>
</custom-pager>
</div>
</v-window-item>
</v-window>
</v-card-text>
</v-card>
<addOrUpdateDialog
:show-dialog="data.showDialog"
:form="data.dialogForm"
:process-type="data.processType"
@close="method.closeDialog"
@saveSuccess="method.saveSuccess"
/>
</div>
</div>
</template>

<script lang="ts" setup>
import { computed, ref, reactive, watch, nextTick, onActivated } from 'vue'
import { VxePagerEvents } from 'vxe-table'
import { computedCardHeight, computedTableHeight } from '@/constant/style'
import { WarehouseAdjustVO } from '@/types/WarehouseWorking/WarehouseAdjust'
import { PAGE_SIZE, PAGE_LAYOUT, DEFAULT_PAGE_SIZE } from '@/constant/vxeTable'
import { hookComponent } from '@/components/system'
import { deleteStockAdjust, getStockAdjustList, getStockAdjustOne, confirmStockAdjust } from '@/api/wms/warehouseAdjust'
import { PROCESS_JOB_COMBINE } from '@/constant/warehouseWorking'
import { DEBOUNCE_TIME } from '@/constant/system'
import { setSearchObject } from '@/utils/common'
import { SearchObject } from '@/types/System/Form'
import { formatAdjustJobType } from '@/utils/format/formatWarehouseWorking'
import tooltipBtn from '@/components/tooltip-btn.vue'
import addOrUpdateDialog from './add-or-update-adjust.vue'
import i18n from '@/languages/i18n'
import customPager from '@/components/custom-pager.vue'
import { exportData } from '@/utils/exportTable'

const xTable = ref()

const data = reactive({
showDialog: false,
processType: PROCESS_JOB_COMBINE,
timer: ref<any>(null),
activeTab: null,
searchForm: {
job_code: ''
},
tableData: ref<WarehouseAdjustVO[]>([]),
dialogForm: {
id: 0,
// job_type: AdjustJobType.TAKE,
job_code: '',
sku_id: 0,
goods_owner_id: 0,
goods_location_id: 0,
qty: 0,
is_update_stock: false,
source_table_id: 0,
spu_code: '',
spu_name: '',
sku_code: '',
warehouse_name: '',
location_name: ''
},
tablePage: reactive({
total: 0,
pageIndex: 1,
pageSize: DEFAULT_PAGE_SIZE,
searchObjects: ref<Array<SearchObject>>([])
})
})

const method = reactive({
// Open a dialog to add
add: () => {
data.dialogForm = {
id: 0,
// job_type: AdjustJobType.TAKE,
job_code: '',
sku_id: 0,
goods_owner_id: 0,
goods_location_id: 0,
qty: 0,
is_update_stock: false,
source_table_id: 0,
spu_code: '',
spu_name: '',
sku_code: '',
warehouse_name: '',
location_name: ''
}
nextTick(() => {
data.showDialog = true
})
},

// After add or update success.
saveSuccess: () => {
method.refresh()
method.closeDialog()
},

// Refresh data
refresh: () => {
method.getStockProcessList()
},

// Shut add or update dialog
closeDialog: () => {
data.showDialog = false
},

getStockProcessList: async () => {
const { data: res } = await getStockAdjustList(data.tablePage)
if (!res.isSuccess) {
hookComponent.$message({
type: 'error',
content: res.errorMessage
})
return
}
data.tableData = res.data.rows
data.tablePage.total = res.data.totals
},

viewRow: async (row: WarehouseAdjustVO) => {
await method.getOne(row.id)
nextTick(() => {
data.showDialog = true
})
},

getOne: async (id: number) => {
const { data: res } = await getStockAdjustOne(id)
if (!res.isSuccess) {
hookComponent.$message({
type: 'error',
content: res.errorMessage
})
return
}

data.dialogForm = res.data
},

deleteRow(row: WarehouseAdjustVO) {
hookComponent.$dialog({
content: i18n.global.t('system.tips.beforeDeleteMessage'),
handleConfirm: async () => {
if (row.id) {
const { data: res } = await deleteStockAdjust(row.id)
if (!res.isSuccess) {
hookComponent.$message({
type: 'error',
content: res.errorMessage
})
return
}

hookComponent.$message({
type: 'success',
content: `${ i18n.global.t('system.page.delete') }${ i18n.global.t('system.tips.success') }`
})
method.refresh()
}
}
})
},

handlePageChange: ref<VxePagerEvents.PageChange>(({ currentPage, pageSize }) => {
data.tablePage.pageIndex = currentPage
data.tablePage.pageSize = pageSize
method.refresh()
}),

exportTable: () => {
const $table = xTable.value
exportData({
table: $table,
filename: i18n.global.t('router.sideBar.warehouseAdjust'),
columnFilterMethod({ column }: any) {
return !['checkbox'].includes(column?.type) && !['operate'].includes(column?.field)
}
})
},

sureSearch: () => {
data.tablePage.searchObjects = setSearchObject(data.searchForm)
method.refresh()
},

// The btn will become disabled when the 'process_status' is false
// confirmAdjustBtnDisabled: (row: WarehouseAdjustVO) => row.is_update_stock === true,

confirmAdjust: async (row: WarehouseAdjustVO) => {
hookComponent.$dialog({
content: i18n.global.t('wms.warehouseWorking.warehouseAdjust.beforeConfirmAdjust'),
handleConfirm: async () => {
if (row.id) {
const { data: res } = await confirmStockAdjust(row.id)
if (!res.isSuccess) {
hookComponent.$message({
type: 'error',
content: res.errorMessage
})
return
}
hookComponent.$message({
type: 'success',
content: `${ i18n.global.t('wms.warehouseWorking.warehouseAdjust.confirmAdjust') }${ i18n.global.t('system.tips.success') }`
})
method.refresh()
}
}
})
}
})

onActivated(() => {
method.refresh()
})

const cardHeight = computed(() => computedCardHeight({ hasTab: false }))
const tableHeight = computed(() => computedTableHeight({ hasTab: false }))

watch(
() => data.searchForm,
() => {
// debounce
if (data.timer) {
clearTimeout(data.timer)
}
data.timer = setTimeout(() => {
data.timer = null
method.sureSearch()
}, DEBOUNCE_TIME)
},
{
deep: true
}
)
</script>

<style scoped lang="less">
.operateArea {
width: 100%;
min-width: 760px;
display: flex;
align-items: center;
border-radius: 10px;
padding: 0 10px;
}

.col {
display: flex;
align-items: center;
}
</style>


<!-- Warehouse Adjust Operate Dialog -->
<template>
<v-dialog v-model="isShow" :width="'30%'" transition="dialog-top-transition" :persistent="true">
<template #default>
<v-card>
<v-toolbar class="" color="white" :title="`${$t('router.sideBar.warehouseAdjust')}`"></v-toolbar>
<v-card-text>
<v-form ref="formRef">
<v-select
v-model="data.form.job_type"
:items="data.combobox.job_type"
item-title="label"
item-value="value"
:rules="data.rules.job_type"
:label="$t('wms.warehouseWorking.warehouseAdjust.job_type')"
variant="outlined"
clearable
:disabled="isAssociatedJobType"
@update:model-value="method.changeJobType"
></v-select>
<v-text-field
v-model="data.form.spu_code"
:label="$t('base.commodityManagement.spu_code')"
:rules="data.rules.spu_code"
variant="outlined"
readonly
clearable
:disabled="isAssociatedJobType"
@click="method.openCommoditySelect"
@click:clear="method.clearCommodity"
></v-text-field>
<v-text-field
v-model="data.form.spu_name"
:label="$t('base.commodityManagement.spu_name')"
:rules="data.rules.spu_name"
variant="outlined"
disabled
></v-text-field>
<v-text-field
v-model="data.form.sku_code"
:label="$t('base.commodityManagement.sku_code')"
:rules="data.rules.sku_code"
variant="outlined"
disabled
></v-text-field>
<v-text-field
v-model="data.form.warehouse_name"
:label="$t('wms.warehouseWorking.warehouseAdjust.warehouse')"
:rules="data.rules.warehouse_name"
variant="outlined"
disabled
></v-text-field>
<v-text-field
v-model="data.form.location_name"
:label="$t('wms.warehouseWorking.warehouseAdjust.location_name')"
:rules="data.rules.location_name"
variant="outlined"
disabled
></v-text-field>
<v-text-field
v-model="data.form.qty"
:label="$t('wms.warehouseWorking.warehouseAdjust.qty')"
:rules="data.rules.qty"
variant="outlined"
></v-text-field>
</v-form>
</v-card-text>
<v-card-actions class="justify-end">
<v-btn variant="text" @click="method.closeDialog">{{ $t('system.page.close') }}</v-btn>
<v-btn color="primary" variant="text" :disabled="operateDisabled" @click="method.submit">{{ $t('system.page.submit') }}</v-btn>
</v-card-actions>
</v-card>
</template>
</v-dialog>

<commodity-select
:show-dialog="data.showCommodityDialogSelect"
@close="method.closeCommodityDialogSelect"
@sureSelect="method.sureSelectCommodity"
/>
</template>

<script lang="ts" setup>
import { reactive, computed, ref, watch } from 'vue'
import i18n from '@/languages/i18n'
import { hookComponent } from '@/components/system/index'
import { addStockAdjust } from '@/api/wms/warehouseAdjust'
import { WarehouseAdjustVO, AdjustJobType } from '@/types/WarehouseWorking/WarehouseAdjust'
import { removeObjectNull } from '@/utils/common'
import commoditySelect from '@/components/select/commodity-select.vue'
import { IsInteger } from '@/utils/dataVerification/formRule'

const formRef = ref()
const emit = defineEmits(['close', 'saveSuccess'])
const isUpdate = computed(() => props.form.id && props.form.id > 0)
const operateDisabled = computed(() => !!isUpdate.value)

const props = defineProps<{
showDialog: boolean
form: WarehouseAdjustVO
// If 'associatedJobType' has value, the 'job_type' and 'sku' should be disabled.
associatedJobType?: AdjustJobType
}>()

const isShow = computed(() => props.showDialog)
const isAssociatedJobType = computed(() => !!(props.associatedJobType && props.associatedJobType > 0))

const data = reactive({
showCommodityDialogSelect: false,
showLocationDialogSelect: false,

form: ref<WarehouseAdjustVO>({
id: 0,
// job_type: AdjustJobType.UNKNOW,
job_code: '',
sku_id: 0,
goods_owner_id: 0,
goods_location_id: 0,
qty: 0,
is_update_stock: false,
source_table_id: 0,
spu_code: '',
spu_name: '',
sku_code: '',
warehouse_name: '',
location_name: '',
creator: '',
create_time: ''
}),
rules: {
job_type: [],
qty: [
(val: string) => !!val || `${ i18n.global.t('system.checkText.mustInput') }${ i18n.global.t('wms.warehouseWorking.warehouseAdjust.qty') }!`,
(val: number) => IsInteger(val, 'greaterThanZero') === '' || IsInteger(val, 'greaterThanZero')
],
warehouse_name: [
(val: string) => !!val || `${ i18n.global.t('system.checkText.mustInput') }${ i18n.global.t('wms.warehouseWorking.warehouseAdjust.warehouse') }!`
],
location_name: [
(val: string) => !!val || `${ i18n.global.t('system.checkText.mustInput') }${ i18n.global.t('wms.warehouseWorking.warehouseAdjust.location_name') }!`
],
spu_code: [(val: string) => !!val || `${ i18n.global.t('system.checkText.mustInput') }${ i18n.global.t('base.commodityManagement.spu_code') }!`],
spu_name: [(val: string) => !!val || `${ i18n.global.t('system.checkText.mustInput') }${ i18n.global.t('base.commodityManagement.spu_name') }!`],
sku_code: [(val: string) => !!val || `${ i18n.global.t('system.checkText.mustInput') }${ i18n.global.t('base.commodityManagement.sku_code') }!`],
sku_name: [(val: string) => !!val || `${ i18n.global.t('system.checkText.mustInput') }${ i18n.global.t('base.commodityManagement.sku_name') }!`]
},
combobox: ref<{
job_type: {
label: string
value: AdjustJobType
}[]
}>({
job_type: [
{
label: i18n.global.t('wms.warehouseWorking.warehouseAdjust.warehouseTake'),
value: AdjustJobType.TAKE
},
{
label: i18n.global.t('wms.warehouseWorking.warehouseAdjust.processCombine'),
value: AdjustJobType.PROCESS_COMBINE
},
{
label: i18n.global.t('wms.warehouseWorking.warehouseAdjust.processSplit'),
value: AdjustJobType.PROCESS_SPLIT
},
{
label: i18n.global.t('wms.warehouseWorking.warehouseAdjust.warehouseMove'),
value: AdjustJobType.MOVE
}
]
})
})

const method = reactive({
closeDialog: () => {
emit('close')
},

initForm: () => {
data.form = props.form

if (isAssociatedJobType.value) {
data.form.job_type = props.associatedJobType
}
},

changeJobType: (value: any) => {
// Find the ID corresponding value
const jobType = data.combobox.job_type.find((item) => item.value === value)
if (jobType) {
data.form.job_type = jobType.value
}
},

openCommoditySelect: () => {
// Open select modal after UI rendered.
setTimeout(() => {
data.showCommodityDialogSelect = true
}, 100)
},

closeCommodityDialogSelect: () => {
data.showCommodityDialogSelect = false
},

sureSelectCommodity: (selectRecords: any) => {
try {
data.form.sku_id = selectRecords[0].sku_id
data.form.goods_owner_id = selectRecords[0].goods_owner_id
data.form.goods_location_id = selectRecords[0].goods_location_id
data.form.warehouse_name = selectRecords[0].warehouse
data.form.location_name = selectRecords[0].location_name
data.form.spu_code = selectRecords[0].spu_code
data.form.spu_name = selectRecords[0].spu_name
data.form.sku_code = selectRecords[0].sku_code
} catch (error) {
// console.error(error)
}
},

clearCommodity: () => {
data.form.sku_id = 0
data.form.goods_location_id = 0
data.form.warehouse_name = ''
data.form.location_name = ''
data.form.spu_code = ''
data.form.spu_name = ''
data.form.sku_code = ''
},

submit: async () => {
const { valid } = await formRef.value.validate()

const form = method.constructFormBody()

if (valid) {
const { data: res } = await addStockAdjust(form)
if (!res.isSuccess) {
hookComponent.$message({
type: 'error',
content: res.errorMessage
})
return
}
hookComponent.$message({
type: 'success',
content: `${ i18n.global.t('system.page.submit') }${ i18n.global.t('system.tips.success') }`
})
emit('saveSuccess')
} else {
hookComponent.$message({
type: 'error',
content: i18n.global.t('system.checkText.checkFormFail')
})
}
},

constructFormBody: () => {
let form = { ...data.form }
form = removeObjectNull(form)

return form
}
})

watch(
() => isShow.value,
(val) => {
if (val) {
method.initForm()
}
}
)
</script>

<style scoped lang="less">
.v-form {
div {
margin-bottom: 7px;
}
}
</style>


2.2 接口代码

[Route("stockadjust")]
[ApiController]
[ApiExplorerSettings(GroupName = "WMS")]
public class StockadjustController : BaseController
{
#region Args

/// <summary>
/// stockadjust Service
/// </summary>
private readonly IStockadjustService _stockadjustService;

/// <summary>
/// Localizer Service
/// </summary>
private readonly IStringLocalizer<ModernWMS.Core.MultiLanguage> _stringLocalizer;
#endregion

#region constructor
/// <summary>
/// constructor
/// </summary>
/// <param name="stockadjustService">stockadjust Service</param>
/// <param name="stringLocalizer">Localizer</param>
public StockadjustController(
IStockadjustService stockadjustService
, IStringLocalizer<ModernWMS.Core.MultiLanguage> stringLocalizer
)
{
this._stockadjustService = stockadjustService;
this._stringLocalizer= stringLocalizer;
}
#endregion

#region Api
/// <summary>
/// page search
/// </summary>
/// <param name="pageSearch">args</param>
/// <returns></returns>
[HttpPost("list")]
public async Task<ResultModel<PageData<StockadjustViewModel>>> PageAsync(PageSearch pageSearch)
{
var (data, totals) = await _stockadjustService.PageAsync(pageSearch, CurrentUser);

return ResultModel<PageData<StockadjustViewModel>>.Success(new PageData<StockadjustViewModel>
{
Rows = data,
Totals = totals
});
}

#endregion

}


3.库存盘点


3.1 页面代码

<!-- Warehouse Taking -->
<template>
<div class="container">
<div>
<!-- Main Content -->
<v-card class="mt-5">
<v-card-text>
<v-window v-model="data.activeTab">
<v-window-item>
<div class="operateArea">
<v-row no-gutters>
<!-- Operate Btn -->
<v-col cols="3" class="col">
<tooltip-btn icon="mdi-plus" :tooltip-text="$t('system.page.add')" @click="method.add()"></tooltip-btn>
<tooltip-btn icon="mdi-refresh" :tooltip-text="$t('system.page.refresh')" @click="method.refresh"></tooltip-btn>
<tooltip-btn icon="mdi-export-variant" :tooltip-text="$t('system.page.export')" @click="method.exportTable"> </tooltip-btn>
</v-col>

<!-- Search Input -->
<v-col cols="9">
<v-row no-gutters @keyup.enter="method.sureSearch">
<v-col cols="4"></v-col>
<v-col cols="4"></v-col>
<v-col cols="4">
<v-text-field
v-model="data.searchForm.job_code"
clearable
hide-details
density="comfortable"
class="searchInput ml-5 mt-1"
:label="$t('wms.warehouseWorking.warehouseTaking.job_code')"
variant="solo"
>
</v-text-field>
</v-col>
</v-row>
</v-col>
</v-row>
</div>

<!-- Table -->
<div
class="mt-5"
:style="{
height: cardHeight
}"
>
<vxe-table ref="xTable" :column-config="{ minWidth: '100px' }" :data="data.tableData" :height="tableHeight" align="center">
<template #empty>
{{ i18n.global.t('system.page.noData') }}
</template>
<vxe-column type="seq" width="60"></vxe-column>
<vxe-column type="checkbox" width="50"></vxe-column>
<vxe-column field="job_code" width="150px" :title="$t('wms.warehouseWorking.warehouseTaking.job_code')"></vxe-column>
<vxe-column field="job_status" width="150px" :title="$t('wms.warehouseWorking.warehouseTaking.job_status')">
<template #default="{ row, column }">
<span>{{ formatTakingJobStatus(row[column.property]) }}</span>
</template>
</vxe-column>
<vxe-column field="spu_code" width="150px" :title="$t('base.commodityManagement.spu_code')"></vxe-column>
<vxe-column field="spu_name" width="150px" :title="$t('base.commodityManagement.spu_name')"></vxe-column>
<vxe-column field="sku_code" width="150px" :title="$t('base.commodityManagement.sku_code')"></vxe-column>
<vxe-column field="warehouse_name" width="150px" :title="$t('wms.warehouseWorking.warehouseTaking.warehouse')"></vxe-column>
<vxe-column field="location_name" width="150px" :title="$t('wms.warehouseWorking.warehouseTaking.location_name')"></vxe-column>
<vxe-column field="book_qty" width="150px" :title="$t('wms.warehouseWorking.warehouseTaking.book_qty')"></vxe-column>
<vxe-column field="counted_qty" width="150px" :title="$t('wms.warehouseWorking.warehouseTaking.counted_qty')"></vxe-column>
<vxe-column field="difference_qty" width="150px" :title="$t('wms.warehouseWorking.warehouseTaking.difference_qty')"></vxe-column>
<vxe-column field="handler" width="150px" :title="$t('wms.warehouseWorking.warehouseTaking.handler')"></vxe-column>
<vxe-column field="handle_time" width="170px" :title="$t('wms.warehouseWorking.warehouseTaking.handle_time')">
<template #default="{ row, column }">
<span>{{ formatDate(row[column.property]) }}</span>
</template>
</vxe-column>
<vxe-column field="creator" :title="$t('wms.warehouseWorking.warehouseTaking.creator')"></vxe-column>
<vxe-column field="create_time" width="170px" :title="$t('wms.warehouseWorking.warehouseTaking.create_time')"></vxe-column>
<vxe-column field="operate" :title="$t('system.page.operate')" width="250" :resizable="false" show-overflow>
<template #default="{ row }">
<tooltip-btn
:flat="true"
icon="mdi-eye-outline"
:tooltip-text="$t('system.page.view')"
@click="method.viewRow(row)"
></tooltip-btn>
<tooltip-btn
:flat="true"
icon="mdi-book-check-outline"
:tooltip-text="$t('wms.warehouseWorking.warehouseTaking.confirmTaking')"
:disabled="method.isConfirmTaking(row)"
@click="method.confirmTaking(row)"
></tooltip-btn>
<tooltip-btn
:flat="true"
icon="mdi-book-open-outline"
:tooltip-text="$t('wms.warehouseWorking.warehouseProcessing.confirmAdjust')"
:disabled="method.confirmAdjustBtnDisabled(row)"
@click="method.confirmAdjust(row)"
></tooltip-btn>
<tooltip-btn
:flat="true"
icon="mdi-delete-outline"
:tooltip-text="$t('system.page.delete')"
:icon-color="errorColor"
:disabled="method.isConfirmTaking(row)"
@click="method.deleteRow(row)"
></tooltip-btn>
</template>
</vxe-column>
</vxe-table>
<custom-pager
:current-page="data.tablePage.pageIndex"
:page-size="data.tablePage.pageSize"
perfect
:total="data.tablePage.total"
:page-sizes="PAGE_SIZE"
:layouts="PAGE_LAYOUT"
@page-change="method.handlePageChange"
>
</custom-pager>
</div>
</v-window-item>
</v-window>
</v-card-text>
</v-card>
<addOrUpdateDialog :show-dialog="data.showDialog" :form="data.dialogForm" @close="method.closeDialog" @saveSuccess="method.saveSuccess" />
<number-input
:show-dialog="data.showDialogNumberInput"
:form="data.dialogForm"
@close="method.closeDialogNumberInput"
@saveSuccess="method.saveSuccessNumberInput"
/>
</div>
</div>
</template>

<script lang="ts" setup>
import { computed, ref, reactive, onActivated, watch, nextTick } from 'vue'
import { VxePagerEvents } from 'vxe-table'
import { computedCardHeight, computedTableHeight, errorColor } from '@/constant/style'
import { WarehouseTakingVO } from '@/types/WarehouseWorking/WarehouseTaking'
import { PAGE_SIZE, PAGE_LAYOUT, DEFAULT_PAGE_SIZE } from '@/constant/vxeTable'
import { hookComponent } from '@/components/system'
import { TAKING_JOB_FINISH, TAKING_JOB_UNFINISH } from '@/constant/warehouseWorking'
import { deleteStockTaking, getStockTakingList, getStockTakingOne, confirmAdjustment } from '@/api/wms/warehouseTaking'
import { DEBOUNCE_TIME } from '@/constant/system'
import { setSearchObject } from '@/utils/common'
import { SearchObject } from '@/types/System/Form'
import { formatTakingJobStatus } from '@/utils/format/formatWarehouseWorking'
import { formatDate } from '@/utils/format/formatSystem'
import tooltipBtn from '@/components/tooltip-btn.vue'
import addOrUpdateDialog from './add-or-update-taking.vue'
import numberInput from './number-input.vue'
import i18n from '@/languages/i18n'
import customPager from '@/components/custom-pager.vue'
import { exportData } from '@/utils/exportTable'

const xTable = ref()

const data = reactive({
showDialog: false,
showDialogNumberInput: false,
timer: ref<any>(null),
activeTab: null,
tableData: ref<WarehouseTakingVO[]>([]),
dialogForm: ref<WarehouseTakingVO>({
id: 0,
job_code: '',
job_status: TAKING_JOB_FINISH,
sku_id: 0,
goods_owner_id: 0,
goods_location_id: 0,
book_qty: 0,
counted_qty: 0,
difference_qty: 0,
spu_code: '',
spu_name: '',
sku_code: '',
warehouse_name: '',
location_name: '',
handler: '',
handle_time: '',
adjust_status: false,
creator: '',
create_time: ''
}),
searchForm: {
job_code: ''
},
tablePage: reactive({
total: 0,
pageIndex: 1,
pageSize: DEFAULT_PAGE_SIZE,
searchObjects: ref<Array<SearchObject>>([])
})
})

const method = reactive({
// Open a dialog to add
add: () => {
data.dialogForm = {
id: 0,
job_code: '',
job_status: TAKING_JOB_FINISH,
sku_id: 0,
goods_owner_id: 0,
goods_location_id: 0,
book_qty: 0,
counted_qty: 0,
difference_qty: 0,
spu_code: '',
spu_name: '',
sku_code: '',
warehouse_name: '',
location_name: '',
handler: '',
handle_time: '',
adjust_status: false,
creator: '',
create_time: ''
}
nextTick(() => {
data.showDialog = true
})
},

// After add or update success.
saveSuccess: () => {
method.refresh()
method.closeDialog()
},

// After confirm taking.
saveSuccessNumberInput: () => {
method.refresh()
method.closeDialogNumberInput()
},

// Refresh data
refresh: () => {
method.getStockProcessList()
},

// Shut add or update dialog
closeDialog: () => {
data.showDialog = false
},

// Shut number input dialog
closeDialogNumberInput: () => {
data.showDialogNumberInput = false
},

getStockProcessList: async () => {
const { data: res } = await getStockTakingList(data.tablePage)
if (!res.isSuccess) {
hookComponent.$message({
type: 'error',
content: res.errorMessage
})
return
}
data.tableData = res.data.rows
data.tablePage.total = res.data.totals
},

viewRow: async (row: WarehouseTakingVO) => {
await method.getOne(row.id)
nextTick(() => {
data.showDialog = true
})
},

getOne: async (id: number) => {
const { data: res } = await getStockTakingOne(id)
if (!res.isSuccess) {
hookComponent.$message({
type: 'error',
content: res.errorMessage
})
return
}

data.dialogForm = res.data
},

deleteRow(row: WarehouseTakingVO) {
hookComponent.$dialog({
content: i18n.global.t('system.tips.beforeDeleteMessage'),
handleConfirm: async () => {
if (row.id) {
const { data: res } = await deleteStockTaking(row.id)
if (!res.isSuccess) {
hookComponent.$message({
type: 'error',
content: res.errorMessage
})
return
}

hookComponent.$message({
type: 'success',
content: `${ i18n.global.t('system.page.delete') }${ i18n.global.t('system.tips.success') }`
})
method.refresh()
}
}
})
},

handlePageChange: ref<VxePagerEvents.PageChange>(({ currentPage, pageSize }) => {
data.tablePage.pageIndex = currentPage
data.tablePage.pageSize = pageSize
method.refresh()
}),

exportTable: () => {
const $table = xTable.value
exportData({
table: $table,
filename: i18n.global.t('router.sideBar.warehouseTaking'),
columnFilterMethod({ column }: any) {
return !['checkbox'].includes(column?.type) && !['operate'].includes(column?.field)
}
})
},

sureSearch: () => {
data.tablePage.searchObjects = setSearchObject(data.searchForm)
method.refresh()
},

// The btn will become disabled when the 'job_status' is false
confirmAdjustBtnDisabled: (row: WarehouseTakingVO) => row.job_status === TAKING_JOB_UNFINISH || !!row.adjust_status,

// The btn will become disabled when the 'job_status' is true
isConfirmTaking: (row: WarehouseTakingVO) => row.job_status === TAKING_JOB_FINISH,

confirmTaking: async (row: WarehouseTakingVO) => {
data.dialogForm = row
nextTick(() => {
data.showDialogNumberInput = true
})
},

confirmAdjust: async (row: WarehouseTakingVO) => {
hookComponent.$dialog({
content: i18n.global.t('wms.warehouseWorking.warehouseTaking.beforeConfirmAdjust'),
handleConfirm: async () => {
if (row.id) {
const { data: res } = await confirmAdjustment(row.id)
if (!res.isSuccess) {
hookComponent.$message({
type: 'error',
content: res.errorMessage
})
return
}
hookComponent.$message({
type: 'success',
content: `${ i18n.global.t('wms.warehouseWorking.warehouseTaking.confirmAdjust') }${ i18n.global.t('system.tips.success') }`
})
method.refresh()
}
}
})
}
})

onActivated(() => {
method.refresh()
})

const cardHeight = computed(() => computedCardHeight({ hasTab: false }))
const tableHeight = computed(() => computedTableHeight({ hasTab: false }))

watch(
() => data.searchForm,
() => {
// debounce
if (data.timer) {
clearTimeout(data.timer)
}
data.timer = setTimeout(() => {
data.timer = null
method.sureSearch()
}, DEBOUNCE_TIME)
},
{
deep: true
}
)
</script>

<style scoped lang="less">
.operateArea {
width: 100%;
min-width: 760px;
display: flex;
align-items: center;
border-radius: 10px;
padding: 0 10px;
}

.col {
display: flex;
align-items: center;
}
</style>


<!-- Warehouse Taking Operate Dialog -->
<template>
<v-dialog v-model="isShow" :width="'30%'" transition="dialog-top-transition" :persistent="true">
<template #default>
<v-card>
<v-toolbar color="white" :title="`${$t('router.sideBar.warehouseTaking')}`"></v-toolbar>
<v-card-text>
<div class="header">
<div class="headerBtn">
<tooltip-btn
size="x-small"
icon="mdi-home-plus-outline"
:tooltip-text="$t('wms.warehouseWorking.warehouseTaking.addFromStock')"
@click="method.openCommoditySelect()"
></tooltip-btn>
<tooltip-btn
size="x-small"
icon="mdi-store-plus-outline"
:tooltip-text="$t('wms.warehouseWorking.warehouseTaking.addFromCommodity')"
:disabled="isFromStock"
@click="method.openSkuSelect()"
></tooltip-btn>
</div>
<div class="headerTips">
<v-tooltip :text="$t('wms.warehouseWorking.warehouseTaking.addTips')">
<template #activator="{ props }">
<v-btn v-bind="props" size="x-small" variant="text" icon="mdi-help-circle" color="blue-lighten-2"></v-btn>
</template>
</v-tooltip>
</div>
</div>
<v-form ref="formRef">
<v-text-field
v-model="data.form.spu_code"
:label="$t('base.commodityManagement.spu_code')"
:rules="data.rules.spu_code"
variant="outlined"
readonly
clearable
@click:clear="method.clearCommodity"
></v-text-field>
<v-text-field
v-model="data.form.spu_name"
:label="$t('base.commodityManagement.spu_name')"
:rules="data.rules.spu_name"
variant="outlined"
disabled
></v-text-field>
<v-text-field
v-model="data.form.sku_code"
:label="$t('base.commodityManagement.sku_code')"
:rules="data.rules.sku_code"
variant="outlined"
disabled
></v-text-field>
<v-text-field
v-model="data.form.warehouse_name"
:label="$t('wms.warehouseWorking.warehouseTaking.warehouse')"
:rules="data.rules.warehouse_name"
variant="outlined"
disabled
></v-text-field>
<v-text-field
v-model="data.form.location_name"
:label="$t('wms.warehouseWorking.warehouseTaking.location_name')"
:rules="data.rules.location_name"
variant="outlined"
:disabled="isFromStock"
@click="method.openLocationSelect"
@click:clear="method.clearLocation"
></v-text-field>
<v-text-field
v-model="data.form.book_qty"
:label="$t('wms.warehouseWorking.warehouseTaking.book_qty')"
:rules="data.rules.book_qty"
variant="outlined"
disabled
></v-text-field>
</v-form>
</v-card-text>
<v-card-actions class="justify-end">
<v-btn variant="text" @click="method.closeDialog">{{ $t('system.page.close') }}</v-btn>
<v-btn color="primary" variant="text" :disabled="operateDisabled" @click="method.submit">{{ $t('system.page.submit') }}</v-btn>
</v-card-actions>
</v-card>
</template>
</v-dialog>

<commodity-select
:show-dialog="data.showCommodityDialogSelect"
@close="method.closeCommodityDialogSelect"
@sureSelect="method.sureSelectCommodity"
/>
<sku-select :show-dialog="data.showSkuDialogSelect" @close="method.closeDialogSelectSku" @sureSelect="method.sureSelectSku" />
<location-select :show-dialog="data.showLocationDialogSelect" @close="method.closeDialogSelectLocation" @sureSelect="method.sureSelectLocation" />
</template>

<script lang="ts" setup>
import { reactive, computed, ref, watch } from 'vue'
import { hookComponent } from '@/components/system/index'
import { addStockTaking } from '@/api/wms/warehouseTaking'
import { WarehouseTakingVO } from '@/types/WarehouseWorking/WarehouseTaking'
import { removeObjectNull } from '@/utils/common'
import { TAKING_JOB_FINISH } from '@/constant/warehouseWorking'
import i18n from '@/languages/i18n'
import commoditySelect from '@/components/select/commodity-select.vue'
import tooltipBtn from '@/components/tooltip-btn.vue'
import locationSelect from '@/components/select/location-select.vue'
import skuSelect from '@/components/select/sku-select.vue'

const formRef = ref()
const emit = defineEmits(['close', 'saveSuccess'])
const isUpdate = computed(() => props.form.id && props.form.id > 0)
const operateDisabled = computed(() => !!isUpdate.value)
const isFromStock = computed(() => data.curStockID > 0)

const props = defineProps<{
showDialog: boolean
form: WarehouseTakingVO
}>()

const isShow = computed(() => props.showDialog)

const data = reactive({
showCommodityDialogSelect: false,
showLocationDialogSelect: false,
showSkuDialogSelect: false,

// There has value when choose from stock.
curStockID: 0,

form: ref<WarehouseTakingVO>({
id: 0,
job_code: '',
job_status: TAKING_JOB_FINISH,
sku_id: 0,
goods_owner_id: 0,
goods_location_id: 0,
book_qty: 0,
counted_qty: 0,
difference_qty: 0,
spu_code: '',
spu_name: '',
sku_code: '',
warehouse_name: '',
location_name: '',
handler: '',
handle_time: '',
adjust_status: false,
creator: '',
create_time: ''
}),
rules: {
job_type: [],
book_qty: [],
warehouse_name: [
(val: string) => !!val || `${ i18n.global.t('system.checkText.mustInput') }${ i18n.global.t('wms.warehouseWorking.warehouseTaking.warehouse') }!`
],
location_name: [
(val: string) => !!val || `${ i18n.global.t('system.checkText.mustInput') }${ i18n.global.t('wms.warehouseWorking.warehouseTaking.location_name') }!`
],
spu_code: [(val: string) => !!val || `${ i18n.global.t('system.checkText.mustInput') }${ i18n.global.t('base.commodityManagement.spu_code') }!`],
spu_name: [(val: string) => !!val || `${ i18n.global.t('system.checkText.mustInput') }${ i18n.global.t('base.commodityManagement.spu_name') }!`],
sku_code: [(val: string) => !!val || `${ i18n.global.t('system.checkText.mustInput') }${ i18n.global.t('base.commodityManagement.sku_code') }!`],
sku_name: [(val: string) => !!val || `${ i18n.global.t('system.checkText.mustInput') }${ i18n.global.t('base.commodityManagement.sku_name') }!`]
}
})

const method = reactive({
closeDialog: () => {
emit('close')
},

initForm: () => {
data.form = props.form
},

openCommoditySelect: () => {
data.showCommodityDialogSelect = true
},

closeCommodityDialogSelect: () => {
data.showCommodityDialogSelect = false
},

sureSelectCommodity: (selectRecords: any) => {
try {
data.curStockID = selectRecords[0].id
data.form.sku_id = selectRecords[0].sku_id
data.form.goods_owner_id = selectRecords[0].goods_owner_id
data.form.goods_location_id = selectRecords[0].goods_location_id
data.form.warehouse_name = selectRecords[0].warehouse_name
data.form.location_name = selectRecords[0].location_name
data.form.spu_code = selectRecords[0].spu_code
data.form.spu_name = selectRecords[0].spu_name
data.form.sku_code = selectRecords[0].sku_code
data.form.book_qty = selectRecords[0].qty_available
} catch (error) {
// console.error(error)
}
},

clearCommodity: () => {
data.curStockID = 0
data.form.sku_id = 0
data.form.goods_location_id = 0
data.form.warehouse_name = ''
data.form.location_name = ''
data.form.spu_code = ''
data.form.spu_name = ''
data.form.sku_code = ''
data.form.book_qty = 0
},

openSkuSelect: () => {
data.showSkuDialogSelect = true
},

closeDialogSelectSku: () => {
data.showSkuDialogSelect = false
},

sureSelectSku: (selectRecords: any) => {
if (selectRecords.length > 0) {
data.form.sku_id = selectRecords[0].sku_id
data.form.goods_owner_id = selectRecords[0].goods_owner_id
data.form.goods_location_id = selectRecords[0].goods_location_id
// The book qty should zero when the data from commodity.
data.form.book_qty = 0
data.form.spu_code = selectRecords[0].spu_code
data.form.spu_name = selectRecords[0].spu_name
data.form.sku_code = selectRecords[0].sku_code
}
},

openLocationSelect: () => {
data.showLocationDialogSelect = true
},

closeDialogSelectLocation: () => {
data.showLocationDialogSelect = false
},

sureSelectLocation: (selectRecords: any) => {
if (selectRecords.length > 0) {
data.form.goods_location_id = selectRecords[0].id
data.form.warehouse_name = selectRecords[0].warehouse_name
data.form.location_name = selectRecords[0].location_name
}
},

clearLocation: () => {
data.form.goods_location_id = 0
data.form.warehouse_name = ''
data.form.location_name = ''
},

submit: async () => {
const { valid } = await formRef.value.validate()

const form = method.constructFormBody()

if (valid) {
const { data: res } = await addStockTaking(form)
if (!res.isSuccess) {
hookComponent.$message({
type: 'error',
content: res.errorMessage
})
return
}
hookComponent.$message({
type: 'success',
content: `${ i18n.global.t('system.page.submit') }${ i18n.global.t('system.tips.success') }`
})
emit('saveSuccess')
} else {
hookComponent.$message({
type: 'error',
content: i18n.global.t('system.checkText.checkFormFail')
})
}
},

constructFormBody: () => {
let form = { ...data.form }
form = removeObjectNull(form)

return form
}
})

watch(
() => isShow.value,
(val) => {
if (val) {
method.initForm()
}
}
)
</script>

<style scoped lang="less">
.v-form {
div {
margin-bottom: 7px;
}
}

.header {
display: flex;
margin-bottom: 15px;
justify-content: space-between;
}

.headerBtn {
}
.headerTips {
display: flex;
align-items: center;
}
</style>


<!-- Warehouse Taking Number Input -->
<template>
<v-dialog v-model="isShow" :width="'20%'" transition="dialog-top-transition" :persistent="true">
<template #default>
<v-card>
<v-toolbar color="white" :title="`${$t('router.sideBar.warehouseTaking')}`"></v-toolbar>
<v-card-text>
<p class="mb-4" style="color: #999999">{{ $t('wms.warehouseWorking.warehouseTaking.book_qty') }}:{{ data.form.book_qty }}</p>
<v-form ref="formRef">
<v-text-field
v-model="data.form.counted_qty"
:label="$t('wms.warehouseWorking.warehouseTaking.counted_qty')"
:rules="data.rules.counted_qty"
variant="outlined"
clearable
></v-text-field>
</v-form>
</v-card-text>
<v-card-actions class="justify-end">
<v-btn variant="text" @click="method.closeDialog">{{ $t('system.page.close') }}</v-btn>
<v-btn color="primary" variant="text" @click="method.submit">{{ $t('system.page.submit') }}</v-btn>
</v-card-actions>
</v-card>
</template>
</v-dialog>
</template>

<script lang="ts" setup>
import { reactive, computed, ref, watch } from 'vue'
import { hookComponent } from '@/components/system/index'
import { confirmStockTaking } from '@/api/wms/warehouseTaking'
import { WarehouseTakingVO } from '@/types/WarehouseWorking/WarehouseTaking'
import { removeObjectNull } from '@/utils/common'
import { TAKING_JOB_FINISH } from '@/constant/warehouseWorking'
import i18n from '@/languages/i18n'
import { IsInteger } from '@/utils/dataVerification/formRule'

const formRef = ref()
const emit = defineEmits(['close', 'saveSuccess'])

const props = defineProps<{
showDialog: boolean
form: WarehouseTakingVO
}>()

const isShow = computed(() => props.showDialog)

const data = reactive({
showCommodityDialogSelect: false,
showLocationDialogSelect: false,

form: ref<WarehouseTakingVO>({
id: 0,
job_code: '',
job_status: TAKING_JOB_FINISH,
sku_id: 0,
goods_owner_id: 0,
goods_location_id: 0,
book_qty: 0,
counted_qty: 0,
difference_qty: 0,
spu_code: '',
spu_name: '',
sku_code: '',
warehouse_name: '',
location_name: '',
adjust_status: false,
handler: '',
handle_time: ''
}),
rules: {
book_qty: [
(val: string) => !!val || `${ i18n.global.t('system.checkText.mustInput') }${ i18n.global.t('wms.warehouseWorking.warehouseTaking.book_qty') }!`
],
counted_qty: [
(val: string) => !!val || `${ i18n.global.t('system.checkText.mustInput') }${ i18n.global.t('wms.warehouseWorking.warehouseTaking.counted_qty') }!`,
(val: number) => IsInteger(val, 'nonNegative') === '' || IsInteger(val, 'nonNegative')
]
}
})

const method = reactive({
closeDialog: () => {
emit('close')
},

initForm: () => {
data.form = props.form
},

submit: async () => {
const { valid } = await formRef.value.validate()

const form = method.constructFormBody()

if (valid) {
const { data: res } = await confirmStockTaking(form)
if (!res.isSuccess) {
hookComponent.$message({
type: 'error',
content: res.errorMessage
})
return
}
hookComponent.$message({
type: 'success',
content: `${ i18n.global.t('system.page.submit') }${ i18n.global.t('system.tips.success') }`
})
emit('saveSuccess')
} else {
hookComponent.$message({
type: 'error',
content: i18n.global.t('system.checkText.checkFormFail')
})
}
},

constructFormBody: () => {
let form = { ...data.form }
form = removeObjectNull(form)

return form
}
})

watch(
() => isShow.value,
(val) => {
if (val) {
method.initForm()
}
}
)
</script>

<style scoped lang="less">
.v-form {
div {
margin-bottom: 7px;
}
}
</style>


3.2 接口代码

[Route("stocktaking")]
[ApiController]
[ApiExplorerSettings(GroupName = "WMS")]
public class StocktakingController : BaseController
{
#region Args

/// <summary>
/// stocktaking Service
/// </summary>
private readonly IStocktakingService _stocktakingService;

/// <summary>
/// Localizer Service
/// </summary>
private readonly IStringLocalizer<ModernWMS.Core.MultiLanguage> _stringLocalizer;
#endregion

#region constructor
/// <summary>
/// constructor
/// </summary>
/// <param name="stocktakingService">stocktaking Service</param>
/// <param name="stringLocalizer">Localizer</param>
public StocktakingController(
IStocktakingService stocktakingService
, IStringLocalizer<ModernWMS.Core.MultiLanguage> stringLocalizer
)
{
this._stocktakingService = stocktakingService;
this._stringLocalizer= stringLocalizer;
}
#endregion

#region Api
/// <summary>
/// page search
/// </summary>
/// <param name="pageSearch">args</param>
/// <returns></returns>
[HttpPost("list")]
public async Task<ResultModel<PageData<StocktakingViewModel>>> PageAsync(PageSearch pageSearch)
{
var (data, totals) = await _stocktakingService.PageAsync(pageSearch, CurrentUser);

return ResultModel<PageData<StocktakingViewModel>>.Success(new PageData<StocktakingViewModel>
{
Rows = data,
Totals = totals
});
}

/// <summary>
/// get a record by id
/// </summary>
/// <returns>args</returns>
[HttpGet]
public async Task<ResultModel<StocktakingViewModel>> GetAsync(int id)
{
var data = await _stocktakingService.GetAsync(id);
if (data!=null)
{
return ResultModel<StocktakingViewModel>.Success(data);
}
else
{
return ResultModel<StocktakingViewModel>.Error(_stringLocalizer["not_exists_entity"]);
}
}
/// <summary>
/// add a new record
/// </summary>
/// <param name="viewModel">args</param>
/// <returns></returns>
[HttpPost]
public async Task<ResultModel<int>> AddAsync(StocktakingBasicViewModel viewModel)
{
var (id, msg) = await _stocktakingService.AddAsync(viewModel,CurrentUser);
if (id > 0)
{
return ResultModel<int>.Success(id);
}
else
{
return ResultModel<int>.Error(msg);
}
}

/// <summary>
/// Confirm a record
/// </summary>
/// <param name="viewModel">args</param>
/// <returns></returns>
[HttpPut]
public async Task<ResultModel<bool>> PutAsync(StocktakingConfirmViewModel viewModel)
{
var (flag, msg) = await _stocktakingService.PutAsync(viewModel, CurrentUser);
if (flag)
{
return ResultModel<bool>.Success(flag);
}
else
{
return ResultModel<bool>.Error(msg, 400, flag);
}
}

/// <summary>
/// confrim a record and change stock and add to stockadjust
/// </summary>
/// <param name="id">id</param>
/// <returns></returns>
[HttpPut("adjustment-confirm")]
public async Task<ResultModel<bool>> ConfirmAsync(int id)
{
var (flag, msg) = await _stocktakingService.ConfirmAsync(id, CurrentUser);
if (flag)
{
return ResultModel<bool>.Success(flag);
}
else
{
return ResultModel<bool>.Error(msg, 400, flag);
}
}

/// <summary>
/// delete a record
/// </summary>
/// <param name="id">id</param>
/// <returns></returns>
[HttpDelete]
public async Task<ResultModel<string>> DeleteAsync(int id)
{
var (flag, msg) = await _stocktakingService.DeleteAsync(id);
if (flag)
{
return ResultModel<string>.Success(msg);
}
else
{
return ResultModel<string>.Error(msg);
}
}
#endregion

}



【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。