Compare commits
7 Commits
18.0
...
d53ba8bd64
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d53ba8bd64 | ||
|
|
1664bb043a | ||
|
|
f9f1c98580 | ||
|
|
0a7814ad1a | ||
|
|
a97bd8ecca | ||
|
|
8ab14340f1 | ||
|
|
9192465ca2 |
@@ -1 +1,2 @@
|
||||
|
||||
from . import models, controllers
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
from . import custom_portal
|
||||
|
||||
from . import custom_portal
|
||||
@@ -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
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
|
||||
from . import project_project
|
||||
from . import res_partner
|
||||
from . import res_partner
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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[ ]]></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[ ]]></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>
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
# Project Link From Invoice
|
||||
|
||||

|
||||
|
||||
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.
|
||||
@@ -1,2 +0,0 @@
|
||||
|
||||
from . import models
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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)"
|
||||
@@ -1,2 +0,0 @@
|
||||
|
||||
from . import account_move
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
from . import task_service
|
||||
from . import request_type
|
||||
from . import project_task
|
||||
from . import project_task
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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,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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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 >= 80 and record.billable_progress <= 100"
|
||||
/>
|
||||
<t
|
||||
t-set="badge"
|
||||
t-value="'badge-danger'"
|
||||
t-if="record.billable_remaining_hours.raw_value < 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 >= 80 and record.billable_progress <= 100"/>
|
||||
<t t-set="badge" t-value="'badge-danger'" t-if="record.billable_remaining_hours.raw_value < 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>
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
|
||||
from . import models
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
from . import res_users
|
||||
from . import res_users
|
||||
@@ -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')
|
||||
@@ -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>
|
||||
2
project_working_time_task_portal/.gitignore
vendored
2
project_working_time_task_portal/.gitignore
vendored
@@ -1,2 +1,2 @@
|
||||
*.*~
|
||||
*pyc
|
||||
*pyc
|
||||
@@ -1 +1 @@
|
||||
from . import models
|
||||
from . import models
|
||||
@@ -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": [
|
||||
|
||||
@@ -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)]])
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
|
||||
@@ -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="{"widget": "timesheet_uom_no_toggle"}"
|
||||
/>
|
||||
<span t-field="task.allocated_hours" t-options="{"widget": "timesheet_uom_no_toggle"}"/>
|
||||
<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="{"widget": "timesheet_uom_no_toggle"}"
|
||||
/>
|
||||
<span t-field="task.subtask_allocated_hours" t-options="{"widget": "timesheet_uom_no_toggle"}"/>
|
||||
</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="{"widget": "float_time"}"
|
||||
/>
|
||||
<div t-if="task.allocated_hours > 0">Remaining Hours: <span
|
||||
t-field="task.billable_remaining_hours"
|
||||
t-options="{"widget": "float_time"}"
|
||||
/></div>
|
||||
<div t-esc="round(sum(timesheets.filtered(lambda t: not t.exclude_from_sale_order).mapped('unit_amount')), 2)"
|
||||
t-options="{"widget": "float_time"}"/>
|
||||
<div t-if="task.allocated_hours > 0">Remaining Hours: <span t-field="task.billable_remaining_hours" t-options="{"widget": "float_time"}"/></div>
|
||||
</th>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
@@ -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 >= 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 < 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 >= 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 < 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 >= 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 < 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 >= 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 < 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 < 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 < 0" nolabel="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user