first push message

This commit is contained in:
2026-07-01 14:41:49 +07:00
parent 6667dec2bf
commit 58b5f46cc4
2951 changed files with 316619 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
from . import controllers
from . import models
+48
View File
@@ -0,0 +1,48 @@
{
'name': "gantt_view_ck",
'summary': "Short (1 phrase/line) summary of the module's purpose",
'description': """
Long description of module's purpose
""",
'author': "My Company",
'website': "https://www.yourcompany.com",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
'category': 'Uncategorized',
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['base','project'],
# always loaded
'data': [
# 'security/ir.model.access.csv',
'views/project_view.xml',
'views/views.xml',
],
# only loaded in demonstration mode
'demo': [
'demo/demo.xml',
],
'assets': {
'web.assets_backend': [
# 1. Load the Frappe Gantt Library FIRST
'gantt_view_ck/static/src/lib/frappe-gantt.min.css',
'gantt_view_ck/static/src/lib/frappe-gantt.min.js',
# 2. Load your custom module CSS and OWL components
'gantt_view_ck/static/src/css/gantt_styles.css',
'gantt_view_ck/static/src/xml/gantt_view_template.xml',
'gantt_view_ck/static/src/js/gantt_controller.js',
],
},
'installable': True,
'application': True,
'license': 'LGPL-3',
}
+1
View File
@@ -0,0 +1 @@
from . import controllers
+39
View File
@@ -0,0 +1,39 @@
from odoo import http
from odoo.http import request
import json
import xml.etree.ElementTree as ET
class GanttExportController(http.Controller):
@http.route('/gantt_view_ck/export/data', type='http', auth='user')
def export_data(self, format='json'):
# For simplicity, exporting all tasks. You can filter by active project.
tasks = request.env['project.task'].search([])
if format == 'json':
data = [{'name': t.name, 'start': str(t.gantt_start_date), 'end': str(t.gantt_end_date)} for t in tasks]
return request.make_json_response(data)
elif format == 'xml':
root = ET.Element("GanttProject")
for t in tasks:
task_el = ET.SubElement(root, "Task")
task_el.set("name", t.name)
task_el.set("start", str(t.gantt_start_date))
return request.make_response(ET.tostring(root), headers=[('Content-Type', 'application/xml')])
elif format == 'ical':
ical = "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//CK Gantt//EN\n"
for t in tasks:
if t.gantt_start_date:
ical += "BEGIN:VEVENT\n"
ical += f"UID:{t.id}@odoo\n"
ical += f"DTSTART:{t.gantt_start_date.strftime('%Y%m%d')}T000000Z\n"
if t.gantt_end_date:
ical += f"DTEND:{t.gantt_end_date.strftime('%Y%m%d')}T000000Z\n"
ical += f"SUMMARY:{t.name}\nEND:VEVENT\n"
ical += "END:VCALENDAR"
return request.make_response(ical, headers=[('Content-Type', 'text/calendar')])
return request.not_found()
+30
View File
@@ -0,0 +1,30 @@
<odoo>
<data>
<!--
<record id="object0" model="gantt_view_ck.gantt_view_ck">
<field name="name">Object 0</field>
<field name="value">0</field>
</record>
<record id="object1" model="gantt_view_ck.gantt_view_ck">
<field name="name">Object 1</field>
<field name="value">10</field>
</record>
<record id="object2" model="gantt_view_ck.gantt_view_ck">
<field name="name">Object 2</field>
<field name="value">20</field>
</record>
<record id="object3" model="gantt_view_ck.gantt_view_ck">
<field name="name">Object 3</field>
<field name="value">30</field>
</record>
<record id="object4" model="gantt_view_ck.gantt_view_ck">
<field name="name">Object 4</field>
<field name="value">40</field>
</record>
-->
</data>
</odoo>
+3
View File
@@ -0,0 +1,3 @@
from . import project_project
from . import project_task
from . import project_task_type
+10
View File
@@ -0,0 +1,10 @@
from odoo import models, fields
class ProjectProject(models.Model):
_inherit = 'project.project'
gantt_enable_dynamic_text = fields.Boolean(string="Dynamic Text", default=True)
gantt_enable_dynamic_progress = fields.Boolean(string="Dynamic Progress", default=True)
gantt_hide_holidays = fields.Boolean(string="Hide Holiday Day")
gantt_days_off = fields.Char(string="Days Off (e.g., 5,6 for Sat,Sun)", default="5,6")
gantt_mail_timesheet_user = fields.Many2many('res.users', string="Mail Timesheet Users")
+31
View File
@@ -0,0 +1,31 @@
from odoo import models, fields, api
class ProjectTask(models.Model):
_inherit = 'project.task'
gantt_start_date = fields.Date(string="Start Date", default=fields.Date.today)
gantt_end_date = fields.Date(string="End Date")
task_progress = fields.Float(string="Progress (%)", default=0.0)
constraint_type = fields.Selection([
('asap', 'As Soon As Possible'),
('snet', 'Start No Earlier Than'),
('snlt', 'Start No Later Than'),
('fnet', 'Finish No Earlier Than'),
('fnlt', 'Finish No Later Than'),
], string="Constraint Type", default='asap')
constraint_date = fields.Datetime(string="Constraint Date")
is_milestone = fields.Boolean(string="Milestone")
unschedule = fields.Boolean(string="Unschedule")
schedule_mode = fields.Selection([
('auto', 'Auto'),
('manual', 'Manual')
], string="Schedule Mode", default='manual')
# Computed color based on task stage
stage_color = fields.Char(string="Stage Color", compute='_compute_stage_color', store=True)
@api.depends('stage_id.gantt_stage_color')
def _compute_stage_color(self):
for task in self:
task.stage_color = task.stage_id.gantt_stage_color or '#17a2b8'
+11
View File
@@ -0,0 +1,11 @@
from odoo import models, fields
class ProjectTaskType(models.Model):
_inherit = 'project.task.type'
gantt_stage_color = fields.Selection([
('#28a745', 'Green (Completed)'),
('#dc3545', 'Red (Not Started)'),
('#ffc107', 'Yellow (In Progress)'),
('#17a2b8', 'Blue (Default)'),
], string="Gantt Stage Color", default='#17a2b8')
@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_gantt_view_ck_gantt_view_ck,gantt_view_ck.gantt_view_ck,model_gantt_view_ck_gantt_view_ck,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_gantt_view_ck_gantt_view_ck gantt_view_ck.gantt_view_ck model_gantt_view_ck_gantt_view_ck base.group_user 1 1 1 1
@@ -0,0 +1,14 @@
.o_gantt_view_container {
display: flex; flex-direction: column; height: 100%; background: #fff;
}
.o_gantt_action_bar {
padding: 12px; border-bottom: 1px solid #dee2e6; display: flex; gap: 10px;
}
.o_gantt_layout { display: flex; flex: 1; overflow: hidden; }
.o_gantt_left_grid {
width: 40%; border-right: 1px solid #dee2e6; overflow-y: auto;
}
.o_gantt_right_panel {
width: 60%; overflow: auto; position: relative;
}
.o_gantt_chart_area { min-height: 100%; padding: 20px; }
@@ -0,0 +1,96 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { Component, onMounted, useState, useRef } from "@odoo/owl";
class GanttViewController extends Component {
setup() {
this.orm = useService("orm");
this.action = useService("action");
this.notification = useService("notification");
this.ganttContainer = useRef("gantt-container");
this.state = useState({
tasks: [],
criticalPathEnabled: false,
});
onMounted(async () => {
await this.loadTasks();
this.renderGanttChart();
});
}
async loadTasks() {
const tasks = await this.orm.searchRead(
"project.task",
[["project_id", "!=", false]],
["name", "gantt_start_date", "gantt_end_date", "task_progress", "is_milestone", "stage_color", "constraint_type", "date_deadline"]
);
this.state.tasks = tasks;
}
renderGanttChart() {
const chart_area = this.ganttContainer.el.querySelector(".o_gantt_chart_area");
chart_area.innerHTML = '<p class="text-center text-muted mt-5">Initializing Gantt Engine...<br/>(Integrate Frappe Gantt or DHTMLX JS library here)</p>';
/*
INTEGRATION POINT:
Import your JS library (e.g., Frappe Gantt) in __manifest__.py
const gantt = new Gantt(chart_area, this.state.tasks.map(t => ({
id: t.id, name: t.name, start: t.gantt_start_date, end: t.gantt_end_date, progress: t.task_progress
})), {
on_date_change: (task, start, end) => this.onDateChange(task, start, end)
});
*/
}
async onDateChange(task, start, end) {
await this.orm.write("project.task", [task.id], {
gantt_start_date: start,
gantt_end_date: end,
});
this.notification.add("Task dates updated successfully!", { type: "success" });
}
zoomToFit() { this.notification.add("Zoom to Fit triggered", { type: "info" }); }
toggleCriticalPath() {
this.state.criticalPathEnabled = !this.state.criticalPathEnabled;
this.notification.add(`Critical Path ${this.state.criticalPathEnabled ? 'Enabled' : 'Disabled'}`, { type: "info" });
}
goToToday() { this.notification.add("Scrolled to Today", { type: "info" }); }
toggleFullScreen() {
if (!document.fullscreenElement) this.ganttContainer.el.requestFullscreen();
else document.exitFullscreen();
}
editTask(task) {
this.action.doAction({
type: "ir.actions.act_window",
res_model: "project.task",
res_id: task.id,
views: [[false, "form"]],
target: "current",
});
}
async deleteTask(task) {
if (confirm("Are you sure you want to delete this task?")) {
await this.orm.unlink("project.task", [task.id]);
this.notification.add("Task deleted", { type: "warning" });
await this.loadTasks();
this.renderGanttChart();
}
}
exportData() {
window.location.href = `/gantt_view_ck/export/data?format=excel`;
}
}
GanttViewController.template = "gantt_view_ck.GanttViewTemplate";
registry.category("actions").add("gantt_view_ck.gantt_action", GanttViewController);
+1
View File
@@ -0,0 +1 @@
.gantt .grid-background{fill:none}.gantt .grid-header{fill:#fff;stroke:#e0e0e0;stroke-width:1.4}.gantt .grid-row{fill:#fff}.gantt .grid-row:nth-child(even){fill:#f5f5f5}.gantt .row-line{stroke:#ebeff2}.gantt .tick{stroke:#e0e0e0;stroke-width:.2}.gantt .tick.thick{stroke-width:.4}.gantt .today-highlight{fill:#fcf8e3;opacity:.5}.gantt .arrow{fill:none;stroke:#666;stroke-width:1.4}.gantt .bar{fill:#b8c2cc;stroke:#8d99a6;stroke-width:0;transition:stroke-width .3s ease;user-select:none}.gantt .bar-progress{fill:#a3a3ff}.gantt .bar-invalid{fill:rgba(0,0,0,0);stroke:#8d99a6;stroke-width:1;stroke-dasharray:5}.gantt .bar-invalid~.bar-label{fill:#555}.gantt .bar-label{fill:#fff;dominant-baseline:central;text-anchor:middle;font-size:12px;font-weight:lighter}.gantt .bar-label.big{fill:#555;text-anchor:start}.gantt .handle{fill:#ddd;cursor:ew-resize;opacity:0;visibility:hidden;transition:opacity .3s ease}.gantt .bar-wrapper{cursor:pointer;outline:none}.gantt .bar-wrapper:hover .bar{fill:#a9b5c1}.gantt .bar-wrapper:hover .bar-progress{fill:#8a8aff}.gantt .bar-wrapper:hover .handle{visibility:visible;opacity:1}.gantt .bar-wrapper.active .bar{fill:#a9b5c1}.gantt .bar-wrapper.active .bar-progress{fill:#8a8aff}.gantt .lower-text,.gantt .upper-text{font-size:12px;text-anchor:middle}.gantt .upper-text{fill:#555}.gantt .lower-text{fill:#333}.gantt .hide{display:none}.gantt-container{position:relative;overflow:auto;font-size:12px}.gantt-container .popup-wrapper{position:absolute;top:0;left:0;background:rgba(0,0,0,.8);padding:0;color:#959da5;border-radius:3px}.gantt-container .popup-wrapper .title{border-bottom:3px solid #a3a3ff;padding:10px}.gantt-container .popup-wrapper .subtitle{padding:10px;color:#dfe2e5}.gantt-container .popup-wrapper .pointer{position:absolute;height:5px;margin:0 0 0 -5px;border:5px solid rgba(0,0,0,0);border-top-color:rgba(0,0,0,.8)}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="gantt_view_ck.GanttViewTemplate">
<div class="o_gantt_view_container">
<div class="o_gantt_action_bar">
<button class="btn btn-primary o_gantt_btn" t-on-click="zoomToFit">Zoom to Fit</button>
<button class="btn btn-secondary o_gantt_btn" t-on-click="toggleCriticalPath">Critical Path</button>
<button class="btn btn-secondary o_gantt_btn" t-on-click="goToToday">Today</button>
<button class="btn btn-secondary o_gantt_btn" t-on-click="toggleFullScreen">Full Screen</button>
<button class="btn btn-success o_gantt_btn" t-on-click="exportData">Export</button>
</div>
<div class="o_gantt_layout">
<div class="o_gantt_left_grid">
<table class="table table-sm table-hover mb-0">
<thead class="table-light">
<tr>
<th>Task Name</th>
<th>Start</th>
<th>End</th>
<th>%</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<t t-foreach="state.tasks" t-as="task" t-key="task.id">
<tr>
<td>
<span t-attf-style="background-color: {{task.stage_color}}; width: 12px; height: 12px; display: inline-block; border-radius: 50%; margin-right: 8px;"></span>
<t t-esc="task.name"/>
<i t-if="task.is_milestone" class="fa fa-flag ms-2 text-warning"></i>
</td>
<td><t t-esc="task.gantt_start_date"/></td>
<td><t t-esc="task.gantt_end_date"/></td>
<td><t t-esc="task.task_progress"/></td>
<td>
<button class="btn btn-sm btn-link" t-on-click="() => this.editTask(task)">Edit</button>
<button class="btn btn-sm btn-link text-danger" t-on-click="() => this.deleteTask(task)">Del</button>
</td>
</tr>
</t>
</tbody>
</table>
</div>
<div class="o_gantt_right_panel" t-ref="gantt-container">
<div class="o_gantt_chart_area"></div>
</div>
</div>
</div>
</t>
</templates>
+58
View File
@@ -0,0 +1,58 @@
<odoo>
<record id="project_project_form_view_gantt" model="ir.ui.view">
<field name="name">project.project.form.gantt</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.edit_project"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='settings']" position="inside">
<group string="CK Gantt Settings">
<group>
<field name="gantt_enable_dynamic_text"/>
<field name="gantt_enable_dynamic_progress"/>
<field name="gantt_hide_holidays"/>
<field name="gantt_days_off"/>
</group>
<group>
<field name="gantt_mail_timesheet_user" widget="many2many_tags"/>
</group>
</group>
</xpath>
</field>
</record>
<record id="project_task_form_view_gantt" model="ir.ui.view">
<field name="name">project.task.form.gantt</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_form2"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='extra_info']" position="after">
<page string="Gantt Details" name="gantt_details">
<group>
<group>
<field name="gantt_start_date"/>
<field name="gantt_end_date"/>
<field name="is_milestone"/>
<field name="unschedule"/>
<field name="schedule_mode"/>
</group>
<group>
<field name="constraint_type"/>
<field name="constraint_date" invisible="constraint_type == 'asap'"/>
</group>
</group>
</page>
</xpath>
</field>
</record>
<record id="project_task_type_form_view_gantt" model="ir.ui.view">
<field name="name">project.task.type.form.gantt</field>
<field name="model">project.task.type</field>
<field name="inherit_id" ref="project.task_type_edit"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='fold']" position="after">
<field name="gantt_stage_color" widget="radio"/>
</xpath>
</field>
</record>
</odoo>
+24
View File
@@ -0,0 +1,24 @@
<odoo>
<data>
<!--
<template id="listing">
<ul>
<li t-foreach="objects" t-as="object">
<a t-attf-href="#{ root }/objects/#{ object.id }">
<t t-esc="object.display_name"/>
</a>
</li>
</ul>
</template>
<template id="object">
<h1><t t-esc="object.display_name"/></h1>
<dl>
<t t-foreach="object._fields" t-as="field">
<dt><t t-esc="field"/></dt>
<dd><t t-esc="object[field]"/></dd>
</t>
</dl>
</template>
-->
</data>
</odoo>
+12
View File
@@ -0,0 +1,12 @@
<odoo>
<record id="action_gantt_view_ck" model="ir.actions.client">
<field name="name">CK Gantt Chart</field>
<field name="tag">gantt_view_ck.gantt_action</field>
</record>
<menuitem id="menu_gantt_view_ck"
name="CK Gantt View"
parent="project.menu_main_pm"
action="action_gantt_view_ck"
sequence="10"/>
</odoo>