diff --git a/src/api/sampleTracking/sampleTracking.js b/src/api/sampleTracking/sampleTracking.js
index edefdb9..76767cc 100644
--- a/src/api/sampleTracking/sampleTracking.js
+++ b/src/api/sampleTracking/sampleTracking.js
@@ -23,6 +23,9 @@ export const oneKeyUpdateProofTracking = data => createAPI('/sampleTracking/proo
export const createProofTrackingRecord = data => createAPI('/sampleTracking/proofTracking/createProofRecord', 'POST', data)
export const updateProofTrackingProcess = data => createAPI('/sampleTracking/proofTracking/updateProcess', 'POST', data)
export const updateProofTrackingComments = data => createAPI('/sampleTracking/proofTracking/updateComments', 'POST', data)
+export const updateProofTrackingCost = data => createAPI('/sampleTracking/proofTracking/updateCost', 'POST', data)
+export const saveProofTrackingExchangeRate = data => createAPI('/sampleTracking/proofTracking/saveExchangeRate', 'POST', data)
+export const refreshProofTrackingCostByRate = data => createAPI('/sampleTracking/proofTracking/refreshCostByRate', 'POST', data)
export const finishProofTracking = data => createAPI('/sampleTracking/proofTracking/finishProofing', 'POST', data)
export const deleteProofTracking = data => createAPI('/sampleTracking/proofTracking/delete', 'POST', data)
export const deleteProofTrackingBatch = data => createAPI('/sampleTracking/proofTracking/deleteBatch', 'POST', data)
diff --git a/src/views/modules/sampleTracking/projectProofTracking.vue b/src/views/modules/sampleTracking/projectProofTracking.vue
index f106b09..2c3a308 100644
--- a/src/views/modules/sampleTracking/projectProofTracking.vue
+++ b/src/views/modules/sampleTracking/projectProofTracking.vue
@@ -58,6 +58,7 @@
>事业部进度设置
默认进度设置
删除
+ 按汇率重算
@@ -67,6 +68,19 @@
+
+ 汇率
+
+
@@ -298,6 +312,49 @@
v-else-if="item.columnProp === 'deliveryVariance'"
:class="{ 'delivery-variance-alert': isDeliveryVarianceAlert(scope.row) }"
>{{ getTrackingColumnDisplayValue(scope.row, item.columnProp) }}
+
+
+
+
+
+ {{ getCostManualDisplay(scope.row, item.columnProp) }}
+
+ {{ formatCostAmount(getCostCalculatedValue(scope.row, item.columnProp)) }}
{{ getTrackingColumnDisplayValue(scope.row, item.columnProp) }}
@@ -917,7 +974,7 @@
-
+
{
@@ -4387,10 +4585,13 @@ export default {
// 列表会整表换行数据,未提交的工序日期编辑态不能带到新行上。
this.clearProcessDateEditClickTimer()
this.exitProcessDateEdit()
+ this.exitCostEdit()
searchProofTracking(inData).then(({ data }) => {
this.dataListLoading = false
this.resetProcessTooltip()
if (data && data.code === 0) {
+ this.usdCnyExchangeRate = data.usdCnyExchangeRate == null ? null : data.usdCnyExchangeRate
+ this.usdCnyExchangeRateDraft = this.usdCnyExchangeRate == null ? '' : this.formatCostDisplay(this.usdCnyExchangeRate)
this.dataList = (data.page && data.page.list) || []
this.syncDataListProcessValues()
this.resetDataListPageOrderIndexes()
@@ -4766,6 +4967,438 @@ export default {
this.$message.error('Comments保存异常')
})
},
+ isCostManualColumn (columnProp) {
+ return COST_MANUAL_COLUMNS.indexOf(columnProp) > -1
+ },
+ isCostCalculatedColumn (columnProp) {
+ return COST_CALCULATED_COLUMNS.indexOf(columnProp) > -1
+ },
+ isCostEditing (row, columnProp) {
+ return !!row && this.costEditingKey === `${row.trackingId}:${columnProp}`
+ },
+ isCostEditingRow (row) {
+ if (!row || !this.costEditingKey) {
+ return false
+ }
+ return this.costEditingKey.indexOf(`${row.trackingId}:`) === 0
+ },
+ formatCostDisplay (value) {
+ if (value == null || String(value).trim() === '') {
+ return ''
+ }
+ const number = Number(value)
+ if (!Number.isFinite(number)) {
+ return String(value)
+ }
+ return number.toFixed(6).replace(/\.?0+$/, '')
+ },
+ // 费用金额固定两位。汇率仍走 formatCostDisplay,避免把 6.685421 收成 6.69。
+ formatCostAmount (value) {
+ if (value == null || String(value).trim() === '') {
+ return ''
+ }
+ const number = Number(value)
+ if (!Number.isFinite(number)) {
+ return String(value)
+ }
+ return this.roundCost(number).toFixed(2)
+ },
+ roundCost (value) {
+ const number = Number(value)
+ if (!Number.isFinite(number)) {
+ return null
+ }
+ return Math.round((number + Number.EPSILON) * 100) / 100
+ },
+ toCostNumber (value) {
+ if (value == null || String(value).trim() === '') {
+ return null
+ }
+ const number = Number(value)
+ return Number.isFinite(number) ? number : null
+ },
+ getRowSampleQty (row) {
+ if (!row) {
+ return null
+ }
+ const qty = row.sampleQty != null && row.sampleQty !== '' ? row.sampleQty : row.proofingNumber
+ return this.toCostNumber(qty)
+ },
+ getCostRateNumber () {
+ const rate = this.toCostNumber(this.usdCnyExchangeRate)
+ return rate != null && rate > 0 ? rate : null
+ },
+ getCostManualDisplay (row, columnProp) {
+ if (!row) {
+ return ''
+ }
+ if (columnProp === 'customizedForMasAls') {
+ return this.isBlankValue(row.customizedForMasAls) ? '' : String(row.customizedForMasAls)
+ }
+ return this.formatCostAmount(row[columnProp])
+ },
+ normalizeCostDraft (columnProp, rawValue) {
+ if (columnProp === 'customizedForMasAls') {
+ const text = rawValue == null ? '' : String(rawValue).trim()
+ if (!text) {
+ return { value: null, error: '' }
+ }
+ if (text.length > 100) {
+ return { value: null, error: 'Customized for MAS/ALS不能超过100个字符' }
+ }
+ return { value: text, error: '' }
+ }
+ const text = rawValue == null ? '' : String(rawValue).trim()
+ if (!text) {
+ return { value: null, error: '' }
+ }
+ if (!/^\d+(\.\d+)?$/.test(text)) {
+ return { value: null, error: '请输入不小于0的数字' }
+ }
+ const number = Number(text)
+ if (!Number.isFinite(number) || number >= 1000000000000) {
+ return { value: null, error: '数值超出可保存范围' }
+ }
+ return { value: this.roundCost(number), error: '' }
+ },
+ isSameCostDraft (columnProp, normalizedValue, originalDisplay) {
+ if (columnProp === 'customizedForMasAls') {
+ const left = normalizedValue == null ? '' : String(normalizedValue).trim()
+ const right = originalDisplay == null ? '' : String(originalDisplay).trim()
+ return left === right
+ }
+ return this.formatCostAmount(normalizedValue) === this.formatCostAmount(originalDisplay)
+ },
+ sumPreviewCost (left, right) {
+ if (left == null && right == null) {
+ return null
+ }
+ return this.roundCost((left || 0) + (right || 0))
+ },
+ // 列表默认显示已保存结果。只有正在改的那个数字,才按当天汇率预览受影响的计算列。
+ getCostCalculatedValue (row, columnProp) {
+ if (!row) {
+ return null
+ }
+ if (!this.isCostEditingRow(row)) {
+ return row[columnProp]
+ }
+ const editingProp = this.costEditingKey.slice(this.costEditingKey.indexOf(':') + 1)
+ const preview = this.previewEditedCost(row, editingProp)
+ if (preview && Object.prototype.hasOwnProperty.call(preview, columnProp)) {
+ return preview[columnProp]
+ }
+ return row[columnProp]
+ },
+ previewEditedCost (row, editingProp) {
+ const normalized = this.normalizeCostDraft(editingProp, this.costEditDraft)
+ if (normalized.error) {
+ return null
+ }
+ const rate = this.getCostRateNumber()
+ if (editingProp === 'tpKctUsd') {
+ const qty = this.getRowSampleQty(row)
+ const sampleCostUsd = normalized.value == null || qty == null
+ ? null
+ : this.roundCost(normalized.value * qty / 1000)
+ return {
+ sampleCostUsd: sampleCostUsd,
+ sampleAndToolingCostUsd: this.sumPreviewCost(sampleCostUsd, this.toCostNumber(row.toolingCostUsd))
+ }
+ }
+ if (editingProp === 'toolingCostCn') {
+ const toolingCostUsd = normalized.value == null || rate == null
+ ? null
+ : this.roundCost(normalized.value / rate)
+ return {
+ toolingCostUsd: toolingCostUsd,
+ sampleAndToolingCostUsd: this.sumPreviewCost(this.toCostNumber(row.sampleCostUsd), toolingCostUsd)
+ }
+ }
+ if (editingProp === 'sampleCostFromFnCn') {
+ return {
+ sampleCostFromFnUsd: normalized.value == null || rate == null
+ ? null
+ : this.roundCost(normalized.value / rate)
+ }
+ }
+ return null
+ },
+ getCostCalculatedTitle (row, columnProp) {
+ const rate = this.formatCostDisplay(this.usdCnyExchangeRate)
+ const rateText = rate ? `汇率 ${rate}` : '未设置汇率,不能折算'
+ if (columnProp === 'sampleCostUsd') {
+ return '修改 TP 时按 TP(KCT/USD) × Sample Qty / 1000 计算'
+ }
+ if (columnProp === 'toolingCostUsd') {
+ return `修改 Tooling cost/CN 时按人民币 / ${rateText}`
+ }
+ if (columnProp === 'sampleAndToolingCostUsd') {
+ return 'Sample Cost/USD + Tooling Cost/USD。点“按汇率重算”会用当前汇率重算全部自动计算列'
+ }
+ if (columnProp === 'sampleCostFromFnUsd') {
+ return `修改 Sample Cost from FN/CN 时按人民币 / ${rateText}`
+ }
+ return ''
+ },
+ normalizeExchangeRateDraft (rawValue) {
+ const text = rawValue == null ? '' : String(rawValue).trim()
+ if (!text) {
+ return { value: null, error: '' }
+ }
+ if (!/^\d+(\.\d{1,6})?$/.test(text)) {
+ return { value: null, error: '汇率请输入大于0的数字,最多6位小数' }
+ }
+ const number = Number(text)
+ if (!Number.isFinite(number) || number <= 0 || number >= 1000) {
+ return { value: null, error: '汇率必须大于0且小于1000' }
+ }
+ return { value: Math.round((number + Number.EPSILON) * 1000000) / 1000000, error: '' }
+ },
+ isSameExchangeRate (left, right) {
+ if (left == null || right == null) {
+ return left == null && right == null
+ }
+ return Math.abs(Number(left) - Number(right)) < 0.0000005
+ },
+ // 汇率写在 siteid=2。空值允许保存,但之后不能把人民币折成美元。
+ saveExchangeRate () {
+ const normalized = this.normalizeExchangeRateDraft(this.usdCnyExchangeRateDraft)
+ if (normalized.error) {
+ this.$message.warning(normalized.error)
+ return Promise.resolve(false)
+ }
+ if (this.isSameExchangeRate(normalized.value, this.toCostNumber(this.usdCnyExchangeRate))) {
+ return Promise.resolve(true)
+ }
+ if (this._exchangeRateSavePromise) {
+ return this._exchangeRateSavePromise
+ }
+ this.costRateSaving = true
+ this._exchangeRateSavePromise = saveProofTrackingExchangeRate({
+ usdCnyRate: normalized.value,
+ updateBy: this.$store.state.user.name
+ }).then(({ data }) => {
+ if (!data || data.code !== 0) {
+ this.$message.error((data && data.msg) || '汇率保存失败')
+ return false
+ }
+ this.usdCnyExchangeRate = data.usdCnyExchangeRate == null ? null : data.usdCnyExchangeRate
+ this.usdCnyExchangeRateDraft = this.usdCnyExchangeRate == null ? '' : this.formatCostDisplay(this.usdCnyExchangeRate)
+ return true
+ }).catch(() => {
+ this.$message.error('汇率保存失败')
+ return false
+ }).finally(() => {
+ this.costRateSaving = false
+ this._exchangeRateSavePromise = null
+ })
+ return this._exchangeRateSavePromise
+ },
+ recalculateCostByRate () {
+ const normalized = this.normalizeExchangeRateDraft(this.usdCnyExchangeRateDraft)
+ if (normalized.error) {
+ this.$message.warning(normalized.error)
+ return
+ }
+ if (normalized.value == null) {
+ this.$message.warning('未设置汇率,无法计算')
+ return
+ }
+ this.$confirm('将按当前汇率重算全部自动计算的费用列并保存,是否继续?', '提示', {
+ confirmButtonText: '确定',
+ cancelButtonText: '取消',
+ type: 'warning'
+ }).then(() => {
+ this.costRateRecalcLoading = true
+ return this.saveExchangeRate()
+ }).then(saved => {
+ if (saved !== true) {
+ return null
+ }
+ return refreshProofTrackingCostByRate({ updateBy: this.$store.state.user.name })
+ }).then(response => {
+ if (!response) {
+ return
+ }
+ const data = response.data
+ if (data && data.code === 0) {
+ this.$message.success(data.msg || '费用已按汇率重算')
+ this.getDataList()
+ return
+ }
+ this.$message.error((data && data.msg) || '按汇率重算失败')
+ }).catch(error => {
+ if (error === 'cancel' || error === 'close') {
+ return
+ }
+ this.$message.error('按汇率重算失败')
+ }).finally(() => {
+ this.costRateRecalcLoading = false
+ })
+ },
+ resolveCostEditor () {
+ let editor = this.$refs.costEditor
+ if (Array.isArray(editor)) {
+ editor = editor.filter(item => item)[0]
+ }
+ return editor || null
+ },
+ focusCostEditor (retried) {
+ const editor = this.resolveCostEditor()
+ if (!editor) {
+ if (!retried) {
+ this.$nextTick(() => {
+ this.focusCostEditor(true)
+ })
+ }
+ return
+ }
+ if (typeof editor.focus === 'function') {
+ editor.focus()
+ }
+ if (typeof editor.toggleMenu === 'function' && !editor.visible) {
+ editor.toggleMenu()
+ }
+ const input = editor.$refs && editor.$refs.input
+ if (input && typeof input.select === 'function' && typeof editor.toggleMenu !== 'function') {
+ input.select()
+ }
+ },
+ findCostEditingTarget () {
+ if (!this.costEditingKey) {
+ return null
+ }
+ const splitIndex = this.costEditingKey.indexOf(':')
+ if (splitIndex < 0) {
+ return null
+ }
+ const trackingId = this.costEditingKey.slice(0, splitIndex)
+ const columnProp = this.costEditingKey.slice(splitIndex + 1)
+ const row = (this.dataList || []).find(item => item && String(item.trackingId) === String(trackingId))
+ if (!row) {
+ return null
+ }
+ return { row: row, columnProp: columnProp }
+ },
+ exitCostEdit () {
+ this.costEditingKey = ''
+ this.costEditDraft = ''
+ this.costEditOriginal = ''
+ this.costSelectReady = false
+ },
+ cancelCostEdit (row) {
+ if (row && !this.isCostEditingRow(row)) {
+ return
+ }
+ this.exitCostEdit()
+ },
+ startCostEdit (row, columnProp) {
+ if (!row || !row.trackingId || !this.isCostManualColumn(columnProp)) {
+ return
+ }
+ const nextKey = `${row.trackingId}:${columnProp}`
+ if (this.costEditingKey === nextKey) {
+ return
+ }
+ if (this.costEditingKey) {
+ const editing = this.findCostEditingTarget()
+ if (editing) {
+ this.saveCostEdit(editing.row, editing.columnProp)
+ } else {
+ this.exitCostEdit()
+ }
+ }
+ this.costEditingKey = nextKey
+ this.costEditOriginal = this.getCostManualDisplay(row, columnProp)
+ this.costEditDraft = this.costEditOriginal
+ this.costSelectReady = false
+ this.$nextTick(() => {
+ this.focusCostEditor()
+ // 下拉刚挂上时会先关一次面板,这段时间不把关闭当成保存。
+ setTimeout(() => {
+ if (this.costEditingKey === nextKey) {
+ this.costSelectReady = true
+ }
+ }, 200)
+ })
+ },
+ handleCostSelectVisible (visible, row, columnProp) {
+ if (visible || !this.isCostEditing(row, columnProp) || !this.costSelectReady) {
+ return
+ }
+ const editor = this.resolveCostEditor()
+ const typed = editor && editor.query != null ? String(editor.query).trim() : ''
+ if (typed && typed !== String(this.costEditDraft || '').trim()) {
+ this.costEditDraft = typed
+ }
+ this.saveCostEdit(row, columnProp)
+ },
+ saveCostEdit (row, columnProp) {
+ if (!this.isCostEditing(row, columnProp)) {
+ return
+ }
+ const normalized = this.normalizeCostDraft(columnProp, this.costEditDraft)
+ if (normalized.error) {
+ this.$message.warning(normalized.error)
+ return
+ }
+ if (this.isSameCostDraft(columnProp, normalized.value, this.costEditOriginal)) {
+ this.exitCostEdit()
+ return
+ }
+ const version = (row._costSaveVersion || 0) + 1
+ this.$set(row, '_costSaveVersion', version)
+ this.$set(row, columnProp, normalized.value)
+ const payload = {
+ trackingId: row.trackingId,
+ customizedForMasAls: this.isBlankValue(row.customizedForMasAls) ? null : String(row.customizedForMasAls).trim(),
+ tpKctUsd: this.toCostNumber(row.tpKctUsd),
+ toolingCostCn: this.toCostNumber(row.toolingCostCn),
+ sampleCostFromFnCn: this.toCostNumber(row.sampleCostFromFnCn),
+ updateBy: this.$store.state.user.name
+ }
+ this.exitCostEdit()
+ updateProofTrackingCost(payload).then(({ data }) => {
+ const current = (this.dataList || []).find(item => item && item.trackingId === row.trackingId)
+ if (!current || (current._costSaveVersion != null && current._costSaveVersion !== version)) {
+ return
+ }
+ if (data && data.code === 0) {
+ this.applySavedCost(current, data.row || {})
+ if (data.msg && String(data.msg).indexOf('未设置汇率') > -1) {
+ this.$message.warning(data.msg)
+ }
+ return
+ }
+ this.$message.error((data && data.msg) || '费用保存失败')
+ this.getDataList()
+ }).catch(() => {
+ const current = (this.dataList || []).find(item => item && item.trackingId === row.trackingId)
+ if (!current || (current._costSaveVersion != null && current._costSaveVersion !== version)) {
+ return
+ }
+ this.$message.error('费用保存异常')
+ this.getDataList()
+ })
+ },
+ applySavedCost (row, saved) {
+ const fields = [
+ 'customizedForMasAls',
+ 'tpKctUsd',
+ 'toolingCostCn',
+ 'sampleCostFromFnCn',
+ 'sampleCostUsd',
+ 'toolingCostUsd',
+ 'sampleAndToolingCostUsd',
+ 'sampleCostFromFnUsd'
+ ]
+ fields.forEach(field => {
+ if (saved && Object.prototype.hasOwnProperty.call(saved, field)) {
+ this.$set(row, field, saved[field])
+ }
+ })
+ },
handleTrackingTableSortChange (sortData) {
this.trackingTableSortProp = sortData && sortData.prop ? String(sortData.prop) : ''
this.trackingTableSortOrder = sortData && sortData.order ? String(sortData.order) : ''
@@ -5727,8 +6360,7 @@ export default {
baseline: current.baseline || '',
pmInqueryTime: this.formatDate(current.pmInqueryTime),
importance: this.normalizeImportanceValue(current.importance),
- needDate: project.needDate || part.needDate || current.needDate || '',
- remark: part.remark || project.remark || current.remark || ''
+ needDate: project.needDate || part.needDate || current.needDate || ''
})
form.projectManagerName = this.getRoleDisplayName(form.projectManager)
form.projectOwnerName = this.getRoleDisplayName(form.projectOwner)
@@ -5855,9 +6487,9 @@ export default {
: ''
const nextStatus = inData.proofingStatus ? String(inData.proofingStatus).trim() : ''
const proofingStatusChanged = this.oneKeyDialogMode !== 'create' && nextStatus !== previousStatus
- if (proofingStatusChanged && this.proofingStatusChangeOptions.indexOf(nextStatus) === -1) {
+ if (proofingStatusChanged && !this.isAllowedOneKeyProofingStatusChange(previousStatus, nextStatus)) {
this.saveOneKeyLoading = false
- this.$message.warning('打样状态只能更改为终止、取消、报废')
+ this.$message.warning('打样状态只能更改为终止、取消、报废;当前为终止、取消、报废时可以改回进行中')
return
}
if (proofingStatusChanged) {
@@ -6483,7 +7115,23 @@ export default {
return '#046e97'
},
isProofingClosedStatus (row) {
- return this.proofingStatusChangeOptions.indexOf(this.getNormalizedProofingStatus(row)) > -1
+ return this.isClosedProofingStatusValue(this.getNormalizedProofingStatus(row))
+ },
+ isClosedProofingStatusValue (status) {
+ const normalizedStatus = status ? String(status).trim() : ''
+ return this.proofingStatusChangeOptions.indexOf(normalizedStatus) > -1
+ },
+ isAllowedOneKeyProofingStatusChange (previousStatus, nextStatus) {
+ const previous = previousStatus ? String(previousStatus).trim() : ''
+ const next = nextStatus ? String(nextStatus).trim() : ''
+ if (!next || next === previous) {
+ return true
+ }
+ if (this.proofingStatusChangeOptions.indexOf(next) > -1) {
+ return true
+ }
+ // 重新打开只允许从终止/取消/报废回到进行中,避免草稿等状态被改成进行中。
+ return next === '进行中' && this.isClosedProofingStatusValue(previous)
},
canDeleteProofStatus (row) {
return this.deletableProofingStatuses.indexOf(this.getNormalizedProofingStatus(row)) > -1
@@ -7630,6 +8278,50 @@ export default {
min-width: 28px;
}
+.cost-rate-tip {
+ display: inline-flex;
+ align-items: center;
+ margin-left: 10px;
+ color: #606266;
+ font-size: 12px;
+ line-height: 18px;
+ white-space: nowrap;
+}
+
+.cost-rate-input {
+ width: 120px;
+ margin-left: 6px;
+}
+
+.cost-cell {
+ min-height: 24px;
+}
+
+.cost-display--editable {
+ display: inline-block;
+ width: 100%;
+ min-height: 24px;
+ line-height: 24px;
+ padding: 0 4px;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ cursor: pointer;
+ box-sizing: border-box;
+}
+
+.cost-display--editable:hover {
+ border-color: #dcdfe6;
+ background: #fff;
+}
+
+.el-table.proof-tracking-table .cost-editor {
+ width: 100%;
+}
+
+.cost-calculated-text {
+ color: #606266;
+}
+
/* Comments 两行共 28px,与原单元格内容高度一致,不撑高整行 */
.el-table.proof-tracking-table .comments-text {
display: -webkit-box;