Initial commit
This commit is contained in:
commit
04a449ccfa
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Node modules
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.* # .env.production, .env.development, etc.
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# System files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# IDE/editor config
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Coverage or build output (if using any)
|
||||||
|
coverage/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# PM2 logs (if using PM2 for deployment)
|
||||||
|
pids/
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Optional: Docker-related files
|
||||||
|
*.local
|
||||||
|
docker-compose.override.yml
|
98
README.md
Normal file
98
README.md
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# Odoo FTI Backend Integration
|
||||||
|
|
||||||
|
This project is an Express.js backend API service designed to interface with an Odoo ERP system. It provides endpoints for managing and syncing **employee** and **attendance** data through XML-RPC.
|
||||||
|
|
||||||
|
## 🚀 Features
|
||||||
|
|
||||||
|
- Connects to Odoo using **JSON-RPC** (via `axios`)
|
||||||
|
- Environment-based configuration (supports `.env.development` / `.env.production`)
|
||||||
|
- RESTful API endpoints for:
|
||||||
|
- Fetching employee records
|
||||||
|
- Logging attendance (check-in/check-out)
|
||||||
|
- Modular architecture for scalability and clarityecture: separation of concerns for config, routes, controllers, and models
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
odoo-fti-be/
|
||||||
|
│
|
||||||
|
├── app.js # Main entry point
|
||||||
|
├── package.json # Project metadata and dependencies
|
||||||
|
├── config/
|
||||||
|
│ └── odooConfig.js # XML-RPC connection configuration
|
||||||
|
├── controllers/
|
||||||
|
│ ├── attendanceController.js
|
||||||
|
│ └── employeeController.js
|
||||||
|
├── models/
|
||||||
|
│ ├── odooService.js # Central Odoo client logic
|
||||||
|
│ └── odoo/
|
||||||
|
│ ├── attendance.js # Attendance-related logic
|
||||||
|
│ ├── employee.js # Employee-related logic
|
||||||
|
│ └── index.js
|
||||||
|
├── routes/
|
||||||
|
│ ├── attendanceRoutes.js # Attendance endpoints
|
||||||
|
│ └── employeeRoutes.js # Employee endpoints
|
||||||
|
└── utils/
|
||||||
|
└── date.js # Date utility for formatting
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Installation
|
||||||
|
|
||||||
|
1. **Clone the repository**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/yourusername/odoo-fti-be.git
|
||||||
|
cd odoo-fti-be
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install dependencies**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Configure Odoo Connection**
|
||||||
|
Update `config/odooConfig.js` with your Odoo server's URL, database, username, and password.
|
||||||
|
|
||||||
|
```js
|
||||||
|
module.exports = {
|
||||||
|
url: "http://your-odoo-host",
|
||||||
|
db: "your-db-name",
|
||||||
|
username: "your-username",
|
||||||
|
password: "your-password",
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 API Endpoints
|
||||||
|
|
||||||
|
### 👥 Employee
|
||||||
|
|
||||||
|
- **GET** `/employees`
|
||||||
|
- List all employees from Odoo
|
||||||
|
|
||||||
|
### ⏱️ Attendance
|
||||||
|
|
||||||
|
- **POST** `/attendance/check`
|
||||||
|
- Check in or checkout employee
|
||||||
|
- Body:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"employee_id": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Technologies Used
|
||||||
|
|
||||||
|
- **Node.js** with **Express.js**
|
||||||
|
- **odoo-xmlrpc** for Odoo integration
|
||||||
|
- Modular code structure for scalability and maintainability
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧾 License
|
32
app.js
Normal file
32
app.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// app.js
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
dotenv.config(); // Load from default .env file
|
||||||
|
|
||||||
|
import express from "express";
|
||||||
|
import { authenticate } from "./models/odooService.js";
|
||||||
|
|
||||||
|
import attendanceRoutes from "./routes/attendanceRoutes.js";
|
||||||
|
import employeeRoutes from "./routes/employeeRoutes.js";
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
app.use("/api/employees", employeeRoutes);
|
||||||
|
app.use("/api/attendances", attendanceRoutes);
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
app.listen(PORT, async () => {
|
||||||
|
console.log(`✅ Server running on http://localhost:${PORT}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uid = await authenticate();
|
||||||
|
if (uid) {
|
||||||
|
console.log(`🟢 Connected to Odoo! UID: ${uid}`);
|
||||||
|
} else {
|
||||||
|
console.log(`🔴 Odoo connection failed: Invalid credentials.`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`❌ Error connecting to Odoo:`, err.message);
|
||||||
|
}
|
||||||
|
});
|
10
config/odooConfig.js
Normal file
10
config/odooConfig.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// config/odooConfig.js
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
dotenv.config(); // Load from default .env file
|
||||||
|
|
||||||
|
export const odooConfig = {
|
||||||
|
url: process.env.ODOO_URL,
|
||||||
|
db: process.env.ODOO_DB,
|
||||||
|
email: process.env.ODOO_EMAIL,
|
||||||
|
apiKey: process.env.ODOO_API_KEY,
|
||||||
|
};
|
22
controllers/attendanceController.js
Normal file
22
controllers/attendanceController.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { checkEmployeeExists } from "../models/odoo/employee.js";
|
||||||
|
import { toggleEmployeeAttendance } from "../models/odoo/attendance.js";
|
||||||
|
|
||||||
|
export const toggleAttendance = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { employee_id } = req.body;
|
||||||
|
|
||||||
|
if (!employee_id) {
|
||||||
|
return res.status(400).json({ error: "employee_id is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = await checkEmployeeExists(employee_id);
|
||||||
|
if (!exists) {
|
||||||
|
return res.status(404).json({ error: "Employee not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await toggleEmployeeAttendance(employee_id);
|
||||||
|
res.status(200).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
};
|
33
controllers/employeeController.js
Normal file
33
controllers/employeeController.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// controllers/employeeController.js
|
||||||
|
import { fetchEmployees, fetchEmployeeById } from "../models/odoo/employee.js";
|
||||||
|
|
||||||
|
export const getEmployees = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const employees = await fetchEmployees();
|
||||||
|
|
||||||
|
if (!employees || employees.length === 0) {
|
||||||
|
return res
|
||||||
|
.status(404)
|
||||||
|
.json({ message: "No employee records found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(employees);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ error: "Failed to fetch employees" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEmployeeById = async (req, res) => {
|
||||||
|
const { employee_id } = req.params;
|
||||||
|
try {
|
||||||
|
const employee = await fetchEmployeeById(parseInt(employee_id));
|
||||||
|
if (!employee) {
|
||||||
|
return res.status(404).json({ error: "Employee not found" });
|
||||||
|
}
|
||||||
|
res.json(employee);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ error: "Failed to fetch employee" });
|
||||||
|
}
|
||||||
|
};
|
137
models/odoo/attendance.js
Normal file
137
models/odoo/attendance.js
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
// models/odoo/attendance.js
|
||||||
|
import axios from "axios";
|
||||||
|
import { authenticate, JSON_RPC_URL, odooConfig } from "./index.js";
|
||||||
|
import { toOdooDatetimeFormat } from "../../utils/date.js";
|
||||||
|
|
||||||
|
export const toggleEmployeeAttendance = async (employee_id) => {
|
||||||
|
try {
|
||||||
|
const uid = await authenticate();
|
||||||
|
|
||||||
|
if (!uid) throw new Error("Authentication failed.");
|
||||||
|
|
||||||
|
// Step 1: Check if employee exists
|
||||||
|
const empCheck = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "call",
|
||||||
|
params: {
|
||||||
|
service: "object",
|
||||||
|
method: "execute_kw",
|
||||||
|
args: [
|
||||||
|
odooConfig.db,
|
||||||
|
uid,
|
||||||
|
odooConfig.apiKey,
|
||||||
|
"hr.employee",
|
||||||
|
"search",
|
||||||
|
[[["id", "=", employee_id]]],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
id: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const empRes = await axios.post(JSON_RPC_URL, empCheck);
|
||||||
|
if (!empRes.data.result || empRes.data.result.length === 0) {
|
||||||
|
throw new Error(`Employee ${employee_id} not found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Look for open attendance
|
||||||
|
const searchPayload = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "call",
|
||||||
|
params: {
|
||||||
|
service: "object",
|
||||||
|
method: "execute_kw",
|
||||||
|
args: [
|
||||||
|
odooConfig.db,
|
||||||
|
uid,
|
||||||
|
odooConfig.apiKey,
|
||||||
|
"hr.attendance",
|
||||||
|
"search_read",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
["employee_id", "=", employee_id],
|
||||||
|
["check_out", "=", false],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
{ fields: ["id", "check_in"], limit: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
id: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchRes = await axios.post(JSON_RPC_URL, searchPayload);
|
||||||
|
const openRecords = searchRes.data.result;
|
||||||
|
|
||||||
|
if (openRecords.length > 0) {
|
||||||
|
// Step 3: Write check_out
|
||||||
|
const attendanceId = openRecords[0].id;
|
||||||
|
const now = toOdooDatetimeFormat(new Date());
|
||||||
|
|
||||||
|
const writePayload = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "call",
|
||||||
|
params: {
|
||||||
|
service: "object",
|
||||||
|
method: "execute_kw",
|
||||||
|
args: [
|
||||||
|
odooConfig.db,
|
||||||
|
uid,
|
||||||
|
odooConfig.apiKey,
|
||||||
|
"hr.attendance",
|
||||||
|
"write",
|
||||||
|
[[attendanceId], { check_out: now }],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
id: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeRes = await axios.post(JSON_RPC_URL, writePayload);
|
||||||
|
|
||||||
|
if (!writeRes.data.result) throw new Error("Check-out failed.");
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "Checked out",
|
||||||
|
attendance_id: attendanceId,
|
||||||
|
check_out: now,
|
||||||
|
odoo_response: writeRes.data,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Step 4: Create new check_in
|
||||||
|
const now = toOdooDatetimeFormat(new Date());
|
||||||
|
|
||||||
|
const createPayload = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "call",
|
||||||
|
params: {
|
||||||
|
service: "object",
|
||||||
|
method: "execute_kw",
|
||||||
|
args: [
|
||||||
|
odooConfig.db,
|
||||||
|
uid,
|
||||||
|
odooConfig.apiKey,
|
||||||
|
"hr.attendance",
|
||||||
|
"create",
|
||||||
|
[{ employee_id, check_in: now }],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
id: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRes = await axios.post(JSON_RPC_URL, createPayload);
|
||||||
|
|
||||||
|
if (createRes.data.error) {
|
||||||
|
console.error("Odoo error:", createRes.data.error);
|
||||||
|
throw new Error(createRes.data.error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "Checked in",
|
||||||
|
attendance_id: createRes.data.result,
|
||||||
|
check_in: now,
|
||||||
|
odoo_response: createRes.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("toggleEmployeeAttendance error:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
81
models/odoo/employee.js
Normal file
81
models/odoo/employee.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// models/odoo/employee.js
|
||||||
|
import axios from "axios";
|
||||||
|
import { authenticate, JSON_RPC_URL, odooConfig } from "./index.js";
|
||||||
|
|
||||||
|
export const fetchEmployees = async () => {
|
||||||
|
const uid = await authenticate();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "call",
|
||||||
|
params: {
|
||||||
|
service: "object",
|
||||||
|
method: "execute_kw",
|
||||||
|
args: [
|
||||||
|
odooConfig.db,
|
||||||
|
uid,
|
||||||
|
odooConfig.apiKey,
|
||||||
|
"hr.employee",
|
||||||
|
"search_read",
|
||||||
|
[],
|
||||||
|
{ fields: ["id", "name", "work_email"], limit: 10 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
id: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data } = await axios.post(JSON_RPC_URL, payload);
|
||||||
|
return data.result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkEmployeeExists = async (employee_id) => {
|
||||||
|
const uid = await authenticate();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "call",
|
||||||
|
params: {
|
||||||
|
service: "object",
|
||||||
|
method: "execute_kw",
|
||||||
|
args: [
|
||||||
|
odooConfig.db,
|
||||||
|
uid,
|
||||||
|
odooConfig.apiKey,
|
||||||
|
"hr.employee",
|
||||||
|
"search",
|
||||||
|
[[["id", "=", employee_id]]],
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
id: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await axios.post(JSON_RPC_URL, payload);
|
||||||
|
return res.data.result && res.data.result.length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchEmployeeById = async (employee_id) => {
|
||||||
|
const uid = await authenticate();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "call",
|
||||||
|
params: {
|
||||||
|
service: "object",
|
||||||
|
method: "execute_kw",
|
||||||
|
args: [
|
||||||
|
odooConfig.db,
|
||||||
|
uid,
|
||||||
|
odooConfig.apiKey,
|
||||||
|
"hr.employee",
|
||||||
|
"search_read",
|
||||||
|
[[["id", "=", employee_id]]],
|
||||||
|
{ fields: ["id", "name", "work_email"], limit: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
id: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data } = await axios.post(JSON_RPC_URL, payload);
|
||||||
|
return data.result[0] || null;
|
||||||
|
};
|
23
models/odoo/index.js
Normal file
23
models/odoo/index.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// models/odoo/index.js
|
||||||
|
import axios from "axios";
|
||||||
|
import { odooConfig } from "../../config/odooConfig.js";
|
||||||
|
|
||||||
|
export const JSON_RPC_URL = `${odooConfig.url}/jsonrpc`;
|
||||||
|
|
||||||
|
export const authenticate = async () => {
|
||||||
|
const payload = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "call",
|
||||||
|
params: {
|
||||||
|
service: "common",
|
||||||
|
method: "login",
|
||||||
|
args: [odooConfig.db, odooConfig.email, odooConfig.apiKey],
|
||||||
|
},
|
||||||
|
id: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data } = await axios.post(JSON_RPC_URL, payload);
|
||||||
|
return data.result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { odooConfig }; // re-export for other modules
|
236
models/odooService.js
Normal file
236
models/odooService.js
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
// models/odooService.js
|
||||||
|
import axios from "axios";
|
||||||
|
import { odooConfig } from "../config/odooConfig.js";
|
||||||
|
import { toOdooDatetimeFormat } from "../utils/date.js"; // Adjust path if needed
|
||||||
|
|
||||||
|
const JSON_RPC_URL = `${odooConfig.url}/jsonrpc`;
|
||||||
|
|
||||||
|
export const authenticate = async () => {
|
||||||
|
const payload = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "call",
|
||||||
|
params: {
|
||||||
|
service: "common",
|
||||||
|
method: "login",
|
||||||
|
args: [odooConfig.db, odooConfig.email, odooConfig.apiKey],
|
||||||
|
},
|
||||||
|
id: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data } = await axios.post(JSON_RPC_URL, payload);
|
||||||
|
return data.result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchEmployees = async () => {
|
||||||
|
const uid = await authenticate();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "call",
|
||||||
|
params: {
|
||||||
|
service: "object",
|
||||||
|
method: "execute_kw",
|
||||||
|
args: [
|
||||||
|
odooConfig.db,
|
||||||
|
uid,
|
||||||
|
odooConfig.apiKey,
|
||||||
|
"hr.employee",
|
||||||
|
"search_read",
|
||||||
|
[],
|
||||||
|
{ fields: ["id", "name", "work_email"], limit: 10 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
id: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data } = await axios.post(JSON_RPC_URL, payload);
|
||||||
|
return data.result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkEmployeeExists = async (employee_id) => {
|
||||||
|
const uid = await authenticate();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "call",
|
||||||
|
params: {
|
||||||
|
service: "object",
|
||||||
|
method: "execute_kw",
|
||||||
|
args: [
|
||||||
|
odooConfig.db,
|
||||||
|
uid,
|
||||||
|
odooConfig.apiKey,
|
||||||
|
"hr.employee",
|
||||||
|
"search",
|
||||||
|
[[["id", "=", employee_id]]],
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
id: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await axios.post(JSON_RPC_URL, payload);
|
||||||
|
return res.data.result && res.data.result.length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles attendance check-in/check-out for an employee
|
||||||
|
* @param {number} employee_id
|
||||||
|
* @returns {object} status and attendance record info
|
||||||
|
*/
|
||||||
|
export const toggleEmployeeAttendance = async (employee_id) => {
|
||||||
|
try {
|
||||||
|
const uid = await authenticate();
|
||||||
|
|
||||||
|
if (!uid) {
|
||||||
|
throw new Error("Authentication failed: no user ID returned.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: Verify employee exists
|
||||||
|
const checkEmployeePayload = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "call",
|
||||||
|
params: {
|
||||||
|
service: "object",
|
||||||
|
method: "execute_kw",
|
||||||
|
args: [
|
||||||
|
odooConfig.db,
|
||||||
|
uid,
|
||||||
|
odooConfig.apiKey,
|
||||||
|
"hr.employee",
|
||||||
|
"search",
|
||||||
|
[[["id", "=", employee_id]]],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
id: 0,
|
||||||
|
};
|
||||||
|
const empRes = await axios.post(JSON_RPC_URL, checkEmployeePayload);
|
||||||
|
if (!empRes.data.result || empRes.data.result.length === 0) {
|
||||||
|
throw new Error(`Employee with ID ${employee_id} not found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Search for open attendance record (check_out = false)
|
||||||
|
const searchPayload = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "call",
|
||||||
|
params: {
|
||||||
|
service: "object",
|
||||||
|
method: "execute_kw",
|
||||||
|
args: [
|
||||||
|
odooConfig.db,
|
||||||
|
uid,
|
||||||
|
odooConfig.apiKey,
|
||||||
|
"hr.attendance",
|
||||||
|
"search_read",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
["employee_id", "=", employee_id],
|
||||||
|
["check_out", "=", false],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
{ fields: ["id", "check_in"], limit: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
id: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchRes = await axios.post(JSON_RPC_URL, searchPayload);
|
||||||
|
const openRecords = searchRes.data.result;
|
||||||
|
|
||||||
|
if (openRecords.length > 0) {
|
||||||
|
// Step 2: Write check_out datetime
|
||||||
|
const attendanceId = openRecords[0].id;
|
||||||
|
const now = toOdooDatetimeFormat(new Date());
|
||||||
|
|
||||||
|
const writePayload = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "call",
|
||||||
|
params: {
|
||||||
|
service: "object",
|
||||||
|
method: "execute_kw",
|
||||||
|
args: [
|
||||||
|
odooConfig.db,
|
||||||
|
uid,
|
||||||
|
odooConfig.apiKey,
|
||||||
|
"hr.attendance",
|
||||||
|
"write",
|
||||||
|
[[attendanceId], { check_out: now }],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
id: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeRes = await axios.post(JSON_RPC_URL, writePayload);
|
||||||
|
|
||||||
|
if (!writeRes.data.result) {
|
||||||
|
throw new Error("Failed to write check_out datetime.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "Checked out",
|
||||||
|
attendance_id: attendanceId,
|
||||||
|
check_out: now,
|
||||||
|
odoo_response: writeRes.data,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Step 3: Create new attendance record with check_in
|
||||||
|
const now = toOdooDatetimeFormat(new Date());
|
||||||
|
|
||||||
|
const createPayload = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "call",
|
||||||
|
params: {
|
||||||
|
service: "object",
|
||||||
|
method: "execute_kw",
|
||||||
|
args: [
|
||||||
|
odooConfig.db,
|
||||||
|
uid,
|
||||||
|
odooConfig.apiKey,
|
||||||
|
"hr.attendance",
|
||||||
|
"create",
|
||||||
|
[{ employee_id, check_in: now }],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
id: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRes = await axios.post(JSON_RPC_URL, createPayload);
|
||||||
|
|
||||||
|
// DEBUG LOGGING - print entire response from Odoo
|
||||||
|
console.log(
|
||||||
|
"Create attendance response from Odoo:",
|
||||||
|
JSON.stringify(createRes.data, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (createRes.data.error) {
|
||||||
|
// Odoo returned an error object - log it and throw
|
||||||
|
console.error(
|
||||||
|
"Odoo error during attendance creation:",
|
||||||
|
createRes.data.error
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
createRes.data.error.message ||
|
||||||
|
"Unknown Odoo error during attendance creation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!createRes.data.result) {
|
||||||
|
throw new Error(
|
||||||
|
"Failed to create attendance record - no result returned."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const attendanceId = createRes.data.result;
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "Checked in",
|
||||||
|
attendance_id: attendanceId,
|
||||||
|
check_in: now,
|
||||||
|
odoo_response: createRes.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("toggleEmployeeAttendance error:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
1314
package-lock.json
generated
Normal file
1314
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
package.json
Normal file
28
package.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "odoo-fti-be",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"main": "app.js",
|
||||||
|
"description": "FTI BACKEND API FOR HRIS",
|
||||||
|
"author": "https://obanana.com",
|
||||||
|
"license": "ISC",
|
||||||
|
"keywords": [
|
||||||
|
"odoo",
|
||||||
|
"express",
|
||||||
|
"api",
|
||||||
|
"hris",
|
||||||
|
"fti"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"dev": "nodemon app.js",
|
||||||
|
"start": "node app.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.9.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.5.0",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"nodemon": "^3.1.10"
|
||||||
|
}
|
||||||
|
}
|
8
routes/attendanceRoutes.js
Normal file
8
routes/attendanceRoutes.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import express from "express";
|
||||||
|
import { toggleAttendance } from "../controllers/attendanceController.js";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post("/check", toggleAttendance);
|
||||||
|
|
||||||
|
export default router;
|
13
routes/employeeRoutes.js
Normal file
13
routes/employeeRoutes.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// routes/employeeRoutes.js
|
||||||
|
import express from "express";
|
||||||
|
import {
|
||||||
|
getEmployees,
|
||||||
|
getEmployeeById,
|
||||||
|
} from "../controllers/employeeController.js";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get("/", getEmployees);
|
||||||
|
router.get("/:employee_id", getEmployeeById);
|
||||||
|
|
||||||
|
export default router;
|
11
utils/date.js
Normal file
11
utils/date.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// utils/date.js
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a JS Date or ISO string to Odoo datetime format: 'YYYY-MM-DD HH:mm:ss'
|
||||||
|
* @param {Date|string} date
|
||||||
|
* @returns {string} formatted datetime
|
||||||
|
*/
|
||||||
|
export function toOdooDatetimeFormat(date) {
|
||||||
|
const d = new Date(date);
|
||||||
|
return d.toISOString().slice(0, 19).replace("T", " ");
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user