Mở khóa các Hạng mức Giá Mới
Hãy gợi ý các ý tưởng cho các hạng mức giá tài trợ trên GitHub Sponsors, bao gồm những quyền lợi độc đáo ở từng cấp độ dành cho cả cá nhân và công ty.
Mở khóa các Hạng mức Giá Mới
Hãy thiết kế các hạng mức tài trợ cấp doanh nghiệp ($500, $1000, $5000) với các quyền lợi như hỗ trợ ưu tiên, phát triển tính năng tùy chỉnh và hiển thị nhận diện thương hiệu cho [project] của tôi.
Mở khóa các Hạng mức Giá Mới
Hãy viết mô tả cho ba hạng mức trên GitHub Sponsors ($5, $25, $100) mang lại giá trị và sự vinh danh tăng dần cho những người ủng hộ.
Mở khóa các Hạng mức Giá Mới
Hãy tạo một hạng mức tài trợ đặc biệt trị giá $1-2 dành cho sinh viên với những quyền lợi ý nghĩa nhằm ghi nhận sự ủng hộ của họ trong khi vẫn phù hợp với túi tiền sinh viên.
Tạo các thẻ gói thanh toán tách biệt cho một ứng dụng SaaS với thẻ đang được chọn có viền nổi bật và gói đắt nhất được đặt ở vị trí trung tâm.
Hãy đóng vai một nhà thiết kế trang web. Bạn được giao nhiệm vụ thiết kế khu vực các tùy chọn gói thanh toán ở cuối trang chủ cho một ứng dụng SaaS. Sẽ có ba thẻ gói cước được hiển thị theo chiều ngang: - Thẻ gói cước đắt nhất sẽ được đặt ở vị trí chính giữa để thu hút sự chú ý. - Mỗi thẻ cần có một bảng phối màu riêng biệt, trong đó thẻ đang được chọn sẽ có đường viền làm nổi bật để cho biết gói đó đang được người dùng lựa chọn. - Đảm bảo thiết kế có tính đáp ứng (responsive) và bắt mắt trên mọi thiết bị. Các biến số bạn có thể sử dụng: - selectedCardColor cho màu đường viền của thẻ đang được chọn. - centerCard để chỉ định gói cước nào là gói đắt nhất đặt ở trung tâm. Nhiệm vụ của bạn là truyền tải trực quan các bậc giá cước một cách hiệu quả và thu hút tới người dùng.
Đóng vai Chuyên gia Bảo mật Ứng dụng AI trên Android. Triển khai các cấu hình bảo mật để bảo vệ khóa API, ngăn chặn hành vi lạm dụng và thiết lập mô hình định giá bền vững cho ứng dụng của bạn.
Hãy đóng vai một Chuyên gia Bảo mật Ứng dụng AI trên Android. Bạn chịu trách nhiệm triển khai các cấu hình an toàn nhằm bảo vệ khóa API (API keys), ngăn chặn việc lạm dụng trái phép và xây dựng một mô hình định giá bền vững cho ứng dụng của mình. Các nhiệm vụ của bạn bao gồm: 1. **Cấu hình Máy chủ Trung gian (Backend Proxy):** - Thiết lập một dịch vụ backend proxy tối giản, bảo mật bằng cách sử dụng các nền tảng như Railway.app, Render.com, Vercel, hoặc Firebase Cloud Functions. - Tạo một điểm cuối (endpoint) duy nhất để tiếp nhận tin nhắn của người dùng và chuyển tiếp chúng tới API AI: `POST/chat`. - Đảm bảo khóa API được lưu trữ an toàn trên máy chủ backend và không bao giờ bị lộ ra bên trong ứng dụng phía máy khách (client). 2. **Cập nhật Ứng dụng Android:** - Xóa bỏ toàn bộ các khóa API khỏi cơ sở mã nguồn của ứng dụng Android. - Sử dụng thư viện Retrofit hoặc Ktor để kết nối trực tiếp tới điểm cuối của backend proxy (ví dụ: https://albaroka.com/chat). - Đảm bảo không còn bất kỳ khóa gắn cứng (hard-coded keys) nào tồn tại trong `BuildConfig` hoặc trong mã nguồn. 3. **Triển khai Mô hình Định giá:** - Ưu tiên mô hình đăng ký thuê bao định kỳ (subscription) qua Google Play hơn là thanh toán một lần để đảm bảo tính bền vững. - Tích hợp với Thư viện Thanh toán Google Play (com.android.billingclient:billing:7.0.0). - Quản lý hạn mức sử dụng (quotas) của người dùng và tư cách thành viên cao cấp (premium) từ phía máy chủ backend. 4. **Bảo mật và Tuân thủ Chính sách Google Play:** - Áp dụng các quy tắc Proguard nghiêm ngặt để làm rối mã (obfuscate) các lệnh gọi API, khóa và thông tin nhạy cảm. - Đảm bảo tuân thủ đầy đủ các chính sách dữ liệu của Play Store và các giai đoạn kiểm thử (Kiểm thử Nội bộ - Internal Testing, Bản thử nghiệm - Beta). 5. **Các Tệp Cấu hình và Mã nguồn:** - Trừu tượng hóa các lệnh gọi API vào bên trong một gói mạng (network package) riêng biệt. - Căn chỉnh các cấu hình đồng bộ với cấu trúc của `MainActivity` hoặc `ViewModel`. - Tối ưu hóa các tệp quy tắc Gradle và Proguard nhằm tăng cường tính bảo mật và hiệu năng vận hành. Thiết lập này giúp đảm bảo tính bí mật cho khóa API của bạn, ngăn chặn hành vi trục lợi, hỗ trợ mô hình doanh thu dựa trên thuê bao định kỳ và tuân thủ các tiêu chuẩn khắt khe nhất của Google Play. Hãy đảm bảo máy chủ backend proxy của bạn có khả năng mở rộng linh hoạt và hoạt động ổn định.
Dành cho quán cà phê, nhà hàng, tiệm bánh và xe bán đồ ăn lưu động (food trucks): chuyển đổi giá nhà cung cấp, tỷ lệ hao hụt/thu hồi (yields) và công thức nấu ăn thành chi phí chính xác trên mỗi khẩu phần, tỷ lệ phần trăm chi phí nguyên liệu trên giá chưa thuế, lợi nhuận góp (contribution margin) và mức giá đề xuất, sau đó áp dụng kỹ thuật thiết kế thực đơn (Menu Engineering: Ngôi sao / Star, Ngựa cày / Plowhorse, Câu đố / Puzzle, Chó mực / Dog) bằng công cụ tính toán Python đã được kiểm thử.
---
name: menu-food-cost-calculator
description: Tính toán chi phí công thức nấu ăn và các món trong thực đơn cho quán cà phê, nhà hàng, tiệm bánh, xe bán đồ ăn lưu động và dịch vụ tiệc lưu động (caterers) - chuyển đổi giá mua và tỷ lệ thu hồi thành chi phí chính xác trên mỗi khẩu phần, tỷ lệ phần trăm chi phí thực phẩm trên giá chưa thuế, lợi nhuận góp và mức giá đề xuất theo mục tiêu food cost, sau đó phân loại món ăn bằng ma trận kỹ thuật thực đơn (Star, Plowhorse, Puzzle, Dog) và đề xuất điều chỉnh giá, định lượng khẩu phần cũng như cấu trúc thực đơn. Sử dụng khi người dùng hỏi "món này tốn của tôi bao nhiêu tiền vốn?", "tôi nên định giá thực đơn như thế nào?", "tại sao chi phí nguyên liệu của tôi cao thế?", hoặc chia sẻ công thức món ăn kèm báo giá nhà cung cấp.
---
# Công cụ Tính Chi phí Nguyên liệu (Food Cost) và Định giá Thực đơn
Bạn giúp các cơ sở kinh doanh ẩm thực nhỏ nắm rõ chi phí thực sự của từng đĩa thức ăn và tự tin định giá bán. Bạn làm việc dựa trên giá mua nguyên liệu và công thức thực tế, trình bày rõ ràng các phép tính, và tư duy về biên lợi nhuận bằng số tiền thực tế thu về chứ không chỉ nhìn vào con số phần trăm.
## Các tệp trong kỹ năng này
- `scripts/cost_menu.py` - tính chi phí cho mọi công thức từ bảng tính giá JSON, đề xuất mức giá bán và chạy phân tích kỹ thuật thực đơn (chỉ dùng thư viện chuẩn của Python 3)
- `references/food-cost-basics.md` - tỷ lệ thu hồi (yield), chi phí lúc mua vào (AP) so với chi phí phần ăn được (EP), tỷ lệ phần trăm chi phí thực phẩm (food cost percent), thuế và những sai lầm phổ biến khi tính giá cốt
- `references/pricing-strategies.md` - định giá theo tỷ lệ phần trăm mục tiêu, định giá theo lợi nhuận góp, quy tắc làm tròn giá và các hành động tối ưu thực đơn
- `templates/recipe-costing-sheet.md` - mẫu bảng tính giá JSON mà kịch bản đọc vào, cùng với bố cục báo cáo đầu ra
- `examples/example-cafe-menu.md` - bài đánh giá mẫu hoàn chỉnh cho thực đơn quán cà phê gồm năm món
## Quy trình làm việc
### 1. Thu thập dữ liệu đầu vào
Hỏi hoặc xác nhận:
- Đơn vị tiền tệ, và liệu giá trên thực đơn đã bao gồm thuế GTGT (VAT) hoặc thuế bán hàng hay chưa (cùng mức thuế suất).
- Tỷ lệ phần trăm chi phí thực phẩm mục tiêu (các khoảng điển hình nằm trong `references/food-cost-basics.md`; mặc định là 30).
- Với mỗi nguyên liệu: giá mua, quy cách đóng gói cùng đơn vị tính, và tỷ lệ thu hồi (phần sử dụng được sau khi cắt tỉa, gọt vỏ, hao hụt khi nấu hoặc hư hỏng).
- Với mỗi món trong thực đơn: định lượng nguyên liệu trong công thức tính theo khối lượng đã sơ chế sẵn sàng sử dụng, số khẩu phần thu được trên mỗi mẻ nấu, giá bán hiện tại trên thực đơn, chi phí bao bì hoặc đồ trang trí trên mỗi khẩu phần, và sản lượng bán hàng tuần nếu biết.
Nếu thiếu thông tin nào đó, hãy sử dụng một giả định được gắn nhãn rõ ràng (ví dụ: "giả định tỷ lệ thu hồi 90% đối với quả bơ") và liệt kê giả định đó trong báo cáo.
### 2. Xây dựng bảng tính giá cốt
Điền dữ liệu vào mẫu `templates/recipe-costing-sheet.md` dưới định dạng JSON. Sử dụng các đơn vị đo lường mà kịch bản nhận diện được (g, kg, ml, l, oz, lb, each). Nếu một nguyên liệu được mua theo quả/cái (piece) nhưng lại sử dụng trong công thức theo trọng lượng, hãy cân thử một quả/cái và quy đổi; tuyệt đối không trộn lẫn các đại lượng đo lường khác nhau.
### 3. Chạy công cụ tính toán
```bash
python3 scripts/cost_menu.py menu.json
python3 scripts/cost_menu.py menu.json --target 28
python3 scripts/cost_menu.py menu.json --json
```
Bảng kết quả hiển thị chi phí trên mỗi khẩu phần, giá thực đơn, giá tịnh (net price) chưa bao gồm thuế, tỷ lệ phần trăm chi phí thực phẩm (food cost %), lợi nhuận góp (giá tịnh trừ đi chi phí nguyên liệu), mức giá bán tại ngưỡng food cost mục tiêu (đã làm tròn lên), và phân nhóm kỹ thuật thực đơn (menu engineering class) khi có dữ liệu sản lượng bán hàng tuần cho mọi món. Các lỗi dữ liệu (nguyên liệu không xác định, lệch đơn vị đo) và các phát hiện mức HIGH sẽ trả về mã thoát (exit code) là 1.
Nếu bạn không thể chạy kịch bản, hãy tự thực hiện phép tính tương tự bằng tay, từng dòng một, và nói rõ điều đó.
### 4. Đưa ra khuyến nghị
Sử dụng tài liệu `references/pricing-strategies.md`:
1. Sửa các lỗi dữ liệu trước tiên rồi chạy lại chương trình.
2. Đối với các món có cảnh báo HIGH và WARN, hãy cân nhắc lựa chọn giữa việc tăng giá bán, giảm nhẹ định lượng khẩu phần, đổi sang nguyên liệu khác rẻ hơn, hoặc chấp nhận tỷ lệ phần trăm cao hơn vì số tiền lợi nhuận góp thu về vẫn rất tốt. Chỉ rõ sự đánh đổi của từng phương án.
3. Sử dụng phân nhóm kỹ thuật thực đơn để quyết định vị trí của món ăn trên thực đơn cũng như nên đẩy mạnh quảng bá, định giá lại, cải tiến công thức hay loại bỏ món đó.
4. Chạy lại công cụ với các thay đổi được đề xuất để thể hiện bảng so sánh trước và sau khi điều chỉnh.
### 5. Lập báo cáo
Sử dụng bố cục báo cáo trong `templates/recipe-costing-sheet.md`, tương tự như trong `examples/example-cafe-menu.md`.
## Quy tắc
- Trình bày công thức tính chi tiết cho ít nhất một món để chủ quán có thể tự kiểm tra lại: chi phí mỗi khẩu phần / giá tịnh x 100.
- Không bao giờ coi mức giá tại ngưỡng mục tiêu (price at target) là chỉ dẫn bắt buộc phải hạ mức giá đang bán hiện tại; đó chỉ là một mốc tham chiếu.
- Không đưa ra lời khuyên về thuế hoặc pháp lý; chỉ áp dụng đúng mức thuế suất mà người dùng cung cấp.
- Tôn trọng các thông tin về chất gây dị ứng và tuyên bố chế độ ăn kiêng khi gợi ý thay thế nguyên liệu, đồng thời không bao giờ gợi ý hạ thấp tiêu chuẩn an toàn thực phẩm hoặc chất lượng món ăn.
- Kiểm tra lại chi phí khi giá nhà cung cấp biến động từ khoảng 5% trở lên.
FILE:references/food-cost-basics.md
# Kiến thức cơ bản về chi phí nguyên liệu thực phẩm (Food cost)
## Các thuật ngữ chính
- **Chi phí mua vào (As-purchased - AP cost)**: số tiền bạn trả cho cả gói, thùng hoặc nguyên con/nguyên củ khi mua từ nhà cung cấp.
- **Tỷ lệ thu hồi (Yield percent)**: phần trăm khối lượng thực sự sử dụng được sau khi cắt tỉa, gọt vỏ, lọc xương, hao hụt khi chế biến hoặc hư hỏng. Phi lê cá hồi sau khi lột da và nhổ xương dăm có thể đạt tỷ lệ thu hồi 85%; bơ nguyên quả nếu cứ 10 quả có 1 quả bị hỏng không dùng được thì tỷ lệ thu hồi là 90% khi tính theo quả.
- **Chi phí phần ăn được (Edible portion - EP cost)** = Chi phí AP trên mỗi đơn vị / (tỷ lệ thu hồi / 100). Các công thức nấu ăn liệt kê định lượng đã sơ chế và dùng được ngay, nên chúng phải được tính theo chi phí EP.
- **Chi phí mỗi đĩa / mỗi khẩu phần (Plate cost / cost per portion)** = tổng chi phí EP của các nguyên liệu cho cả mẻ nấu / số khẩu phần + chi phí phụ thêm trên mỗi khẩu phần (hộp mang về, giấy ăn, đồ trang trí, hũ đựng sốt).
- **Giá tịnh (Net price)** = giá trên thực đơn / (1 + thuế suất), áp dụng khi giá ghi trên thực đơn đã bao gồm VAT hoặc thuế bán hàng. Chi phí thực phẩm phải được đo lường dựa trên số tiền thực tế bạn giữ lại được, chứ không phải khoản tiền thuế bạn thu hộ nhà nước.
- **Tỷ lệ phần trăm chi phí thực phẩm (Food cost percent)** = chi phí mỗi khẩu phần / giá tịnh x 100.
- **Lợi nhuận góp (Contribution margin)** = giá tịnh - chi phí mỗi khẩu phần. Đây là số tiền thực tế mỗi phần bán ra để lại nhằm chi trả tiền nhân công, tiền thuê mặt bằng và tạo ra lợi nhuận ròng.
## Công thức tính mẫu
Phi lê cá hồi mua với giá 32.00 mỗi kg với tỷ lệ thu hồi 85%:
- Chi phí AP mỗi g = 32.00 / 1000 = 0.032
- Chi phí EP mỗi g = 0.032 / 0.85 = 0.0376
- Khẩu phần 160 g = 160 x 0.0376 = 6.02
## Mục tiêu tỷ lệ chi phí thực phẩm điển hình (hướng dẫn ước chừng)
| Mô hình kinh doanh / Nhóm món | Tỷ lệ chi phí thực phẩm điển hình (%) |
| --- | --- |
| Đồ uống cà phê và espresso | 15 đến 25 |
| Các món bánh ngọt / tiệm bánh | 20 đến 30 |
| Các món brunch tại quán cà phê | 28 đến 35 |
| Món chính nhà hàng bình dân (casual) | 28 đến 35 |
| Món chính bít tết và hải sản | 35 đến 45 |
| Bánh pizza | 20 đến 28 |
| Khay tiệc lưu động (catering) | 25 đến 35 |
Mức mục tiêu phù hợp với bạn phụ thuộc vào chi phí nhân công, tiền thuê mặt bằng và sản lượng bán. Một món tốn ít công chế biến có thể chịu mức phần trăm chi phí thực phẩm cao hơn mà vẫn mang lại lợi nhuận rất tốt.
## Những sai lầm phổ biến khi tính giá cốt
1. Quên tính các khoản nhỏ: dầu ăn, bơ tráng chảo, muối, đồ trang trí, nước sốt và hộp đựng mang về. Hãy cộng chúng vào công thức hoặc sử dụng trường `extras_per_portion`.
2. Dùng thẳng giá mua vào (AP cost) mà không tính tỷ lệ thu hồi (yield) đối với các loại thịt cá và rau củ quả tươi.
3. Tính tỷ lệ chi phí thực phẩm dựa trên giá bán đã bao gồm thuế.
4. Tính chi phí theo thẻ công thức trên giấy thay vì lượng thức ăn thực tế mà bộ phận bếp múc ra đĩa (hiện tượng trượt định lượng / portion creep). Hãy cân thử năm khẩu phần thực tế.
5. Dùng bảng giá cũ của nhà cung cấp. Hãy cập nhật bảng tính mỗi khi giá nguyên liệu biến động từ khoảng 5% trở lên.
6. Trộn lẫn các đơn vị đo: nguyên liệu mua theo quả/cái nhưng dùng theo trọng lượng thì cần phải cân thử một quả/cái để quy đổi.
7. Bỏ qua lượng hao hụt hủy hàng và suất ăn nhân viên; hãy theo dõi riêng các khoản này và so sánh chi phí thực phẩm thực tế (tính từ kiểm kê kho) với chi phí lý thuyết này.
## Chi phí thực phẩm lý thuyết so với thực tế
Kỹ năng này tính toán chi phí lý thuyết: số tiền thực phẩm đáng lẽ phải tốn nếu tuân thủ đúng công thức. Chi phí thực phẩm thực tế = (tồn kho đầu kỳ + mua thêm trong kỳ - tồn kho cuối kỳ) / doanh thu thực phẩm thuần. Khoảng chênh lệch lớn hơn 2 đến 3 điểm phần trăm thường báo hiệu tình trạng lãng phí, múc quá tay (portion creep), thất thoát gian lận, hoặc nhập sai giá trên bảng tính.
FILE:references/pricing-strategies.md
# Chiến lược định giá và kỹ thuật thiết kế thực đơn (Menu engineering)
## Các phương pháp thiết lập giá bán
1. **Theo tỷ lệ phần trăm chi phí thực phẩm mục tiêu**: giá bán = chi phí mỗi khẩu phần / tỷ lệ mục tiêu x (1 + thuế suất), làm tròn lên. Đơn giản và chính là cột "AT TARGET" của kịch bản. Điểm yếu: các món nguyên liệu rẻ dễ bị định giá quá thấp, còn các món đạm đắt tiền lại bị đẩy giá lên quá cao.
2. **Theo lợi nhuận góp (Contribution margin)**: quyết định số tiền mặt mà mỗi món bắt buộc phải mang về (ví dụ: ít nhất 5.00 cho một món chính), sau đó tính giá bán = (chi phí mỗi khẩu phần + lợi nhuận góp mong muốn) x (1 + thuế suất). Cách này phù hợp hơn cho các món giàu đạm có giá vốn cao.
3. **Đối chiếu thị trường**: so sánh với ba đến năm quán tương tự trong khu vực lân cận. Cảm nhận về giá của thực khách cũng quan trọng không kém chi phí giá vốn.
4. **Kết hợp hài hòa (Blend)**: bắt đầu từ mức giá theo tỷ lệ mục tiêu, kiểm tra lại số tiền lợi nhuận góp thu về, sau đó đối chiếu tính hợp lý với mặt bằng thị trường.
## Làm tròn số và trình bày giá
- Làm tròn lên theo bậc giá mà thực đơn của bạn sử dụng (0.10, 0.50, hoặc số nguyên chẵn). Thực đơn nhà hàng cao cấp thường dùng số nguyên không kèm ký hiệu tiền tệ; thực đơn quán bình dân thường kết thúc bằng đuôi .50 hoặc .90.
- Tránh tăng giá lắt nhắt hàng loạt trên toàn bộ thực đơn cùng một lúc; hãy ưu tiên tăng giá những món có biên lợi nhuận yếu nhất trước.
- Giữ khoảng cách giá hợp lý: khoản phụ thu khi đổi sang sữa yến mạch phải bù đắp được phần chi phí tăng thêm của nó (sữa yến mạch thường đắt gấp đôi sữa bò tươi tính trên mỗi lít).
## Kỹ thuật thiết kế thực đơn (Menu engineering)
Cần có dữ liệu số lượng bán ra hàng tuần cho mọi món. Kịch bản sử dụng hai đường chuẩn:
- **Đường mức độ phổ biến (Popularity line)**: một món được coi là phổ biến (bán chạy) nếu lượng bán đạt ít nhất 70% của mức thị phần chia đều (ví dụ với 5 món: 0.7 x 20% = 14% tổng số phần bán ra).
- **Đường biên lợi nhuận (Margin line)**: mức lợi nhuận góp trung bình có trọng số theo sản lượng bán.
| Phân nhóm | Độ phổ biến | Lợi nhuận góp | Hành động cần làm |
| --- | --- | --- | --- |
| Star (Ngôi sao) | cao | cao | Giữ chất lượng và định lượng khẩu phần thật ổn định, đặt ở vị trí vàng trên thực đơn, việc tăng giá nhẹ thường khá an toàn. |
| Plowhorse (Ngựa cày) | cao | thấp | Tăng giá thêm một chút, cắt giảm chi phí (định lượng, đồ trang trí, đàm phán nhà cung cấp), hoặc bán kèm với một món phụ có biên lợi nhuận cao. Tuyệt đối không xóa khỏi thực đơn. |
| Puzzle (Câu đố) | thấp | cao | Đẩy mạnh quảng bá: đổi sang vị trí dễ nhìn hơn trên thực đơn, viết mô tả hấp dẫn, nhờ nhân viên gợi ý, thêm hình ảnh. Kiểm tra xem mức giá hiện tại có đang làm khách e ngại hay không. |
| Dog (Chó mực) | thấp | thấp | Cải tiến lại công thức hoặc mức giá, hoặc mạnh dạn loại bỏ khỏi thực đơn, trừ khi món đó phục vụ một mục đích riêng (thực đơn trẻ em, lựa chọn ăn kiêng, món biểu tượng). |
## Lựa chọn giải pháp cho món có chi phí nguyên liệu (food cost) cao
| Phương án | Phù hợp khi | Rủi ro |
| --- | --- | --- |
| Tăng giá bán | món ăn đang bán chạy và mặt bằng thị trường cho phép | lượng bán giảm xuống nếu mức tăng quá đột ngột |
| Giảm định lượng khẩu phần | khẩu phần hiện tại đang lớn hơn mức kỳ vọng của thực khách | khách hàng nhận ra sự sụt giảm; cần giữ vững cảm nhận về giá trị |
| Đổi nguyên liệu thay thế | có nguyên liệu khác rẻ hơn nhưng chất lượng tương đương | thay đổi về chất gây dị ứng và hương vị; cần cập nhật lại mô tả trên thực đơn |
| Chấp nhận tỷ lệ phần trăm cao hơn | số tiền lợi nhuận góp thu về cao nhất trên thực đơn | cần có sản lượng bán đủ lớn để bù đắp |
| Loại bỏ món ăn | món đó thuộc nhóm Dog và không có vai trò chiến lược nào | khách quen có thể sẽ tiếc nuối |
Luôn chạy lại công cụ tính toán với phương án thay đổi được đề xuất và trình bày kết quả so sánh trước và sau.
FILE:templates/recipe-costing-sheet.md
# Bảng tính giá công thức (đầu vào cho scripts/cost_menu.py)
Lưu dưới tên `menu.json`. Định lượng trong công thức là khối lượng đã sơ chế (sử dụng được ngay).
```json
{
"currency": "EUR",
"target_food_cost_pct": 30,
"menu_price_includes_tax_pct": 10,
"price_rounding": 0.10,
"ingredients": [
{"name": "flour", "price": 0.95, "per": "1 kg"},
{"name": "butter", "price": 9.80, "per": "1 kg"},
{"name": "eggs", "price": 3.60, "per": "12 each"},
{"name": "blueberries", "price": 16.00, "per": "1 kg", "yield_pct": 95}
],
"recipes": [
{
"name": "Blueberry Muffin",
"portions": 12,
"menu_price": 3.20,
"sold_per_week": 90,
"extras_per_portion": 0.06,
"items": [["flour", "500 g"], ["butter", "180 g"], ["eggs", "3 each"], ["blueberries", "300 g"]]
}
]
}
```
Ghi chú các trường dữ liệu:
- `per`: quy cách gói hàng bạn mua vào, viết dưới dạng "<số lượng> <đơn vị>" (g, kg, mg, ml, cl, dl, l, oz, lb, each).
- `yield_pct`: từ 1 đến 100, mặc định là 100.
- `menu_price_includes_tax_pct`: điền 0 nếu giá hiển thị trên thực đơn là giá chưa bao gồm thuế.
- `sold_per_week`: điền đầy đủ cho tất cả các món (hoặc bỏ trống tất cả) để nhận kết quả phân nhóm kỹ thuật thực đơn.
- `extras_per_portion`: chi phí bao bì, giấy ăn, đồ trang trí, hũ đựng sốt, tính bằng tiền.
Lệnh chạy: `python3 scripts/cost_menu.py menu.json [--target 30] [--json]`
---
# Báo cáo tính giá thực đơn: <tên cơ sở kinh doanh> - <ngày tháng>
**Mục tiêu food cost:** <x>% **Giá đã gồm thuế:** <thuế suất hoặc không> **Tiền tệ:** <mã tiền tệ>
**Các giả định:** <tỷ lệ thu hồi, báo giá còn thiếu, trọng lượng khẩu phần>
## Kết quả (trước khi điều chỉnh)
| Món ăn | Giá vốn/khẩu phần | Giá bán | Giá tịnh | Food % | Lợi nhuận góp | Giá tại mục tiêu | Phân nhóm |
| --- | --- | --- | --- | --- | --- | --- | --- |
## Kiểm tra công thức tính
<diễn giải chi tiết từng dòng cho một món>
## Những điểm cần lưu ý
1. **<món ăn>** - <phát hiện>. Các lựa chọn: <giá bán / khẩu phần / nguyên liệu / chấp nhận>. Khuyến nghị: <một phương án>.
## Đề xuất thay đổi và kết quả (sau khi điều chỉnh)
<nêu các thay đổi, sau đó hiển thị bảng mới hoặc các dòng đã thay đổi>
## Các hành động theo kỹ thuật thiết kế thực đơn
- Nhóm Star (Ngôi sao): <các món và hành động>
- Nhóm Plowhorse (Ngựa cày): <các món và hành động>
- Nhóm Puzzle (Câu đố): <các món và hành động>
- Nhóm Dog (Chó mực): <các món và hành động>
## Các bước tiếp theo
- <cân thực tế các khẩu phần, cập nhật giá nhà cung cấp, theo dõi food cost thực tế hàng tháng>
FILE:examples/example-cafe-menu.md
# Ví dụ: thực đơn quán cà phê gồm năm món
**Người dùng:** "Chúng tôi là một quán cà phê brunch nhỏ. Giá bán đã bao gồm 10% thuế VAT và tôi muốn tỷ lệ chi phí nguyên liệu (food cost) ở mức khoảng 30%. Dưới đây là giá nhà cung cấp và công thức các món của tôi. Tại sao biên lợi nhuận của tôi lại mỏng đến vậy?"
Bảng dữ liệu gồm 16 nguyên liệu và 5 món kèm doanh số bán hàng tuần (tỷ lệ thu hồi của bơ là 90% vì cứ khoảng 10 quả thì có 1 quả hỏng không dùng được; cá hồi đạt 85% sau khi cắt tỉa).
**Lệnh chạy:**
```bash
python3 scripts/cost_menu.py cafe-menu.json
```
**Kết quả đầu ra (trước khi điều chỉnh):**
```
ITEM COST PRICE NET FOOD% MARGIN AT TARGET CLASS
Avocado Toast 3.13 9.50 8.64 36.2 5.51 11.50 Star
Salmon Spinach Bowl 8.02 14.50 13.18 60.8 5.16 29.50 Puzzle
Flat White 0.72 3.80 3.45 21.0 2.73 2.70 Plowhorse
Oat Flat White 0.91 4.20 3.82 23.9 2.91 3.40 Plowhorse
Blueberry Muffin 0.79 3.20 2.91 27.2 2.12 3.00 Dog
Findings (8):
[HIGH] Salmon Spinach Bowl: food cost 60.8% is far above the 30% target; price EUR 29.50 or cut cost 4.06 per portion
[WARN] Avocado Toast: food cost 36.2% is above the 30% target; price at target would be EUR 11.50
[INFO] Salmon Spinach Bowl: salmon fillet is 76% of the cost; its price or portion matters most
[INFO] menu engineering: weighted average margin EUR 3.22, popularity line 14.0% of items sold
[INFO] ingredient 'truffle oil' is not used in any recipe
```
---
# Báo cáo tính giá thực đơn: quán cà phê brunch - Tháng 10
**Mục tiêu food cost:** 30% **Giá đã gồm thuế:** 10% VAT **Tiền tệ:** EUR
**Các giả định:** tỷ lệ thu hồi của bơ 90%, cá hồi 85%, cải bó xôi (spinach) 90%; chi phí phụ thêm (extras) 0.10 cho mỗi món ăn, 0.12 cho mỗi ly cà phê (ly giấy và nắp), 0.06 cho mỗi bánh muffin.
## Kiểm tra công thức tính (Salmon Spinach Bowl, trước khi điều chỉnh)
- Cá hồi 160 g x (32.00 / 1000 / 0.85) = 6.02
- Cải bó xôi 70 g x (14.00 / 1000 / 0.90) = 1.09; trứng 0.30; cà chua 0.36; dầu ô liu 0.15; chi phí phụ thêm 0.10
- Chi phí mỗi khẩu phần = 8.02; giá tịnh = 14.50 / 1.10 = 13.18; tỷ lệ food cost = 8.02 / 13.18 x 100 = 60.8%
## Những điểm cần lưu ý
1. **Salmon Spinach Bowl (HIGH, Puzzle)** - chi phí nguyên liệu chiếm tới 60.8%; riêng phi lê cá hồi chiếm 76% tổng giá vốn của món. Việc định giá đúng theo tỷ lệ mục tiêu (29.50) là phi thực tế đối với một quán cà phê. Khuyến nghị: giảm lượng cá hồi xuống 120 g (vẫn là một khẩu phần đầy đặn cho một tô bowl) và tăng giá bán lên 17.50; chủ động chấp nhận mức food cost khoảng 40% vì số tiền lợi nhuận góp thu về trên mỗi tô sẽ trở thành mức cao nhất trên toàn thực đơn.
2. **Avocado Toast (WARN, Star)** - 36.2%. Đây là món bán chạy nhất quán, do đó việc tăng thêm 1.00 lên mức 10.50 có rủi ro rất thấp.
3. **Flat White và Oat Flat White (Plowhorses)** - tỷ lệ phần trăm rất tốt (21 đến 24%) nhưng số tiền lợi nhuận góp trên mỗi ly nhỏ; tuyệt đối không giảm giá. Giữ nguyên mức phụ thu sữa yến mạch là 0.40: sữa yến mạch tốn chi phí cao hơn sữa tươi 0.19 trên mỗi ly.
4. **Blueberry Muffin (Dog)** - tỷ lệ phần trăm ổn, nhưng lợi nhuận góp thấp và lượng bán chậm. Hãy thử bán theo combo kèm cà phê trước khi quyết định loại bỏ.
5. **Dầu nấm cục (Truffle oil)** có tên trên bảng nguyên liệu nhưng không xuất hiện trong bất kỳ công thức nào: hãy ngừng đặt mua hoặc xóa khỏi bảng tính.
## Đề xuất thay đổi và kết quả (sau khi điều chỉnh)
Avocado Toast tăng lên 10.50; Salmon Spinach Bowl dùng 120 g cá hồi với giá bán 17.50. Chạy lại lệnh: `python3 scripts/cost_menu.py cafe-menu-revised.json`
```
ITEM COST PRICE NET FOOD% MARGIN AT TARGET CLASS
Avocado Toast 3.13 10.50 9.55 32.8 6.42 11.50 Star
Salmon Spinach Bowl 6.51 17.50 15.91 40.9 9.40 23.90 Puzzle
Flat White 0.72 3.80 3.45 21.0 2.73 2.70 Plowhorse
Oat Flat White 0.91 4.20 3.82 23.9 2.91 3.40 Plowhorse
Blueberry Muffin 0.79 3.20 2.91 27.2 2.12 3.00 Dog
Findings (6):
[WARN] Salmon Spinach Bowl: food cost 40.9% is above the 30% target; price at target would be EUR 23.90
[INFO] menu engineering: weighted average margin EUR 3.56, popularity line 14.0% of items sold
```
Mã thoát (Exit code) 0. Cảnh báo WARN còn lại được chấp nhận có chủ đích: lợi nhuận góp đạt 9.40 mỗi tô so với mức 5.16 trước đây.
## Các hành động theo kỹ thuật thiết kế thực đơn
- Nhóm Star (Ngôi sao): Avocado Toast - giữ chất lượng công thức thật đồng đều, đặt ở vị trí đầu tiên của mục brunch.
- Nhóm Plowhorse (Ngựa cày): Flat White, Oat Flat White - không giảm giá; gợi ý khách dùng kèm bánh ngọt cho mỗi ly cà phê.
- Nhóm Puzzle (Câu đố): Salmon Spinach Bowl - viết thêm dòng mô tả ngắn hấp dẫn và nhờ nhân viên giới thiệu cho khách; kiểm tra lại doanh số sau 4 tuần áp dụng mức giá mới.
- Nhóm Dog (Chó mực): Blueberry Muffin - thử nghiệm combo cà phê + bánh muffin trong 4 tuần rồi mới đưa ra quyết định cuối cùng.
## Các bước tiếp theo
- Cân thử năm khẩu phần cá hồi thực tế ngay trong tuần này để xác nhận bộ phận bếp tuân thủ đúng định lượng 120 g.
- Cập nhật giá nhà cung cấp hàng tháng và chạy lại bảng tính.
- Đối chiếu với chi phí thực phẩm thực tế từ số liệu kiểm kê kho vào cuối tháng.
FILE:scripts/cost_menu.py
#!/usr/bin/env python3
"""Cost menu items from recipes and purchase prices, and suggest menu prices.
Usage:
python3 cost_menu.py menu.json [--target 30] [--json]
cat menu.json | python3 cost_menu.py -
Input JSON (see templates/recipe-costing-sheet.md):
{
"currency": "EUR",
"target_food_cost_pct": 30, # optional, default 30 (or --target)
"menu_price_includes_tax_pct": 10, # optional; VAT/sales tax included in menu prices
"price_rounding": 0.10, # optional; suggested prices round UP to this step
"ingredients": [
{"name": "butter", "price": 9.80, "per": "1 kg", "yield_pct": 100}
],
"recipes": [
{"name": "Croissant", "portions": 12, "menu_price": 3.20, "sold_per_week": 180,
"extras_per_portion": 0.05, # optional: packaging, napkin, garnish
"items": [["butter", "600 g"], ["flour", "1 kg"]]}
]
}
Units: g, kg, mg, ml, cl, dl, l, oz, lb, each (also pc, pcs, piece, unit, egg).
Recipe quantities are the prepared (usable) amounts. yield_pct is the usable
share of what you buy after trimming, peeling, cooking loss or spoilage.
Per recipe: cost per portion, food cost percent of the net (tax-free) menu
price, contribution margin, suggested price at the target, and the three
biggest cost drivers. With sold_per_week on every recipe, adds a menu
engineering class (Star, Plowhorse, Puzzle, Dog).
Exit code: 0 ok, 1 errors in the data or HIGH findings, 2 usage or input error.
Standard library only.
"""
import json
import math
import re
import sys
UNITS = { # unit -> (dimension, factor to base unit g / ml / each)
"mg": ("mass", 0.001), "g": ("mass", 1.0), "kg": ("mass", 1000.0),
"oz": ("mass", 28.3495), "lb": ("mass", 453.592),
"ml": ("volume", 1.0), "cl": ("volume", 10.0), "dl": ("volume", 100.0), "l": ("volume", 1000.0),
"each": ("count", 1.0), "pc": ("count", 1.0), "pcs": ("count", 1.0), "piece": ("count", 1.0),
"pieces": ("count", 1.0), "unit": ("count", 1.0), "units": ("count", 1.0), "egg": ("count", 1.0), "eggs": ("count", 1.0),
}
BASE = {"mass": "g", "volume": "ml", "count": "each"}
def usage(msg):
print(f"error: {msg}\n", file=sys.stderr)
print(__doc__.strip().split("\n\n")[1], file=sys.stderr)
sys.exit(2)
def parse_qty(text):
"""'600 g' -> (600.0, 'mass', 600.0 in base units)."""
m = re.fullmatch(r"\s*(\d+(?:[.,]\d+)?)\s*([a-zA-Z]+)?\s*", str(text))
if not m:
raise ValueError(f"cannot read quantity {text!r}")
qty = float(m.group(1).replace(",", "."))
unit = (m.group(2) or "each").lower()
if unit not in UNITS:
raise ValueError(f"unknown unit {unit!r} in {text!r}")
dim, factor = UNITS[unit]
return qty, dim, qty * factor
def round_up(value, step):
return math.ceil(round(value / step, 6)) * step
def analyze(data, target_override=None):
errors, findings = [], []
cur = data.get("currency", "")
target = float(target_override or data.get("target_food_cost_pct", 30))
tax = float(data.get("menu_price_includes_tax_pct", 0))
step = float(data.get("price_rounding", 0.10))
ingredients = {}
for ing in data.get("ingredients", []):
name = str(ing.get("name", "")).strip().lower()
try:
_, dim, base_qty = parse_qty(ing["per"])
price = float(ing["price"])
except (KeyError, ValueError, TypeError) as e:
errors.append(f"ingredient {name or '?'}: {e}")
continue
y = float(ing.get("yield_pct", 100))
if not 0 < y <= 100:
errors.append(f"ingredient {name}: yield_pct must be between 1 and 100")
continue
ingredients[name] = {"dim": dim, "cost_per_base": price / base_qty / (y / 100), "yield": y, "used": False}
results = []
for rec in data.get("recipes", []):
rname = rec.get("name", "?")
portions = float(rec.get("portions", 1) or 1)
lines, bad = [], False
for item in rec.get("items", []):
iname, qtext = str(item[0]).strip().lower(), item[1]
ing = ingredients.get(iname)
if not ing:
errors.append(f"{rname}: unknown ingredient {iname!r} (add it to ingredients)")
bad = True
continue
ing["used"] = True
try:
_, dim, base_qty = parse_qty(qtext)
except ValueError as e:
errors.append(f"{rname}: {e}")
bad = True
continue
if dim != ing["dim"]:
errors.append(f"{rname}: {iname} is bought by {BASE[ing['dim']]} but used by {BASE[dim]} ({qtext}); "
f"convert it (for example weigh one piece)")
bad = True
continue
lines.append((iname, base_qty * ing["cost_per_base"]))
if bad:
continue
batch = sum(c for _, c in lines)
extras = float(rec.get("extras_per_portion", 0))
cost = batch / portions + extras
price = float(rec.get("menu_price", 0))
net = price / (1 + tax / 100) if price else 0.0
pct = 100 * cost / net if net else None
suggested = round_up(cost / (target / 100) * (1 + tax / 100), step)
drivers = sorted(lines, key=lambda x: -x[1])[:3]
r = {"name": rname, "portions": portions, "cost_per_portion": round(cost, 3), "menu_price": price,
"net_price": round(net, 2), "food_cost_pct": None if pct is None else round(pct, 1),
"contribution_margin": round(net - cost, 2) if net else None,
"suggested_price_at_target": round(suggested, 2), "sold_per_week": rec.get("sold_per_week"),
"drivers": [{"ingredient": n, "share_pct": round(100 * c / batch, 1) if batch else 0} for n, c in drivers]}
results.append(r)
if pct is None:
findings.append(("WARN", rname, f"no menu_price; suggested {cur} {suggested:.2f} at {target:g}% food cost"))
elif pct > target + 15:
findings.append(("HIGH", rname, f"food cost {pct:.1f}% is far above the {target:g}% target; "
f"price {cur} {suggested:.2f} or cut cost {cost - net * target / 100:.2f} per portion"))
elif pct > target + 5:
findings.append(("WARN", rname, f"food cost {pct:.1f}% is above the {target:g}% target; "
f"price at target would be {cur} {suggested:.2f}"))
elif pct < target - 15:
findings.append(("INFO", rname, f"food cost only {pct:.1f}%; check the recipe lists every ingredient and portion size"))
if drivers and batch and drivers[0][1] / batch > 0.5:
findings.append(("INFO", rname, f"{drivers[0][0]} is {100 * drivers[0][1] / batch:.0f}% of the cost; "
f"its price or portion matters most"))
sold = [r for r in results if isinstance(r["sold_per_week"], (int, float)) and r["contribution_margin"] is not None]
if sold and len(sold) == len(results) and len(sold) >= 3:
total = sum(r["sold_per_week"] for r in sold)
pop_line = 0.7 / len(sold)
avg_cm = sum(r["contribution_margin"] * r["sold_per_week"] for r in sold) / total if total else 0
for r in sold:
high_pop = total and r["sold_per_week"] / total >= pop_line
high_cm = r["contribution_margin"] >= avg_cm
r["menu_class"] = {(True, True): "Star", (True, False): "Plowhorse",
(False, True): "Puzzle", (False, False): "Dog"}[(bool(high_pop), high_cm)]
findings.append(("INFO", None, f"menu engineering: weighted average margin {cur} {avg_cm:.2f}, "
f"popularity line {100 * pop_line:.1f}% of items sold"))
for name, ing in ingredients.items():
if not ing["used"]:
findings.append(("INFO", None, f"ingredient {name!r} is not used in any recipe"))
return {"currency": cur, "target_pct": target, "tax_pct": tax, "recipes": results,
"errors": errors, "findings": [{"severity": s, "recipe": n, "message": m} for s, n, m in findings]}
def main(argv):
target, as_json, paths = None, False, []
it = iter(argv)
for a in it:
if a == "--target":
try:
target = float(next(it, ""))
except ValueError:
usage("--target needs a number, for example 30")
if not 5 <= target <= 80:
usage("--target should be a food cost percent between 5 and 80")
elif a == "--json":
as_json = True
elif a.startswith("--"):
usage(f"unknown option {a}")
else:
paths.append(a)
if len(paths) != 1:
usage("give exactly one menu JSON file, or - for stdin")
try:
raw = sys.stdin.read() if paths[0] == "-" else open(paths[0], encoding="utf-8").read()
data = json.loads(raw)
except (OSError, ValueError) as e:
print(f"error: cannot read menu JSON: {e}", file=sys.stderr)
return 2
if not data.get("recipes"):
print("error: no recipes in the input", file=sys.stderr)
return 2
rep = analyze(data, target)
if as_json:
print(json.dumps(rep, indent=2))
else:
cur = rep["currency"]
print(f"Target food cost {rep['target_pct']:g}% | prices include {rep['tax_pct']:g}% tax | currency {cur}\n")
print(f"{'ITEM':<22} {'COST':>7} {'PRICE':>7} {'NET':>7} {'FOOD%':>6} {'MARGIN':>7} {'AT TARGET':>9} CLASS")
for r in rep["recipes"]:
pct = "-" if r["food_cost_pct"] is None else f"{r['food_cost_pct']:.1f}"
cm = "-" if r["contribution_margin"] is None else f"{r['contribution_margin']:.2f}"
print(f"{r['name'][:22]:<22} {r['cost_per_portion']:>7.2f} {r['menu_price']:>7.2f} {r['net_price']:>7.2f} "
f"{pct:>6} {cm:>7} {r['suggested_price_at_target']:>9.2f} {r.get('menu_class', '-')}")
print("\nTop cost drivers:")
for r in rep["recipes"]:
print(f" {r['name']}: " + ", ".join(f"{d['ingredient']} {d['share_pct']:g}%" for d in r["drivers"]))
if rep["errors"]:
print(f"\nErrors ({len(rep['errors'])}):")
for e in rep["errors"]:
print(f" [ERROR] {e}")
print(f"\nFindings ({len(rep['findings'])}):")
order = {"HIGH": 0, "WARN": 1, "INFO": 2}
for f in sorted(rep["findings"], key=lambda f: order[f["severity"]]):
print(f" [{f['severity']}] {f['recipe'] + ': ' if f['recipe'] else ''}{f['message']}")
bad = rep["errors"] or any(f["severity"] == "HIGH" for f in rep["findings"])
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))Chuyển đổi các ghi chú và mô tả hình ảnh của một món đồ đã qua sử dụng thành một gói thông tin đăng bán lại theo chuẩn JSON nghiêm ngặt: phân loại tình trạng trung thực công khai mọi khuyết điểm, mức giá bán nhanh, giá niêm yết và giá sàn dựa trên bằng chứng thực tế, tiêu đề và mô tả bài đăng, danh sách kiểm tra ảnh chụp, câu hỏi thường gặp của người mua (FAQ), kế hoạch thương lượng, và các bước bàn giao an toàn.
1{2 "role": "Người bán hàng giàu kinh nghiệm trên sàn đồ cũ, chuyên viết các bài đăng bán hàng trung thực, chốt đơn nhanh và định giá món đồ đã qua sử dụng dựa trên bằng chứng thực tế",3 "task": "Chuyển đổi các ghi chú và mô tả ảnh chụp của tôi về một món đồ đã qua sử dụng thành một gói bài đăng bán lại hoàn chỉnh, trung thực dưới dạng một đối tượng JSON nghiêm ngặt duy nhất: phân loại tình trạng, định giá kèm lập luận, tiêu đề và mô tả sẵn sàng đăng lên các sàn thương mại, danh sách kiểm tra ảnh chụp, câu hỏi thường gặp của người mua, kế hoạch thương lượng, và các bước bàn giao an toàn.",4 "context": {5 "item": "${item:standing desk with electric height adjustment}",6 "brand_and_model": "${brand_model:unknown brand, about 140 x 70 cm top}",7 "age_and_usage": "${age_usage:bought three years ago, used daily in a home office}",8 "seller_notes": "${notes:small scratch on the front edge, one motor makes a light hum, original manual lost}",9 "photo_descriptions": "${photos:front view, top surface close-up, control panel, legs at full height}",10 "original_price": "${original_price:450 EUR}",...+122 dòng nữa
Biến danh sách những món đồ cần bán thành bản kế hoạch bán đồ cũ tại nhà (garage sale) định dạng YAML: quy tắc phân loại, hướng dẫn định giá bán nhanh kèm combo, lịch trình chuẩn bị, sơ đồ bố trí bàn, nội dung biển báo và bài đăng, thiết lập tiền mặt và thanh toán, phân công vai trò, lịch trình trong ngày diễn ra, các câu trả lời khi mặc cả, quy tắc an toàn và kế hoạch cho những món đồ không bán được.
1role: Người lập kế hoạch bán đồ cũ tại nhà (garage sale) ngăn nắp, nhiệt huyết, giúp các hộ gia đình dọn dẹp đồ đạc lộn xộn và kiếm được một khoản tiền hợp lý trong một ngày tổ chức bài bản2task: >3 Biến các ghi chú của tôi về những món đồ tôi muốn bán, không gian của tôi và ngày tổ chức thành một bản kế hoạch bán đồ cũ tại nhà hoàn chỉnh4 ở định dạng YAML: hướng dẫn phân loại và định giá, lịch trình chuẩn bị, sơ đồ bố trí khu vực bán hàng,5 nội dung biển báo và bài đăng rao bán, thiết lập tiền mặt và thanh toán, lịch trình trong ngày kèm phân công vai trò, quy tắc mặc cả,6 nhắc nhở an toàn, và kế hoạch xử lý cho mọi món đồ không bán được.78inputs:9 sale_date_and_hours: ${date:Saturday, 08:00 to 13:00}10 location: ${location:driveway and open garage of a house on a quiet residential street}...+83 dòng nữa
Tính toán mức đơn giá theo giờ và theo ngày mà một freelancer cần thu dựa trên mức thu nhập thực nhận mong muốn. Tính đủ thuế, chi phí kinh doanh, khoản tiết kiệm, kỳ nghỉ phép, ngày lễ, ngày ốm, tỷ lệ giờ làm tính phí thực tế và khoản đệm rủi ro; hiển thị sự biến đổi của đơn giá theo thời gian tính phí và định giá dự án kèm khoản dự phòng. Cảnh báo các mức giá thấp hơn điểm hòa vốn hoặc mục tiêu, số giờ tính phí thiếu thực tế và mức giá lệch chuẩn thị trường. Bao gồm công cụ tính viết bằng thư viện chuẩn Python có hỗ trợ --self-test.
---
name: freelance-hourly-rate-calculator
description: Tính toán mức đơn giá theo giờ và theo ngày mà một freelancer cần thu dựa trên mức thu nhập thực nhận mong muốn. Tính đủ thuế, chi phí kinh doanh, khoản tiết kiệm, kỳ nghỉ phép, ngày lễ, ngày ốm, tỷ lệ giờ làm tính phí thực tế và khoản đệm rủi ro; hiển thị sự biến đổi của đơn giá theo thời gian tính phí và định giá dự án kèm khoản dự phòng. Cảnh báo các mức giá thấp hơn điểm hòa vốn hoặc mục tiêu, số giờ tính phí thiếu thực tế và mức giá lệch chuẩn thị trường. Bao gồm công cụ tính viết bằng thư viện chuẩn Python có hỗ trợ --self-test.
---
# Công Cụ Tính Đơn Giá Theo Giờ Cho Freelancer
Bạn giúp các freelancer và những người sắp chuyển sang làm freelance tìm ra mức đơn giá đủ để trang trải cuộc sống. Hầu hết mọi người đều định giá quá thấp vì họ lấy mức lương chia cho 2080 giờ mà quên mất thuế, bảo hiểm, trang thiết bị, những ngày nghỉ lễ không lương, ngày nghỉ ốm cũng như số giờ phải dành cho việc tìm kiếm khách hàng và quản trị hành chính. Bạn tính ngược từ mức thu nhập thực nhận (take-home income) mà họ mong muốn để tìm ra đơn giá theo giờ tối thiểu, làm tròn thành mức giá có thể báo cho khách hàng, chỉ rõ mức giá hiện tại thực chất mang lại cho họ bao nhiêu tiền và định giá cho dự án tiếp theo của họ.
## Các tệp trong kỹ năng này
- `scripts/rate_calc.py` - tính toán doanh thu cần thiết, số giờ tính phí (billable hours), đơn giá tối thiểu, đơn giá báo khách, đơn giá theo ngày và đơn giá hòa vốn, bảng tỷ lệ giờ tính phí, khoảng cách chênh lệch của mức giá hiện tại và giá trọn gói dự án (chỉ dùng thư viện chuẩn Python 3)
- `references/rate-math.md` - các công thức tính từng bước, danh sách chi phí điển hình, tỷ lệ giờ tính phí theo loại hình công việc, hướng dẫn ước tính thuế sơ bộ và những sai lầm thường gặp
- `references/pricing-and-raising-rates.md` - cách định giá theo ngày, theo dự án, theo gói duy trì (retainer) và theo giá trị mang lại, cách gửi báo giá và cách tăng giá với khách hàng hiện tại
- `templates/rate-card.md` - bảng báo giá (rate card) gói gọn trong một trang gồm giá theo giờ, theo ngày, giá theo gói và các điều khoản
- `examples/ux-designer-rate-plan.json` - ví dụ về một nhà thiết kế UX tại Berlin muốn thu nhập thực nhận 48.000 EUR và hiện đang tính phí 65 EUR/giờ
## Quy trình thực hiện
### 1. Thu thập các con số
Hãy hỏi người dùng về: mức thu nhập thực nhận mong muốn mỗi năm (sau khi trừ thuế và chi phí kinh doanh), các khoản tiết kiệm muốn để dành riêng (lương hưu, quỹ dự phòng khẩn cấp), mức thuế suất thực tế và tỷ lệ đóng bảo hiểm xã hội (nếu chưa rõ, hãy dùng hướng dẫn ước tính trong `references/rate-math.md` và nhắc họ xác nhận lại với kế toán), mọi chi phí kinh doanh (bảo hiểm, phần mềm, thiết bị, không gian làm việc, kế toán, tiếp thị, đào tạo), số giờ làm việc mỗi tuần, số tuần nghỉ phép, số ngày nghỉ lễ, số ngày nghỉ ốm và học tập nâng cao, cùng tỷ lệ thời gian làm việc thực sự có thể tính phí cho khách hàng. Đồng thời hỏi thêm về mức giá hiện tại của họ, mức giá thị trường phổ biến cho lĩnh vực và khu vực của họ, cũng như ước tính cho một dự án sắp tới nếu có.
### 2. Chạy công cụ tính toán
```
python3 scripts/rate_calc.py examples/ux-designer-rate-plan.json
python3 scripts/rate_calc.py plan.json --round 10 --json
python3 scripts/rate_calc.py --self-test
```
Mã thoát (exit code) 1 nghĩa là có phát hiện mức độ HIGH, 2 nghĩa là dữ liệu đầu vào không hợp lệ.
### 3. Giải thích kết quả
Mở đầu bằng mức đơn giá tối thiểu và mức đơn giá dùng để báo khách, sau đó phân tích chuỗi tính toán từ thu nhập thực nhận đến tổng doanh thu để họ thấy rõ dòng tiền đi đâu. Sử dụng bảng tỷ lệ giờ tính phí để chứng minh lý do vì sao mức 60% thời gian tính phí là con số mặc định thực tế. Nếu mức giá hiện tại quá thấp, hãy chỉ ra khoản thiếu hụt và số giờ tính phí cần thiết để bù đắp khoản đó, sau đó đưa ra các phương án từ `references/pricing-and-raising-rates.md`: ưu tiên tăng giá cho khách hàng mới trước, chuyển sang định giá theo ngày hoặc theo dự án, cắt giảm chi phí hoặc hạ thấp mục tiêu thu nhập.
### 4. Bàn giao
Điền vào mẫu `templates/rate-card.md` với đơn giá theo giờ, theo ngày và theo gói dịch vụ, các điều khoản thanh toán cùng ngày rà soát điều chỉnh giá tiếp theo.
## Ví dụ kết quả
Đối với `examples/ux-designer-rate-plan.json`: 48.000 EUR thu nhập thực nhận + 7.800 EUR khoản để dành = 83.284 EUR lợi nhuận trước mức thuế 33%; + 13.500 EUR chi phí và 8% khoản đệm rủi ro = 105.200 EUR doanh thu. 214 ngày làm việc với 60% thời gian tính phí = 1027 giờ tính phí, do đó mức tối thiểu là 102,41 EUR/giờ: nên báo giá 105 EUR/giờ hoặc 840 EUR/ngày. Với mức giá hiện tại 65 EUR/giờ, thu nhập thực nhận sẽ chỉ đạt 24.311 EUR (thiếu hụt 23.689 EUR), và để đạt được mục tiêu thì cần tới 94,5% thời gian làm việc đều tính phí được (HIGH BELOW_REQUIRED). Dự án thiết kế lại ứng dụng phòng khám dài 120 giờ cộng thêm 20% dự phòng sẽ có giá 15.120 EUR thay vì 9.360 EUR.
## Quy tắc
- Các con số về thuế chỉ là ước tính sơ bộ, không phải tư vấn thuế chuyên nghiệp; hãy nhắc người dùng xác nhận mức thuế suất với kế toán tại địa phương.
- Tuyệt đối không giả định thời gian tính phí vượt quá 70% nếu không có bằng chứng từ dữ liệu theo dõi thời gian thực tế của chính họ.
- Hiển thị rõ các công thức và dữ liệu đầu vào để họ có thể thay đổi một giả định và chạy lại.
- Mức giá thấp hơn thị trường thì có thể tăng lên; nhưng nếu mức tối thiểu cao hơn mặt bằng thị trường thì cần thay đổi gói giải pháp cung cấp chứ không chỉ đơn thuần đưa ra một con số cao hơn.
- Sử dụng đúng đơn vị tiền tệ và bối cảnh thuế mà họ cung cấp; không trộn lẫn các loại tiền tệ.
FILE:references/rate-math.md
# Công Thức Tính Đơn Giá
## Chuỗi công thức chi tiết từng bước
1. **Số tiền cần có sau thuế** = thu nhập thực nhận mục tiêu + các khoản để dành (lương hưu, quỹ khẩn cấp, mục tiêu tiết kiệm)
2. **Lợi nhuận trước thuế** = số tiền cần có sau thuế / (1 - thuế suất thực tế và tỷ lệ đóng bảo hiểm xã hội)
3. **Doanh thu cần thiết** = (lợi nhuận trước thuế + chi phí kinh doanh) / (1 - khoản đệm rủi ro)
4. **Số ngày làm việc** = 260 ngày trong tuần - số tuần nghỉ phép x 5 - số ngày nghỉ lễ - số ngày nghỉ ốm - số ngày đào tạo
5. **Số giờ tính phí (Billable hours)** = số ngày làm việc x số giờ làm mỗi tuần / 5 x tỷ lệ thời gian tính phí
6. **Đơn giá tối thiểu** = doanh thu cần thiết / số giờ tính phí
7. **Đơn giá báo khách** = đơn giá tối thiểu làm tròn lên (theo bội số của 5 hoặc 10); **đơn giá theo ngày** = đơn giá báo khách x số giờ làm mỗi ngày
8. **Đơn giá hòa vốn** = chi phí kinh doanh / (1 - khoản đệm) / số giờ tính phí (chỉ đủ trả các hóa đơn chi phí, bạn không nhận được đồng nào)
Kiểm tra ngược: thu nhập thực nhận tại mức giá R = (R x số giờ tính phí x (1 - khoản đệm) - chi phí) x (1 - thuế) - các khoản để dành.
## Tại sao công thức "lương / 2080" lại sai lầm
2080 = 52 tuần x 40 giờ. Một freelancer không hề được trả tiền cho bất kỳ khoản nào sau đây: kỳ nghỉ phép (20-30 ngày), ngày nghỉ lễ (8-14 ngày), ngày nghỉ ốm (5-10 ngày), thời gian học tập nâng cao (3-10 ngày), và 30-50% thời gian làm việc phải dành cho việc bán hàng, viết đề xuất (proposal), hành chính, xuất hóa đơn và học hỏi. Mức lương 60.000 khi đi làm công ăn lương còn đi kèm với khoản đóng góp hưu trí và bảo hiểm xã hội từ phía công ty, nghỉ phép có lương và trang thiết bị làm việc. Quy tắc ước lượng thông thường là đơn giá theo giờ = tiền lương / 1000 đến 1200, con số này khá sát với kết quả của phép tính đầy đủ.
## Tỷ lệ giờ tính phí theo loại hình công việc
| Tình huống | Tỷ lệ giờ tính phí thực tế |
|---|---|
| Freelancer mới, vẫn đang tìm kiếm khách hàng | 35-50% |
| Kết hợp nhiều dự án, có khách hàng quen | 50-65% |
| Hợp đồng toàn thời gian dài hạn với một khách hàng | 70-85% (nhưng hoạt động bán hàng bị ngưng trệ, và khoảng trống sau khi kết thúc hợp đồng thường kéo dài) |
| Mô hình kiểu agency với khối lượng quản lý tài khoản khách hàng lớn | 45-60% |
Cách đo lường: theo dõi mọi giờ làm việc trong vòng 4-8 tuần, sau đó sử dụng tỷ lệ thực tế thu được.
## Các chi phí kinh doanh điển hình
- Bảo hiểm y tế, bảo hiểm mất khả năng lao động và bảo hiểm trách nhiệm nghề nghiệp (khi phải tự đóng)
- Khoản đóng quỹ hưu trí nếu bắt buộc (hoặc đưa vào mục các khoản để dành)
- Phần cứng tính theo chu kỳ thay mới (laptop 3 năm, điện thoại 2-3 năm), thuê bao phần mềm, dịch vụ đám mây và tên miền
- Không gian làm việc: văn phòng chia sẻ (coworking), phần chi phí văn phòng tại nhà, internet, điện thoại
- Kế toán, phí ngân hàng, phí cổng thanh toán, công cụ xuất hóa đơn
- Tiếp thị: trang web, hồ sơ năng lực (portfolio), phí nền tảng, sự kiện, chi phí đi lại gặp khách hàng
- Đào tạo: khóa học, sách vở, hội thảo
- Phí thành viên hiệp hội nghề nghiệp, giấy phép, mẫu hợp đồng pháp lý
## Thuế suất thực tế và tỷ lệ đóng bảo hiểm (chỉ mang tính tham khảo sơ bộ)
Tỷ lệ thực tế là tổng thuế thu nhập và các khoản đóng bảo hiểm xã hội bắt buộc chia cho lợi nhuận. Con số này phụ thuộc vào quốc gia, khu vực, loại hình pháp lý, hoàn cảnh gia đình và các khoản giảm trừ. Ước tính sơ bộ ban đầu cho một người độc thân có lợi nhuận khoảng 60.000-100.000 tại Châu Âu hoặc Bắc Mỹ thường rơi vào khoảng 25-40%; các chế độ thuế thấp và chương trình thuế khoán có thể thấp hơn nhiều. Luôn luôn xác nhận với kế toán địa phương và thay thế con số ước tính này.
## Khoản đệm rủi ro
Dùng để bù đắp cho các hóa đơn thanh toán chậm hoặc bị nợ xấu, khoảng trống giữa các đợt khách hàng, phần phạm vi công việc phát sinh không thể tính phí và những tháng vắng khách. Đặt ở mức 5% đối với các hợp đồng duy trì (retainer) ổn định, 10-15% đối với công việc theo dự án làm cùng khách hàng mới.
## Những sai lầm thường gặp
- Coi tổng doanh thu gộp là thu nhập bỏ túi (thuế và chi phí sẽ bị trừ đi sau đó)
- Lên kế hoạch như thể 100% số giờ làm việc đều tính phí được
- Quên rằng thuế GTGT (VAT/sales tax) không phải là thu nhập (các mức giá tính ở đây đều chưa bao gồm VAT)
- Không tính chi phí ngày nghỉ vào giá: mỗi tuần nghỉ ngơi đều phải được bù đắp bởi những tuần làm việc
- Giảm giá theo giờ cho các dự án dài hạn mà không có cam kết đảm bảo khối lượng công việc
FILE:references/pricing-and-raising-rates.md
# Các Mô Hình Định Giá Và Cách Tăng Giá
## Lựa chọn mô hình định giá
| Mô hình | Khi nào nên dùng | Cách tính từ đơn giá tối thiểu |
|---|---|---|
| Theo giờ (Hourly) | Phạm vi chưa rõ ràng, công việc hỗ trợ, chỉnh sửa nhỏ | Đơn giá báo khách; tính phí theo từng bước 15 hoặc 30 phút |
| Theo ngày (Day rate) | Buổi workshop, làm việc cắm chốt cùng nhóm khách hàng, hợp đồng ngắn hạn | Đơn giá báo khách x số giờ mỗi ngày; nửa ngày = 55-60% của cả ngày (do chi phí chuyển đổi ngữ cảnh) |
| Theo dự án (Giá cố định) | Đầu mục bàn giao rõ ràng, bạn ước lượng thời gian chuẩn xác | Số giờ ước tính x (1 + tỷ lệ dự phòng) x đơn giá báo khách; liệt kê rõ những gì bao gồm trong gói và số vòng chỉnh sửa |
| Gói duy trì (Retainer) | Công việc diễn ra đều đặn hàng tháng | Số giờ đặt trước mỗi tháng x đơn giá báo khách, thường giảm 5-10% nhờ khối lượng được cam kết; số giờ không dùng hết sẽ hết hạn |
| Dựa trên giá trị (Value-based) | Công việc của bạn tạo ra doanh thu hoặc khoản tiết kiệm đo lường được | 5-20% giá trị mang lại trong năm đầu tiên, tuyệt đối không thấp hơn giá tính theo dự án |
Tỷ lệ dự phòng (contingency) cho giá cố định: 10-15% đối với công việc bạn đã làm nhiều lần, 20-30% đối với loại công việc mới, 30-50% khi yêu cầu chưa rõ ràng hoặc có nhiều bên liên quan phê duyệt.
## Cách gửi báo giá
- Báo một con số cụ thể, không báo một khoảng giá; khoảng giá sẽ luôn bị đàm phán ép xuống mức thấp nhất.
- Khi định giá theo dự án, hãy hiển thị mức giá theo từng gói hoặc hạng mục bàn giao, không hiển thị số giờ.
- Đưa các điều khoản thanh toán vào báo giá: tạm ứng trước 30-50% đối với khách hàng mới, hạn thanh toán trong 14 ngày (net 14) hoặc 30 ngày (net 30), phí trả chậm, và quy định xử lý khi phạm vi công việc thay đổi.
- Nêu rõ đơn giá chưa bao gồm thuế GTGT (VAT/sales tax).
## Khi đơn giá tối thiểu thấp hơn thị trường
Hãy báo giá ít nhất bằng mức trung bình của thị trường cho cấp độ kỹ năng của bạn. Thu phí thấp hơn sẽ phát đi tín hiệu về chất lượng thấp và không để lại khoản dư nào cho những tháng ít việc.
## Khi đơn giá tối thiểu cao hơn thị trường
Chỉ đưa ra một con số cao hơn sẽ không thuyết phục được khách hàng. Các phương án giải quyết:
1. Chuyên môn hóa sâu (theo ngành nghề hoặc vấn đề cụ thể) để cạnh tranh với ít người hơn và có thể định giá cao hơn mặt bằng chung.
2. Đóng gói theo kết quả đầu ra và áp dụng định giá theo dự án hoặc theo giá trị.
3. Cắt giảm những chi phí không giúp mang lại công việc.
4. Tăng tỷ lệ giờ tính phí bằng các hợp đồng duy trì (retainer) và làm việc với ít khách hàng hơn nhưng quy mô lớn hơn.
5. Hạ mục tiêu thu nhập trong năm đầu tiên và lên lộ trình tăng giá theo từng giai đoạn.
## Cách tăng giá với khách hàng hiện tại
- Khách hàng mới áp dụng ngay mức giá mới.
- Khách hàng hiện tại: thông báo trước 30-60 ngày, nêu rõ ngày bắt đầu áp dụng, lý do ngắn gọn (thêm kinh nghiệm, phạm vi công việc, mặt bằng thị trường) và không cần xin lỗi.
- Mỗi lần tăng khoảng 10-20%, mỗi năm một lần hoặc khi bạn đã kín lịch làm việc từ 3 tháng trở lên.
- Đề xuất giữ nguyên mức giá cũ nếu khách hàng mua trả trước một gói giờ làm việc hoặc ký hợp đồng retainer dài hạn hơn khi bạn muốn giữ chân một khách hàng trọng điểm.
Mẫu thông báo tham khảo:
> Kể từ ngày 1 tháng 1, đơn giá của tôi cho các công việc mới sẽ là 105 EUR mỗi giờ (840 EUR mỗi ngày). Các công việc đã thỏa thuận trước đó vẫn giữ nguyên mức giá hiện tại cho đến khi hoàn thành. Cảm ơn sự hợp tác tuyệt vời của quý vị trong năm qua; tôi rất mong chờ được đồng hành trong các dự án tiếp theo.
## Các dấu hiệu cho thấy mức giá đang quá thấp
- Bạn kín lịch làm việc trong nhiều tháng liền và không ai thắc mắc hay phàn nàn về giá cả.
- Bạn thắng thầu gần như mọi bản đề xuất (proposal) gửi đi.
- Bạn không dám nghỉ phép hoặc không đủ khả năng chi trả cho việc học tập nâng cao.
FILE:templates/rate-card.md
# Bảng Báo Giá (Rate Card) - <name>, <role>
**Có hiệu lực từ:** <date> **Kỳ rà soát tiếp theo:** <date + 12 months> **Tiền tệ:** <EUR>, giá chưa bao gồm VAT
## Đơn giá cơ bản
| Dịch vụ | Mức giá |
|---|---|
| Đơn giá theo giờ | <105> mỗi giờ |
| Đơn giá theo ngày (8 giờ) | <840> mỗi ngày |
| Nửa ngày (4 giờ) | <480> |
| Gói duy trì hàng tháng (<20> giờ, giờ không dùng hết sẽ hết hạn) | <2,000> mỗi tháng |
## Các gói dịch vụ trọn gói
| Gói dịch vụ | Bao gồm | Mức giá |
|---|---|---|
| <UX audit> | <đánh giá chuyên sâu 10 màn hình chính, báo cáo sắp xếp theo mức độ ưu tiên, cuộc gọi trao đổi 1 giờ> | <2,400> |
| <App redesign> | <nghiên cứu, wireframe, thiết kế UI cho <n> màn hình, 2 vòng chỉnh sửa, hệ thống thiết kế (design system)> | <15,120> |
## Điều khoản
- Thanh toán: <tạm ứng 40%, phần còn lại khi bàn giao>, hóa đơn thanh toán trong vòng <14> ngày; phí trả chậm <x%> mỗi tháng
- Thay đổi phạm vi công việc: báo giá riêng trước khi bắt đầu thực hiện
- Chỉnh sửa: bao gồm <2> vòng chỉnh sửa, sau đó tính phí theo giờ
- Hủy dịch vụ: <thời gian báo trước / phí hủy>
- Đi lại: <ngoài thành phố tính theo chi phí thực tế + thời gian di chuyển tính bằng 50% đơn giá>
## Các con số hậu trường (riêng tư, không gửi cho khách hàng)
- Đơn giá tối thiểu: <102.41>/giờ ở mức <60>% thời gian tính phí (<1027> giờ/năm)
- Đơn giá hòa vốn: <14.29>/giờ
- Thu nhập thực nhận mục tiêu: <48,000>, doanh thu cần thiết: <105,200>
FILE:examples/ux-designer-rate-plan.json
{
"name": "Deniz, freelance UX/UI designer (Berlin)",
"currency": "EUR",
"target_net_income": 48000,
"set_asides": [
{"name": "retirement", "annual": 6000},
{"name": "emergency fund top-up", "monthly": 150}
],
"tax": {"effective_rate_pct": 33},
"business_costs": [
{"name": "health and care insurance (self-paid)", "monthly": 520},
{"name": "coworking desk", "monthly": 190},
{"name": "software (Figma, Adobe, Notion)", "monthly": 95},
{"name": "laptop and display (3-year write-off)", "annual": 1100},
{"name": "accountant", "monthly": 110},
{"name": "liability insurance", "annual": 280},
{"name": "website, portfolio and domain", "annual": 240},
{"name": "courses and conferences", "annual": 900}
],
"time": {
"hours_per_week": 40,
"vacation_weeks": 5,
"public_holidays_days": 10,
"sick_days": 6,
"training_days": 5,
"billable_pct": 60,
"hours_per_day": 8
},
"risk_buffer_pct": 8,
"current_rate": 65,
"market_rate": {"low": 70, "high": 110},
"project": {"name": "Clinic booking app redesign", "estimated_hours": 120, "contingency_pct": 20}
}
FILE:scripts/rate_calc.py
#!/usr/bin/env python3
"""Freelance hourly rate calculator (Python 3 standard library only).
Works backwards from the take-home income you want to the hourly and day rate you must charge,
counting taxes, business costs, unpaid time and a risk buffer. Compares the result with your
current rate and the market, and prices a project.
Input JSON (amounts per year unless "monthly" is given):
{"name": "...", "currency": "EUR",
"target_net_income": 48000, # take-home after tax and costs
"set_asides": [{"name": "retirement", "annual": 6000}], (optional, after tax)
"tax": {"effective_rate_pct": 32}, # income tax + social contributions on profit
"business_costs": [{"name": "coworking", "monthly": 180}, {"name": "laptop", "annual": 2400}],
"time": {"hours_per_week": 40, "vacation_weeks": 5, "public_holidays_days": 10,
"sick_days": 6, "training_days": 5, "billable_pct": 60, "hours_per_day": 8},
"risk_buffer_pct": 10, # unpaid invoices, gaps between clients
"current_rate": 55, "market_rate": {"low": 60, "high": 95}, (optional)
"project": {"name": "...", "estimated_hours": 120, "contingency_pct": 20}} (optional)
Formulas:
needed after tax = target net + set-asides
profit before tax = needed after tax / (1 - tax rate)
revenue needed = (profit before tax + business costs) / (1 - risk buffer)
working days = 260 - vacation weeks x 5 - holidays - sick days - training days
billable hours = working days x hours per week / 5 x billable %
minimum rate = revenue needed / billable hours; quoted rate = minimum rounded up to
the next --round (default 5); project price rounded up to 10; day rate = quoted x hours per day
break-even rate = business costs / (1 - buffer) / billable hours (no income at all)
net at a rate = (rate x billable hours x (1 - buffer) - costs) x (1 - tax) - set-asides
Findings (severity):
HIGH NO_TAX tax rate missing or 0
HIGH BELOW_BREAK_EVEN current rate does not even cover business costs
HIGH BELOW_REQUIRED current rate below the minimum rate (yearly shortfall shown)
MEDIUM ABOVE_MARKET minimum rate above the market high: niche, value pricing or lower target
MEDIUM BILLABLE_HIGH billable share above 75% of working time
MEDIUM LONG_HOURS more than 50 hours per week
MEDIUM SHORT_VACATION under 3 weeks of vacation
LOW UNDER_MARKET quoted rate below the market low: you can likely charge more
LOW NO_BUFFER risk buffer 0
LOW NO_SICK_DAYS no sick days planned
LOW NO_CONTINGENCY project quote without contingency
Usage:
python3 rate_calc.py plan.json
python3 rate_calc.py plan.json --round 10 --json
python3 rate_calc.py --self-test
Exit code: 0 no HIGH findings, 1 HIGH findings, 2 bad input.
"""
import argparse
import json
import math
import sys
SEV_ORDER = {"HIGH": 0, "MEDIUM": 1, "LOW": 2}
class DataError(ValueError):
pass
def annual(item):
if "annual" in item:
return float(item["annual"])
if "monthly" in item:
return float(item["monthly"]) * 12
raise DataError(f"cost '{item.get('name', '?')}' needs 'annual' or 'monthly'")
def round_up(x, step):
return math.ceil(x / step - 1e-9) * step
def calc(cfg, step=5):
t = cfg.get("time") or {}
hpw = float(t.get("hours_per_week", 40))
hpd = float(t.get("hours_per_day", hpw / 5))
billable = float(t.get("billable_pct", 60)) / 100
vac, hol = float(t.get("vacation_weeks", 4)), float(t.get("public_holidays_days", 10))
sick, train = float(t.get("sick_days", 5)), float(t.get("training_days", 0))
tax = float((cfg.get("tax") or {}).get("effective_rate_pct", 0)) / 100
buffer = float(cfg.get("risk_buffer_pct", 0)) / 100
if "target_net_income" not in cfg:
raise DataError("target_net_income is required")
net = float(cfg["target_net_income"])
if not (0 <= tax < 0.9) or not (0 <= buffer < 0.5):
raise DataError("tax must be 0-90% and risk buffer 0-50%")
if not (0 < billable <= 1) or hpw <= 0:
raise DataError("billable_pct must be 1-100 and hours_per_week > 0")
costs = [{"name": c.get("name", "cost"), "annual": annual(c)} for c in cfg.get("business_costs", [])]
sets = [{"name": s.get("name", "set-aside"), "annual": annual(s)} for s in cfg.get("set_asides", [])]
cost_total = sum(c["annual"] for c in costs)
set_total = sum(s["annual"] for s in sets)
working_days = 260 - vac * 5 - hol - sick - train
if working_days <= 0:
raise DataError("no working days left after vacation, holidays, sick and training days")
avail = working_days * hpw / 5
bill_h = avail * billable
after_tax = net + set_total
pre_tax = after_tax / (1 - tax)
revenue = (pre_tax + cost_total) / (1 - buffer)
min_rate = revenue / bill_h
rate = round_up(min_rate, step)
breakeven = cost_total / (1 - buffer) / bill_h
def net_at(r, hours=bill_h):
return (r * hours * (1 - buffer) - cost_total) * (1 - tax) - set_total
sens = []
for pct in (40, 50, 60, 70, 80):
h = avail * pct / 100
sens.append({"billable_pct": pct, "billable_hours": round(h), "min_rate": round(revenue / h, 2)})
res = {"name": cfg.get("name", "freelancer"), "currency": cfg.get("currency", ""),
"costs": costs, "set_asides": sets, "business_costs": round(cost_total, 2), "set_aside_total": round(set_total, 2),
"target_net_income": net, "profit_before_tax": round(pre_tax, 2), "revenue_needed": round(revenue, 2),
"working_days": working_days, "available_hours": round(avail, 1), "billable_hours": round(bill_h, 1),
"billable_pct": billable * 100, "min_rate": round(min_rate, 2), "quoted_rate": rate,
"day_rate": round_up(rate * hpd, step), "hours_per_day": hpd, "break_even_rate": round(breakeven, 2),
"net_at_quoted": round(net_at(rate), 2), "sensitivity": sens, "findings": []}
findings = res["findings"]
def add(sev, code, msg):
findings.append({"severity": sev, "code": code, "message": msg})
cur = cfg.get("current_rate")
cur_info = None
if cur:
cur = float(cur)
hours_needed = revenue / cur
cur_info = {"rate": cur, "net_income": round(net_at(cur), 2), "shortfall": round(net - net_at(cur), 2),
"billable_hours_needed": round(hours_needed), "billable_pct_needed": round(hours_needed / avail * 100, 1)}
res["current"] = cur_info
if tax == 0:
add("HIGH", "NO_TAX", "no tax rate given; income tax and social contributions usually take 25-45% of profit")
if cur_info and cur < breakeven:
add("HIGH", "BELOW_BREAK_EVEN", f"current rate {cur:g} is below the break-even rate {breakeven:.2f}; every billed hour loses money")
elif cur_info and cur < min_rate:
add("HIGH", "BELOW_REQUIRED", f"current rate {cur:g} is {min_rate - cur:.2f} below the minimum {min_rate:.2f}; take-home falls {cur_info['shortfall']:,.0f} short "
f"(you would need {cur_info['billable_hours_needed']} billable hours, {cur_info['billable_pct_needed']}% of working time)")
m = cfg.get("market_rate") or {}
if m.get("high") and min_rate > float(m["high"]):
add("MEDIUM", "ABOVE_MARKET", f"minimum rate {min_rate:.2f} is above the market high {m['high']}; specialise, price by value, cut costs or lower the target")
elif m.get("low") and rate < float(m["low"]):
add("LOW", "UNDER_MARKET", f"quoted rate {rate:g} is below the market low {m['low']}; quote at least {m['low']}")
if billable > 0.75:
add("MEDIUM", "BILLABLE_HIGH", f"{billable * 100:.0f}% billable leaves too little time for sales, admin and learning; 50-70% is realistic")
if hpw > 50:
add("MEDIUM", "LONG_HOURS", f"{hpw:g} hours per week is not sustainable for a year")
if vac < 3:
add("MEDIUM", "SHORT_VACATION", f"only {vac:g} weeks of vacation planned; nobody pays you for holidays, so price them in")
if buffer == 0:
add("LOW", "NO_BUFFER", "no risk buffer for unpaid invoices and gaps between clients; 5-15% is common")
if sick == 0:
add("LOW", "NO_SICK_DAYS", "no sick days planned; budget at least 5")
p = cfg.get("project")
if p:
hrs = float(p.get("estimated_hours", 0))
cont = float(p.get("contingency_pct", 0)) / 100
res["project"] = {"name": p.get("name", "project"), "estimated_hours": hrs, "contingency_pct": cont * 100,
"quoted_hours": round(hrs * (1 + cont), 1), "price": round_up(hrs * (1 + cont) * rate, 10),
"price_at_current": round_up(hrs * (1 + cont) * cur, 10) if cur else None}
if cont == 0:
add("LOW", "NO_CONTINGENCY", "project estimate has no contingency; add 15-30% for unknowns and revisions")
findings.sort(key=lambda f: SEV_ORDER[f["severity"]])
return res
def money(x, cur=""):
return f"{x:,.0f} {cur}".strip() if abs(x) >= 100 else f"{x:,.2f} {cur}".strip()
def rate_fmt(x, cur=""):
return f"{x:,.2f} {cur}".strip()
def render(r):
c = r["currency"]
out = [f"# Freelance rate plan: {r['name']}", "",
f"Minimum rate {rate_fmt(r['min_rate'], c)}/h -> quote **{rate_fmt(r['quoted_rate'], c)}/h**, day rate {money(r['day_rate'], c)} ({r['hours_per_day']:g} h)",
f"Break-even (covers costs, no income): {rate_fmt(r['break_even_rate'], c)}/h | take-home at the quoted rate: {money(r['net_at_quoted'], c)}/year", "",
"## From take-home to revenue", "| Step | Amount |", "|---|---|",
f"| Target take-home | {money(r['target_net_income'], c)} |",
f"| + set-asides ({', '.join(s['name'] for s in r['set_asides']) or 'none'}) | {money(r['set_aside_total'], c)} |",
f"| = profit before tax | {money(r['profit_before_tax'], c)} |",
f"| + business costs | {money(r['business_costs'], c)} |",
f"| = revenue needed (incl. risk buffer) | {money(r['revenue_needed'], c)} |", "",
"## Time", f"{r['working_days']:g} working days, {r['available_hours']:g} working hours, {r['billable_pct']:g}% billable = **{r['billable_hours']:g} billable hours**", "",
"| Billable share | Billable hours | Minimum rate |", "|---|---|---|"]
for s in r["sensitivity"]:
out.append(f"| {s['billable_pct']}% | {s['billable_hours']} | {rate_fmt(s['min_rate'], c)} |")
out += ["", "## Business costs", "| Cost | Per year |", "|---|---|"]
for k in r["costs"]:
out.append(f"| {k['name']} | {money(k['annual'], c)} |")
if r.get("current"):
cu = r["current"]
out += ["", "## Current rate", f"At {rate_fmt(cu['rate'], c)}/h: take-home {money(cu['net_income'], c)}/year "
f"({'short by ' + money(cu['shortfall'], c) if cu['shortfall'] > 0 else 'target met'}); target needs {cu['billable_hours_needed']} billable hours ({cu['billable_pct_needed']}% of working time)"]
if r.get("project"):
p = r["project"]
out += ["", f"## Project quote: {p['name']}", f"{p['estimated_hours']:g} h + {p['contingency_pct']:g}% contingency = {p['quoted_hours']:g} h x {rate_fmt(r['quoted_rate'], c)} -> **{money(p['price'], c)}**"
+ (f" (at the current rate: {money(p['price_at_current'], c)})" if p.get("price_at_current") else "")]
out += ["", "## Findings"]
if not r["findings"]:
out.append("- none")
for f in r["findings"]:
out.append(f"- {f['severity']} {f['code']}: {f['message']}")
return "\n".join(out) + "\n"
def self_test():
tests, ok = [], 0
def check(name, cond):
nonlocal ok
tests.append(name)
print(("PASS " if cond else "FAIL ") + name)
ok += bool(cond)
base = {"target_net_income": 50000, "tax": {"effective_rate_pct": 30}, "business_costs": [{"name": "x", "annual": 10000}],
"time": {"hours_per_week": 40, "vacation_weeks": 4, "public_holidays_days": 10, "sick_days": 5, "training_days": 5, "billable_pct": 60},
"risk_buffer_pct": 10}
r = calc(base)
check("working days 260-20-10-5-5 = 220", r["working_days"] == 220)
check("billable hours 220 x 8 x 60% = 1056", r["billable_hours"] == 1056)
check("profit before tax 50000/0.7", abs(r["profit_before_tax"] - 71428.57) < 0.01)
check("revenue (71428.57+10000)/0.9", abs(r["revenue_needed"] - 90476.19) < 0.01)
check("minimum rate 90476.19/1056 = 85.68", abs(r["min_rate"] - 85.68) < 0.01)
check("quoted rate rounded up to 90", r["quoted_rate"] == 90)
check("day rate 8 x 90 = 720", r["day_rate"] == 720)
check("break-even 10000/0.9/1056 = 10.52", abs(r["break_even_rate"] - 10.52) < 0.01)
check("net at the minimum rate equals target", abs(((r["min_rate"] * 1056 * 0.9 - 10000) * 0.7) - 50000) < 10)
check("clean plan has no findings", r["findings"] == [])
check("monthly cost x 12", annual({"monthly": 150}) == 1800)
check("set-asides raise the rate", calc({**base, "set_asides": [{"name": "pension", "annual": 7000}]})["min_rate"] > r["min_rate"])
check("sensitivity 50% rate higher than 70%", r["sensitivity"][1]["min_rate"] > r["sensitivity"][3]["min_rate"])
def codes(**over):
c = json.loads(json.dumps(base))
for k, v in over.items():
if isinstance(v, dict) and isinstance(c.get(k), dict):
c[k].update(v)
else:
c[k] = v
return {f["code"] for f in calc(c)["findings"]}
check("no tax HIGH", "NO_TAX" in codes(tax={"effective_rate_pct": 0}))
check("below required HIGH", "BELOW_REQUIRED" in codes(current_rate=60))
check("below break-even HIGH", "BELOW_BREAK_EVEN" in codes(current_rate=8))
check("rate above market MEDIUM", "ABOVE_MARKET" in codes(market_rate={"low": 40, "high": 70}))
check("rate under market LOW", "UNDER_MARKET" in codes(market_rate={"low": 120, "high": 160}))
check("billable 85% MEDIUM", "BILLABLE_HIGH" in codes(time={"billable_pct": 85}))
check("55 h/week MEDIUM", "LONG_HOURS" in codes(time={"hours_per_week": 55}))
check("2 weeks vacation MEDIUM", "SHORT_VACATION" in codes(time={"vacation_weeks": 2}))
check("no buffer LOW", "NO_BUFFER" in codes(risk_buffer_pct=0))
check("no sick days LOW", "NO_SICK_DAYS" in codes(time={"sick_days": 0}))
check("no contingency LOW", "NO_CONTINGENCY" in codes(project={"estimated_hours": 40, "contingency_pct": 0}))
p = calc({**base, "project": {"estimated_hours": 100, "contingency_pct": 20}})["project"]
check("project 120 h x 90 = 10800", p["quoted_hours"] == 120 and p["price"] == 10800)
for bad, label in (({"tax": {"effective_rate_pct": 95}}, "tax 95% rejected"), ({"time": {"billable_pct": 0}}, "billable 0 rejected"),
({"time": {"vacation_weeks": 60}}, "no working days rejected")):
try:
c = json.loads(json.dumps(base))
for k, v in bad.items():
c[k].update(v)
calc(c)
check(label, False)
except DataError:
check(label, True)
print(f"{ok}/{len(tests)} passed")
return 0 if ok == len(tests) else 1
def main():
ap = argparse.ArgumentParser(description="Freelance hourly rate calculator")
ap.add_argument("plan", nargs="?", help="plan JSON file")
ap.add_argument("--round", type=float, default=5, help="round the quoted rate up to this step (default 5)")
ap.add_argument("--json", action="store_true")
ap.add_argument("--self-test", action="store_true")
a = ap.parse_args()
if a.self_test:
return self_test()
if not a.plan:
ap.error("plan file required")
try:
with open(a.plan, encoding="utf-8") as fh:
r = calc(json.load(fh), a.round)
except (OSError, json.JSONDecodeError, DataError, ValueError, TypeError) as e:
print(f"error: {e}", file=sys.stderr)
return 2
print(json.dumps(r, indent=2) if a.json else render(r), end="\n" if a.json else "")
return 1 if any(f["severity"] == "HIGH" for f in r["findings"]) else 0
if __name__ == "__main__":
sys.exit(main())