iT邦幫忙

2025 iThome 鐵人賽

DAY 19
1
Odoo

一起認識 Odoo:開源 ERP 的另一種選擇系列 第 19

《第一次海外拓銷就混亂到爆:Odoo 出差任務 × 預算》|Day 19

  • 分享至 

  • xImage
  •  

🏁 開場:未知的冒險

https://ithelp.ithome.com.tw/upload/images/20250919/201783467GuPdaYWEN.jpg

當大家還在忙著整理上次說的 SIIR 補助的預算書與 WBS 時,

沈如蘭忽然在早上開會時丟下一句話:
「我們要去拉美看市場,兩週後出差。」

會議室瞬間炸開:

  • 德華 驚叫:「護照要先辦吧?要不要簽證?誰知道要不要打疫苗?」
  • 阿美 馬上查航班:「機票誰先墊?飯店要先刷還是報帳?」
  • 芳芳 抬頭皺眉:「「去拉美的差旅津貼……是不是要比平常多個一成半啊?」」
  • 阿哲 則默默在筆記本寫下三個名字:「老闆、德華、我。」

沈如蘭補了一句:
「TAITRA 有個拉美拓銷團,名單我報上去了。」
所有人愣住兩秒,心中冒出一個聲音:
「我們,就這樣踏上一場未知的冒險了嗎?」

阿哲 瞥了一眼後台,發現系統裡忽然多了好幾套進階模組,
心裡暗暗想著:
「……難怪老闆最近一直說要衝海外業務。
不拓銷回來,根本燒不起這套系統。」


🔑 為什麼一定要出差?

德華 一邊收起筆電,一邊小聲問 阿哲
「老實說,我們為什麼要親自飛一趟?」

阿哲 闔上筆記本,壓低聲音:
「因為我們剛查到,TAITRA 的海外拓銷團正在徵件,
如果申請通過,機票、住宿、展位費等也許可獲部分補助,
趁現在把這趟出差綁進計畫裡,成本就不會太高。」

沈如蘭 補了一句:
「還有一個理由:台灣市場已經接近飽合了。
要走出去,爭取成長空間。」

德華 皺著眉:
「可是為什麼是拉美?不是去東南亞比較近?」

沈如蘭 看著牆上的世界地圖,若有所思地說,
腦中閃過很多畫面,臉也微微別過去:
「……因為那裡缺貨、缺人、缺數位系統,
我們的產品,剛好可以補上這些缺口。」

她的語氣很輕,但眼神像是在對自己說話。

阿哲 忍不住偷偷笑了。
他可是曾經在拉美邦交國當過替代役,終於有機會在老闆面前大顯身手了。

德華 則默默打開手機記事本,
心裡已習慣性地盤算起:要搭哪家航線、哪些信用卡既能累哩程又能進貴賓室、
當地治安狀況怎樣、要不要先做外交部旅外登錄、
還有要準備幾套西裝才能撐過整趟行程。


🔑 為什麼 SME 的第一次出差會大混亂

  • 沒有制度:簽證、護照、預支、保險、報帳全靠人記
  • 沒有人負責:HR 不想管,會計怕風險,主管說「不是我決定的」
  • 資訊分散:報名表在信箱、護照在抽屜、發票在行李,報銷時全部不見

沈如蘭 冷冷地說:
「這不是度假,是投資。預算、行程、成果...全要記錄。」


💡 Odoo:讓出差從一開始就制度化

阿哲 決定在 Odoo ERP 裡先搭出「出差任務 × 預算」制度:


📌 前置設定:

  • 需安裝 hr_expense 模組
  • 需於 approvals.request 新增自訂欄位:x_project_id, x_date_start, x_reason
  • 需建立 XML ID:approval_category_business_trip

📍 第一步:出差申請(Approvals)

模組:approvals

新增「出差申請」審核類型,欄位包含:

  • 地點、日期、行程目的、預估費用、出差人員名單
  • 核准後自動建立 Project 任務
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)

模組: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 拉美拓銷團實際出訪巴西、哥倫比亞、祕魯與智利聖地牙哥,本文為模擬案例,非依實際行程安排。

📌 參考資料


上一篇
《補助不是魔法:用 Odoo 當 SIIR 計畫的藍圖》|Day 18
下一篇
《墨西哥:制度嚴苛、文件繁瑣,Odoo 怎麼幫 SME 跨過第一道門?》|Day 20
系列文
一起認識 Odoo:開源 ERP 的另一種選擇20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言