7 Commits

Author SHA1 Message Date
Stéphan Sainléger
d53ba8bd64 [FIX] project_working_time_task_portal: correction on portal task list display of billable hours 2026-05-19 16:13:53 +02:00
Stéphan Sainléger
1664bb043a [MIG] project_working_time_task_portal: migrate to 18.0 2026-03-13 17:33:14 +01:00
Stéphan Sainléger
f9f1c98580 [MIG] project_task_billable_hours: migrate to 18.0 2026-03-13 17:25:48 +01:00
Stéphan Sainléger
0a7814ad1a [MIG] project_request_data: migrate to 18.0 2026-01-20 17:24:51 +01:00
Stéphan Sainléger
a97bd8ecca [MIG] project_only_current_project_tasks_as_parent: migrate to 18.0
migrates add-on project_parent_task to 18.0 and change the add-on name
for more understanding.
2026-01-20 17:19:47 +01:00
Stéphan Sainléger
8ab14340f1 [MIG] project_average_acceptable_time: migrate to 18.0 2026-01-20 17:09:37 +01:00
Stéphan Sainléger
9192465ca2 [MIG] project_user_default_project: migrate to 18.0 2026-01-20 15:28:22 +01:00
45 changed files with 166 additions and 817 deletions

View File

@@ -1 +1,2 @@
from . import models, controllers

View File

@@ -5,13 +5,17 @@
"name": "project_average_acceptable_time",
"version": "18.0.1.0.0",
"author": "Elabore",
"website": "https://git.elabore.coop/elabore/project-tools",
"website": "https://github.com/elabore-coop/project-tools",
"maintainer": "Clément Thomas",
"license": "AGPL-3",
"category": "Tools",
"summary": "Task validation without customer agreement",
# any module necessary for this one to work correctly
"depends": ["base", "project", "project_user_default_project"],
"depends": [
"base",
"project",
"project_user_default_project"
],
"qweb": [
# "static/src/xml/*.xml",
],
@@ -19,7 +23,10 @@
"python": [],
},
# always loaded
"data": ["views/project_project.xml", "views/portal_home_template.xml"],
"data": [
"views/project_project.xml",
"views/portal_home_template.xml"
],
# only loaded in demonstration mode
"demo": [],
"js": [],

View File

@@ -1 +1,2 @@
from . import custom_portal
from . import custom_portal

View File

@@ -1,26 +1,21 @@
# Copyright 2020 Lokavaluto ()
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.http import request, route
from odoo.addons.portal.controllers.portal import CustomerPortal
class CustomCustomerPortal(CustomerPortal):
@route(["/my/account"], type="http", auth="user", website=True)
def account(self, redirect=None, **post):
self.OPTIONAL_BILLING_FIELDS.append(
"average_acceptable_time"
) # unecessary save in res partner, but necessary to avoid error on form post
response = super().account(redirect, **post)
self.OPTIONAL_BILLING_FIELDS.append("average_acceptable_time") #unecessary save in res partner, but necessary to avoid error on form post
response = super(CustomCustomerPortal, self).account(redirect, **post)
if post and request.httprequest.method == "POST":
error, error_message = self.details_form_validate(post)
if not error:
user = request.env.user
if user.default_project_id and post["average_acceptable_time"]:
user.default_project_id.average_acceptable_time = post[
"average_acceptable_time"
]
user.default_project_id.average_acceptable_time = post["average_acceptable_time"]
return response

View File

@@ -1,2 +1,3 @@
from . import project_project
from . import res_partner
from . import res_partner

View File

@@ -1,7 +1,10 @@
from odoo import fields, models
from odoo import models, fields, _, api
class Project(models.Model):
_inherit = "project.project"
average_acceptable_time = fields.Float("Average acceptable time")
average_acceptable_time = fields.Float('Average acceptable time')

View File

@@ -1,9 +1,10 @@
from odoo import fields, models
from odoo import models, fields, _, api
class ResPartner(models.Model):
_inherit = "res.partner"
average_acceptable_time = fields.Float(
"Average acceptable time"
) # not used, but necessary to post custom field from /my/account
average_acceptable_time = fields.Float('Average acceptable time') # not used, but necessary to post custom field from /my/account

View File

@@ -1,42 +1,25 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Template /my/home -->
<template
id="portal_my_home_average_acceptable_time"
name="Portal My Home: Average acceptable time"
inherit_id="portal.side_content"
priority="40"
>
<template id="portal_my_home_average_acceptable_time" name="Portal My Home: Average acceptable time" inherit_id="portal.side_content" priority="40">
<xpath expr="//div[@class='o_portal_my_details']" position="inside">
<hr class="mt-1 mb-0" />
<b>Average acceptable time</b>: <t
t-esc="user_id.default_project_id.average_acceptable_time"
t-options="{'widget': 'float_time'}"
/>
<hr class="mt-1 mb-0"/>
<b>Average acceptable time</b>: <t t-esc="user_id.default_project_id.average_acceptable_time" t-options="{'widget': 'float_time'}"/>
</xpath>
</template>
<!-- Template /my/account -->
<template
id="portal_my_details_average_acceptable_time"
name="Portal My details: Average acceptable time"
inherit_id="portal.portal_my_details_fields"
>
<template id="portal_my_details_average_acceptable_time" name="Portal My details: Average acceptable time" inherit_id="portal.portal_my_details_fields">
<xpath expr="//input[@name='csrf_token']/.." position="inside">
<div
t-attf-class="form-group #{error.get('average_acceptable_time') and 'o_has_error' or ''} col-xl-6"
>
<label
class="col-form-label"
for="average_acceptable_time"
>Average acceptable time (h)</label>
<div t-attf-class="form-group #{error.get('average_acceptable_time') and 'o_has_error' or ''} col-xl-6">
<label class="col-form-label" for="average_acceptable_time">Average acceptable time (h)</label>
<input
type="text"
name="average_acceptable_time"
t-attf-class="form-control #{error.get('average_acceptable_time') and 'is-invalid' or ''}"
t-att-value="average_acceptable_time or user_id.default_project_id.average_acceptable_time"
t-options="{'widget': 'float_time'}"
/>
/>
</div>
</xpath>
</template>

View File

@@ -1,44 +1,33 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!--Project KANBAN -->
<record
id="project_project_view_kanban_inherit_average_acceptable_time"
model="ir.ui.view"
>
<field
name="name"
>project.project.kanban.inherit.average.acceptable.time</field>
<record id="project_project_view_kanban_inherit_average_acceptable_time" model="ir.ui.view">
<field name="name">project.project.kanban.inherit.average.acceptable.time</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.view_project_kanban" />
<field name="inherit_id" ref="project.view_project_kanban"/>
<field name="priority">999</field>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('o_project_kanban_main')]" position="inside">
<div class="o_project_kanban_boxes">
<span>Average acceptable time:<![CDATA[&nbsp;]]></span>
<field name="average_acceptable_time" widget="float_time" />
<field name="average_acceptable_time" widget="float_time"/>
</div>
</xpath>
</field>
</record>
<!-- Project FORM -->
<record
id="project_project_form_inherit_average_acceptable_time"
model="ir.ui.view"
>
<record id="project_project_form_inherit_average_acceptable_time" model="ir.ui.view">
<field name="name">project.project.form.inherit.average.acceptable.time</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.edit_project" />
<field name="inherit_id" ref="project.edit_project"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='extra_settings']" position="inside">
<div
class="o_settings_average_acceptable_time"
style="margin-top:10px;"
>
<div class="o_settings_average_acceptable_time" style="margin-top:10px;">
<!-- <span>Average acceptable time:<![CDATA[&nbsp;]]></span> -->
<label for="average_acceptable_time" />
<field name="average_acceptable_time" widget="float_time" />
<label for="average_acceptable_time"/>
<field name="average_acceptable_time" widget="float_time"/>
</div>
</xpath>
</field>

View File

@@ -1,60 +0,0 @@
# Project Link From Invoice
![License: AGPL-3](https://img.shields.io/badge/licence-AGPL--3-blue.svg)
Easily access your projects directly from invoices:
- Displays project name(s) in the invoice list view.
- Adds a smart button on invoice form view to open the related project(s).
## Installation
This module depends on:
- `project` (Odoo core)
- `account` (Odoo core)
- `sale_project` (Odoo core)
Use Odoo's normal procedure to install add-ons.
## Configuration
No specific configuration is required.
## Usage
Open an invoice that is linked to a sale order. If the sale order has tasks
in a project, a "Project(s)" button appears in the header of the invoice form.
Clicking it opens the related project(s) in form or list view.
The invoice list view also displays the project name(s) in a dedicated column.
## Known issues / Roadmap
None yet.
## Bug Tracker
Bugs are tracked on [our issues website](https://github.com/elabore-coop/project-tools/issues).
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smash it by providing detailed and welcomed feedback.
## Credits
### Images
- Elabore: [Icon](https://elabore.coop/web/image/res.company/1/logo)
### Contributors
- Clément Thomas
### Funders
The development of this module has been financially supported by:
- Elabore (<https://elabore.coop>)
### Maintainer
This module is maintained by Elabore.

View File

@@ -1,2 +0,0 @@
from . import models

View File

@@ -1,34 +0,0 @@
# Copyright 2022 Stéphan Sainléger (Elabore)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "project_link_from_invoice",
"version": "18.0.1.0.0",
"author": "Elabore",
"website": "https://git.elabore.coop/elabore/project-tools",
"maintainer": "Clément Thomas",
"license": "AGPL-3",
"category": "Tools",
"summary": "Add link from invoice to project",
# any module necessary for this one to work correctly
"depends": [
"base",
"project",
"account",
"sale_project",
],
"external_dependencies": {
"python": [],
},
# always loaded
"data": [
"views/account_move_view.xml",
],
# only loaded in demonstration mode
"demo": [],
"installable": True,
# Install this module automatically if all dependency have been previously
# and independently installed. Used for synergetic or glue modules.
"auto_install": False,
"application": False,
}

View File

@@ -1,58 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * project_link_from_invoice
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-04-27 12:47+0000\n"
"PO-Revision-Date: 2023-04-27 12:47+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: project_link_from_invoice
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_move__display_name
msgid "Display Name"
msgstr "Nom affiché"
#. module: project_link_from_invoice
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_move__id
msgid "ID"
msgstr ""
#. module: project_link_from_invoice
#: model:ir.model,name:project_link_from_invoice.model_account_move
msgid "Journal Entry"
msgstr "Pièce comptable"
#. module: project_link_from_invoice
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_move____last_update
msgid "Last Modified on"
msgstr "Dernière modification le"
#. module: project_link_from_invoice
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_bank_statement_line__project_ids
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_move__project_ids
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_payment__project_ids
msgid "Project"
msgstr "Projet"
#. module: project_link_from_invoice
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_bank_statement_line__project_count
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_move__project_count
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_payment__project_count
msgid "Project Count"
msgstr "Numbre de projets"
#. module: project_link_from_invoice
#: model_terms:ir.ui.view,arch_db:project_link_from_invoice.view_move_form_project_link_from_invoice_inherit
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_bank_statement_line__projects_name
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_move__projects_name
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_payment__projects_name
msgid "Project(s)"
msgstr "Projet(s)"

View File

@@ -1,2 +0,0 @@
from . import account_move

View File

@@ -1,49 +0,0 @@
# Copyright 2022 Stéphan Sainléger (Elabore)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class AccountMove(models.Model):
_inherit = "account.move"
project_ids = fields.Many2many(
"project.project", string="Projects", compute="_get_related_project_ids"
)
project_count = fields.Integer("Project Count", compute="_get_related_project_ids")
projects_name = fields.Char("Project(s)", compute="_get_related_project_ids")
def action_open_projects(self):
"""Open related projects, in form or list view depending on project numbers."""
project_ids = self.project_ids.ids
action = self.env["ir.actions.actions"]._for_xml_id(
"project.open_view_project_all"
)
if self.project_count == 1:
action["res_id"] = project_ids[0]
action["views"] = [[False, "form"]]
else:
action["views"] = [[False, "list"], [False, "form"]]
action["domain"] = [("id", "in", project_ids)]
del action["target"] # to display breadcrumbs
return action
@api.depends("line_ids.sale_line_ids")
def _get_related_project_ids(self):
for move in self:
projects = self.env["project.task"].search(
[
(
"sale_order_id",
"in",
move.line_ids.sale_line_ids.order_id.ids,
)
]
).project_id
move.project_ids = projects.ids
move.projects_name = " ; ".join([p.name for p in projects])
move.project_count = len(projects)

View File

@@ -1,4 +0,0 @@
# Copyright 2022 Stéphan Sainléger (Elabore)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import test_account_move

View File

@@ -1,193 +0,0 @@
# Copyright 2022 Stéphan Sainléger (Elabore)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import Command
from odoo.tests import tagged
from odoo.addons.sale_project.tests.common import TestSaleProjectCommon
@tagged("post_install", "-at_install")
class TestProjectLinkFromInvoice(TestSaleProjectCommon):
"""Tests for the project_link_from_invoice module.
Verifies that invoices correctly expose related project(s) via
the computed fields project_ids, project_count and projects_name.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
# Service product that creates a task in a new project on SO confirmation
cls.service_product = cls.env["product.product"].create(
{
"name": "Service with Project",
"type": "service",
"invoice_policy": "order",
"service_tracking": "task_in_project",
}
)
# Partner used for orders and invoices
cls.partner = cls.env["res.partner"].create({"name": "Test Partner"})
def _confirm_sale_order(self, product, qty=1):
"""Create and confirm a sale order with one service line."""
so = self.env["sale.order"].create(
{
"partner_id": self.partner.id,
"order_line": [
Command.create(
{
"product_id": product.id,
"product_uom_qty": qty,
}
)
],
}
)
so.action_confirm()
return so
def _invoice_sale_order(self, sale_order):
"""Create and post an invoice from a confirmed sale order."""
invoice_wizard = (
self.env["sale.advance.payment.inv"]
.with_context(
active_model="sale.order",
active_ids=sale_order.ids,
active_id=sale_order.id,
)
.create({"advance_payment_method": "delivered"})
)
invoices = invoice_wizard._create_invoices(sale_order)
invoices.action_post()
return invoices
# ------------------------------------------------------------------
# Tests
# ------------------------------------------------------------------
def test_no_project_on_unrelated_invoice(self):
"""An invoice not linked to any sale order has no related project."""
invoice = self.env["account.move"].create(
{
"move_type": "out_invoice",
"partner_id": self.partner.id,
"invoice_line_ids": [
Command.create(
{
"name": "Manual line",
"quantity": 1,
"price_unit": 100.0,
}
)
],
}
)
self.assertEqual(invoice.project_count, 0)
self.assertFalse(invoice.project_ids)
self.assertEqual(invoice.projects_name, "")
def test_single_project_linked_to_invoice(self):
"""An invoice from a SO that created a task has one related project."""
so = self._confirm_sale_order(self.service_product)
# The SO must have generated a task in a project
task = so.tasks_ids[:1]
self.assertTrue(task, "Sale order confirmation should have created a task")
project = task.project_id
self.assertTrue(project, "Task should belong to a project")
invoice = self._invoice_sale_order(so)
self.assertEqual(len(invoice), 1)
self.assertEqual(invoice.project_count, 1)
self.assertIn(project, invoice.project_ids)
self.assertIn(project.name, invoice.projects_name)
def test_multiple_projects_linked_to_invoice(self):
"""An invoice from two SOs with different projects shows both projects."""
so1 = self._confirm_sale_order(self.service_product)
so2 = self._confirm_sale_order(self.service_product)
project1 = so1.tasks_ids[:1].project_id
project2 = so2.tasks_ids[:1].project_id
self.assertTrue(project1 and project2)
# Ensure the two SOs created different projects
self.assertNotEqual(project1, project2)
# Create a single invoice covering both sale orders
invoice_wizard = (
self.env["sale.advance.payment.inv"]
.with_context(
active_model="sale.order",
active_ids=(so1 + so2).ids,
)
.create({"advance_payment_method": "delivered"})
)
invoices = invoice_wizard._create_invoices(so1 + so2)
invoices.action_post()
# Find the invoice that covers both SO lines
combined_invoice = invoices[:1]
self.assertGreaterEqual(combined_invoice.project_count, 2)
self.assertIn(project1, combined_invoice.project_ids)
self.assertIn(project2, combined_invoice.project_ids)
def test_action_open_projects_single(self):
"""action_open_projects returns a form view when there is one project."""
so = self._confirm_sale_order(self.service_product)
invoice = self._invoice_sale_order(so)
self.assertEqual(invoice.project_count, 1)
action = invoice.action_open_projects()
self.assertEqual(action["res_id"], invoice.project_ids.id)
view_types = [v[1] for v in action["views"]]
self.assertIn("form", view_types)
self.assertNotIn("list", view_types)
def test_action_open_projects_multiple(self):
"""action_open_projects returns a list view when there are multiple projects."""
so1 = self._confirm_sale_order(self.service_product)
so2 = self._confirm_sale_order(self.service_product)
invoice_wizard = (
self.env["sale.advance.payment.inv"]
.with_context(
active_model="sale.order",
active_ids=(so1 + so2).ids,
)
.create({"advance_payment_method": "delivered"})
)
invoices = invoice_wizard._create_invoices(so1 + so2)
invoices.action_post()
combined_invoice = invoices[:1]
if combined_invoice.project_count < 2:
self.skipTest("Combined invoice does not span multiple projects")
action = combined_invoice.action_open_projects()
view_types = [v[1] for v in action["views"]]
self.assertIn("list", view_types)
self.assertIn("form", view_types)
self.assertNotIn("target", action)
def test_projects_name_separator(self):
"""projects_name joins multiple project names with ' ; '."""
so1 = self._confirm_sale_order(self.service_product)
so2 = self._confirm_sale_order(self.service_product)
invoice_wizard = (
self.env["sale.advance.payment.inv"]
.with_context(
active_model="sale.order",
active_ids=(so1 + so2).ids,
)
.create({"advance_payment_method": "delivered"})
)
invoices = invoice_wizard._create_invoices(so1 + so2)
invoices.action_post()
combined_invoice = invoices[:1]
if combined_invoice.project_count >= 2:
self.assertIn(" ; ", combined_invoice.projects_name)

View File

@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_move_form_project_link_from_invoice_inherit" model="ir.ui.view">
<field name="name">account.move.form.project.link</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form" />
<field name="priority" eval="99" />
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<button
name="action_open_projects"
class="oe_stat_button"
icon="fa-puzzle-piece"
type="object"
invisible="project_count == 0"
>
<field name="project_count" widget="statinfo" string="Project(s)" />
</button>
</xpath>
</field>
</record>
<record id="view_invoice_tree_project_link_from_invoice_inherit" model="ir.ui.view">
<field name="name">account.invoice.list.project.link</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_invoice_tree"/>
<field name="arch" type="xml">
<xpath expr="//list/field[@name='invoice_date']" position="before">
<field name="projects_name" />
</xpath>
</field>
</record>
</odoo>

View File

@@ -5,7 +5,7 @@
"name": "project_only_current_project_tasks_as_parent",
"version": "18.0.1.0.0",
"author": "Elabore",
"website": "https://git.elabore.coop/elabore/project-tools",
"website": "https://elabore.coop",
"maintainer": "Laetitia Da Costa",
"license": "AGPL-3",
"category": "Project",

View File

@@ -1,17 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record model="ir.ui.view" id="view_tasks_form2_parent_project_inherited">
<field name="name">view.tasks.form2.parent.project.inherited</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']//field[@name='parent_id']"
position="attributes"
>
<attribute
name="domain"
>[('project_id','=', project_id), ('stage_id.fold', '=', False)]</attribute>
<xpath expr="//page[@name='extra_info']//field[@name='parent_id']" position="attributes">
<attribute name="domain">[('project_id','=', project_id), ('stage_id.fold', '=', False)]</attribute>
</xpath>
</field>
</record>

View File

@@ -5,7 +5,7 @@
"name": "project_request_data",
"version": "18.0.1.0.0",
"author": "Elabore",
"website": "https://git.elabore.coop/elabore/project-tools",
"website": "https://elabore.coop",
"maintainer": "Stéphan Sainléger",
"license": "AGPL-3",
"category": "Project",
@@ -89,4 +89,4 @@ This module is maintained by Elabore.
# and independently installed. Used for synergetic or glue modules.
"auto_install": False,
"application": False,
}
}

View File

@@ -1,3 +1,3 @@
from . import task_service
from . import request_type
from . import project_task
from . import project_task

View File

@@ -1,8 +1,9 @@
from odoo import fields, models
from odoo import models, fields
class Task(models.Model):
_inherit = "project.task"
service_id = fields.Many2one("task.service", string="Service")
request_type_id = fields.Many2one("request.type", string="Request Type")
service_id = fields.Many2one('task.service', string='Service')
request_type_id = fields.Many2one('request.type', string='Request Type')

View File

@@ -1,9 +1,9 @@
from odoo import fields, models
from odoo import models, fields
class RequestType(models.Model):
_name = "request.type"
_description = "Request Type"
name = fields.Char("name", required=True)
sequence = fields.Integer()
name = fields.Char('name', required=True)
sequence = fields.Integer()

View File

@@ -1,9 +1,9 @@
from odoo import fields, models
from odoo import models, fields
class TaskService(models.Model):
_name = "task.service"
_description = "Task service"
name = fields.Char("name", required=True)
sequence = fields.Integer()
name = fields.Char('name', required=True)
sequence = fields.Integer()

View File

@@ -2,4 +2,4 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_task_service_user,project.task_service.user,model_task_service,,1,0,0,0
access_task_service_manager,project.task_service.manager,model_task_service,project.group_project_manager,1,1,1,1
access_request_type_user,project.request_type.user,model_request_type,,1,0,0,0
access_request_type_manager,project.request_type.manager,model_request_type,project.group_project_manager,1,1,1,1
access_request_type_manager,project.request_type.manager,model_request_type,project.group_project_manager,1,1,1,1
1 id name model_id/id group_id/id perm_read perm_write perm_create perm_unlink
2 access_task_service_user project.task_service.user model_task_service 1 0 0 0
3 access_task_service_manager project.task_service.manager model_task_service project.group_project_manager 1 1 1 1
4 access_request_type_user project.request_type.user model_request_type 1 0 0 0
5 access_request_type_manager project.request_type.manager model_request_type project.group_project_manager 1 1 1 1

View File

@@ -1,15 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template
id="portal_my_task_request_data"
name="My Task: Request Data"
inherit_id="project.portal_my_task"
priority="40"
>
<xpath
expr="//div[@id='card_body']/div[hasclass('row','mb-4','container')]"
position="after"
>
<template id="portal_my_task_request_data" name="My Task: Request Data" inherit_id="project.portal_my_task" priority="40">
<xpath expr="//div[@id='card_body']/div[hasclass('row','mb-4','container')]" position="after">
<div id="request_data" class="row mb-4">
<div class="col-12 col-md-6" t-if="task.service_id">
<strong>Service:</strong>
@@ -22,4 +14,4 @@
</div>
</xpath>
</template>
</odoo>
</odoo>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_task_form2_request_data" model="ir.ui.view">
<field name="name">project.task.form.request.data</field>
@@ -18,19 +18,12 @@
<field name="model">project.task</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="before">
<div style="font-size:11px; margin-bottom: 8px;"><field
name="create_date"
widget="date"
/></div>
<div style="font-size:11px; margin-bottom: 8px;"><field name="create_date" widget="date"/></div>
</xpath>
<xpath expr="//field[@name='tag_ids']" position="before">
<span style="background:lightblue; font-size:11px"><field
name="service_id"
/></span>
<span style="background:lightsteelblue; font-size:11px"><field
name="request_type_id"
/></span>
<span style="background:lightblue; font-size:11px"><field name="service_id"/></span>
<span style="background:lightsteelblue; font-size:11px"><field name="request_type_id"/></span>
</xpath>
</field>
</record>
</odoo>
</odoo>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="request_type_view_tree" model="ir.ui.view">
@@ -18,12 +18,6 @@
<field name="view_mode">list</field>
</record>
<menuitem
id="menu_request_types"
action="act_request_types_list"
parent="project.menu_project_config"
sequence="99"
name="Request Types"
/>
<menuitem id="menu_request_types" action="act_request_types_list" parent="project.menu_project_config" sequence="99" name="Request Types" />
</odoo>
</odoo>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="task_service_view_tree" model="ir.ui.view">
@@ -18,12 +18,6 @@
<field name="view_mode">list</field>
</record>
<menuitem
id="menu_task_services"
action="act_task_services_list"
parent="project.menu_project_config"
sequence="99"
name="Task Services"
/>
<menuitem id="menu_task_services" action="act_task_services_list" parent="project.menu_project_config" sequence="99" name="Task Services" />
</odoo>
</odoo>

View File

@@ -5,15 +5,19 @@
"name": "Project task billable hours",
"version": "18.0.1.0.0",
"author": "Elabore",
"website": "https://git.elabore.coop/elabore/project-tools",
"website": "https://github.com/elabore-coop/project-tools",
"maintainer": "Laetitia Da Costa",
"license": "AGPL-3",
"category": "Tools",
"summary": "In task kanban view, display remaining billable hours instead of remaining hours",
"description": "",
"depends": ["project", "hr_timesheet", "project_working_time_task_portal"],
"depends": [
"project",
"hr_timesheet",
"project_working_time_task_portal"
],
"data": [
"views/project_task_views.xml",
"views/project_task_views.xml",
],
"installable": True,
"application": False,

View File

@@ -1,36 +1,18 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- Replace remaining hours by billable remaining hours in kanban view -->
<record id="view_task_kanban_billable_remaining_hours_inherit" model="ir.ui.view">
<field name="name">project.task.kanban.billable.remaining.hours.inherit</field>
<field name="model">project.task</field>
<field
name="inherit_id"
ref="hr_timesheet.view_task_kanban_inherited_progress"
/>
<field name="inherit_id" ref="hr_timesheet.view_task_kanban_inherited_progress"/>
<field name="arch" type="xml">
<xpath expr="//t[@name='allocated_hours']" position="replace">
<t t-set="badge" t-value="" />
<t
t-set="badge"
t-value="'badge-warning'"
t-if="record.billable_progress &gt;= 80 and record.billable_progress &lt;= 100"
/>
<t
t-set="badge"
t-value="'badge-danger'"
t-if="record.billable_remaining_hours.raw_value &lt; 0"
/>
<t
t-set="title"
t-value="'Remaining days'"
t-if="record.encode_uom_in_days.raw_value"
/>
<t t-set="title" t-value="'Billable remaining hours'" t-else="" />
<div
t-attf-class="oe_kanban_align badge {{ badge }}"
t-att-title="title"
>
<t t-set="badge" t-value=""/>
<t t-set="badge" t-value="'badge-warning'" t-if="record.billable_progress &gt;= 80 and record.billable_progress &lt;= 100"/>
<t t-set="badge" t-value="'badge-danger'" t-if="record.billable_remaining_hours.raw_value &lt; 0"/>
<t t-set="title" t-value="'Remaining days'" t-if="record.encode_uom_in_days.raw_value"/>
<t t-set="title" t-value="'Billable remaining hours'" t-else=""/>
<div t-attf-class="oe_kanban_align badge {{ badge }}" t-att-title="title">
<field name="billable_remaining_hours" widget="timesheet_uom" />
</div>
</xpath>

View File

@@ -1 +1,2 @@
from . import models

View File

@@ -5,7 +5,7 @@
"name": "project_user_default_project",
"version": "18.0.1.0.0",
"author": "Elabore",
"website": "https://git.elabore.coop/elabore/project-tools",
"website": "https://elabore.coop",
"maintainer": "Stéphan Sainléger",
"license": "AGPL-3",
"category": "Tools",
@@ -62,8 +62,7 @@ This module is maintained by Elabore.
""",
# any module necessary for this one to work correctly
"depends": [
"base",
"project",
"base", "project",
],
"qweb": [
# "static/src/xml/*.xml",
@@ -84,4 +83,4 @@ This module is maintained by Elabore.
# and independently installed. Used for synergetic or glue modules.
"auto_install": False,
"application": False,
}
}

View File

@@ -1 +1 @@
from . import res_users
from . import res_users

View File

@@ -1,7 +1,6 @@
from odoo import fields, models
from odoo import _, api, fields, models
class Users(models.Model):
_inherit = "res.users"
default_project_id = fields.Many2one("project.project", string="Default Project")
default_project_id = fields.Many2one('project.project', string='Default Project')

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_users_form_inherit_default_project" model="ir.ui.view">
<field name="name">view.users.form.inherit.default.project</field>
@@ -10,4 +10,4 @@
</xpath>
</field>
</record>
</odoo>
</odoo>

View File

@@ -1,2 +1,2 @@
*.*~
*pyc
*pyc

View File

@@ -1 +1 @@
from . import models
from . import models

View File

@@ -5,7 +5,7 @@
"name": "project_working_time_task_portal",
"version": "18.0.1.0.0",
"author": "Elabore",
"website": "https://git.elabore.coop/elabore/project-tools",
"website": "https://elabore.coop",
"maintainer": "Boris Gallet",
"license": "AGPL-3",
"category": "tools",
@@ -23,9 +23,9 @@
},
# always loaded
"data": [
# views/hr_timesheet_portal.xml",
#views/hr_timesheet_portal.xml",
"views/hr_timesheet_view_task_form2.xml",
"views/portal_tasks_list.xml",
"views/portal_tasks_list.xml"
],
"assets": {
"web.assets_frontend": [

View File

@@ -4,10 +4,9 @@
from odoo import models
from odoo.osv import expression
class AccountAnalyticLine(models.Model):
_inherit = "account.analytic.line"
_inherit = 'account.analytic.line'
def _timesheet_get_portal_domain(self):
domain = super()._timesheet_get_portal_domain()
return expression.AND([domain, [("exclude_from_sale_order", "=", False)]])
return expression.AND([domain, [('exclude_from_sale_order', '=', False)]])

View File

@@ -1,7 +1,9 @@
/**
* Main
* Main
*/
.oe-project_working_time_task_portal.progressbar {
height: 20px;
background-color: #7c7cac; /* Couleur de la barre de progression */
@@ -9,3 +11,4 @@
color: white; /* Couleur du texte dans la barre de progression */
line-height: 20px; /* Aligner le texte verticalement */
}

View File

@@ -1,41 +1,24 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<template
id="portal_my_task_allocated_hours"
name="My Task: planned hours"
inherit_id="hr_timesheet.portal_my_task"
priority="40"
>
<xpath
expr="//t[@t-call='hr_timesheet.portal_timesheet_table']"
position="before"
>
<template id="portal_my_task_allocated_hours" name="My Task: planned hours" inherit_id="hr_timesheet.portal_my_task" priority="40">
<xpath expr="//t[@t-call='hr_timesheet.portal_timesheet_table']" position="before">
<div class="row">
<div class="col-12 col-md-6 pb-2">
<span style="font-weight:bold;">Planned Hours: </span>
<span
t-field="task.allocated_hours"
t-options="{&quot;widget&quot;: &quot;timesheet_uom_no_toggle&quot;}"
/>
<span t-field="task.allocated_hours" t-options="{&quot;widget&quot;: &quot;timesheet_uom_no_toggle&quot;}"/>
<t t-if="task.allow_subtasks and task.subtask_count > 0">
<div>
<strong>Subtask Planned Hours: </strong>
<span
t-field="task.subtask_allocated_hours"
t-options="{&quot;widget&quot;: &quot;timesheet_uom_no_toggle&quot;}"
/>
<span t-field="task.subtask_allocated_hours" t-options="{&quot;widget&quot;: &quot;timesheet_uom_no_toggle&quot;}"/>
</div>
</t>
</div>
<t t-if="task.billable_progress > 0">
<div class="col-12 col-md-6 pb-2">
<strong>Progress: </strong>
<t t-set="progress" t-value="task.billable_progress" />
<div
class="oe-project_working_time_task_portal progressbar"
t-attf-style="width: #{progress}%;"
>
<t t-esc="progress" />%
<t t-set="progress" t-value="task.billable_progress"/>
<div class="oe-project_working_time_task_portal progressbar" t-attf-style="width: #{progress}%;">
<t t-esc="progress"/>%
</div>
</div>
</t>
@@ -43,25 +26,15 @@
</xpath>
</template>
<template
id="portal_my_task_remaining_hours"
name="My Task: remaining hours"
inherit_id="hr_timesheet.portal_timesheet_table"
priority="40"
>
<template id="portal_my_task_remaining_hours" name="My Task: remaining hours" inherit_id="hr_timesheet.portal_timesheet_table" priority="40">
<xpath expr="//tr[@t-foreach='timesheets']" position="attributes">
<attribute name="t-if">not timesheet.exclude_from_sale_order</attribute>
</xpath>
<xpath expr="//tfoot/tr/th[last()]" position="replace">
<th class="text-end">
<div
t-esc="round(sum(timesheets.filtered(lambda t: not t.exclude_from_sale_order).mapped('unit_amount')), 2)"
t-options="{&quot;widget&quot;: &quot;float_time&quot;}"
/>
<div t-if="task.allocated_hours > 0">Remaining Hours: <span
t-field="task.billable_remaining_hours"
t-options="{&quot;widget&quot;: &quot;float_time&quot;}"
/></div>
<div t-esc="round(sum(timesheets.filtered(lambda t: not t.exclude_from_sale_order).mapped('unit_amount')), 2)"
t-options="{&quot;widget&quot;: &quot;float_time&quot;}"/>
<div t-if="task.allocated_hours > 0">Remaining Hours: <span t-field="task.billable_remaining_hours" t-options="{&quot;widget&quot;: &quot;float_time&quot;}"/></div>
</th>
</xpath>
</template>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="project_working_time_view_task_form" model="ir.ui.view">
@@ -9,165 +9,54 @@
<field name="arch" type="xml">
<xpath expr="//field[@name='effective_hours']" position="after">
<span>
<label
class="font-weight-bold"
for="billable_effective_hours"
string="Billable Effective Hours"
invisible="non_billable_effective_hours == 0.0 or encode_uom_in_days"
/>
<label
class="font-weight-bold"
for="billable_effective_hours"
string="Billable Effective Days"
invisible="non_billable_effective_hours == 0.0 or not encode_uom_in_days"
/>
<label class="font-weight-bold" for="billable_effective_hours" string="Billable Effective Hours" invisible="non_billable_effective_hours == 0.0 or encode_uom_in_days"/>
<label class="font-weight-bold" for="billable_effective_hours" string="Billable Effective Days" invisible="non_billable_effective_hours == 0.0 or not encode_uom_in_days"/>
</span>
<field
name="billable_effective_hours"
widget="timesheet_uom"
class="mt-2"
invisible="non_billable_effective_hours == 0.0"
nolabel="1"
/>
<field name="billable_effective_hours" widget="timesheet_uom" class="mt-2" invisible="non_billable_effective_hours == 0.0" nolabel="1"/>
<span>
<label
class="font-weight-bold"
for="non_billable_effective_hours"
string="Non Billable Effective Hours"
invisible="non_billable_effective_hours == 0.0 or encode_uom_in_days"
/>
<label
class="font-weight-bold"
for="non_billable_effective_hours"
string="Non Billable Effective Days"
invisible="non_billable_effective_hours == 0.0 or not encode_uom_in_days"
/>
<label class="font-weight-bold" for="non_billable_effective_hours" string="Non Billable Effective Hours" invisible="non_billable_effective_hours == 0.0 or encode_uom_in_days"/>
<label class="font-weight-bold" for="non_billable_effective_hours" string="Non Billable Effective Days" invisible="non_billable_effective_hours == 0.0 or not encode_uom_in_days"/>
</span>
<field
name="non_billable_effective_hours"
widget="timesheet_uom"
class="mt-2"
invisible="non_billable_effective_hours == 0.0"
nolabel="1"
/>
<field name="non_billable_effective_hours" widget="timesheet_uom" class="mt-2" invisible="non_billable_effective_hours == 0.0" nolabel="1"/>
</xpath>
<xpath expr="//field[@name='subtask_effective_hours']" position="after">
<span>
<label
class="font-weight-bold"
for="subtask_billable_effective_hours"
string="Subtask Billable Effective Hours"
invisible="subtask_effective_hours == 0.0 or encode_uom_in_days"
/>
<label
class="font-weight-bold"
for="subtask_billable_effective_hours"
string="Subtask Billable Effective Days"
invisible="subtask_effective_hours == 0.0 or not encode_uom_in_days"
/>
<label class="font-weight-bold" for="subtask_billable_effective_hours" string="Subtask Billable Effective Hours" invisible="subtask_effective_hours == 0.0 or encode_uom_in_days"/>
<label class="font-weight-bold" for="subtask_billable_effective_hours" string="Subtask Billable Effective Days" invisible="subtask_effective_hours == 0.0 or not encode_uom_in_days"/>
</span>
<field
name="subtask_billable_effective_hours"
widget="timesheet_uom"
class="mt-2"
invisible="subtask_effective_hours == 0.0"
nolabel="1"
/>
<field name="subtask_billable_effective_hours" widget="timesheet_uom" class="mt-2" invisible="subtask_effective_hours == 0.0" nolabel="1"/>
<span>
<label
class="font-weight-bold"
for="subtask_non_billable_effective_hours"
string="Subtask Non Billable Effective Hours"
invisible="subtask_effective_hours == 0.0 or encode_uom_in_days"
/>
<label
class="font-weight-bold"
for="subtask_non_billable_effective_hours"
string="Subtask Non Billable Effective Days"
invisible="subtask_effective_hours == 0.0 or not encode_uom_in_days"
/>
<label class="font-weight-bold" for="subtask_non_billable_effective_hours" string="Subtask Non Billable Effective Hours" invisible="subtask_effective_hours == 0.0 or encode_uom_in_days"/>
<label class="font-weight-bold" for="subtask_non_billable_effective_hours" string="Subtask Non Billable Effective Days" invisible="subtask_effective_hours == 0.0 or not encode_uom_in_days"/>
</span>
<field
name="subtask_non_billable_effective_hours"
widget="timesheet_uom"
class="mt-2"
invisible="subtask_effective_hours == 0.0"
nolabel="1"
/>
<field name="subtask_non_billable_effective_hours" widget="timesheet_uom" class="mt-2" invisible="subtask_effective_hours == 0.0" nolabel="1"/>
</xpath>
<xpath expr="//field[@name='total_hours_spent']" position="after">
<span invisible="subtask_effective_hours == 0.0">
<label
for="total_billable_hours_spent"
string="Total Billable Hours"
invisible="encode_uom_in_days"
/>
<label
for="total_billable_hours_spent"
string="Total Billable Days"
invisible="not encode_uom_in_days"
/>
<label for="total_billable_hours_spent" string="Total Billable Hours"
invisible="encode_uom_in_days"/>
<label for="total_billable_hours_spent" string="Total Billable Days"
invisible="not encode_uom_in_days"/>
</span>
<field
name="total_billable_hours_spent"
widget="timesheet_uom"
nolabel="1"
invisible="subtask_effective_hours == 0.0"
/>
<field name="total_billable_hours_spent" widget="timesheet_uom" nolabel="1"
invisible="subtask_effective_hours == 0.0"/>
<span invisible="subtask_effective_hours == 0.0">
<label
for="total_non_billable_hours_spent"
string="Total Non Billable Hours"
invisible="encode_uom_in_days"
/>
<label
for="total_non_billable_hours_spent"
string="Total Non Billable Days"
invisible="not encode_uom_in_days"
/>
<label for="total_non_billable_hours_spent" string="Total Non Billable Hours"
invisible="encode_uom_in_days"/>
<label for="total_non_billable_hours_spent" string="Total Non Billable Days"
invisible="not encode_uom_in_days"/>
</span>
<field
name="total_non_billable_hours_spent"
widget="timesheet_uom"
nolabel="1"
invisible="subtask_effective_hours == 0.0"
/>
<field name="total_non_billable_hours_spent" widget="timesheet_uom" nolabel="1"
invisible="subtask_effective_hours == 0.0"/>
</xpath>
<xpath expr="//field[@name='remaining_hours']" position="after">
<span>
<label
class="font-weight-bold text-danger"
for="billable_remaining_hours"
string="Billable Remaining Hours"
invisible="allocated_hours == 0.0 or encode_uom_in_days or billable_remaining_hours &gt;= 0"
/>
<label
class="font-weight-bold"
for="billable_remaining_hours"
string="Billable Remaining Hours"
invisible="allocated_hours == 0.0 or encode_uom_in_days or billable_remaining_hours &lt; 0"
/>
<label
class="font-weight-bold text-danger"
for="billable_remaining_hours"
string="Billable Remaining Days"
invisible="allocated_hours == 0.0 or not encode_uom_in_days or billable_remaining_hours &gt;= 0"
/>
<label
class="font-weight-bold"
for="billable_remaining_hours"
string="Billable Remaining Days"
invisible="allocated_hours == 0.0 or not encode_uom_in_days or billable_remaining_hours &lt; 0"
/>
<label class="font-weight-bold text-danger" for="billable_remaining_hours" string="Billable Remaining Hours" invisible="allocated_hours == 0.0 or encode_uom_in_days or billable_remaining_hours &gt;= 0"/>
<label class="font-weight-bold" for="billable_remaining_hours" string="Billable Remaining Hours" invisible="allocated_hours == 0.0 or encode_uom_in_days or billable_remaining_hours &lt; 0"/>
<label class="font-weight-bold text-danger" for="billable_remaining_hours" string="Billable Remaining Days" invisible="allocated_hours == 0.0 or not encode_uom_in_days or billable_remaining_hours &gt;= 0"/>
<label class="font-weight-bold" for="billable_remaining_hours" string="Billable Remaining Days" invisible="allocated_hours == 0.0 or not encode_uom_in_days or billable_remaining_hours &lt; 0"/>
</span>
<field
name="billable_remaining_hours"
widget="timesheet_uom"
class="oe_subtotal_footer_separator"
invisible="allocated_hours == 0.0"
decoration-danger="billable_remaining_hours &lt; 0"
nolabel="1"
/>
<field name="billable_remaining_hours" widget="timesheet_uom" class="oe_subtotal_footer_separator" invisible="allocated_hours == 0.0" decoration-danger="billable_remaining_hours &lt; 0" nolabel="1"/>
</xpath>
</field>
</record>

View File

@@ -1,29 +1,16 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template
id="portal_tasks_list_inherit"
inherit_id="hr_timesheet.portal_tasks_list_inherit"
>
<xpath
expr="//thead//t[@t-if='not project or project.allow_timesheets']/th[@class='text-end']"
position="replace"
>
<template id="portal_tasks_list_inherit" inherit_id="hr_timesheet.portal_tasks_list_inherit">
<xpath expr="//thead//t[@t-if='not project or project.allow_timesheets']/th[@class='text-end']" position="replace">
<th class="text-end">Billable hours</th>
</xpath>
<xpath
expr="//tbody//td[@t-if='not project or project.allow_timesheets']"
position="replace"
>
<xpath expr="//tbody//td[@t-if='not project or project.allow_timesheets']" position="replace">
<td t-if="not project or project.allow_timesheets" class="text-end">
<t t-if="task.allow_timesheets and not is_uom_day">
<span
t-field="task.billable_effective_hours"
t-options='{"widget": "float_time"}'
/>
<t t-if="task.allocated_hours > 0"> / <span
t-field="task.allocated_hours"
t-options='{"widget": "float_time"}'
/>
<span t-field="task.billable_effective_hours"
t-options='{"widget": "float_time"}' />
<t t-if="task.allocated_hours > 0"> / <span t-field="task.allocated_hours"
t-options='{"widget": "float_time"}' />
</t>
</t>
</td>