當大家還在忙著整理上次說的 SIIR 補助的預算書與 WBS 時,
沈如蘭忽然在早上開會時丟下一句話:
「我們要去拉美看市場,兩週後出差。」
會議室瞬間炸開:
沈如蘭補了一句:
「TAITRA 有個拉美拓銷團,名單我報上去了。」
所有人愣住兩秒,心中冒出一個聲音:
「我們,就這樣踏上一場未知的冒險了嗎?」
阿哲 瞥了一眼後台,發現系統裡忽然多了好幾套進階模組,
心裡暗暗想著:
「……難怪老闆最近一直說要衝海外業務。
不拓銷回來,根本燒不起這套系統。」
德華 一邊收起筆電,一邊小聲問 阿哲:
「老實說,我們為什麼要親自飛一趟?」
阿哲 闔上筆記本,壓低聲音:
「因為我們剛查到,TAITRA 的海外拓銷團正在徵件,
如果申請通過,機票、住宿、展位費等也許可獲部分補助,
趁現在把這趟出差綁進計畫裡,成本就不會太高。」
沈如蘭 補了一句:
「還有一個理由:台灣市場已經接近飽合了。
要走出去,爭取成長空間。」
德華 皺著眉:
「可是為什麼是拉美?不是去東南亞比較近?」
沈如蘭 看著牆上的世界地圖,若有所思地說,
腦中閃過很多畫面,臉也微微別過去:
「……因為那裡缺貨、缺人、缺數位系統,
我們的產品,剛好可以補上這些缺口。」
她的語氣很輕,但眼神像是在對自己說話。
阿哲 忍不住偷偷笑了。
他可是曾經在拉美邦交國當過替代役,終於有機會在老闆面前大顯身手了。
而 德華 則默默打開手機記事本,
心裡已習慣性地盤算起:要搭哪家航線、哪些信用卡既能累哩程又能進貴賓室、
當地治安狀況怎樣、要不要先做外交部旅外登錄、
還有要準備幾套西裝才能撐過整趟行程。
沈如蘭 冷冷地說:
「這不是度假,是投資。預算、行程、成果...全要記錄。」
阿哲 決定在 Odoo ERP 裡先搭出「出差任務 × 預算」制度:
hr_expense
模組approvals.request
新增自訂欄位:x_project_id
, x_date_start
, x_reason
approval_category_business_trip
模組:approvals
新增「出差申請」審核類型,欄位包含:
class ApprovalRequest(models.Model):
_inherit = 'approval.request'
def action_approve(self):
res = super().action_approve()
trip_cat = self.env.ref('your_module_name.approval_category_business_trip', raise_if_not_found=False)
for req in self.filtered(lambda r: r.category_id == trip_cat):
if not req.x_project_id:
continue
self.env['project.task'].create({
'name': f"出差任務:{req.name}",
'project_id': req.x_project_id.id,
'user_ids': [(6, 0, [req.request_owner_id.id])],
'date_deadline': req.x_date_start,
'description': req.x_reason,
})
return res
模組:project
, analytic_account
, hr_expense
analytic_account
,以利後續統計成本project.project
加上 planned_budget
欄位控制預算上限hr.expense
核准前檢查是否超支from odoo import api, fields, models
from odoo.exceptions import UserError
class ProjectProject(models.Model):
_inherit = 'project.project'
planned_budget = fields.Float(string="預算上限")
def _create_analytic_account(self):
res = super()._create_analytic_account()
for project in self:
if not project.analytic_account_id:
project.analytic_account_id = self.env['account.analytic.account'].create({
'name': project.name,
'project_ids': [(6, 0, [project.id])]
})
return res
class HrExpense(models.Model):
_inherit = 'hr.expense'
project_id = fields.Many2one(
'project.project',
string='關聯專案',
compute='_compute_project_id',
store=True,
readonly=False
)
@api.depends('analytic_account_id')
def _compute_project_id(self):
for expense in self:
if expense.analytic_account_id and expense.analytic_account_id.project_ids:
expense.project_id = expense.analytic_account_id.project_ids[0]
else:
expense.project_id = False
def action_submit_expenses(self):
for expense in self:
if expense.project_id and expense.project_id.planned_budget > 0:
project = expense.project_id
approved_expenses = self.search([
('project_id', '=', project.id),
('state', '=', 'approve')
])
spent_amount = sum(approved_expenses.mapped('total_amount'))
if spent_amount + expense.total_amount > project.planned_budget:
raise UserError(
f"費用「{expense.name}」超出專案「{project.name}」的預算!\n"
f"預算上限:{project.planned_budget}\n"
f"已核准花費:{spent_amount}\n"
f"本次申請:{expense.total_amount}"
)
return super().action_submit_expenses()
阿哲提交出差申請
↓ Approvals
沈如蘭審核 → 建立出差任務 (Project)
↓
系統綁定 Analytic Account
↓
預算金額寫入 planned_budget
↓
準備申請預支款、機票、簽證、住宿…
補助幫你買機票,制度才幫你飛得遠。
在緊張與混亂之中,
他們第一次明白:
海外任務不能靠臨時衝刺,
必須讓一切有跡可循。
Odoo 讓出差不再是混亂的冒險,
而是可以複製、可以前進的路線圖。
沈如蘭的話,
像在一張古老地圖上,
劃下第一個光點。
這不是出差,
而是一場遠征。
👉 阿哲 看著機票確認信,深吸一口氣: 下一站,拉丁美洲。
註:今年的 TAITRA 拉美拓銷團實際出訪巴西、哥倫比亞、祕魯與智利聖地牙哥,本文為模擬案例,非依實際行程安排。