Add odex25_project

This commit is contained in:
expert 2024-06-24 14:17:49 +03:00
parent 3a3f834c54
commit 432ae2c303
538 changed files with 53148 additions and 1 deletions

View File

@ -1,2 +1,2 @@
# odex25-standard-moduless
# odex25-standard-modules
This Repo contains general standard modules for all projects.

View File

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from . import models
from . import reports

View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# (Odex - Extending the base module).
# Copyright (C) 2024 Expert Co. Ltd. (<http://exp-sa.com>).
#
##############################################################################
{
"name": "Odex -KPI",
"version": "1.0",
"author": "Expert Co. Ltd.",
"category": "Odex25-base/Odex-Base25",
"website": "http://www.exp-sa.com",
"installable": True,
"depends": [
"kpi_scorecard",
],
"data": [
"security/ir.model.access.csv",
"views/kpi_category.xml",
"views/kpi_item.xml",
"views/kpi_scorecard_line.xml",
'views/menu_security_cus.xml'
],
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
from . import kpi_category
from . import kpi_item
from . import kpi_scorecard_line
from . import kpi_period
from . import partner_target

View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# (Odex - Extending the base module).
# Copyright (C) 2024 Expert Co. Ltd. (<http://exp-sa.com>).
#
##############################################################################
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class kpi_category(models.Model):
_inherit = "kpi.category"
code = fields.Char(string="Code")
weight = fields.Float(string="Weight")
type = fields.Selection([
("pillar", "Pillar"),
("st_goal", "Strategic Goal"),
("op_goal", "Operational Goal"),
("other", "Other")],string="Type", default="op_goal")
def name_get(self):
return super(models.Model, self).name_get()
def write(self, data):
res = super(kpi_category, self).write(data)
if data.get('weight'):
self.env["kpi.item"].compute_automated_weight(self)
return res

View File

@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# (Odex - Extending the base module).
# Copyright (C) 2024 Expert Co. Ltd. (<http://exp-sa.com>).
#
##############################################################################
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class kpi_item(models.Model):
_inherit = "kpi.item"
code = fields.Char(string="KPI Code", required=True)
weight = fields.Float(string="Weight")
automated_weight = fields.Boolean(string="Automated Weight", default=True)
kpi_domain = fields.Selection([
("university", "University Strategy"),
("research", "Scientific Research"),
("institute", "Institutional Accreditation"),
("program", "Program Accreditation")], string="KPI Domain", required=True, default="university")
program_id = fields.Many2one("kpi.category", string="Program")
kpi_type = fields.Selection([
("efficiency", "Efficiency"),
("effectiveness", "Effectiveness"),
("productivity", "Productivity"),
("financial", "Financial"),
("quality", "Quality"),
("security", "Security")], string="KPI Type", required=True, default="efficiency")
kpi_type_region = fields.Selection([
("inputs", "Inputs"),
("processes", "Processes"),
("outputs", "Outputs"),], string="KPI Type By Region", required=True, default="inputs")
data_source = fields.Selection([
("achievement", "Achievement"),
("direct_data", "Direct Data"),
("poll", "Opinion Polls"),], string="Data source", required=True, default="achievement")
measurement_method = fields.Selection([
("data_entry", "Data entry"),
("integration", "integration"),], string="Measurement Method", required=True, default="data_entry")
measurement_periodic= fields.Selection([
("periodical", "Periodical"),
("annually", "Annually"),], string="Measurement Periodicity", required=True, default="annually")
kpi_owner_id = fields.Many2one("res.partner", string="KPI Owner")
internal_bench_ids = fields.Many2many("res.partner",
"res_partner_kpi_item_rel_internal",
"res_partner_id", "kpi_item_id", string="Internal Benchmarking")
external_bench_ids = fields.Many2many("res.partner",
"res_partner_kpi_item_rel_external",
"res_partner_id", "kpi_item_id", string="External Benchmarking")
kpi_period = fields.Selection([
("1", "1"),
("2", "2"),
("3", "3"),
("4", "4"),("5", "5") ], string="KPI Period", default="5")
line_ids = fields.One2many("kpi.scorecard.line", "kpi_id", string="KPI Targets", copy=True)
def compute_automated_weight(self, category_ids):
for category in category_ids:
goal_kpis = self.env["kpi.item"].sudo().search([ ("category_id", "=", category.id),("automated_weight", "=", True)])
goal_kpis_edit = self.env["kpi.item"].sudo().search([ ("category_id", "=", category.id),("automated_weight", "=", False)])
goal_weight = category.weight - sum(goal_kpis_edit.mapped('weight'))
if len(goal_kpis) > 0:
weight = round(goal_weight / len(goal_kpis) , 2)
goal_kpis.write({"weight": weight})
@api.model
def create(self, data):
res = super(kpi_item, self).create(data)
category_ids =[]
for kpi in res:
category_ids.append(kpi.category_id)
self.compute_automated_weight(category_ids)
return res
def write(self, data):
category_ids =[]
for kpi in self:
if data.get('category_id'):
category_ids.append(kpi.category_id)
res = super(kpi_item, self).write(data)
if data.keys() & {'automated_weight', 'active', 'category_id'}:
for kpi in self:
category_ids.append(kpi.category_id)
if data.get('weight') and not data.get('automated_weight'):
for kpi in self:
if not kpi.automated_weight:
category_ids.append(kpi.category_id)
self.compute_automated_weight(category_ids)
return res
def unlink(self):
category_ids =[]
for kpi in self:
category_ids.append(kpi.category_id)
res = super(kpi_item, self).unlink()
self.compute_automated_weight(category_ids)
return res
@api.onchange('category_id')
def onchange_category_id(self):
'''
This function generates the kpi code automatically with the possibility to modify it
'''
if self.category_id and self.category_id.code:
parent_code = self.category_id.code
count = len (self.search([("category_id", "=", self.category_id.id)])) or 1
serial = int(count)
added = 0
code = ''
while True:
serial += added
code_length = len(str(parent_code)) + len(str(serial))
code = parent_code +str(serial)
if not self.search([('code','=',code)]):
break
added += 1
self.code = code

View File

@ -0,0 +1,111 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError, UserError
import base64
import logging
import tempfile
_logger = logging.getLogger(__name__)
try:
import xlsxwriter
except ImportError:
_logger.warning("Cannot import xlsxwriter")
xlsxwriter = False
class kpi_period(models.Model):
_inherit = "kpi.period"
def name_get(self):
return super(models.Model, self).name_get()
def action_export_scorecard(self):
"""
The method to prepare the xls table
Methods:
* _get_xls_table of kpi.scorecard.line
Returns:
* action of downloading the xlsx table
Extra info:
* Expected singleton
"""
self.ensure_one()
if not xlsxwriter:
raise UserError(_("The Python library xlsxwriter is installed. Contact your system administrator"))
file_name = u"{}.xlsx".format(self.name_get()[0][1])
file_path = tempfile.mktemp(suffix='.xlsx')
workbook = xlsxwriter.Workbook(file_path)
main_header_style = workbook.add_format({
'bold': True,
'font_size': 11,
'border': 1,
'align': 'center',
'valign': 'vcenter',
'bg_color': 'silver',
'border_color': 'gray',
})
main_cell_style_dict = {
'font_size': 11,
'border': 1,
'border_color': 'gray',
}
worksheet = workbook.add_worksheet(file_name)
column_keys = [
{"key": "A", "label": _("KPI Code"), "width": 14},
{"key": "B", "label": _("KPI"), "width": 60},
{"key": "C", "label": _("Target"), "width": 14},
{"key": "D", "label": _("Success"), "width": 14},
{"key": "E", "label": _("Actual"), "width": 14},
{"key": "F", "label": _("Progress"), "width": 14},
{"key": "G", "label": _("Weight"), "width": 14},
{"key": "H", "label": _("Weight progress"), "width": 14},
{"key": "I", "label": _("Notes"), "width": 80},
]
total_row_number = len(self.line_ids)
cell_values = self.line_ids._get_xls_table()
for ccolumn in column_keys:
ckey = ccolumn.get("key")
# set columns
worksheet.set_column('{c}:{c}'.format(c=ckey), ccolumn.get("width"))
# set header row
worksheet.write("{}1".format(ckey), ccolumn.get("label"), main_header_style)
# set column values
for row_number in range(2, total_row_number + 2):
cell_number = "{}{}".format(ckey, row_number)
cell_value_dict = cell_values.get(cell_number)
cell_value = ""
cell_level = 0
cell_style = main_cell_style_dict.copy()
if cell_value_dict:
cell_value = cell_value_dict.get("value")
cell_style.update(cell_value_dict.get("style"))
cell_level = cell_value_dict.get("level") or 0
cell_style = workbook.add_format(cell_style)
if ckey == "A":
cell_style.set_indent(cell_level)
worksheet.write(
cell_number,
cell_value,
cell_style,
)
worksheet.set_row(0, 24)
workbook.close()
with open(file_path, 'rb') as r:
xls_file = base64.b64encode(r.read())
att_vals = {
'name': file_name,
'type': 'binary',
'datas': xls_file,
}
attachment_id = self.env['ir.attachment'].create(att_vals)
self.env.cr.commit()
action = {
'type': 'ir.actions.act_url',
'url': '/web/content/{}?download=true'.format(attachment_id.id, ),
'target': 'self',
}
return action

View File

@ -0,0 +1,216 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# (Odex - Extending the base module).
# Copyright (C) 2024 Expert Co. Ltd. (<http://exp-sa.com>).
#
##############################################################################
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError, UserError
import base64
import logging
import tempfile
_logger = logging.getLogger(__name__)
try:
import xlsxwriter
except ImportError:
_logger.warning("Cannot import xlsxwriter")
xlsxwriter = False
class kpi_scorecard_line(models.Model):
_inherit = "kpi.scorecard.line"
@api.depends("target_value", "actual_value", "computation_error", "kpi_id", "kpi_id.result_appearance",
"kpi_id.result_preffix", "kpi_id.result_suffix", "kpi_id.result_type")
def _compute_formatted_actual_value(self):
"""
Compute method for formatted_actual_value, result, formatted_target_value
Methods:
* _return_formated_appearance of kpi.item
"""
for line in self:
kpi_id = line.kpi_id
company_currency = line.period_id.company_id.currency_id
if line.computation_error:
line.formatted_actual_value = _("N/A")
line.result = "error"
line.formatted_target_value = kpi_id._return_formated_appearance(
line.target_value, novalue_change=True, currency=company_currency,
)
line.formatted_planed_value = kpi_id._return_formated_appearance(
line.planed_value, novalue_change=True, currency=company_currency,
)
else:
actual_value = line.actual_value
line.formatted_actual_value = kpi_id._return_formated_appearance(
line.actual_value, novalue_change=False, currency=company_currency,
)
line.formatted_planed_value = kpi_id._return_formated_appearance(
line.planed_value, novalue_change=True, currency=company_currency,
)
line.formatted_target_value = kpi_id._return_formated_appearance(
line.target_value, novalue_change=True, currency=company_currency,
)
result_type = kpi_id.result_type
actual_value = line.kpi_id.result_appearance == "percentage" and (actual_value * 100) or actual_value
bigger_result = actual_value >= line.target_value
if result_type == "more":
line.result = bigger_result and "success" or "failure"
elif result_type == "less":
line.result = bigger_result and "failure" or "success"
weight = fields.Float(string="Weight", related="kpi_id.weight", store=True)
code = fields.Char(string="KPI Code", related="kpi_id.code", store=True)
progress = fields.Float("Progress", compute='_compute_progress_kpi', store=True, group_operator="avg")
weight_progress = fields.Float("Weight Progress", compute='_compute_progress_kpi', store=True, )
target_value = fields.Float(string="Success Value")
planed_value = fields.Float(string="Target Value")
formatted_planed_value = fields.Char(
string="Target",
compute=_compute_formatted_actual_value,
compute_sudo=True,
store=True,
)
pillar_categ_id = fields.Many2one(comodel_name='kpi.category', compute='_compute_parent_category', store=True, )
strategic_category_id = fields.Many2one(comodel_name='kpi.category', compute='_compute_parent_category',
store=True, )
plan_progress = fields.Float(string="Planned Progress", compute='_compute_progress_kpi', store=True,
)
def get_strategic_category(self, category_id):
"""
Get the last parent category with type 'st_goal' in the chain of categories.
:param category_id: The initial category for which to find the last 'pillar' parent.
:return: The last Strategic Goal parent category or None if not found.
"""
if category_id.type == 'st_goal':
return category_id
while category_id.parent_id:
category_id = category_id.parent_id
if category_id.type == 'st_goal':
return category_id
return None
def get_pillar_category(self, category_id):
"""
Get the last parent category with type 'pillar' in the chain of categories.
:param category_id: The initial category for which to find the last 'pillar' parent.
:return: The last 'pillar' parent category or None if not found.
"""
if category_id.type == 'pillar':
return category_id
while category_id.parent_id:
category_id = category_id.parent_id
if category_id.type == 'pillar':
return category_id
return None
def action_compare_with_partners_targets(self):
return self.env['ir.actions.act_window']._for_xml_id('kpi_scorecard_odex.scorecard_line_tree_action')
@api.depends('kpi_id', 'kpi_id.category_id')
def _compute_parent_category(self):
for line in self:
last_pillar_categ = line.get_pillar_category(line.category_id)
last_strategic_categ = line.get_strategic_category(line.category_id)
line.pillar_categ_id = last_pillar_categ.id if last_strategic_categ else False
line.strategic_category_id = last_strategic_categ.id if last_strategic_categ else False
@api.depends('target_value', 'actual_value', 'weight')
def _compute_progress_kpi(self):
for kpi in self:
if (kpi.target_value > 0.0):
if kpi.actual_value > kpi.target_value:
kpi.progress = 100
else:
kpi.progress = round(100.0 * kpi.actual_value / kpi.target_value, 2)
else:
kpi.progress = 0.0
kpi.weight_progress = round((kpi.progress * kpi.weight) / 100.0, 2)
kpi.plan_progress = kpi.progress * kpi.weight
def _get_xls_table(self):
"""
The method to prepare dict of values for xls row
Args:
* spaces - str - to add at the beginning of the name
Methods:
* _return_xls_formatting - of kpi.item
Returns:
* dict
"""
result = {}
row = 2
previous_kpis = {}
for line in self:
parent_id = line.kpi_id.parent_id.id
level = previous_kpis.get(parent_id) is not None and previous_kpis.get(parent_id) + 1 or 0
previous_kpis.update({line.kpi_id.id: level})
description = line.description or ""
target_value = line.target_value
planed_value = line.planed_value
actual_value = line.actual_value
overall_style = {
"color": line.result == "success" and "black" or line.result == "failure" and "red" or "orange"
}
if line.computation_error:
target_value = 0
planed_value = 0
actual_value = 0
description = "{} {}".format(line.computation_error, description)
overall_style.update({"color": "orange"})
planed_value = line.kpi_id._return_xls_formatting(line.planed_value, False)
target_value = line.kpi_id._return_xls_formatting(line.target_value, False)
actual_value = line.kpi_id._return_xls_formatting(line.actual_value, True)
num_style = overall_style.copy()
num_style.update({
"align": "center",
})
num_style1 = num_style.copy()
if line.kpi_id.result_appearance == "percentage":
num_style.update({"num_format": 10}),
overall_style.update({
"valign": "vjustify",
})
result.update({
"A{}".format(row): {"value": line.kpi_id.code, "style": overall_style, "level": level},
"B{}".format(row): {"value": line.kpi_id.name, "style": overall_style, "level": level},
"C{}".format(row): {"value": planed_value, "style": num_style},
"D{}".format(row): {"value": target_value, "style": num_style},
"E{}".format(row): {"value": actual_value, "style": num_style},
"F{}".format(row): {"value": line.progress, "style": num_style1},
"G{}".format(row): {"value": line.kpi_id.weight, "style": num_style1},
"H{}".format(row): {"value": line.weight_progress, "style": num_style1},
"I{}".format(row): {"value": description, "style": overall_style},
})
row += 1
return result
def action_assign_target_values(self):
self.ensure_one()
partner_target_ids = self.env['partner.target'].search([('kbi_line_id', '=', self.id)])
action = {
'name': _('Targets'),
'type': 'ir.actions.act_window',
'res_model': 'partner.target',
'view_mode': 'tree',
'context': {'default_kbi_line_id': self.id},
'target': 'current',
'domain': [('id', 'in', partner_target_ids.ids)],
}
return action

View File

@ -0,0 +1,11 @@
from odoo import fields, models, api
class PartnerTarget(models.Model):
_name = 'partner.target'
kbi_line_id = fields.Many2one(comodel_name='kpi.scorecard.line')
partner_id = fields.Many2one(comodel_name='res.partner')
planned_value = fields.Float()
target_value=fields.Float(related='kbi_line_id.target_value')
actual_value=fields.Float(related='kbi_line_id.actual_value')

View File

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_partner_target,access.partner.target,model_partner_target,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_partner_target access.partner.target model_partner_target 1 1 1 1

View File

@ -0,0 +1,38 @@
<?xml version="1.0"?>
<odoo>
<record id="kpi_category_view_form" model="ir.ui.view">
<field name="name">kpi.category.form.inherit</field>
<field name="model">kpi.category</field>
<field name="inherit_id" ref="kpi_scorecard.kpi_category_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='parent_id']" position="before">
<field name="code" />
<field name="type" />
<field name="weight" />
</xpath>
</field>
</record>
<record id="kpi_category_view_tree" model="ir.ui.view">
<field name="name">kpi.category.tree.inherit</field>
<field name="model">kpi.category</field>
<field name="inherit_id" ref="kpi_scorecard.kpi_category_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//tree" position="replace">
<tree decoration-bf="type in ('pillar','st_goal')" decoration-warning="type=='pillar'" decoration-info="type=='op_goal'">
<field name="sequence" widget="handle"/>
<field name="type"/>
<field name="code"/>
<field name="name"/>
<field name="parent_id"/>
<field name="weight" />
<field name="active" invisible="1"/>
<field name="child_ids" invisible="1"/>
</tree>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,253 @@
<?xml version="1.0"?>
<odoo>
<record id="kpi_item_view_form" model="ir.ui.view">
<field name="name">kpi.item.form.inherit</field>
<field name="model">kpi.item</field>
<field name="inherit_id" ref="kpi_scorecard.kpi_item_view_form"/>
<field name="arch" type="xml">
<xpath expr="//form" position="replace">
<form>
<sheet>
<div class="oe_title">
<h1>
<field name="name" placeholder="KPI"/>
</h1>
</div>
<group>
<group>
<field name="category_id" string="Operational Goal"/>
<field name="kpi_domain"/>
<field name="program_id" attrs="{'invisible': [('kpi_domain', '!=', 'program')]}"/>
<field name="kpi_owner_id"/>
<field name="company_id" options="{'no_create_edit': 1, 'no_quick_create': 1}"
groups="base.group_multi_company"/>
</group>
<group>
<field name="code"/>
<field name="kpi_type"/>
<field name="kpi_type_region"/>
</group>
</group>
<group>
<field name="description"
placholder="notes... would be shown as explanation title on scorecard target line"/>
</group>
<group col="4">
<field name="internal_bench_ids" invisible="1"/>
<field name="external_bench_ids" widget="many2many_tags" invisible="1"/>
</group>
<notebook>
<page string="Settings">
<group>
<group>
<field name="automated_weight"/>
<field name="weight" attrs="{'readonly': [('automated_weight', '=', True)]}"/>
<field name="data_source"/>
<field name="measurement_periodic"/>
<field name="measurement_method"/>
<field name="kpi_period"/>
<field name="sequence"/>
</group>
<group>
<field name="result_type"/>
<field name="result_appearance"/>
<field name="result_preffix"
attrs="{'invisible': [('result_appearance', '=', 'monetory')]}"/>
<field name="result_suffix"
attrs="{'invisible': [('result_appearance', '=', 'monetory')]}"/>
<field name="currency_id"
attrs="{'invisible': [('result_appearance', '!=', 'monetory')]}"/>
<field name="result_rounding"/>
</group>
</group>
</page>
<page string="kpiFormula">
<h2 class="oe_read_only pull-right text-warning"
attrs="{'invisible': [('formula_warning', '=', False)]}">
<field name="formula_warning"/>
</h2>
<field name="formula" widget="kpiFormulaWidget"/>
</page>
<page string="KPI Targets">
<field name="line_ids">
<tree editable="bottom">
<field name="period_id" domain="[('state','=','open')]"/>
<field name="planed_value"/>
<field name="target_value"/>
<button name="action_assign_target_values" type="object" icon="fa-list"
width="0.1" role="img" title="Assign Partners Target"/>
<field name="pillar_categ_id" invisible="1"/>
<field name="strategic_category_id" invisible="1"/>
</tree>
</field>
</page>
<page string="Security">
<group string="Read Rights">
<field name="user_ids" widget="many2many_tags"
options="{'no_create_edit': 1, 'no_quick_create': 1}"/>
<field name="user_group_ids" widget="many2many_tags"
options="{'no_create_edit': 1, 'no_quick_create': 1}"/>
<field name="access_user_ids" widget="many2many_tags" invisible="1"/>
</group>
<group string="Edit Rights">
<field name="edit_user_ids" widget="many2many_tags"
options="{'no_create_edit': 1, 'no_quick_create': 1}"/>
<field name="edit_user_group_ids" widget="many2many_tags"
options="{'no_create_edit': 1, 'no_quick_create': 1}"/>
<field name="edit_access_user_ids" widget="many2many_tags" invisible="1"/>
</group>
</page>
<page string="Technical" invisible="1">
<group>
<field name="active"/>
<field name="all_parent_ids" widget="many2many_tags"/>
</group>
<group string="Measurements">
<field name="measures_ids" widget="many2many_tags"/>
<field name="constant_ids" widget="many2many_tags"/>
<field name="kpi_ids" widget="many2many_tags"/>
</group>
</page>
<page string="Help" attrs="{'invisible': [('help_notes', '=', False)]}">
<field name="kpi_help_dummy" invisible="1"/>
<field name="help_notes"/>
</page>
</notebook>
</sheet>
</form>
</xpath>
</field>
</record>
<record id="kpi_item_view_tree" model="ir.ui.view">
<field name="name">kpi.item.tree.inherit</field>
<field name="model">kpi.item</field>
<field name="inherit_id" ref="kpi_scorecard.kpi_item_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="before">
<field name="code"/>
</xpath>
<xpath expr="//field[@name='name']" position="after">
<field name="category_id" string="Operational Goal"/>
<field name="kpi_owner_id" optional="hide"/>
<field name="kpi_domain" optional="hide"/>
<field name="kpi_type" optional="show"/>
<field name="kpi_type_region" optional="hide"/>
<field name="data_source" optional="hide"/>
<field name="measurement_periodic" optional="hide"/>
<field name="measurement_method" optional="show"/>
<field name="result_appearance" optional="show"/>
<field name="weight" sum="Weight"/>
</xpath>
<xpath expr="//field[@name='formula_warning']" position="replace">
<field name="formula_warning" invisible="1"/>
</xpath>
</field>
</record>
<record id="kpi_item_view_search" model="ir.ui.view">
<field name="name">kpi.item.search.inherit</field>
<field name="model">kpi.item</field>
<field name="inherit_id" ref="kpi_scorecard.kpi_item_view_search"/>
<field name="arch" type="xml">
<search position="inside">
<searchpanel>
<field name="category_id" string="Operational Goal" icon="fa-building" enable_counters="1"/>
<field name="kpi_type" icon="fa-building" enable_counters="1"/>
<field name="kpi_type_region" icon="fa-building" enable_counters="1"/>
</searchpanel>
</search>
</field>
</record>
<record id="kpi_item_view_kanban" model="ir.ui.view">
<field name="name">kpi.item.kanban</field>
<field name="model">kpi.item</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile">
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_content oe_kanban_global_click">
<div class="row">
<div class="col-12">
<strong>
<field name="name"/>
</strong>
</div>
</div>
<div class="oe_kanban_bottom_right mt-1">
<field name="code"/>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="kpi_scorecard.kpi_item_action" model="ir.actions.act_window">
<field name="view_mode">tree,kanban,form</field>
</record>
<record id="kpi_period_view_form" model="ir.ui.view">
<field name="name">kpi.period.form.inherit</field>
<field name="model">kpi.period</field>
<field name="inherit_id" ref="kpi_scorecard.kpi_period_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='target_value']" position="before">
<field name="planed_value"/>
</xpath>
</field>
</record>
<record id="view_partner_target_tree" model="ir.ui.view">
<field name="name">partner.target.tree</field>
<field name="model">partner.target</field>
<field name="arch" type="xml">
<tree editable="bottom" create="1" delete="1">
<field name="kbi_line_id" readonly="1" string="Period"/>
<field name="partner_id" string="Partner" required="1"/>
<field name="planned_value" string="Planned Value"/>
</tree>
</field>
</record>
<record id="partner_target_view_tree" model="ir.ui.view">
<field name="name">partner.target.tree.view</field>
<field name="model">partner.target</field>
<field name="arch" type="xml">
<tree editable="bottom" decoration-danger="(actual_value &lt; planned_value)" >
<field name="partner_id"/>
<field name="planned_value" string="Benchmark"/>
<field name="target_value"/>
<field name="actual_value"/>
</tree>
</field>
</record>
<record id="scorecard_line_tree_action" model="ir.actions.act_window">
<field name="name">Partners Targets</field>
<field name="res_model">partner.target</field>
<field name="view_mode">tree</field>
<field name="target">current</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('partner_target_view_tree')})]"/>
</record>
</odoo>

View File

@ -0,0 +1,145 @@
<?xml version="1.0"?>
<odoo>
<record id="kpi_scorecard_line_view_kanban" model="ir.ui.view">
<field name="name">kpi.scorecard.line.kanban.inherit</field>
<field name="model">kpi.scorecard.line</field>
<field name="inherit_id" ref="kpi_scorecard.kpi_scorecard_line_view_kanban"/>
<field name="arch" type="xml">
<xpath expr="//kanban" position="replace">
<kanban js_class="scorecard_kanban"
class="kpi-kanban-view"
group_create="0"
group_delete="0"
group_edit="0"
archivable="0"
quick_create="0"
records_draggable="0"
>
<field name="id"/>
<field name="kpi_id"/>
<field name="formatted_target_value"/>
<field name="formatted_actual_value"/>
<field name="computation_error"/>
<field name="result"/>
<field name="weight" />
<field name="progress" />
<field name="weight_progress" />
<field name="all_parents"/>
<field name="sequence"/>
<field name="description"/>
<field name="edit_rights"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="kpi-kanban-record-card row mx-0"
t-att-id="id"
>
<div class="col mx-2">
<b><field name="code" /></b>
</div>
<div t-attf-class="text-#{record.result.raw_value == 'error' and 'muted' or ''}"
t-attf-data-parent="#{record.all_parents.raw_value}"
t-attf-data-id="#{record.id.raw_value}"
t-attf-title="#{record.description.raw_value and record.description.raw_value or ''}"
style="flex: 0 0 35%;">
<field name="kpi_id"/>
<i t-if="record.computation_error.raw_value"
class="fa fa-warning text-danger"
t-attf-title="#{record.computation_error.raw_value}"
> </i>
</div>
<div class="col mx-2">
<b><field name="formatted_planed_value" /></b>
<div>Target Value</div>
</div>
<div class="col mx-2">
<b><field name="formatted_target_value" /></b>
<div>Success Value</div>
</div>
<div t-attf-class="col mx-2 text-#{record.result.raw_value == 'success' and 'success' or record.result.raw_value == 'failure' and 'danger' or 'muted'}">
<b><field name="formatted_actual_value" /></b>
<div>Actual Value</div>
</div>
<div class="col mx-2">
<b><field name="progress" widget="progressbar"/></b>
</div>
<div class="col mx-2">
<b><field name="weight" /></b>
<div>Weight</div>
</div>
<div class="col mx-2">
<b><field name="weight_progress" /></b>
<div>Weight progress</div>
</div>
<div style="flex: 0 0 5%;">
<div class="pull-right">
<button type="button"
class="btn btn-primary kpi-edit-target kpi-kanban-row-btn kpi-kanban-button kpi-kanban-conditional"
kpi-required="period,open"
t-attf-data-id="#{record.id.raw_value}"
title="Change Target"
t-if="record.edit_rights.raw_value"
name="kpi-edit-target"
>
<i class="fa fa-edit"></i>
</button>
<button type="button"
class="btn btn-primary kpi-show-history kpi-kanban-row-btn kpi-kanban-button kpi-kanban-conditional"
kpi-required="period"
t-attf-data-id="#{record.id.raw_value}"
title="History"
name="kpi-show-history"
>
<i class="fa fa-signal"></i>
</button>
<button type="button"
class="btn btn-primary kpi-remove-target kpi-kanban-row-btn kpi-kanban-button kpi-kanban-conditional"
kpi-required="period,open"
t-attf-data-id="#{record.id.raw_value}"
title="Delete Target"
t-if="record.edit_rights.raw_value"
name="kpi-remove-target"
>
<i class="fa fa-remove"></i>
</button>
<button name="action_compare_with_partners_targets" type="object" class="btn btn-warning btn-block" title="Compare"
>
<i class="fa fa-bar-chart">Benchmark</i>
</button>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</xpath>
</field>
</record>
<record id="kpi_scorecard_line_view_pivot" model="ir.ui.view">
<field name="name">kpi.scorecard.line.pivot</field>
<field name="model">kpi.scorecard.line</field>
<field name="arch" type="xml">
<pivot string="Kbi" sample="1">
<field name="planed_value" type="measure"/>
<field name="target_value" type="measure"/>
<field name="actual_value" type="measure"/>
<field name="kpi_id" type="row"/>
<field name="period_id" type="col"/>
</pivot>
</field>
</record>
<record id="kpi_scorecard.kpi_scorecard_line_action" model="ir.actions.act_window">
<field name="view_mode">kanban,pivot</field>
</record>
</odoo>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Customize menus security -->
<odoo>
<record model="ir.ui.menu" id="kpi_scorecard.menu_kpi_main">
<field name="groups_id" eval="[(6,0,[ref('kpi_scorecard.group_kpi_user')])]"/>
</record>
</odoo>

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import models

View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
{
'name': 'Helpdesk After Sales',
'category': 'Odex25-Project/Odex25-Project',
'summary': 'Project, Tasks, After Sales',
'author': "Expert Co. Ltd.",
'website': "http://www.exp-sa.com",
'depends': ['odex25_helpdesk', 'sale_management'],
'auto_install': True,
'description': """
Manage the after sale of the products from helpdesk tickets.
""",
'data': [
'views/odex25_helpdesk_views.xml',
],
'demo': ['data/odex25_helpdesk_sale_demo.xml'],
}

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="sale_order_odex25_helpdesk_1" model="sale.order">
<field name="partner_id" ref="base.res_partner_2"/>
<field name="partner_invoice_id" ref="base.res_partner_2"/>
<field name="partner_shipping_id" ref="base.res_partner_2"/>
</record>
<record id="sale_order_line_odex25_helpdesk_1" model="sale.order.line">
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_27').get_product_multiline_description_sale()"/>
<field name="product_id" ref="product.product_product_27"/>
<field name="product_uom_qty">1</field>
<field name="price_unit">3645.00</field>
<field name="order_id" ref="sale_order_odex25_helpdesk_1"/>
</record>
<record id="sale_order_odex25_helpdesk_2" model="sale.order">
<field name="partner_id" ref="base.res_partner_10"/>
<field name="partner_invoice_id" ref="base.res_partner_10"/>
<field name="partner_shipping_id" ref="base.res_partner_10"/>
</record>
<record id="sale_order_line_odex25_helpdesk_2" model="sale.order.line">
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_10').get_product_multiline_description_sale()"/>
<field name="product_id" ref="product.product_product_10"/>
<field name="product_uom_qty">1</field>
<field name="price_unit">14.00</field>
<field name="order_id" ref="sale_order_odex25_helpdesk_2"/>
</record>
<record id="sale_order_odex25_helpdesk_3" model="sale.order">
<field name="partner_id" ref="base.res_partner_2"/>
<field name="partner_invoice_id" ref="base.res_partner_2"/>
<field name="partner_shipping_id" ref="base.res_partner_2"/>
</record>
<record id="sale_order_line_odex25_helpdesk_3" model="sale.order.line">
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_12').get_product_multiline_description_sale()"/>
<field name="product_id" ref="product.product_product_12"/>
<field name="product_uom_qty">1</field>
<field name="price_unit">12.50</field>
<field name="order_id" ref="sale_order_odex25_helpdesk_3"/>
</record>
<record id="odex25_helpdesk.odex25_helpdesk_ticket_13" model="odex25_helpdesk.ticket">
<field name="sale_order_id" ref="sale_order_odex25_helpdesk_2"/>
</record>
<record id="odex25_helpdesk.odex25_helpdesk_ticket_16" model="odex25_helpdesk.ticket">
<field name="sale_order_id" ref="sale_order_odex25_helpdesk_1"/>
</record>
<record id="odex25_helpdesk.odex25_helpdesk_ticket_10" model="odex25_helpdesk.ticket">
<field name="sale_order_id" ref="sale_order_odex25_helpdesk_3"/>
</record>
</odoo>

View File

@ -0,0 +1,59 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * odex25_helpdesk_sale
#
# Translators:
# Mustafa Rawi <mustafa@cubexco.com>, 2020
# Osama Ahmaro <osamaahmaro@gmail.com>, 2020
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~13.5+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-09-01 07:39+0000\n"
"PO-Revision-Date: 2020-09-07 08:20+0000\n"
"Last-Translator: Osama Ahmaro <osamaahmaro@gmail.com>, 2020\n"
"Language-Team: Arabic (https://www.transifex.com/odoo/teams/41243/ar/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Language: ar\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
#. module: odex25_helpdesk_sale
#: model:ir.model.fields,field_description:odex25_helpdesk_sale.field_odex25_helpdesk_ticket__commercial_partner_id
msgid "Commercial Entity"
msgstr "الكيان التجاري"
#. module: odex25_helpdesk_sale
#: model:ir.model.fields,field_description:odex25_helpdesk_sale.field_odex25_helpdesk_ticket__display_name
msgid "Display Name"
msgstr "الاسم المعروض"
#. module: odex25_helpdesk_sale
#: model:ir.model,name:odex25_helpdesk_sale.model_odex25_helpdesk_ticket
msgid "Helpdesk Ticket"
msgstr "تذكرة مكتب المساعدة"
#. module: odex25_helpdesk_sale
#: model:ir.model.fields,field_description:odex25_helpdesk_sale.field_odex25_helpdesk_ticket__id
msgid "ID"
msgstr "المُعرف"
#. module: odex25_helpdesk_sale
#: model:ir.model.fields,field_description:odex25_helpdesk_sale.field_odex25_helpdesk_ticket____last_update
msgid "Last Modified on"
msgstr "آخر تعديل في"
#. module: odex25_helpdesk_sale
#: model:ir.model.fields,field_description:odex25_helpdesk_sale.field_odex25_helpdesk_ticket__sale_order_id
msgid "Ref. Sales Order"
msgstr ""
#. module: odex25_helpdesk_sale
#: model:ir.model.fields,help:odex25_helpdesk_sale.field_odex25_helpdesk_ticket__sale_order_id
msgid ""
"Reference of the Sales Order to which this ticket refers. Setting this "
"information aims at easing your After Sales process and only serves "
"indicative purposes."
msgstr ""

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import odex25_helpdesk

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
class odex25_helpdeskTicket(models.Model):
_inherit = 'odex25_helpdesk.ticket'
commercial_partner_id = fields.Many2one(related='partner_id.commercial_partner_id')
sale_order_id = fields.Many2one('sale.order', string='Ref. Sales Order',
domain="""[
'|', (not commercial_partner_id, '=', 1), ('partner_id', 'child_of', commercial_partner_id or []),
('company_id', '=', company_id)]""",
groups="sales_team.group_sale_salesman,account.group_account_invoice",
help="Reference of the Sales Order to which this ticket refers. Setting this information aims at easing your After Sales process and only serves indicative purposes.")
def copy(self, default=None):
if not self.env.user.has_group('sales_team.group_sale_salesman') and not self.env.user.has_group('account.group_account_invoice'):
if default is None:
default = {'sale_order_id': False}
else:
default.update({'sale_order_id': False})
return super(odex25_helpdeskTicket, self).copy(default=default)

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="odex25_helpdesk_ticket_view_form_inherit_odex25_helpdesk_invoicing" model="ir.ui.view">
<field name='name'>odex25_helpdesk.ticket.form.inherit.invoicing</field>
<field name="model">odex25_helpdesk.ticket</field>
<field name="inherit_id" ref="odex25_helpdesk.odex25_helpdesk_ticket_view_form"/>
<field name="arch" type="xml">
<field name='email_cc' position="after">
<field name="commercial_partner_id" invisible="1"/>
<field name="sale_order_id" options='{"no_open": True}' readonly="1" invisible="1"/>
</field>
<xpath expr="//field[@name='partner_id']" position="attributes">
<attribute name="options">{'always_reload': True}</attribute>
<attribute name="context">{'res_partner_search_mode': 'customer'}</attribute>
</xpath>
</field>
</record>
<record id="quick_create_ticket_form" model="ir.ui.view">
<field name='name'>odex25_helpdesk.ticket.form.quick_create</field>
<field name="model">odex25_helpdesk.ticket</field>
<field name="inherit_id" ref="odex25_helpdesk.quick_create_ticket_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="attributes">
<attribute name="options">{'always_reload': True}</attribute>
<attribute name="context">{'res_partner_search_mode': 'customer'}</attribute>
</xpath>
</field>
</record>
<record id="odex25_helpdesk_ticket_view_form_inherit_sale_user" model="ir.ui.view">
<field name='name'>odex25_helpdesk.ticket.form.inherit.invoicing</field>
<field name="model">odex25_helpdesk.ticket</field>
<field name="inherit_id" ref="odex25_helpdesk_ticket_view_form_inherit_odex25_helpdesk_invoicing"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='sale_order_id']" position="attributes">
<attribute name="options">{"no_create": True}</attribute>
<attribute name="readonly">0</attribute>
</xpath>
</field>
<field name="groups_id" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
</record>
</odoo>

View File

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
from . import models
from . import wizard
from odoo import api, SUPERUSER_ID
def _odex25_helpdesk_timesheet_post_init(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
teams = env['odex25_helpdesk.team'].search([('use_odex25_helpdesk_timesheet', '=', True), ('project_id', '=', False), ('use_odex25_helpdesk_sale_timesheet', '=', False)])
for team in teams:
team.project_id = team._create_project(team.name, team.use_odex25_helpdesk_sale_timesheet, {'allow_timesheets': True, 'allow_timesheets': True})
env['odex25_helpdesk.ticket'].search([('team_id', '=', team.id), ('project_id', '=', False)]).write({'project_id': team.project_id.id})

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
{
'name': 'Helpdesk Timesheet',
'category': 'Odex25-Project/Odex25-Project',
'summary': 'Project, Tasks, Timesheet',
'author': "Expert Co. Ltd.",
'website': "http://www.exp-sa.com",
'depends': ['hr_timesheet', 'odex25_timesheet_grid', 'odex25_helpdesk'],
'description': """
- Allow to set project for Helpdesk team
- Track timesheet for a task from a ticket
""",
'data': [
'security/ir.model.access.csv',
'security/odex25_helpdesk_timesheet_security.xml',
'views/odex25_helpdesk_views.xml',
'views/project_views.xml',
'wizard/odex25_helpdesk_ticket_create_timesheet_views.xml',
'data/odex25_helpdesk_timesheet_data.xml',
],
'demo': ['data/odex25_helpdesk_timesheet_demo.xml'],
'post_init_hook': '_odex25_helpdesk_timesheet_post_init'
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- TODO: remove me in master (function deprecated)
Force project creation for projects allowing timesheet: this will only be for the
first Team activating the 'use_odex25_helpdesk_timesheet' option and installing this module
-->
<function
model="odex25_helpdesk.team"
name="_init_data_create_project"
eval="[]"/>
</odoo>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="odex25_helpdesk.odex25_helpdesk_team3" model="odex25_helpdesk.team">
<field name="use_odex25_helpdesk_timesheet" eval="True"/>
</record>
<record id="odex25_helpdesk_timesheet_1" model="account.analytic.line">
<field name="name">Fix Drawer Slides</field>
<field name="date" eval="(DateTime.now() + relativedelta(days=-1)).strftime('%Y-%m-%d')"/>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="unit_amount">01.00</field>
<field name="odex25_helpdesk_ticket_id" ref="odex25_helpdesk.odex25_helpdesk_ticket_16"/>
<field name="account_id" ref="analytic.analytic_internal"/>
</record>
<record id="odex25_helpdesk_timesheet_2" model="account.analytic.line">
<field name="name">Changed Drawer Handle</field>
<field name="date" eval="(DateTime.now() + relativedelta(days=+1)).strftime('%Y-%m-%d')"/>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="unit_amount">0.5</field>
<field name="odex25_helpdesk_ticket_id" ref="odex25_helpdesk.odex25_helpdesk_ticket_16"/>
<field name="account_id" ref="analytic.analytic_internal"/>
</record>
</odoo>

View File

@ -0,0 +1,404 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * odex25_helpdesk_timesheet
#
# Translators:
# Sherif Abd Ekmoniem <sherif.tsupport@gmail.com>, 2020
# Mustafa Rawi <mustafa@cubexco.com>, 2020
# Akram Alfusayal <akram_ma@hotmail.com>, 2020
# amrnegm <amrnegm.01@gmail.com>, 2020
# Martin Trigaux, 2020
# Mohammed Ibrahim <m.ibrahim@mussder.com>, 2020
# Osama Ahmaro <osamaahmaro@gmail.com>, 2020
# Shaima Safar <shaima.safar@open-inside.com>, 2020
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-11-27 13:48+0000\n"
"PO-Revision-Date: 2020-09-07 08:20+0000\n"
"Last-Translator: Shaima Safar <shaima.safar@open-inside.com>, 2020\n"
"Language-Team: Arabic (https://www.transifex.com/odoo/teams/41243/ar/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Language: ar\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket___related_task_ids
msgid " Related Task"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_project_project__ticket_count
msgid "# Tickets"
msgstr "# التذاكر"
#. module: odex25_helpdesk_timesheet
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_timesheet.odex25_helpdesk_timer_ticket_view_kanban
msgid "<i class=\"fa fa-pause text-warning\" title=\"Timer is Paused\"/>"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_timesheet.odex25_helpdesk_timer_ticket_view_kanban
msgid "<i class=\"fa fa-play text-success\" title=\"Timer is Running\"/>"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: code:addons/odex25_helpdesk_timesheet/models/odex25_helpdesk.py:0
#, python-format
msgid ""
"All timesheet hours will be assigned to the selected task on save. Discard "
"to avoid the change."
msgstr ""
"عند الحفظ، سيتم إسناد ساعات سجل الأنشطة للمهمة المختارة. اختر تجاهل لتفادي "
"تطبيق التغيير."
#. module: odex25_helpdesk_timesheet
#: model:ir.model,name:odex25_helpdesk_timesheet.model_account_analytic_line
msgid "Analytic Line"
msgstr "البند التحليلي"
#. module: odex25_helpdesk_timesheet
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_timesheet.odex25_helpdesk_ticket_create_timesheet_view_form
msgid "Cancel"
msgstr "الغاء"
#. module: odex25_helpdesk_timesheet
#: code:addons/odex25_helpdesk_timesheet/models/odex25_helpdesk.py:0
#, python-format
msgid "Closed"
msgstr "مغلق"
#. module: odex25_helpdesk_timesheet
#: code:addons/odex25_helpdesk_timesheet/models/odex25_helpdesk.py:0
#, python-format
msgid "Confirm Time Spent"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model:ir.model,name:odex25_helpdesk_timesheet.model_odex25_helpdesk_ticket_create_timesheet
msgid "Create Timesheet from ticket"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket_create_timesheet__create_uid
msgid "Created by"
msgstr "أنشئ بواسطة"
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket_create_timesheet__create_date
msgid "Created on"
msgstr "أنشئ في"
#. module: odex25_helpdesk_timesheet
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_timesheet.odex25_helpdesk_ticket_create_timesheet_view_form
msgid "Describe your activity..."
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket_create_timesheet__description
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_timesheet.odex25_helpdesk_ticket_view_form_inherit_odex25_helpdesk_timesheet
msgid "Description"
msgstr "الوصف"
#. module: odex25_helpdesk_timesheet
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_timesheet.odex25_helpdesk_ticket_view_form_inherit_odex25_helpdesk_timesheet
msgid "Description of the ticket..."
msgstr "وصف التذكرة..."
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_account_analytic_line__display_name
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_team__display_name
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__display_name
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket_create_timesheet__display_name
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_project_project__display_name
msgid "Display Name"
msgstr "الاسم المعروض"
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__display_timer
msgid "Display Timer"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__display_timer_pause
msgid "Display Timer Pause"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__display_timer_resume
msgid "Display Timer Resume"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__display_timer_start_primary
msgid "Display Timer Start Primary"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__display_timer_start_secondary
msgid "Display Timer Start Secondary"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__display_timer_stop
msgid "Display Timer Stop"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__display_timesheet_timer
msgid "Display Timesheet Time"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_team__display_timesheet_timer
msgid "Display Timesheet Timer"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_timesheet.odex25_helpdesk_ticket_create_timesheet_view_form
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_timesheet.odex25_helpdesk_ticket_view_form_inherit_odex25_helpdesk_timesheet
msgid "Duration"
msgstr "المدة"
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__encode_uom_in_days
msgid "Encode Uom In Days"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model:ir.model,name:odex25_helpdesk_timesheet.model_odex25_helpdesk_team
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_project_project__odex25_helpdesk_team
msgid "Helpdesk Team"
msgstr "فريق مكتب المساعدة"
#. module: odex25_helpdesk_timesheet
#: model:ir.model,name:odex25_helpdesk_timesheet.model_odex25_helpdesk_ticket
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_account_analytic_line__odex25_helpdesk_ticket_id
msgid "Helpdesk Ticket"
msgstr "تذكرة مكتب المساعدة"
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_account_analytic_line__id
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_team__id
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__id
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket_create_timesheet__id
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_project_project__id
msgid "ID"
msgstr "المُعرف"
#. module: odex25_helpdesk_timesheet
#: code:addons/odex25_helpdesk_timesheet/models/odex25_helpdesk.py:0
#, python-format
msgid "In Progress"
msgstr "قيد التنفيذ"
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__is_closed
msgid "Is Closed"
msgstr "مقفل"
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__is_task_active
msgid "Is Task Active"
msgstr "هل المهمة نشطة"
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__is_timer_running
msgid "Is Timer Running"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_account_analytic_line____last_update
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_team____last_update
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket____last_update
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket_create_timesheet____last_update
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_project_project____last_update
msgid "Last Modified on"
msgstr "آخر تعديل في"
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket_create_timesheet__write_uid
msgid "Last Updated by"
msgstr "آخر تحديث بواسطة"
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket_create_timesheet__write_date
msgid "Last Updated on"
msgstr "آخر تحديث في"
#. module: odex25_helpdesk_timesheet
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_timesheet.odex25_helpdesk_ticket_view_form_inherit_odex25_helpdesk_timesheet
msgid "Pause"
msgstr "إيقاف"
#. module: odex25_helpdesk_timesheet
#: model:ir.model,name:odex25_helpdesk_timesheet.model_project_project
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_team__project_id
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__project_id
msgid "Project"
msgstr "المشروع"
#. module: odex25_helpdesk_timesheet
#: model:ir.actions.act_window,name:odex25_helpdesk_timesheet.project_project_action_view_odex25_helpdesk_tickets
msgid "Project Tickets"
msgstr "تذاكر المشروع"
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,help:odex25_helpdesk_timesheet.field_odex25_helpdesk_team__project_id
msgid ""
"Project to which the tickets (and the timesheets) will be linked by default."
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_timesheet.odex25_helpdesk_ticket_view_form_inherit_odex25_helpdesk_timesheet
msgid "Resume"
msgstr "استكمال الجلسة"
#. module: odex25_helpdesk_timesheet
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_timesheet.odex25_helpdesk_ticket_create_timesheet_view_form
msgid "Save"
msgstr "حفظ"
#. module: odex25_helpdesk_timesheet
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_timesheet.odex25_helpdesk_ticket_create_timesheet_view_form
msgid "Save time"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_timesheet.odex25_helpdesk_ticket_view_form_inherit_odex25_helpdesk_timesheet
msgid "Start"
msgstr "بدء"
#. module: odex25_helpdesk_timesheet
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_timesheet.odex25_helpdesk_ticket_view_form_inherit_odex25_helpdesk_timesheet
msgid "Stop"
msgstr "إيقاف"
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__task_id
msgid "Task"
msgstr "المهمة"
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,help:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__is_closed
msgid "Tasks in this stage are considered as closed."
msgstr "المهام في هذه المرحلة تعتبر مغلقة."
#. module: odex25_helpdesk_timesheet
#: code:addons/odex25_helpdesk_timesheet/models/odex25_helpdesk.py:0
#, python-format
msgid "The project is required to track time on ticket."
msgstr ""
#. module: odex25_helpdesk_timesheet
#: code:addons/odex25_helpdesk_timesheet/models/odex25_helpdesk.py:0
#, python-format
msgid "The task must be in ticket's project."
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,help:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__task_id
msgid "The task must have the same customer as this ticket."
msgstr "يجب أن يكون العميل المرتبط بالمهمة هو نفسه العميل صاحب التذكرة."
#. module: odex25_helpdesk_timesheet
#: model:ir.model.constraint,message:odex25_helpdesk_timesheet.constraint_odex25_helpdesk_ticket_create_timesheet_time_positive
msgid "The timesheet's time must be positive"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,help:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__use_odex25_helpdesk_timesheet
msgid "This required to have project module installed."
msgstr "يتطلب هذا تثبيت موديول المشروع."
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket_create_timesheet__ticket_id
msgid "Ticket"
msgstr "التذكرة"
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,help:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket_create_timesheet__ticket_id
msgid "Ticket for which we are creating a sales order"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_project_project__ticket_ids
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_timesheet.project_project_view_form_inherit_odex25_helpdesk_timesheet
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_timesheet.project_project_view_project_tickets_kanban_inherited
msgid "Tickets"
msgstr "تذاكر"
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket_create_timesheet__time_spent
msgid "Time"
msgstr "الوقت"
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__timer_pause
msgid "Timer Last Pause"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__timer_start
msgid "Timer Start"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_timesheet.odex25_helpdesk_ticket_view_form_inherit_odex25_helpdesk_timesheet
msgid "Timesheet Activities"
msgstr "أنشطة سجل النشاط"
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_team__timesheet_timer
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__timesheet_timer
msgid "Timesheet Timer"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__use_odex25_helpdesk_timesheet
msgid "Timesheet activated on Team"
msgstr "سجل النشاط المُفعل للفريق"
#. module: odex25_helpdesk_timesheet
#: code:addons/odex25_helpdesk_timesheet/models/odex25_helpdesk.py:0
#, python-format
msgid ""
"Timesheet hours will not be assigned to a customer task. Set a task to "
"charge a customer."
msgstr "لن يتم إسناد ساعات سجل الأنشطة لمهمة عميل. اختر مهمة لتُحسب على عميل."
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__timesheet_ids
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_timesheet.odex25_helpdesk_ticket_view_form_inherit_odex25_helpdesk_timesheet
msgid "Timesheets"
msgstr "سجلات الأنشطة"
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__total_hours_spent
msgid "Total Hours Spent"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_timesheet.odex25_helpdesk_ticket_view_form_inherit_odex25_helpdesk_timesheet
msgid "Total hours"
msgstr "إجمالي الساعات"
#. module: odex25_helpdesk_timesheet
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_timesheet.odex25_helpdesk_team_view_form_inherit_odex25_helpdesk_timesheet
msgid "Track your time using a timer"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: model:ir.model.fields,field_description:odex25_helpdesk_timesheet.field_odex25_helpdesk_ticket__user_timer_id
msgid "User Timer"
msgstr ""
#. module: odex25_helpdesk_timesheet
#: code:addons/odex25_helpdesk_timesheet/models/odex25_helpdesk.py:0
#, python-format
msgid "Warning"
msgstr "تحذير"

View File

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
from . import analytic
from . import odex25_helpdesk
from . import project

View File

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
from odoo.osv import expression
class AccountAnalyticLine(models.Model):
_inherit = 'account.analytic.line'
odex25_helpdesk_ticket_id = fields.Many2one('odex25_helpdesk.ticket', 'Helpdesk Ticket')
def _compute_task_id(self):
super(AccountAnalyticLine, self)._compute_task_id()
for line in self.filtered(lambda line: line.odex25_helpdesk_ticket_id):
line.task_id = line.odex25_helpdesk_ticket_id.task_id
def _timesheet_preprocess(self, vals):
odex25_helpdesk_ticket_id = vals.get('odex25_helpdesk_ticket_id')
if odex25_helpdesk_ticket_id:
ticket = self.env['odex25_helpdesk.ticket'].browse(odex25_helpdesk_ticket_id)
if ticket.project_id:
vals['project_id'] = ticket.project_id.id
if ticket.task_id:
vals['task_id'] = ticket.task_id.id
vals = super(AccountAnalyticLine, self)._timesheet_preprocess(vals)
return vals
def _timesheet_get_portal_domain(self):
domain = super(AccountAnalyticLine, self)._timesheet_get_portal_domain()
return expression.OR([domain, self._timesheet_in_odex25_helpdesk_get_portal_domain()])
def _timesheet_in_odex25_helpdesk_get_portal_domain(self):
return [
'&',
'&',
'&',
('task_id', '=', False),
('odex25_helpdesk_ticket_id', '!=', False),
'|',
'|',
('project_id.message_partner_ids', 'child_of', [self.env.user.partner_id.commercial_partner_id.id]),
('project_id.allowed_portal_user_ids', 'child_of', [self.env.user.id]),
('odex25_helpdesk_ticket_id.message_partner_ids', 'child_of', [self.env.user.partner_id.commercial_partner_id.id]),
('project_id.privacy_visibility', '=', 'portal')
]

View File

@ -0,0 +1,253 @@
# -*- coding: utf-8 -*-
from math import ceil
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class odex25_helpdeskTeam(models.Model):
_inherit = 'odex25_helpdesk.team'
project_id = fields.Many2one("project.project", string="Project", ondelete="restrict", domain="[('allow_timesheets', '=', True), ('company_id', '=', company_id)]",
help="Project to which the tickets (and the timesheets) will be linked by default.")
timesheet_timer = fields.Boolean('Timesheet Timer', default=True)
display_timesheet_timer = fields.Boolean(compute='_compute_display_timesheet_timer')
@api.depends('use_odex25_helpdesk_timesheet')
def _compute_display_timesheet_timer(self):
is_uom_hour = self.env.company.timesheet_encode_uom_id == self.env.ref('uom.product_uom_hour')
for team in self:
team.display_timesheet_timer = team.use_odex25_helpdesk_timesheet and is_uom_hour
@api.depends('use_odex25_helpdesk_timesheet')
def _compute_timesheet_timer(self):
for team in self:
team.timesheet_timer = team.use_odex25_helpdesk_timesheet
def _create_project(self, name, allow_billable, other):
return self.env['project.project'].create({
'name': name,
'type_ids': [
(0, 0, {'name': _('In Progress')}),
(0, 0, {'name': _('Closed'), 'is_closed': True})
],
'allow_timesheets': True,
'allow_timesheet_timer': True,
**other,
})
@api.model
def create(self, vals):
if vals.get('use_odex25_helpdesk_timesheet') and not vals.get('project_id'):
allow_billable = vals.get('use_odex25_helpdesk_sale_timesheet')
vals['project_id'] = self._create_project(vals['name'], allow_billable, {}).id
return super(odex25_helpdeskTeam, self).create(vals)
def write(self, vals):
if 'use_odex25_helpdesk_timesheet' in vals and not vals['use_odex25_helpdesk_timesheet']:
vals['project_id'] = False
result = super(odex25_helpdeskTeam, self).write(vals)
for team in self.filtered(lambda team: team.use_odex25_helpdesk_timesheet and not team.project_id):
team.project_id = team._create_project(team.name, team.use_odex25_helpdesk_sale_timesheet, {'allow_timesheets': True, 'allow_timesheet_timer': True})
self.env['odex25_helpdesk.ticket'].search([('team_id', '=', team.id), ('project_id', '=', False)]).write({'project_id': team.project_id.id})
return result
@api.model
def _init_data_create_project(self):
# TODO: remove me in master
return
class odex25_helpdeskTicket(models.Model):
_inherit = 'odex25_helpdesk.ticket'
# _inherit = ['odex25_helpdesk.ticket', 'timer.mixin']
@api.model
def default_get(self, fields_list):
result = super(odex25_helpdeskTicket, self).default_get(fields_list)
if 'project_id' in fields_list and result.get('team_id') and not result.get('project_id'):
result['project_id'] = self.env['odex25_helpdesk.team'].browse(result['team_id']).project_id.id
return result
# TODO: [XBO] change this field in related and stored (to count the number of tickets per project) field to the one in odex25_helpdesk.team
project_id = fields.Many2one("project.project", string="Project", domain="[('allow_timesheets', '=', True), ('company_id', '=', company_id)]")
# TODO: [XBO] remove me in master
task_id = fields.Many2one(
"project.task", string="Task", compute='_compute_task_id', store=True, readonly=False,
domain="[('id', 'in', _related_task_ids)]", tracking=True,
help="The task must have the same customer as this ticket.")
# TODO: [XBO] remove me in master (since task_id field will be removed too)
_related_task_ids = fields.Many2many('project.task', compute='_compute_related_task_ids')
timesheet_ids = fields.One2many('account.analytic.line', 'odex25_helpdesk_ticket_id', 'Timesheets')
is_closed = fields.Boolean(related="task_id.stage_id.is_closed", string="Is Closed", readonly=True)
# TODO: [XBO] remove me in master (since task_id field will be removed too)
is_task_active = fields.Boolean(related="task_id.active", string='Is Task Active', readonly=True)
use_odex25_helpdesk_timesheet = fields.Boolean('Timesheet activated on Team', related='team_id.use_odex25_helpdesk_timesheet', readonly=True)
timesheet_timer = fields.Boolean(related='team_id.timesheet_timer')
display_timesheet_timer = fields.Boolean("Display Timesheet Time", compute='_compute_display_timesheet_timer')
total_hours_spent = fields.Float(compute='_compute_total_hours_spent', default=0)
display_timer_start_secondary = fields.Boolean(compute='_compute_display_timer_buttons')
display_timer = fields.Boolean(compute='_compute_display_timer')
encode_uom_in_days = fields.Boolean(compute='_compute_encode_uom_in_days')
def _compute_encode_uom_in_days(self):
self.encode_uom_in_days = self.env.company.timesheet_encode_uom_id == self.env.ref('uom.product_uom_day')
@api.depends('display_timesheet_timer', 'timer_start', 'timer_pause', 'total_hours_spent')
def _compute_display_timer_buttons(self):
for ticket in self:
if not ticket.display_timesheet_timer:
ticket.update({
'display_timer_start_primary': False,
'display_timer_start_secondary': False,
'display_timer_stop': False,
'display_timer_pause': False,
'display_timer_resume': False,
})
else:
super(odex25_helpdeskTicket, ticket)._compute_display_timer_buttons()
ticket.display_timer_start_secondary = ticket.display_timer_start_primary
if not ticket.timer_start:
ticket.update({
'display_timer_stop': False,
'display_timer_pause': False,
'display_timer_resume': False,
})
if not ticket.total_hours_spent:
ticket.display_timer_start_secondary = False
else:
ticket.display_timer_start_primary = False
def _compute_display_timer(self):
if self.env.user.has_group('odex25_helpdesk.group_odex25_helpdesk_user') and self.env.user.has_group('hr_timesheet.group_hr_timesheet_user'):
self.display_timer = True
else:
self.display_timer = False
@api.depends('use_odex25_helpdesk_timesheet', 'timesheet_timer', 'timesheet_ids', 'encode_uom_in_days')
def _compute_display_timesheet_timer(self):
for ticket in self:
ticket.display_timesheet_timer = ticket.use_odex25_helpdesk_timesheet and ticket.timesheet_timer and not ticket.encode_uom_in_days
@api.depends('project_id', 'company_id')
def _compute_related_task_ids(self):
# TODO: [XBO] remove me in master because the task_id will be removed, then this compute and the _related_task_ids field will be useless
for t in self:
domain = [('project_id.allow_timesheets', '=', True), ('company_id', '=', t.company_id.id)]
if t.project_id:
domain = [('project_id', '=', t.project_id.id)]
t._related_task_ids = self.env['project.task'].search(domain)._origin
@api.depends('timesheet_ids')
def _compute_total_hours_spent(self):
for ticket in self:
ticket.total_hours_spent = round(sum(ticket.timesheet_ids.mapped('unit_amount')), 2)
@api.depends('project_id')
def _compute_task_id(self):
# TODO: [XBO] remove me in master (task_id field will be removed)
with_different_project = self.filtered(lambda t: t.project_id != t.task_id.project_id)
with_different_project.update({'task_id': False})
@api.onchange('task_id')
def _onchange_task_id(self):
# TODO: remove me in master
return
@api.constrains('project_id', 'team_id')
def _check_project_id(self):
# TODO: [XBO] see in master if we must remove this method, but since project_id will be a related field, this constrains will be useless.
for ticket in self:
if ticket.use_odex25_helpdesk_timesheet and not ticket.project_id:
raise ValidationError(_("The project is required to track time on ticket."))
@api.constrains('project_id', 'task_id')
def _check_task_in_project(self):
# TODO: [XBO] remove me in master (task_id field will be removed in master)
for ticket in self:
if ticket.task_id:
if ticket.task_id.project_id != ticket.project_id:
raise ValidationError(_("The task must be in ticket's project."))
def _get_timesheet(self):
# return not invoiced timesheet
timesheet_ids = self.timesheet_ids
return timesheet_ids.filtered(lambda t: (not t.timesheet_invoice_id or t.timesheet_invoice_id.state == 'cancel'))
@api.model_create_multi
def create(self, value_list):
team_ids = set([value['team_id'] for value in value_list if value.get('team_id')])
teams = self.env['odex25_helpdesk.team'].browse(team_ids)
team_project_map = {} # map with the team that require a project
for team in teams:
if team.use_odex25_helpdesk_timesheet:
team_project_map[team.id] = team.project_id.id
for value in value_list:
if value.get('team_id') and not value.get('project_id') and team_project_map.get(value['team_id']):
value['project_id'] = team_project_map[value['team_id']]
return super(odex25_helpdeskTicket, self).create(value_list)
def write(self, values):
result = super(odex25_helpdeskTicket, self).write(values)
# force timesheet values: changing ticket's task or project will reset timesheet ones
timesheet_vals = {}
for fname in self._timesheet_forced_fields():
if fname in values:
timesheet_vals[fname] = values[fname]
if timesheet_vals:
for timesheet in self.sudo()._get_timesheet():
timesheet.write(timesheet_vals) # sudo since Helpdesk user can change task
return result
# @api.model
# def _fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
# """ Set the correct label for `unit_amount`, depending on company UoM """
# result = super(odex25_helpdeskTicket, self)._fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
# result['arch'] = self.env['account.analytic.line']._apply_timesheet_label(result['arch'])
# return result
def action_view_ticket_task(self):
# TODO: [XBO] remove me in master (task_id field will be removed in master)
self.ensure_one()
return {
'view_mode': 'form',
'res_model': 'project.task',
'type': 'ir.actions.act_window',
'res_id': self.task_id.id,
}
def _timesheet_forced_fields(self):
""" return the list of field that should also be written on related timesheets """
return ['task_id', 'project_id']
def action_timer_start(self):
if not self.user_timer_id.timer_start and self.display_timesheet_timer:
super().action_timer_start()
def action_timer_stop(self):
# timer was either running or paused
if self.user_timer_id.timer_start and self.display_timesheet_timer:
minutes_spent = self.user_timer_id._get_minutes_spent()
minimum_duration = int(self.env['ir.config_parameter'].sudo().get_param('hr_timesheet.timesheet_min_duration', 0))
rounding = int(self.env['ir.config_parameter'].sudo().get_param('hr_timesheet.timesheet_rounding', 0))
minutes_spent = self._timer_rounding(minutes_spent, minimum_duration, rounding)
return self._action_open_new_timesheet(minutes_spent * 60 / 3600)
return False
def _action_open_new_timesheet(self, time_spent):
return {
"name": _("Confirm Time Spent"),
"type": 'ir.actions.act_window',
"res_model": 'odex25_helpdesk.ticket.create.timesheet',
"views": [[False, "form"]],
"target": 'new',
"context": {
**self.env.context,
'active_id': self.id,
'active_model': self._name,
'default_time_spent': time_spent,
},
}

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
class Project(models.Model):
_inherit = 'project.project'
ticket_ids = fields.One2many('odex25_helpdesk.ticket', 'project_id', string='Tickets')
ticket_count = fields.Integer('# Tickets', compute='_compute_ticket_count')
odex25_helpdesk_team = fields.One2many('odex25_helpdesk.team', 'project_id')
@api.depends('ticket_ids.project_id')
def _compute_ticket_count(self):
if not self.user_has_groups('odex25_helpdesk.group_odex25_helpdesk_user'):
self.ticket_count = 0
return
result = self.env['odex25_helpdesk.ticket'].read_group([
('project_id', 'in', self.ids)
], ['project_id'], ['project_id'])
data = {data['project_id'][0]: data['project_id_count'] for data in result}
for project in self:
project.ticket_count = data.get(project.id, 0)
@api.depends('odex25_helpdesk_team.timesheet_timer')
def _compute_allow_timesheet_timer(self):
super(Project, self)._compute_allow_timesheet_timer()
for project in self:
project.allow_timesheet_timer = project.allow_timesheet_timer or project.odex25_helpdesk_team.timesheet_timer

View File

@ -0,0 +1,6 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_project_project_odex25_helpdesk_user,project.project.odex25_helpdesk.user,project.model_project_project,odex25_helpdesk.group_odex25_helpdesk_user,1,0,0,0
access_project_task_odex25_helpdesk_user,project.task.odex25_helpdesk.user,project.model_project_task,odex25_helpdesk.group_odex25_helpdesk_user,1,0,0,0
access_analytic_account_heldpdesk_user,analytic.account.odex25_helpdesk.user,analytic.model_account_analytic_account,odex25_helpdesk.group_odex25_helpdesk_user,1,1,0,0
access_analytic_line_heldpdesk_user,analytic.line.odex25_helpdesk.user,analytic.model_account_analytic_line,odex25_helpdesk.group_odex25_helpdesk_user,1,1,1,1
access_odex25_helpdesk_ticket_create_timesheet,access.odex25_helpdesk.ticket.create.timesheet,model_odex25_helpdesk_ticket_create_timesheet,odex25_helpdesk.group_odex25_helpdesk_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_project_project_odex25_helpdesk_user project.project.odex25_helpdesk.user project.model_project_project odex25_helpdesk.group_odex25_helpdesk_user 1 0 0 0
3 access_project_task_odex25_helpdesk_user project.task.odex25_helpdesk.user project.model_project_task odex25_helpdesk.group_odex25_helpdesk_user 1 0 0 0
4 access_analytic_account_heldpdesk_user analytic.account.odex25_helpdesk.user analytic.model_account_analytic_account odex25_helpdesk.group_odex25_helpdesk_user 1 1 0 0
5 access_analytic_line_heldpdesk_user analytic.line.odex25_helpdesk.user analytic.model_account_analytic_line odex25_helpdesk.group_odex25_helpdesk_user 1 1 1 1
6 access_odex25_helpdesk_ticket_create_timesheet access.odex25_helpdesk.ticket.create.timesheet model_odex25_helpdesk_ticket_create_timesheet odex25_helpdesk.group_odex25_helpdesk_user 1 1 1 0

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo noupdate="1">
<record id="timesheet_line_rule_odex25_helpdesk_user" model="ir.rule">
<field name="name">account.analytic.line.odex25_helpdesk.user</field>
<field name="model_id" ref="analytic.model_account_analytic_line"/>
<field name="domain_force">[('user_id', '=', user.id), ('odex25_helpdesk_ticket_id', '!=', False)]</field>
<field name="groups" eval="[(4, ref('odex25_helpdesk.group_odex25_helpdesk_user'))]"/>
<field name="perm_create" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_unlink" eval="1"/>
<field name="perm_read" eval="0"/>
</record>
<record id="timesheet_line_rule_odex25_helpdesk_manager" model="ir.rule">
<field name="name">account.analytic.line.timesheet.manager</field>
<field name="model_id" ref="analytic.model_account_analytic_line"/>
<field name="domain_force">[('odex25_helpdesk_ticket_id', '!=', False)]</field>
<field name="groups" eval="[(4, ref('odex25_helpdesk.group_odex25_helpdesk_manager'))]"/>
<field name="perm_create" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_unlink" eval="1"/>
<field name="perm_read" eval="0"/>
</record>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import test_project

View File

@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase
class TestTimesheet(TransactionCase):
def setUp(self):
super(TestTimesheet, self).setUp()
self.partner = self.env['res.partner'].create({
'name': 'Customer Task',
'email': 'customer@task.com',
})
self.analytic_account = self.env['account.analytic.account'].create({
'name': 'Analytic Account for Test Customer',
'partner_id': self.partner.id,
'code': 'TEST'
})
def test_allow_timesheets_and_timer(self):
"""
Check that a modification of the 'allow_timesheets' field updates correctly the
'allow_timesheet_timer' field.
"""
Project = self.env['project.project']
odex25_helpdeskTeam = self.env['odex25_helpdesk.team']
# case 1: create a project with allow_timesheets set to FALSE
project_1 = Project.create({
'name': 'Project 1',
'allow_timesheets': False,
'partner_id': self.partner.id
})
self.assertFalse(
project_1.allow_timesheet_timer,
"On project creation with 'allow_timesheets' set to FALSE, 'allow_timesheet_timer' shall be set to FALSE")
# case 2: create a project with allow_timesheets set to TRUE
project_2 = Project.create({
'name': 'Project 2',
'allow_timesheets': True,
'partner_id': self.partner.id,
'analytic_account_id': self.analytic_account.id
})
self.assertTrue(
project_2.allow_timesheet_timer,
"On project creation with 'allow_timesheets' set to TRUE, 'allow_timesheet_timer' shall be set to TRUE")
# case 3: change 'allow_timesheets' from FALSE to TRUE
project_1.write({
'allow_timesheets': True
})
self.assertTrue(
project_1.allow_timesheet_timer,
"On 'allow_timesheets' change to TRUE, 'allow_timesheet_timer' shall be set to TRUE")
# case 4: change 'allow_timesheets' from TRUE to FALSE
project_2.write({
'allow_timesheets': False
})
self.assertFalse(
project_2.allow_timesheet_timer,
"On 'allow_timesheets' change to FALSE, 'allow_timesheet_timer' shall be set to FALSE")
# case 5: a Helpdesk team without timesheet timer
odex25_helpdeskTeam_1 = odex25_helpdeskTeam.create({
'name': 'Team #1',
'project_id': project_1.id,
'timesheet_timer': False,
})
self.assertTrue(
odex25_helpdeskTeam_1.project_id.allow_timesheet_timer,
"If 'allow_timesheets' is TRUE and 'timesheet_timer' is FALSE, 'allow_timesheet_timer' shall be set to TRUE")
# case 5: a Helpdesk team with a timesheet timer
odex25_helpdeskTeam_2 = odex25_helpdeskTeam.create({
'name': 'Team #2',
'project_id': project_2.id,
'timesheet_timer': True,
})
self.assertTrue(
odex25_helpdeskTeam_2.project_id.allow_timesheet_timer,
"If 'allow_timesheets' is FALSE and 'timesheet_timer' is TRUE,"
" 'allow_timesheet_timer' shall be set to TRUE")
# case 6: project with 'allow_timesheets' set to FALSE and team with
# 'timesheet_timer' set to FALSE.
project_3 = Project.create({
'name': 'Project 3',
'allow_timesheets': False,
'partner_id': self.partner.id
})
odex25_helpdeskTeam_3 = odex25_helpdeskTeam.create({
'name': 'Team #3',
'project_id': project_3.id,
'timesheet_timer': False,
})
self.assertFalse(
odex25_helpdeskTeam_3.project_id.allow_timesheet_timer,
"If 'allow_timesheets' is FALSE and 'timesheet_timer' is FALSE,"
" 'allow_timesheet_timer' shall be set to FALSE")

View File

@ -0,0 +1,189 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="odex25_helpdesk_team_view_form_inherit_odex25_helpdesk_timesheet" model="ir.ui.view">
<field name="name">odex25_helpdesk.team.form.inherit.timesheet</field>
<field name="model">odex25_helpdesk.team</field>
<field name="inherit_id" ref="odex25_helpdesk.odex25_helpdesk_team_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='odex25_helpdesk_timesheet']" position='replace'>
<div attrs="{'invisible': [('use_odex25_helpdesk_timesheet', '=', False)]}" class="pt-4">
<label for="project_id"/>
<field name="project_id" class="oe_inline" context="{'default_allow_timesheets': 1, 'default_allow_billable': use_odex25_helpdesk_sale_timesheet}"/>
</div>
</xpath>
<xpath expr="//div[@id='timesheet']" position='after'>
<field name="display_timesheet_timer" invisible="1"/>
<div class="col-lg-6 o_setting_box" attrs="{'invisible': [('display_timesheet_timer', '=', False)]}" id="timer_timesheet">
<div class="o_setting_left_pane">
<field name="timesheet_timer"/>
</div>
<div class="o_setting_right_pane">
<label for="timesheet_timer"/>
<div class="text-muted">
Track your time using a timer
</div>
</div>
</div>
</xpath>
</field>
</record>
<record id="odex25_helpdesk_ticket_view_form_inherit_odex25_helpdesk_timesheet" model="ir.ui.view">
<field name='name'>odex25_helpdesk.ticket.form.inherit.timesheet</field>
<field name="model">odex25_helpdesk.ticket</field>
<field name="inherit_id" ref="odex25_helpdesk.odex25_helpdesk_ticket_view_form"/>
<field name="arch" type="xml">
<button name="assign_ticket_to_self" position="after">
<field name="company_id" invisible="1"/>
<field name="display_timer" invisible="1"/>
<field name="display_timesheet_timer" invisible="1"/>
<field name="timer_pause" invisible="1" />
<field name="display_timer_start_primary" invisible="1"/>
<field name="display_timer_start_secondary" invisible="1"/>
<field name="display_timer_stop" invisible="1"/>
<field name="display_timer_pause" invisible="1"/>
<field name="display_timer_resume" invisible="1"/>
<field name="is_closed" invisible="1"/>
<field name="is_task_active" invisible="1"/>
<field name="encode_uom_in_days" invisible="1"/>
<button class="btn-primary" name="action_timer_start" type="object" string="Start"
attrs="{'invisible': ['|', ('display_timer_start_primary', '=', False), ('display_timer', '=', False)]}" icon="fa-clock-o"/>
<button class="btn-secondary" name="action_timer_start" type="object" string="Start"
attrs="{'invisible': ['|', ('display_timer_start_secondary', '=', False), ('display_timer', '=', False)]}" icon="fa-clock-o"/>
<button class="btn-primary btn-danger o_fsm_stop" name="action_timer_stop" type="object" string="Stop"
attrs="{'invisible': ['|', ('display_timer_stop', '=', False), ('display_timer', '=', False)]}" icon="fa-clock-o"/>
<button class="btn-primary" name="action_timer_pause" type="object" string="Pause"
attrs="{'invisible': ['|', ('display_timer_pause', '=', False), ('display_timer', '=', False)]}"/>
<button class="btn-primary btn-info" name="action_timer_resume" type="object" string="Resume"
attrs="{'invisible': ['|', ('display_timer_resume', '=', False), ('display_timer', '=', False)]}"/>
</button>
<xpath expr="//field[@name='stage_id']" position="attributes">
<attribute name="class">ml-2</attribute>
</xpath>
<xpath expr="//field[@name='stage_id']" position="before">
<field name="display_timer" invisible="1"/>
<field name="timer_start" widget="timer_timer" class="text-danger ml-auto h2 ml-4 font-weight-bold"
attrs="{'invisible': [('display_timer', '=', False)]}"/>
</xpath>
<xpath expr="//field[@name='email_cc']" position="after">
<field name="use_odex25_helpdesk_timesheet" invisible="1"/>
<field name="project_id" invisible="1"/>
<field name="_related_task_ids" invisible="1"/>
<field name="task_id" context="{'default_project_id': project_id, 'default_partner_id': partner_id}" invisible="1" widget="task_with_hours"/>
</xpath>
<xpath expr="//field[@name='description']" position="replace">
<notebook>
<page string="Description" name="description">
<field name="emp_req" invisible="1"/>
<field name="description" placeholder="Description of the ticket..." attrs="{'readonly':[('emp_req','=',True)]}"/>
</page>
<page string="Timesheets" name="timesheets"
attrs="{'invisible': ['|', ('project_id', '=', False), ('use_odex25_helpdesk_timesheet', '=', False)]}"
groups="hr_timesheet.group_hr_timesheet_user">
<field name='timesheet_ids' mode="tree,kanban" attrs="{'readonly': [('task_id', '!=', False), '|', ('is_closed', '=', True), ('is_task_active', '=', False)]}" context="{'default_project_id': project_id, 'default_task_id': task_id}" groups="hr_timesheet.group_hr_timesheet_user">
<tree editable="bottom" string="Timesheet Activities" default_order="date">
<field name="date"/>
<field name="user_id" invisible="1"/>
<field name="employee_id" required="1" widget="many2one_avatar_employee"/>
<field name="name" required="0"/>
<field name="unit_amount" widget="timesheet_uom" decoration-danger="unit_amount &gt; 24"/>
<field name="project_id" invisible="1"/>
<field name="task_id" invisible="1"/>
<field name="company_id" invisible="1"/>
</tree>
<kanban class="o_kanban_mobile">
<field name="date"/>
<field name="user_id"/>
<field name="employee_id"/>
<field name="name"/>
<field name="unit_amount"/>
<field name="project_id"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_card oe_kanban_global_click">
<div class="row">
<div class="col-6">
<strong><span><t t-esc="record.employee_id.value"/></span></strong>
</div>
<div class="col-6 pull-right text-right">
<strong><t t-esc="record.date.value"/></strong>
</div>
</div>
<div class="row">
<div class="col-6 text-muted">
<span><t t-esc="record.name.value"/></span>
</div>
<div class="col-6">
<span class="pull-right text-right">
<field name="unit_amount" widget="float_time" decoration-danger="unit_amount &gt; 24"/>
</span>
</div>
</div>
</div>
</t>
</templates>
</kanban>
<form string="Timesheet Activities">
<sheet>
<group>
<field name="date"/>
<field name="user_id" invisible="1"/>
<field name="employee_id" required="1"/>
<field name="name"/>
<field name="unit_amount" string="Duration" widget="float_time" decoration-danger="unit_amount &gt; 24"/>
<field name="project_id" invisible="1"/>
<field name="company_id" invisible="1"/>
</group>
</sheet>
</form>
</field>
<group>
<group class="oe_subtotal_footer oe_right" name="ticket_hours">
<span>
<label class="font-weight-bold" for="total_hours_spent" string="Hours Spent"
attrs="{'invisible': [('encode_uom_in_days', '=', True)]}"/>
<label class="font-weight-bold" for="total_hours_spent" string="Days Spent"
attrs="{'invisible': [('encode_uom_in_days', '=', False)]}"/>
</span>
<field name="total_hours_spent" widget="timesheet_uom" nolabel="1" />
</group>
</group>
</page>
</notebook>
</xpath>
</field>
</record>
<record id="odex25_helpdesk_ticket_view_search_inherit_odex25_helpdesk_timesheet" model="ir.ui.view">
<field name="name">odex25_helpdesk.ticket.search.inherit.timesheet</field>
<field name="model">odex25_helpdesk.ticket</field>
<field name="inherit_id" ref="odex25_helpdesk.odex25_helpdesk_tickets_view_search"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="after">
<field name="project_id"/>
</xpath>
</field>
</record>
<!-- Improve the ticket kanban in Project -->
<record id="odex25_helpdesk_timer_ticket_view_kanban" model="ir.ui.view">
<field name="name">odex25_helpdesk.ticket.kanban.timer</field>
<field name="model">odex25_helpdesk.ticket</field>
<field name="priority">5</field>
<field name="inherit_id" ref="odex25_helpdesk.odex25_helpdesk_ticket_view_kanban"/>
<field name="arch" type="xml">
<xpath expr="//t[@t-name='kanban-box']//field[@name='activity_ids']" position="before">
<field name="display_timesheet_timer" invisible="1"/>
<field name="timer_start" invisible="1"/>
<field name="timer_pause" invisible="1"/>
<t t-if="record.display_timesheet_timer &amp;&amp; record.timer_pause.raw_value">
<i class="fa fa-pause text-warning" title="Timer is Paused"></i>
</t>
<t t-elif="record.display_timesheet_timer &amp;&amp; record.timer_start.raw_value">
<i class="fa fa-play text-success" title="Timer is Running"></i>
</t>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="project_project_action_view_odex25_helpdesk_tickets" model="ir.actions.act_window">
<field name="name">Project Tickets</field>
<field name="res_model">odex25_helpdesk.ticket</field>
<field name="view_mode">kanban,tree,form,pivot,graph</field>
<field name="search_view_id" ref="odex25_helpdesk_ticket_view_search_inherit_odex25_helpdesk_timesheet"/>
<field name="context">{'search_default_project_id': active_id, 'default_project_id': active_id}</field>
</record>
<record id="project_project_view_project_tickets_kanban_inherited" model="ir.ui.view">
<field name="name">project.project.tickets.kanban.inherited</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.view_project_kanban"/>
<field name="priority">24</field>
<field name="arch" type="xml">
<xpath expr="//a[hasclass('o_project_kanban_box')]" position="after">
<a t-if="record.ticket_count" class="o_project_kanban_box" name="%(project_project_action_view_odex25_helpdesk_tickets)d" type="action" attrs="{'invisible': [('ticket_count', '=', 0)]}" context="{'search_default_project_id': [active_id], 'default_project_id': active_id}" >
<field name="ticket_count" string="Tickets" widget="statinfo"/>
</a>
</xpath>
</field>
<field name="groups_id" eval="[(4, ref('odex25_helpdesk.group_odex25_helpdesk_user'))]"/>
</record>
<record id="project_project_view_form_inherit_odex25_helpdesk_timesheet" model="ir.ui.view">
<field name="name">project.form.inherit.odex25_helpdesk.timesheet</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.edit_project"/>
<field name="arch" type="xml">
<div name="button_box" position="inside">
<button
class="oe_stat_button"
type="action"
name="%(project_project_action_view_odex25_helpdesk_tickets)d"
context="{'search_default_project_id': [active_id], 'default_project_id': active_id}"
icon="fa-life-ring"
attrs="{'invisible': [('ticket_count', '=', 0)]}">
<field name="ticket_count" string="Tickets" widget="statinfo"/>
</button>
</div>
</field>
<field name="groups_id" eval="[(4, ref('odex25_helpdesk.group_odex25_helpdesk_user'))]"/>
</record>
</odoo>

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import odex25_helpdesk_ticket_create_timesheet

View File

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
class odex25_helpdeskTicketCreateTimesheet(models.TransientModel):
_name = 'odex25_helpdesk.ticket.create.timesheet'
_description = "Create Timesheet from ticket"
_sql_constraints = [('time_positive', 'CHECK(time_spent > 0)', "The timesheet's time must be positive" )]
time_spent = fields.Float('Time', digits=(16, 2))
description = fields.Char('Description')
ticket_id = fields.Many2one(
'odex25_helpdesk.ticket', "Ticket", required=True,
default=lambda self: self.env.context.get('active_id', None),
help="Ticket for which we are creating a sales order",
)
def action_generate_timesheet(self):
values = {
'task_id': self.ticket_id.task_id.id,
'project_id': self.ticket_id.project_id.id,
'date': fields.Datetime.now(),
'name': self.description,
'user_id': self.env.uid,
'unit_amount': self.time_spent,
}
timesheet = self.env['account.analytic.line'].create(values)
self.ticket_id.write({
'timer_start': False,
'timer_pause': False
})
self.ticket_id.timesheet_ids = [(4, timesheet.id, None)]
self.ticket_id.user_timer_id.unlink()
return timesheet

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="odex25_helpdesk_ticket_create_timesheet_view_form" model="ir.ui.view">
<field name="name">odex25_helpdesk.ticket.create.timesheet.wizard.form</field>
<field name="model">odex25_helpdesk.ticket.create.timesheet</field>
<field name="arch" type="xml">
<form string="Save time">
<group>
<field name="ticket_id" invisible="True"/>
<field name="time_spent" string="Duration" class="oe_inline" widget="float_time" required="True"/>
<field name="description" widget="text" required="False" placeholder="Describe your activity..."/>
</group>
<footer>
<button string="Save" type="object" name="action_generate_timesheet" class="btn btn-primary"/>
<button string="Cancel" special="cancel" type="object" class="btn btn-secondary"/>
</footer>
</form>
</field>
</record>
</odoo>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (2)" project-jdk-type="Python SDK" />
</project>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/project_base.iml" filepath="$PROJECT_DIR$/.idea/project_base.iml" />
</modules>
</component>
</project>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChangeListManager">
<list default="true" id="32aae643-c2b0-4a35-aea5-e7e201bf9ad3" name="Changes" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="ProjectColorInfo"><![CDATA[{
"associatedIndex": 6
}]]></component>
<component name="ProjectId" id="2gNEeH003Qz6EWNSSDbcZ8gpmuZ" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-python-sdk-babbdf50b680-746f403e7f0c-com.jetbrains.pycharm.community.sharedIndexes.bundled-PC-241.15989.155" />
</set>
</attachedChunks>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="32aae643-c2b0-4a35-aea5-e7e201bf9ad3" name="Changes" comment="" />
<created>1715529731411</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1715529731411</updated>
</task>
<servers />
</component>
</project>

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import models
from . import wizard

View File

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
{
'name': "Odex25 Project Base",
'summary': """Odex25 Project Base""",
'description': """
1- Project Charter
2- Project Stage
3- Project Tasks
4- Project Invoice Requests
""",
'category': 'Odex25-Project/Odex25-Project',
'version': '1.0',
'depends': ['sale_timesheet','hr' ,'project','mail', 'portal', 'base' ,'account'],
'data': [
'data/project_data.xml',
'data/project_cron.xml',
'security/project_security.xml',
'security/ir_rule_allow_users.xml',
'security/ir.model.access.csv',
'wizard/project_hold_reason_view.xml',
'wizard/edit_project_phase_view.xml',
'wizard/down_payment_invoice_advance_views.xml',
'report/project_reports.xml',
'report/project_report_templates.xml',
'report/project_invoice_report_templates.xml',
'views/project_views.xml',
'views/project_invoice_views.xml',
'views/project_phase_view.xml',
'views/res_config_setting.xml',
'views/project_task_views.xml',
'views/asset.xml',
],
}

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="ir_cron_project_status" model="ir.cron">
<field name="name">Project: Update Project Status</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="nextcall" eval="(DateTime.now().replace(hour=2, minute=0) + timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')" />
<field name="doall" eval="False"/>
<field name="model_id" ref="model_project_project"/>
<field name="code">model.update_project_status()</field>
<field name="state">code</field>
</record>
<record id="ir_cron_project_invoice" model="ir.cron">
<field name="name">Project: Send Invoice Issue Notification</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="nextcall" eval="(DateTime.now().replace(hour=2, minute=0) + timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="doall" eval="False"/>
<field name="model_id" ref="model_project_project"/>
<field name="code">model.project_invoice_notification()</field>
<field name="state">code</field>
</record>
</odoo>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
<!--
Project Number
-->
<record id="seq_project_project" model="ir.sequence">
<field name="name">Project Number</field>
<field name="code">project.project</field>
<field name="prefix">P%(y)s-</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
</data>
</odoo>

View File

@ -0,0 +1,48 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * project
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-05-11 15:00+0000\n"
"PO-Revision-Date: 2024-05-11 15:00+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
#: model:ir.actions.act_window,name:project.open_view_project_all
#: model:ir.actions.act_window,name:project.open_view_project_all_config
#: model:ir.model.fields,field_description:project.field_account_analytic_account__project_ids
#: model:ir.model.fields,field_description:project.field_project_delete_wizard__project_ids
#: model:ir.model.fields,field_description:project.field_project_task_type__project_ids
#: model:ir.model.fields,field_description:project.field_project_task_type_delete_wizard__project_ids
#: model:ir.ui.menu,name:project.menu_projects
#: model:ir.ui.menu,name:project.menu_projects_config
#: model_terms:ir.ui.view,arch_db:project.account_analytic_account_view_form_inherit
#: model_terms:ir.ui.view,arch_db:project.portal_layout
#: model_terms:ir.ui.view,arch_db:project.portal_my_home
#: model_terms:ir.ui.view,arch_db:project.portal_my_projects
#: model_terms:ir.ui.view,arch_db:project.view_project
msgid "Projects"
msgstr "المشاريع"
#. module: project
#: model:ir.ui.menu,name:project.menu_main_pm
msgid "Project"
msgstr "المشروع"
#. module: project
#: model_terms:ir.ui.view,arch_db:project.edit_project
msgid "Tasks In Progress"
msgstr "المهام قيد التنفيذ"
#. module: project_base
#: model:ir.model.fields,field_description:project_base.field_project_task__task_progress
msgid "Task Progress"
msgstr "نسبة الانجاز"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
from . import res_config_setting
from . import res_company
from . import project
from . import project_invoice
from . import project_phase
from . import project_task
from . import sale_order

View File

@ -0,0 +1,563 @@
# -*- coding: utf-8 -*-
import ast
import calendar
from datetime import datetime, time, timedelta
from dateutil import relativedelta
from odoo.addons.resource.models.resource import float_to_time, HOURS_PER_DAY
from odoo.osv import expression
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
class Project(models.Model):
_inherit = "project.project"
_order = "project_no desc"
def _get_task_type(self):
"""
:return: project task type if it default
"""
type_ids = self.env['project.task.type'].search([('is_default', '=', True)])
return type_ids
name = fields.Char("Name", index=True, tracking=True, required=False)
project_no = fields.Char("Project Number", default=lambda self: self._get_next_projectno(), tracking=True,
copy=False)
category_id = fields.Many2one('project.category', string='Project Category', tracking=True)
parent_id = fields.Many2one('project.project', index=True, string='Parent Project', tracking=True)
sub_project_id = fields.One2many('project.project', 'parent_id', string='Sub-Project', tracking=True)
department_id = fields.Many2one('hr.department', string='Department')
consultant_id = fields.Many2one('res.partner', string='Consultant')
beneficiary_id = fields.Many2one('res.partner', string='Beneficiary')
lessons_learned = fields.Html(string='Lessons learned')
launch_date = fields.Datetime("Launch date", tracking=True)
start = fields.Date(string="Start Date", index=True, tracking=True)
date = fields.Date(string='End Date', index=True, tracking=True)
project_phase_ids = fields.One2many('project.phase', 'project_id', string="Project Phases")
tag_ids = fields.Many2many('project.tags', string='Tags')
type_ids = fields.Many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id',
string='Tasks Stages', default=_get_task_type)
priority = fields.Selection([('0', 'Normal'),('1', 'Important'),], default='0', index=True, string="Priority")
# address fields
street = fields.Char()
street2 = fields.Char()
zip = fields.Char(change_default=True)
country_id = fields.Many2one('res.country', string='Country', default=lambda self: self.env.company.country_id,
ondelete='restrict', tracking=True)
state_id = fields.Many2one("res.country.state", string='Region', ondelete='restrict')
city = fields.Char()
# Contract fields
contract_number = fields.Char(string="Contract Number", tracking=True)
signature_date = fields.Date(string="Signature Date", tracking=True)
contract_value = fields.Float("Total Contract Value")
tax_amount = fields.Float("Taxes Amount")
contract_value_untaxed = fields.Float("Contract Value")
state = fields.Selection([
('draft', 'PMO'),
('confirm', 'PM'),
('done', 'Done')], string='State', copy=False, default='draft', required=True, tracking=True)
status = fields.Selection([
('new', 'New'), ('open', 'Running'),
('hold', 'Hold'), ('close', 'Closed')], string='Status', copy=False, default='new', tracking=True)
hold_reason = fields.Text(string="Hold Reason")
close_reason = fields.Text(string="Close Reason")
project_team_ids = fields.One2many('project.team', 'project_id', string="Project Team")
invoice_ids = fields.One2many('project.invoice', 'project_id', "Invoice", copy=False, help="The Details of invoice")
total_invoiced_amount = fields.Float('Total Invoiced Amount', compute='compute_total_invoiced_amount')
no_of_invoices = fields.Integer(compute='compute_total_invoiced_payment')
no_of_paid_invoices = fields.Integer(compute='compute_total_invoiced_payment')
total_invoiced_payment = fields.Float('Paid Amount', compute='compute_total_invoiced_payment')
residual_amount = fields.Float('Remaining Amount', compute='compute_residual_amount')
back_log_amount = fields.Float('Back Log Amount', compute='compute_back_log_amount')
# TODO check if this fields needed
is_down_payment = fields.Boolean('Down Payment')
to_invoice = fields.Boolean(compute="compute_to_invoice", store=True)
invoice_method = fields.Selection([
('per_stage', 'Invoicing Per Phase'),
('per_period', 'Invoicing Per Period'),
], string='Invoicing Method', default=lambda self: self.env.company.invoice_method, )
invoice_period = fields.Integer(string="Invoicing Every", default=lambda self: self.env.company.invoice_period)
resource_calendar_id = fields.Many2one('resource.calendar', string='Working Time',
default=lambda self: self.env.company.resource_calendar_id, related=False,
help="Working Time used in this project")
sale_order_amount = fields.Monetary("SO Total Amount", tracking=True)
type = fields.Selection([
('revenue', 'Revenue'),
('expense', 'Expense'),
('internal', 'Internal')], default=lambda self: self.env.company.type, string='Type')
man_hours = fields.Boolean('Man hours', default=False)
progress = fields.Float(compute="get_progress", store=True, string="Progress")
task_count_all = fields.Integer(compute='_compute_task_count_all', string="All Task Count")
task_count_finished = fields.Integer(compute='_compute_task_count_finished', string="Finished Task Count")
task_count_new = fields.Integer(compute='_compute_task_count_new', string="New Task Count")
task_count_inprogress = fields.Integer(compute='_compute_task_count_inprogress', string="In progress Task Count")
privacy_visibility = fields.Selection([
('followers', 'Invited internal users'),
('employees', 'All internal users'),
('portal', 'Invited portal users and all internal users'),
],
string='Visibility', required=False,
default='portal',
help="People to whom this project and its tasks will be visible.\n\n"
"- Invited internal users: when following a project, internal users will get access to all of its tasks without distinction. "
"Otherwise, they will only get access to the specific tasks they are following.\n "
"A user with the project > administrator access right level can still access this project and its tasks, even if they are not explicitly part of the followers.\n\n"
"- All internal users: all internal users can access the project and all of its tasks without distinction.\n\n"
"- Invited portal users and all internal users: all internal users can access the project and all of its tasks without distinction.\n"
"When following a project, portal users will get access to all of its tasks without distinction. Otherwise, they will only get access to the specific tasks they are following.")
allowed_internal_user_ids = fields.Many2many('res.users', 'project_allowed_internal_users_rel',
string="Allowed Internal Users", default=lambda self: self.env.user, domain=[('share', '=', False)])
allowed_portal_user_ids = fields.Many2many('res.users', 'project_allowed_portal_users_rel', string="Allowed Portal Users", domain=[('share', '=', True)])
def get_attached_domain(self):
return ['|','|','|',
'&',('res_model', '=', 'project.project'),
('res_id', 'in', self.ids),
'&',('res_model', '=', 'project.task'),
('res_id', 'in', self.task_ids.ids),
'&',('res_model', '=', 'project.invoice'),
('res_id', 'in', self.invoice_ids.ids),
'&',('res_model', '=', 'project.phase'),
('res_id', 'in', self.project_phase_ids.ids),
]
def _compute_attached_docs_count(self):
Attachment = self.env['ir.attachment']
for project in self:
project.doc_count = Attachment.search_count(self.get_attached_domain())
def attachment_tree_view(self):
action = self.env['ir.actions.act_window']._for_xml_id('base.action_attachment')
action['domain'] = str(self.get_attached_domain())
action['context'] = "{'default_res_model': '%s','default_res_id': %d}" % (self._name, self.id)
return action
def _compute_task_count_all(self):
task_data = self.env['project.task'].read_group([('project_id', 'in', self.ids)], ['project_id'],
['project_id'])
result = dict((data['project_id'][0], data['project_id_count']) for data in task_data)
for project in self:
project.task_count_all = result.get(project.id, 0)
def _compute_task_count_finished(self):
task_data = self.env['project.task'].read_group([('project_id', 'in', self.ids),
('stage_id.is_closed', '=', True)], ['project_id'],
['project_id'])
result = dict((data['project_id'][0], data['project_id_count']) for data in task_data)
for project in self:
project.task_count_finished = result.get(project.id, 0)
def _compute_task_count_new(self):
task_data = self.env['project.task'].read_group([('project_id', 'in', self.ids),
('stage_id.sequence', '=', 0)], ['project_id'], ['project_id'])
result = dict((data['project_id'][0], data['project_id_count']) for data in task_data)
for project in self:
project.task_count_new = result.get(project.id, 0)
def _compute_task_count_inprogress(self):
task_data = self.env['project.task'].read_group(
[('project_id', 'in', self.ids), ('stage_id.is_closed', '=', False),
('stage_id.fold', '=', False), ('stage_id.sequence', '!=', 0)], ['project_id'], ['project_id'])
result = dict((data['project_id'][0], data['project_id_count']) for data in task_data)
for project in self:
project.task_count_inprogress = result.get(project.id, 0)
@api.depends('project_phase_ids', 'project_phase_ids.weight', 'project_phase_ids.progress')
def get_progress(self):
for rec in self:
progress = 0.0
for phase in rec.project_phase_ids:
progress += phase.weight * phase.progress / 100
rec.progress = progress
def compute_total_invoiced_amount(self):
for rec in self:
rec.total_invoiced_amount = sum(rec.invoice_ids.mapped('amount'))
def compute_total_invoiced_payment(self):
for rec in self:
rec.total_invoiced_payment = 0
rec.no_of_invoices = 0
rec.no_of_paid_invoices = 0
if rec.invoice_ids:
rec.no_of_invoices = len(rec.invoice_ids)
rec.no_of_paid_invoices = len(rec.invoice_ids.sudo().filtered(lambda x: x.invoice_id))
rec.total_invoiced_payment = sum(rec.invoice_ids.mapped("payment_amount"))
@api.depends('total_invoiced_amount', 'total_invoiced_payment')
def compute_residual_amount(self):
for rec in self:
rec.residual_amount = rec.total_invoiced_amount - rec.total_invoiced_payment
@api.depends('total_invoiced_amount', 'contract_value')
def compute_back_log_amount(self):
for rec in self:
rec.back_log_amount = rec.contract_value - rec.total_invoiced_amount
@api.model
def _get_next_projectno(self):
next_sequence = "/ "
sequence = self.env['ir.sequence'].search(
['|', ('company_id', '=', self.env.company[0].id), ('company_id', '=', False),
('code', '=', 'project.project')], limit=1)
if sequence:
next_sequence = sequence.get_next_char(sequence.number_next_actual)
return next_sequence
@api.model
def create(self, vals):
company_id = vals.get('company_id', self.default_get(['company_id'])['company_id'])
self_comp = self.with_company(company_id)
if not vals.get('project_no', False):
vals['project_no'] = self_comp.env['ir.sequence'].next_by_code('project.project') or '/'
return super().create(vals)
def write(self, vals):
"""
If invoice method=per_stage when add stage create project invoice
"""
line_ids = []
res = super(Project, self).write(vals)
if self.type != 'internal' and vals.get('project_phase_ids') and self.invoice_method == 'per_stage':
phase_ids = self.invoice_ids.mapped('phase_id').ids
for line in self.sudo().sale_order_id.order_line:
line_ids.append(
(0, 0,
{'order_line_id': line.id, 'product_id': line.product_id.id, 'product_uom': line.product_uom.id,
'price_unit': line.price_unit, 'discount': line.discount, 'tax_id': [(6, 0, line.tax_id.ids)]}))
self.invoice_ids = [(0, 0,
{'name': phase.display_name, 'phase_id': phase.id, 'project_invline_ids': line_ids})
for phase in
self.project_phase_ids.filtered(lambda x: x.id not in phase_ids)]
return res
def name_get(self):
res = []
for record in self:
if record.project_no:
res.append((record.id, ("[" + record.project_no + "] " + (record.name or " "))))
else:
res.append((record.id, record.name))
return res
@api.model
def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None):
args = args or []
if operator == 'ilike' and not (name or '').strip():
domain = []
else:
connector = '&' if operator in expression.NEGATIVE_TERM_OPERATORS else '|'
domain = [connector, ('project_no', operator, name), ('name', operator, name)]
return self._search(expression.AND([domain, args]), limit=limit, access_rights_uid=name_get_uid)
# State funtions
def action_draft(self):
return self.write({'state': 'draft'})
def action_confirm(self):
for record in self:
# Send Notification to Project manger
if record.user_id:
users = [record.user_id]
notification_ids = [(0, 0, {
'res_partner_id': user.partner_id.id,
'notification_type': 'inbox'}) for user in users if user.partner_id]
if notification_ids:
self.env['mail.message'].create({
'message_type': "notification",
'body': _(
"You Have been assign as a manger for The project %s kindly complete project data") % record.display_name,
'subject': _("You Have been assign As project manger"),
'partner_ids': [(4, user.partner_id.id) for user in users if users],
'model': record._name,
'res_id': record.id,
'notification_ids': notification_ids,
'author_id': self.env.user.partner_id and self.env.user.partner_id.id})
if record.type != 'internal':
record.create_project_invoice()
return self.write({'state': 'confirm'})
def action_done(self):
for record in self:
line_ids = []
# check project Stages
if record.type != 'internal':
if not record.project_phase_ids and record.invoice_method == 'per_stage':
raise ValidationError(_("Kindly Enter project Stage first."))
return self.write({'state': 'done'})
def create_project_invoice(self):
"""
Create Project Invoice in confirm base on invoice_method = per_period
"""
for record in self:
line_ids = []
if record.invoice_ids:
return True
if (record.date and record.start) and record.invoice_method == 'per_period':
for line in record.sudo().sale_order_id.order_line.filtered(lambda x: not x.is_downpayment):
line_ids.append(
(0, 0, {'order_line_id': line.id, 'product_id': line.product_id.id,
'product_uom': line.product_uom.id,
'price_unit': line.price_unit, 'discount': line.discount,
'tax_id': [(6, 0, line.tax_id.ids)]}))
diff = record.date - record.start
project_days = diff.days
period = project_days / record.invoice_period
invoice_date = record.start + relativedelta.relativedelta(days=record.invoice_period)
for rec in range(int(period)):
record.invoice_ids = [(0, 0, {'project_invline_ids': line_ids, 'plan_date': invoice_date})]
invoice_date = invoice_date + relativedelta.relativedelta(days=record.invoice_period)
def action_view_tasks_analysis(self):
""" return the action to see the tasks analysis report of the project """
action = self.env['ir.actions.act_window']._for_xml_id('project.action_project_task_user_tree')
action_context = ast.literal_eval(action['context']) if action['context'] else {}
action_context['search_default_project_id'] = self.id
return dict(action, context=action_context)
def action_creat_tasks(self):
action = self.with_context(active_id=self.id, active_ids=self.ids) \
.env.ref('project_base.create_tast_proj') \
.sudo().read()[0]
action['display_name'] = self.name
return action
def action_view_finished_tasks(self):
action = self.with_context(active_id=self.id, active_ids=self.ids) \
.env.ref('project_base.act_project_project_2_project_task_finished') \
.sudo().read()[0]
action['display_name'] = self.name
return action
def action_view_inprogress_tasks(self):
action = self.with_context(active_id=self.id, active_ids=self.ids) \
.env.ref('project_base.act_project_project_2_project_task_inprogress') \
.sudo().read()[0]
action['display_name'] = self.name
return action
def action_view_new_tasks(self):
action = self.with_context(active_id=self.id, active_ids=self.ids) \
.env.ref('project_base.act_project_project_2_project_task_new') \
.sudo().read()[0]
action['display_name'] = self.name
return action
# Status funtions
def action_open(self):
for rec in self:
# check that all invoice planned issue dates is entred
invoice_ids = self.invoice_ids.filtered(lambda x: x.plan_date == False)
if invoice_ids:
raise ValidationError(_('Please be sure to enter all planned issue dates for invoices.'))
return self.write({'status': 'open'})
def action_reopen(self):
return self.write({'status': 'open', 'close_reason': False})
def action_hold(self):
return self.write({'status': 'hold'})
def action_close(self):
return self.write({'status': 'close'})
def action_project_invoice(self):
action_window = {
"type": "ir.actions.act_window",
"res_model": "project.invoice",
"name": "project Invoice",
'view_type': 'form',
'view_mode': 'tree,form',
"context": {"create": True, "project_id": self.id, 'search_default_project': 1},
"domain": [('project_id', '=', self.id)]
}
return action_window
def action_paid_invoice(self):
action_window = {
"type": "ir.actions.act_window",
"res_model": "project.invoice",
"name": "project Invoice",
'view_type': 'form',
'view_mode': 'tree,form',
"context": {"create": True, "project_id": self.id, 'search_default_project': 1},
"domain": [('project_id', '=', self.id), ('state', '=', 'done')]
}
return action_window
def action_view_expense(self):
self.ensure_one()
action_window = {
"type": "ir.actions.act_window",
"res_model": "hr.expense",
"name": "Expenses",
"views": [[False, "tree"]],
"context": {"create": False},
"domain": [('analytic_account_id', '=', self.analytic_account_id.id)]
}
return action_window
def action_view_purchase(self):
self.ensure_one()
action_window = {
"type": "ir.actions.act_window",
"res_model": "purchase.order",
"name": "Purchase",
"views": [[False, "tree"]],
"context": {"create": False},
"domain": [('project_id', '=', self.id)]
}
return action_window
@api.model
def update_project_status(self):
"""
this funtion call by cron to close the project if end date come
"""
projects = self.env['project.project'].search([('status', '=', 'open'), ('date', '<=', fields.Date.today())])
for record in projects:
close_reason = "The Project Auto Closed by system because reach end date %s" % (record.date)
record.write({'status': 'close', 'close_reason': close_reason, 'state': 'confirm'})
users = [record.user_id]
notification_ids = []
if users:
notification_ids = [(0, 0,
{
'res_partner_id': user.partner_id.id,
'notification_type': 'inbox'
}
) for user in users if user.partner_id]
if notification_ids:
self.env['mail.message'].create({
'message_type': "notification",
'body': close_reason,
'subject': _("The project %s was automatically Closed by system") % record.display_name,
'partner_ids': [(4, user.partner_id.id) for user in users if users],
'model': record._name,
'res_id': record.id,
'notification_ids': notification_ids,
'author_id': self.env.user.partner_id and self.env.user.partner_id.id
})
@api.model
def project_invoice_notification(self):
"""
send notification to PM in invoice date
"""
projects = self.env['project.invoice'].search([])
for record in projects:
if record.plan_date == datetime.now().date() and not record.invoice_id:
record.to_invoice = True
user_ids = self.env.ref('project.group_project_manager').users
self.env['mail.message'].create({'message_type': "notification",
'body': _('This invoice is due and must be issue %s' % (record.name)),
'subject': _("Project Invoice Creation"),
'partner_ids': [(6, 0, user_ids.mapped('partner_id').ids)],
'notification_ids': [(0, 0, {'res_partner_id': user.partner_id.id,
'notification_type': 'inbox'})
for user in user_ids if user_ids],
'model': self._name,
'res_id': record.project_id.id,
})
else:
record.to_invoice = False
def create_invoice(self):
self.ensure_one()
line_id = self.env['project.invoice'].search(
[('project_id', '=', self.id), ('to_invoice', '=', True)], limit=1)
return {
'name': _('Invoices'),
'res_model': 'project.invoice',
'type': 'ir.actions.act_window',
'res_id': line_id.id,
'view_mode': 'form',
}
def update_invoices(self, is_down_payment):
InvLine = self.env['project.invoice.line']
for record in self:
if is_down_payment:
downpayment_line = record.sale_order_id.mapped('order_line').filtered(lambda l: l.is_downpayment)
for line in downpayment_line:
for inv in record.invoice_ids:
if not inv.has_downpayment:
values = {'project_invoice_id': inv.id, 'order_line_id': line.id,
'product_id': line.product_id.id, 'product_uom': line.product_uom.id,
'price_unit': line.price_unit, 'product_uom_qty': 0}
InvLine.create(values)
def create_down_payment(self):
''' Opens a wizard to create down payment '''
self.ensure_one()
ctx = {
'active_model': 'sale.order',
'active_ids': self.sale_order_id.ids,
'active_id': self.sale_order_id.id,
'project_id': self.id,
}
return {
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'sale.advance.payment.inv',
'views': [(False, 'form')],
'view_id': False,
'target': 'new',
'context': ctx,
}
@api.depends('invoice_ids', 'invoice_ids.to_invoice')
def compute_to_invoice(self):
for rec in self:
invoice_id = rec.invoice_ids.filtered(lambda x: x.to_invoice == True)
if invoice_id:
rec.to_invoice = True
else:
rec.to_invoice = False
class ProjectCategory(models.Model):
_name = "project.category"
name = fields.Char(string="Name")
company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.user.company_id)
class ProjectHoldReason(models.Model):
_name = "project.hold.reason"
name = fields.Char(string="Name")
company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.user.company_id)
class Stage(models.Model):
_inherit = 'project.task.type'
color_gantt = fields.Char(
string="Color Task Bar",
help="Choose your color for Task Bar",
default="rgba(170,170,13,0.53)")
stage_for = fields.Selection(
string='',
selection=[('project', 'Project'),
('tasks', 'Tasks'), ],
required=True, default='project')
company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.user.company_id)
is_default = fields.Boolean(string="Default in new project")
class ProjectTeam(models.Model):
_name = "project.team"
employee_id = fields.Many2one('hr.employee', string="Employee")
project_id = fields.Many2one('project.project', string="Project")
department_id = fields.Many2one('hr.department', related="employee_id.department_id", string="Department")
job_id = fields.Many2one('hr.job', related="employee_id.job_id", string="Job")
project_job = fields.Char(string="Job In Project")
@api.onchange('job_id')
def _onchange_job(self):
if not self.project_job:
self.project_job = self.job_id and self.job_id.name or ''

View File

@ -0,0 +1,395 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
class ProjectInvoice(models.Model):
_name = "project.invoice"
_inherit = ['mail.thread']
_description = "Project Invoice Request"
name = fields.Char(string='Description',tracking=True,)
phase_id = fields.Many2one('project.phase', string="Stage")
invoice_id = fields.Many2one('account.move', string="Invoice")
amount = fields.Float(string="Invoice Amount", compute="compute_amount", store=True,tracking=True,)
to_invoice = fields.Boolean(string="To invoice", default=False)
project_id = fields.Many2one('project.project', string="Project")
sale_order_id = fields.Many2one('sale.order', related='project_id.sale_order_id', store=True)
company_id = fields.Many2one(related='project_id.company_id', string='Company', store=True, readonly=True)
state = fields.Selection([
('draft', 'Draft'),
('confirm', 'Confirmed'),
('request', 'Requested'),
('done', 'Done'),
('cancel', 'Cancelled'),
],tracking=True, string='Status', readonly=True, copy=False, index=True, default='draft')
plan_date = fields.Date(string='Due Date',tracking=True,)
# related to invoice_date
actual_date = fields.Date(string='Issue Date', compute_sudo=True , compute='_compute_indo_invoice_id')
plan_payment_date = fields.Date(string='Payment Date',tracking=True,)
# related date of first payment created from invoice
actual_payment_date = fields.Date(string='Actual Payment date', compute='get_first_payment_date')
project_invline_ids = fields.One2many('project.invoice.line', 'project_invoice_id', string='Lines',
domain=[('is_downpayment', '=', False)])
project_downinv_ids = fields.One2many('project.invoice.line', 'project_invoice_id', string='Lines',
domain=[('is_downpayment', '=', True)])
currency_id = fields.Many2one(related="project_id.currency_id", store=True)
payment_amount = fields.Monetary(string='Paid Amount', compute="_compute_payment_amount")
residual_amount = fields.Monetary('Remaining Amount', compute_sudo=True , compute='_compute_indo_invoice_id')
invoice_type = fields.Selection([('project', 'Project'), ('consultant','Consultant'),
('variation_order', 'Variation Order')], string='Invoice Type', default='project')
has_downpayment = fields.Boolean(compute="_check_downpayment")
payment_state = fields.Selection(selection=[
('not_paid', 'Not Paid'),
('in_payment', 'In Payment'),
('paid', 'Paid'),
('partial', 'Partially Paid'),
('reversed', 'Reversed'),
('invoicing_legacy', 'Invoicing App Legacy')],compute_sudo=True , compute='_compute_indo_invoice_id')
project_type = fields.Selection(related='project_id.type')
allowed_internal_user_ids = fields.Many2many('res.users', 'project_invoice_allowed_internal_users_rel',
string="Allowed Internal Users", default=lambda self: self.env.user, domain=[('share', '=', False)])
allowed_portal_user_ids = fields.Many2many('res.users', 'project_invoice_allowed_portal_users_rel', string="Allowed Portal Users", domain=[('share', '=', True)])
@api.depends('invoice_id')
def _compute_indo_invoice_id(self):
for record in self:
record.actual_date = record.invoice_id.invoice_date
record.residual_amount = record.invoice_id.amount_residual
record.payment_state = record.invoice_id.payment_state
@api.depends('project_id', 'project_id.is_down_payment', 'project_downinv_ids')
def _check_downpayment(self):
for rec in self:
rec.has_downpayment = False
if rec.project_downinv_ids:
rec.has_downpayment = True
if rec.project_id and rec.project_id.is_down_payment:
rec.has_downpayment = True
@api.constrains('plan_date', 'plan_payment_date')
def _check_plan_dates(self):
for rec in self:
if rec.plan_date and rec.plan_payment_date:
if rec.plan_payment_date < rec.plan_date:
raise ValidationError(_("Planned Collection date cannot be earlier than Planned Issue date."))
def name_get(self):
result = []
for record in self.sudo():
name = '%s' % (record.name and record.name or record.phase_id.name or '/')
result.append((record.id, name))
return result
@api.onchange('phase_id')
def _onchange_phase(self):
for rec in self:
if not rec.name:
rec.name = rec.phase_id.display_name
def get_first_payment_date(self):
invoice_payments = []
payment = self.env['account.payment'].sudo().search([])
for rec in self:
payment = payment.filtered(lambda x: rec.invoice_id.id in x.reconciled_invoice_ids.ids)
if payment:
payment_date = payment.mapped('date')
rec.actual_payment_date = min(payment_date)
else:
rec.actual_payment_date = False
@api.depends('invoice_id', 'invoice_id.amount_residual', 'invoice_id.invoice_payments_widget')
def _compute_payment_amount(self):
for rec in self:
rec.payment_amount = rec.invoice_id.amount_total - rec.residual_amount
def create_invoice(self):
action = self.env["ir.actions.actions"]._for_xml_id("account.action_move_out_invoice_type")
form_view = [(self.env.ref('account.view_move_form').id, 'form')]
# if self.invoice_type == 'project':
# for line in self.project_invline_ids:
# line.order_line_id.phase_qty = line.product_uom_qty
# for line in self.project_downinv_ids:
# line.order_line_id.phase_qty = line.product_uom_qty > 0 and (line.product_uom_qty*-1) or line.product_uom_qty
# invoice_id = self.project_id.sale_order_id.\
# with_context(due_date=self.plan_payment_date, analytic_account_id=self.project_id.analytic_account_id.id).\
# _create_invoices(final=False)
# else:
invoice_vals = self.with_company(self.company_id)._prepare_invoice()
invoice_line_vals = []
for line in self.project_invline_ids:
invoice_line_vals.append(
(0, 0, line._prepare_invoice_line()
))
invoice_vals['invoice_line_ids'] = invoice_line_vals
invoice_id = self.env['account.move'].sudo().with_context(default_move_type='out_invoice').create(invoice_vals)
if abs(sum(self.project_downinv_ids.mapped('price_total'))) > abs(
sum(self.project_invline_ids.mapped('price_total'))):
raise ValidationError(_("Downpayment amount can't be greater than invoiced amount"))
self.invoice_id = invoice_id.id
# self.project_id.purchase_order_id.invoice_id = invoice_id.id
# if self.project_id.type == 'expense' and self.project_id.purchase_order_id:
# self.project_id.purchase_order_id.invoice_ids |= invoice_id
# self.project_id.purchase_order_id.invoice_ids | = [4,invoice_id.id]
self.state = 'done'
action['views'] = form_view
action['res_id'] = invoice_id.id
return action
def _prepare_invoice(self):
"""
Prepare the dict of values to create the new invoice for a variation order. This method may be
overridden to implement custom invoice generation (making sure to call super() to establish
a clean extension chain).
"""
invoice_vals = {}
self.ensure_one()
journal = self.env['account.move'].sudo().with_context(default_move_type='out_invoice')._get_default_journal()
if not journal:
raise UserError(_('Please define an accounting sales journal for the company %s (%s).') % (
self.company_id.name, self.company_id.id))
if self.project_id.type == 'revenue':
journal = self.env['account.move'].sudo().with_context(default_move_type='out_invoice')._get_default_journal()
invoice_vals = {
'move_type': 'out_invoice',
'currency_id': self.currency_id.id,
'partner_id': self.invoice_type == 'consultant' and self.project_id.consultant_id.id or self.project_id.partner_id.id ,
'partner_shipping_id': self.project_id.partner_id.id,
'partner_bank_id': self.company_id.partner_id.bank_ids.filtered(
lambda bank: bank.company_id.id in (self.company_id.id, False))[:1].id,
'journal_id': journal.id,
'invoice_line_ids': [],
'company_id': self.company_id.id,
'invoice_date_due': self.plan_payment_date,
}
elif self.project_id.type == 'expense':
journal = self.env['account.move'].sudo().with_context(default_move_type='in_invoice')._get_default_journal()
invoice_vals = {
'move_type': 'in_invoice',
'currency_id': self.currency_id.id,
'partner_id': self.invoice_type == 'consultant' and self.project_id.consultant_id.id or self.project_id.partner_id.id ,
'partner_shipping_id': self.project_id.partner_id.id,
'partner_bank_id': self.company_id.partner_id.bank_ids.filtered(
lambda bank: bank.company_id.id in (self.company_id.id, False))[:1].id,
'journal_id': journal.id,
'invoice_line_ids': [],
'purchase_id': self.invoice_type== 'consultant' and False or self.project_id.purchase_order_id.id ,
'company_id': self.company_id.id,
'invoice_date_due': self.plan_payment_date,
}
return invoice_vals
def action_confirm(self):
self.ensure_one()
self._set_qty_invoiced()
if not self.plan_date:
raise UserError(_("Kindly Enter Planned Issue Date For this Invoice Request"))
self.state = 'confirm'
# for rec in self:
# return rec.message_post(body=f'Invoice Data /: {rec.name},{rec.state}')
def action_request(self):
self.ensure_one()
if self.project_id.status not in ['open']:
raise ValidationError(_("You cannot Request Invoice for Project that is not in Open status!"))
self._set_qty_invoiced()
self.state = 'request'
user_ids = self.env.ref('account.group_account_manager').users
self.env['mail.message'].create({
'message_type': "notification",
'body': _("Invoice request created for project %s and need your action") % self.project_id.project_no,
'subject': _("Invoice Request "),
'partner_ids': [(6, 0, user_ids.mapped('partner_id').ids)],
'notification_ids': [(0, 0, {'res_partner_id': user.partner_id.id, 'notification_type': 'inbox'})
for user in user_ids if user_ids],
'model': self._name,
'res_id': self.id,
'author_id': self.env.user.partner_id and self.env.user.partner_id.id
})
def _set_qty_invoiced(self):
for rec in self:
for line in rec.project_invline_ids:
line.qty_invoiced = line.order_line_id.qty_invoiced
def action_cancel(self):
self.ensure_one()
self.state = 'cancel'
def action_set_to_draft(self):
self.ensure_one()
self.state = 'draft'
def action_get_invoice(self):
self.ensure_one()
view_id = False
if self.invoice_type == 'variation_order':
view_id = self.env.ref('project_base.project_invoice_vo_form_view').id
action_window = {
"type": "ir.actions.act_window",
"res_model": "project.invoice",
"name": "Invoice",
'view_mode': 'form',
'view_id': view_id,
'res_id': self.id,
"context": {"create": False, 'active_id': self.id, 'active_ids': self.ids, 'id': self.id},
"target": 'new',
}
return action_window
@api.depends('project_invline_ids', 'project_invline_ids.product_uom_qty', 'project_downinv_ids',
'project_downinv_ids.product_uom_qty')
def compute_amount(self):
for rec in self:
rec.amount = sum(rec.project_invline_ids.mapped('price_total')) - abs(
sum(rec.project_downinv_ids.mapped('price_total')))
class ProjectInvoiceLine(models.Model):
_name = "project.invoice.line"
_description = "Project Invoice Line"
_rec_name = "product_id"
project_invoice_id = fields.Many2one('project.invoice', string='Project Invoice', required=True, ondelete='cascade',
index=True, copy=False)
product_id = fields.Many2one('product.product', string='Product')
product_uom_qty = fields.Float(string='Percentage', digits='Product Unit of Measure', required=True, default=0.0)
amount = fields.Monetary("Amount")
product_uom = fields.Many2one('uom.uom', string='Unit of Measure', )
price_unit = fields.Float('Unit Price', digits='Project Amount', compute="get_price_unit_value")
discount = fields.Float(string='Discount (%)', digits='Discount')
price_subtotal = fields.Monetary(string='Subtotal', compute="_compute_amount", store=True)
price_tax = fields.Float(string='Total Tax', compute="_compute_amount", store=True)
price_total = fields.Monetary('Total with Taxes', compute="_compute_amount", store=True)
tax_id = fields.Many2many('account.tax', string='Taxes')
currency_id = fields.Many2one('res.currency', string='Currency', related="project_invoice_id.currency_id",
store=True)
order_line_id = fields.Many2one('sale.order.line', string="Sale Line")
is_downpayment = fields.Boolean(related="order_line_id.is_downpayment", string="Is a down payment", store=True)
qty_invoiced = fields.Float(string='Invoiced Quantity', readonly=True, digits='Product Unit of Measure')
name = fields.Char()
account_id = fields.Many2one(comodel_name='account.account')
@api.depends("project_invoice_id")
def get_price_unit_value(self):
for rec in self:
rec.price_unit = rec.project_invoice_id.project_id.contract_value_untaxed
@api.onchange("price_unit")
def get_project_invoice_id(self):
for rec in self:
rec.product_id = rec.project_invoice_id.project_id.purchase_line_id.product_id
rec.tax_id = rec.project_invoice_id.project_id.purchase_line_id.taxes_id
@api.onchange('amount')
def _onchange_amount(self):
if self.amount:
if self.price_unit > 0:
self.product_uom_qty = self.amount / self.price_unit
else:
self.product_uom_qty = 0
@api.onchange('product_uom_qty')
def _onchange_qty(self):
if self.product_uom_qty:
self.amount = self.product_uom_qty * self.price_unit
@api.onchange('order_line_id')
def _onchange_order_line_id(self):
''' set all invoice line field.
'''
if self.order_line_id:
self.product_id = self.order_line_id.product_id.id
self.product_uom_qty = 1
self.product_uom = self.order_line_id.product_uom.id
self.price_unit = self.project_invoice_id.project_id.sale_order_amount
self.tax_id = self.order_line_id.tax_id.ids
self.qty_invoiced = self.order_line_id.qty_invoiced
@api.depends('product_uom_qty', 'discount', 'price_unit', 'tax_id')
def _compute_amount(self):
"""
Compute the amounts of the IV line.
"""
for line in self:
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
taxes = line.tax_id.compute_all(price, line.currency_id, line.product_uom_qty, product=line.product_id)
line.update({
'price_tax': sum(t.get('amount', 0.0) for t in taxes.get('taxes', [])),
'price_total': taxes['total_included'],
'price_subtotal': taxes['total_excluded'],
})
def _prepare_invoice_line(self, **optional_values):
"""
Prepare the dict of values to create the new invoice line for a project invoice line.
:param qty: float quantity to invoice
:param optional_values: any parameter that should be added to the returned invoice line
"""
self.ensure_one()
res = {
'name': self.name,
'account_id': self.account_id.id,
'product_id': self.product_id.id,
'product_uom_id': self.product_uom.id,
'quantity': self.product_uom_qty,
'discount': self.discount,
'price_unit': self.price_unit,
'tax_ids': [(6, 0, self.tax_id.ids)],
'analytic_account_id': self.project_invoice_id.project_id.analytic_account_id.id
}
if self.project_invoice_id.project_id.purchase_order_id and self.project_invoice_id.project_id.type == 'expense' :
res['analytic_account_id'] = self.project_invoice_id.project_id.purchase_line_id.account_analytic_id.id
res['purchase_line_id'] = self.project_invoice_id.project_id.purchase_line_id.id
# res['account_id'] = self.project_invoice_id.project_id.purchase_line_id.id
return res
@api.constrains('product_id', 'product_uom_qty', 'amount')
def check_product_uom_qty(self):
for rec in self.filtered(lambda l: not l.is_downpayment):
total_qty = sum(rec.order_line_id.project_invoiceline_ids.mapped('product_uom_qty'))
total_amount = sum(rec.order_line_id.project_invoiceline_ids.mapped('amount'))
if total_qty > rec.order_line_id.product_uom_qty:
if total_amount > rec.order_line_id.price_subtotal:
raise ValidationError(
_("The total quantities/Amounts of project invoices must be equal quantities/Amounts in contract item: %s ") % (
rec.order_line_id.name))
for rec in self.filtered(lambda l: l.is_downpayment):
total_amount = 0.0
for line in rec.order_line_id.project_invoiceline_ids:
total_amount += abs(line.amount)
if total_amount > rec.order_line_id.price_unit:
raise ValidationError(
_("The total amount of Downpayment must not exceed it's amount in contract item: %s ") % (
rec.order_line_id.name))
class AccountMove(models.Model):
_inherit = "account.move"
project_invoice_id = fields.Many2one('project.project', string='Project Invoice')
sale_order_id = fields.Many2one('sale.order', 'Sale')
class AccountMove(models.Model):
_inherit = "account.move.line"
@api.onchange('product_id')
def onchange_product_downpayment(self):
downpayment_line = self.move_id.sale_order_id.mapped('order_line').filtered(lambda l: l.is_downpayment)
if downpayment_line and self.product_id == downpayment_line.product_id:
self.sale_line_ids = [(4, downpayment_line.id)]

View File

@ -0,0 +1,196 @@
# -*- coding: utf-8 -*-
from datetime import timedelta
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
class ProjectPhaseType(models.Model):
_name = "project.phase.type"
_description = 'Phase Type'
name = fields.Char(string="Name")
company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.user.company_id)
class ProjectPhase(models.Model):
_name = 'project.phase'
_description = 'Phase'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'project_id,start_date'
name = fields.Char(string='Phase Name', compute='_compute_name', store=True)
phase_id = fields.Many2one('project.phase.type', string="Phase")
sequence = fields.Integer(string='Sequence')
project_id = fields.Many2one('project.project', string='Project',ondelete="restrict",
default=lambda self: self.env.context.get('default_project_id'))
start_date = fields.Date(string='Start Date', copy=True)
end_date = fields.Date(string='End Date', copy=True)
estimated_hours = fields.Float("Allocated By",copy=False)
company_id = fields.Many2one('res.company', related="project_id.company_id", string='Company')
task_count = fields.Integer(compute="get_task", string='Tasks')
task_ids = fields.One2many("project.task",'phase_id',string='Tasks')
weight = fields.Float(string="Weight")
progress = fields.Float(compute="get_progress", store=True, string="Progress")
notes = fields.Text(string='Notes',copy=False)
state = fields.Selection([
('draft', 'Draft'),
('open', 'Running'),
('close', 'Closed')], string='Status',
copy=False, default='draft', required=True, readonly=True, tracking=True)
# phase_id = fields.Many2one('project.phase', string="Stage")
allowed_internal_user_ids = fields.Many2many('res.users', 'project_phase_allowed_internal_users_rel',
string="Allowed Internal Users", default=lambda self: self.env.user, domain=[('share', '=', False)])
allowed_portal_user_ids = fields.Many2many('res.users', 'project_phase_allowed_portal_users_rel', string="Allowed Portal Users", domain=[('share', '=', True)])
# allowed_user_ids = fields.Many2many('res.users', compute='_compute_allowed_users', inverse='_inverse_allowed_user')
# @api.depends('allowed_internal_user_ids', 'allowed_portal_user_ids')
# def _compute_allowed_users(self):
# for project in self:
# users = project.allowed_internal_user_ids | project.allowed_portal_user_ids
# project.allowed_user_ids = users
# def _inverse_allowed_user(self):
# for project in self:
# allowed_users = project.allowed_user_ids
# project.allowed_portal_user_ids = allowed_users.filtered('share')
# project.allowed_internal_user_ids = allowed_users - project.allowed_portal_user_ids
def write(self, values):
if 'start_date' in values or 'end_date' in values:
#send notification to pm to edit ivoice date
if self.project_id.user_id:
users = [self.project_id.user_id]
notification_ids = [(0, 0,
{
'res_partner_id': user.partner_id.id,
'notification_type': 'inbox'
}
) for user in users if user.partner_id]
if notification_ids:
self.env['mail.message'].create({
'message_type': "notification",
'body': _(
"The Stage %s Dates for project %s Edit ,edit invoice dates accordingly") % (self.display_name,self.project_id.display_name),
'subject': _("Stage Dates has been updated"),
'partner_ids': [(4, user.partner_id.id) for user in users if users],
'model': self._name,
'res_id': self.id,
'notification_ids': notification_ids,
'author_id': self.env.user.partner_id and self.env.user.partner_id.id
})
if self._context.get('open_from',False):
if self._context.get('open_from',False) == 'vo':
vo_id = self._context.get('vo_id',[])
vo = self.env['project.variation.order'].browse(vo_id)
message = str('from ' + (vo and vo.name or "VO"))
if self._context.get('edit_reasone',False):
message = message + _(" For Reasone: %s"%(self._context.get('edit_reasone'," ")))
else:
message = _("From Project For Reasone: %s"%(self._context.get('edit_reasone'," ")))
body = _("<p style='color: #FF0000;'><b>Project Phase updated %s</b></p><ul>"%( message ))
fields = self.fields_get()
old_values = self.read(values.keys())[0]
for k, v in values.items():
field = fields.get(k)
if field['type'] == 'selection':
val = dict(field['selection'])[v]
old_val = dict(field['selection'])[old_values[k]]
elif field['type'] == 'many2one':
val = self.env[field['relation']].sudo().browse(v).name_get()[0][1]
old_val = old_values[k][1]
elif field['type'] == 'many2many':
val = ""
old_val = ""
for mv in v:
val += self.env[field['relation']].sudo().browse(mv).name_get()[0][1] +" "
else:
val = v
old_val = old_values[k]
body += "<li>%s: %s -> %s</li>" % (field['string'], old_val, val)
body += "</ul>"
self.env['mail.message'].create({
'body': body,
'model': 'project.phase',
'res_id': self.id,
'subtype_id': '2',
})
return super(ProjectPhase, self).write(values)
def unlink(self):
for rec in self:
invoice_ids = self.env['project.invoice'].search([('phase_id','=',rec.id), ('project_id', '=', rec.project_id.id)])
invoice_ids.unlink()
super(ProjectPhase, self).unlink()
@api.depends('task_ids.weight', 'task_ids.task_progress','task_ids')
def get_progress(self):
for rec in self:
progress = 0.0
if rec.task_ids:
done_task = self.env['project.task'].search([('phase_id', '=', rec.id)])
# import pdb
# pdb.set_trace()
progress = ((sum([x.weight * x.task_progress for x in done_task])) / 100)
# progress = round(100.0 * float((len(done_task) / len(rec.task_ids))), 2)
rec.progress = progress
@api.constrains('start_date', 'end_date')
def _check_dates(self):
for rec in self:
if rec.filtered(lambda c: c.end_date and c.start_date > c.end_date):
raise ValidationError(_('Phase start date must be earlier than Phase end date.'))
if rec.project_id.start and rec.project_id.date and (rec.start_date < rec.project_id.start or rec.start_date > rec.project_id.date) or \
(rec.end_date < rec.project_id.start or rec.end_date > rec.project_id.date):
raise ValidationError(_('Phase dates must be between project dates.'))
def action_draft(self):
return self.write({'state': 'draft'})
def action_confirm(self):
for rec in self:
if not rec.start_date or not rec.end_date:
raise ValidationError(_('Make sure that stage dates is set.'))
return self.write({'state': 'open'})
def action_close(self):
return self.write({'state': 'close'})
def action_reopen(self):
return self.write({'state': 'open'})
@api.depends('phase_id')
def _compute_name(self):
for rec in self:
rec.name = rec.phase_id.name
def action_project_phase_task(self):
self.ensure_one()
return {
'name': 'Tasks',
'type': 'ir.actions.act_window',
'view_mode': 'tree,form',
'res_model': 'project.task',
'domain': [('phase_id', '=', self.id)],
}
def action_view_project_invoice(self):
self.ensure_one()
action_window = {
"type": "ir.actions.act_window",
"res_model": "project.invoice",
"name": "Invoice Requests",
'view_type': 'form',
'view_mode': 'tree,form',
"context": {"create": False, "edit": False},
"domain": [('phase_id', '=', self.id), ('project_id', '=', self.project_id.id)]
}
return action_window
def get_task(self):
for rec in self:
records = self.env['project.task'].search([('phase_id', '=', rec.id)])
rec.task_count = len(records)

View File

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
from datetime import datetime, time, timedelta
from dateutil import relativedelta
from odoo.addons.resource.models.resource import float_to_time, HOURS_PER_DAY
from odoo.osv import expression
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError ,Warning
class ProjectTask(models.Model):
_inherit = "project.task"
phase_id = fields.Many2one('project.phase', string='Project Phase', domain="[('project_id','=',project_id)]")
phase_hours = fields.Float("phase total hours",related="phase_id.estimated_hours")
user_ids = fields.Many2many('res.users', 'project_task_users',
'task_id', 'users_id', string="Employees")
weight = fields.Float(string='Weight', )
task_progress = fields.Float(string='Task Progress')
maximum_rate = fields.Float(string='Maximum Rate', default=1)
allowed_internal_user_ids = fields.Many2many('res.users', 'project_task_allowed_internal_users_rel',
string="Allowed Internal Users", default=lambda self: self.env.user, domain=[('share', '=', False)])
allowed_portal_user_ids = fields.Many2many('res.users', 'project_task_allowed_portal_users_rel', string="Allowed Portal Users", domain=[('share', '=', True)])
@api.constrains('task_progress' , 'weight')
def _check_task_weight_progress(self):
for record in self:
if record.task_progress < 0 or record.task_progress > 100:
raise ValidationError(_("The task progress must be between 0 and 100."))
if record.weight < 0 or record.weight > 100:
raise ValidationError(_("The weight must be between 0 and 100."))
@api.onchange('weight')
def _onchange_weight(self):
done_task = self.env['project.task'].search([('phase_id', '=', self.phase_id.id),('id','!=',self._origin.id)])
weight_done = done_task.mapped('weight')
sum_weight = sum(weight_done) + self.weight
if sum_weight > 100:
raise ValidationError(_("The total weights of the tasks for the stage must not exceed 100"))
@api.onchange('project_id')
def _onchange_project_id(self):
for task in self:
self.phase_id = False
@api.constrains("phase_hours", "planned_hours")
def _check_dept_hours(self):
for record in self:
plan_hours = sum(record.phase_id.mapped("task_ids").mapped("planned_hours"))
if plan_hours > record.phase_hours:
raise ValidationError(
_("Total planned hours for all tasks in stage %s must not exceed Total stage hours %s") % (
record.phase_id.display_name, record.phase_hours))

View File

@ -0,0 +1,22 @@
from odoo import models, fields, _
class ResCompany(models.Model):
_inherit = 'res.company'
invoice_method = fields.Selection([
('per_stage', 'Invoicing per Stage'),
('per_period', 'Invoicing per Period'),
], default='per_stage', string='Invoicing Method')
invoice_period = fields.Integer(string="Invoicing Period",default=30)
module_project_risk_register = fields.Boolean(string="Project Risk Register")
module_project_customer_team = fields.Boolean(string="Project Customer Team")
module_project_scrum_agile = fields.Boolean(string="Project Customer Team")
module_project_helpdisk_task = fields.Boolean(string="Link with Helpdisk")
module_project_variation_order = fields.Boolean(string="Project Variation Order")
module_project_metrics = fields.Boolean(string="Project Metrics")
module_project_budget = fields.Boolean(string="Project Budget")
type = fields.Selection([('revenue', 'Revenue'),
('expense', 'Expense'),
('internal', 'Internal')], default='revenue',string='Type')

View File

@ -0,0 +1,17 @@
from odoo import models, fields, _, api
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
invoice_method = fields.Selection(string='Invoicing Method',related='company_id.invoice_method',readonly=False)
invoice_period = fields.Integer(string="Invoicing Period",related='company_id.invoice_period',readonly=False)
module_project_risk_register = fields.Boolean(string="Project Risk Register",related='company_id.module_project_risk_register',readonly=False)
module_project_customer_team = fields.Boolean(string="Project Customer Team",related='company_id.module_project_customer_team',readonly=False)
module_project_scrum_agile = fields.Boolean(string="Scrum Agile",related='company_id.module_project_scrum_agile',readonly=False)
module_project_helpdisk_task = fields.Boolean(string="Link with Helpdisk",related='company_id.module_project_helpdisk_task',readonly=False)
module_project_variation_order = fields.Boolean(string="Project Variation Order",related='company_id.module_project_variation_order',readonly=False)
module_project_metrics = fields.Boolean(string="Project Metrics",related='company_id.module_project_metrics',readonly=False)
module_project_budget = fields.Boolean(string="Project Budget",related='company_id.module_project_budget',readonly=False)
type = fields.Selection([('revenue', 'Revenue'),
('expense', 'Expense')], string='Type',related='company_id.type',readonly=False)

View File

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
class SaleOrder(models.Model):
_inherit = "sale.order"
project_invoice_count = fields.Integer(string='Project Invoices', compute='_compute_project_inv_ids')
def _prepare_invoice(self):
self.ensure_one()
invoice_vals = super(SaleOrder, self)._prepare_invoice()
if self.project_ids.type == 'revenue':
invoice_vals['journal_id'] = self.env['account.move'].with_context(default_move_type='out_invoice')._get_default_journal().id
invoice_vals['move_type'] = 'out_invoice'
elif self.project_ids.type == 'expense':
invoice_vals['journal_id'] = self.env['account.move'].with_context(default_move_type='in_invoice')._get_default_journal().id
invoice_vals['move_type'] = 'in_invoice'
invoice_vals['invoice_date_due'] = self._context.get('due_date')
invoice_vals['sale_order_id'] = self.id
return invoice_vals
@api.depends('project_ids')
def _compute_project_inv_ids(self):
for order in self:
project_invoice_count = self.env['project.invoice'].sudo().search([('project_id', 'in', order.project_ids.ids)])
order.project_invoice_count = len(project_invoice_count)
def action_view_project_invoice(self):
self.ensure_one()
action = {
'type': 'ir.actions.act_window',
'domain': [('project_id', 'in', self.project_ids.ids)],
'context': {'create': False},
'view_mode': 'tree,form',
'name': _('Projects Invoices'),
'res_model': 'project.invoice',
}
return action
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
phase_qty = fields.Float(string='Quantity', digits='Product Unit of Measure')
project_invoiceline_ids = fields.One2many('project.invoice.line', 'order_line_id', 'Invoice Line')
def _prepare_invoice_line(self, **optional_values):
vals = super(SaleOrderLine, self)._prepare_invoice_line()
vals['quantity'] = self.phase_qty if self.is_downpayment == False else self.qty_to_invoice
vals['analytic_account_id'] = self._context.get('analytic_account_id')
return vals

View File

@ -0,0 +1,156 @@
<odoo>
<template id="print_project_invoice">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="web.external_layout">
<t t-set="o" t-value="o.with_context(lang=lang)"/>
<div class="page">
<h2>Invoices Report</h2>
<h2>Project: <span t-field="o.display_name"/></h2>
<div style="margin-bottom:20px;margin-top:20px;">
<sapn>Summery</sapn>
</div>
<table style="width:80%;margin-bottom:25px;margin-left:25px;">
<tr>
<td style="width:50%;">
<sapn>No.Of Issued Invoices</sapn>
</td>
<td style="border: 1px solid gray;width:30%;text-align: center;">
<span t-field="o.no_of_invoices"/>
</td>
</tr>
<tr>
<td style="">
<sapn>Total Of Issued Invoices</sapn>
</td>
<td style="border: 1px solid gray;text-align: center">
<span t-field="o.total_invoiced_amount"/>
</td>
</tr>
<tr>
<td style="">
<sapn>Total Of Collected Invoices</sapn>
</td>
<td style="border: 1px solid gray;text-align: center;">
<span t-field="o.total_invoiced_payment"/>
</td>
</tr>
<tr>
<td style="">
<sapn>Total residual to collect</sapn>
</td>
<td style="border: 1px solid gray;text-align: center;">
<span t-field="o.residual_amount"/>
</td>
</tr>
<tr>
<td style="">
<sapn>Contract Amount</sapn>
</td>
<td style="border: 1px solid gray;text-align: center;">
<span t-field="o.sale_order_amount"/>
</td>
</tr>
<tr>
<td style="">
<sapn>Back Log Amount</sapn>
</td>
<td style="border: 1px solid gray;text-align: center;">
<span t-field="o.back_log_amount"/>
</td>
</tr>
</table>
<table style="width=100%" class="table table-sm">
<thead>
<tr>
<th style="width:5%">
<sapn>Invoice No</sapn>
</th>
<th style="width:15%">
<sapn>Subject-Deliverable(s)</sapn>
</th>
<th style="width:15%">
<sapn>Planned</sapn>
</th>
<th style="width:15%">
<sapn>Actual Forcast-Issue</sapn>
</th>
<th style="width:10%">
<sapn>Variance Days</sapn>
</th>
<th style="width:15%">
<span>Status</span>
</th>
<th style="width:10%">
<span>Amount</span>
</th>
<th style="width:15%">
<sapn>Expected Collection Date</sapn>
</th>
</tr>
</thead>
<tbody>
<t t-set="counter" t-value="1"/>
<t t-foreach="o.invoice_ids" t-as="record">
<tr t-att-style="record.invoice_type == 'variation_order' and 'color:#0000FF' or None">
<td style="text-align:center">
<t t-esc="counter"/>
</td>
<td style="text-align:center">
<t t-if="record.phase_id">
<t t-esc="record.phase_id.name"/>
</t>
<t t-if="record.name">
<t t-esc="record.name"/>
</t>
</td>
<td style="text-align:center">
<t t-esc="record.plan_date"/>
</td>
<td style="text-align:center">
<t t-esc="record.actual_date"/>
</td>
<td style="text-align:center">
<t t-if="record.actual_date">
<t t-set="from_date"
t-value="datetime.datetime.strptime(str(record.plan_date), '%Y-%m-%d').date()"/>
<t t-set="date_to"
t-value="datetime.datetime.strptime(str(record.actual_date), '%Y-%m-%d').date()"/>
<t t-esc="(date_to - from_date).days"/>
</t>
</td>
<td style="text-align:center">
<t t-if="record.invoice_id">
<span>
Yes
</span>
</t>
<t t-else="">
<span>
No
</span>
</t>
</td>
<td style="text-align:center">
<span t-field="record.amount"/>
</td>
<td style="text-align:center">
<span t-field="record.plan_payment_date"/>
</td>
</tr>
<t t-set="counter" t-value="counter + 1"/>
</t>
</tbody>
</table>
<br/><br/>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@ -0,0 +1,167 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<template id="project_detail_qweb_report" name="project_detail_qweb_report">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="e">
<t t-call="web.external_layout">
<div class="page">
<form>
<div class="container">
<tr class="text-center">
<th>
<t t-if="e.name">
<h2 class="text-center">
<b>
<span t-field="e.project_no"/>
</b>
-
<b>
<span t-field="e.name"/>
</b>
</h2>
</t>
</th>
</tr>
<table class="table table-borderless">
<tr>
<td colspan="4" class="text-muted"><h4>Project Info</h4></td>
</tr>
<tr>
<td>
<b>Sale Order</b>
</td>
<td>
<span t-field="e.sale_order_id.name"/>
</td>
<td>
<b>Project Manager</b>
</td>
<td>
<span t-field="e.user_id.name"/>
</td>
</tr>
<tr>
<td>
<b>Customer</b>
</td>
<td>
<div t-field="e.partner_id" t-options="{&quot;widget&quot;: &quot;contact&quot;, &quot;fields&quot;: [&quot;address&quot;, &quot;name&quot;], &quot;no_marker&quot;: True}"/>
</td>
</tr>
<tr>
<h2>
<b>
<td colspan="4" class="text-muted"><h4>Project Location Info</h4></td>
</b>
</h2>
</tr>
<tr>
<td>
<b>Country</b>
</td>
<td>
<span t-field="e.country_id.name"/>
</td>
</tr>
<tr>
<td>
<b>Project Category</b>
</td>
<td>
<t t-if="e.category_id">
<span t-field="e.category_id.name"/>
</t>
</td>
</tr>
<tr>
<h2>
<b>
<td colspan="4" class="text-muted"><h4>Project Schedule Duration</h4></td>
</b>
</h2>
</tr>
<tr>
<td>
<b>Start Date</b>
</td>
<td>
<span t-field="e.start"/>
</td>
</tr>
<tr>
<td>
<b>Finish Date</b>
</td>
<td>
<span t-field="e.date"/>
</td>
</tr>
<tr>
<td>
<b>Kick-Off Date</b>
</td>
<td>
<span t-field="e.launch_date"/>
</td>
<td>
</td>
<td>
</td>
</tr>
</table>
<p style="page-break-before:always;"> </p>
<table class="table table-bordered">
<tr>
<h2>
<b>
<td colspan="4" class="text-muted"><h4>Project Stages</h4></td>
</b>
</h2>
</tr>
<tr>
<td colspan="2">
<b>#</b>
</td>
<td colspan="2">
<b>stage</b>
</td>
</tr>
<t t-set="counter" t-value="0"/>
<t t-foreach="e.project_phase_ids" t-as="p">
<t t-set="counter" t-value="counter + 1"/>
<tr>
<td colspan="2">
<span t-esc="counter"/>
</td>
<td colspan="2">
<span t-esc="p.name"/>
</td>
</tr>
</t>
</table>
<table class="table table-borderless">
<tr>
<h2>
<b>
<td colspan="4" class="text-muted"><h4>Description</h4></td>
</b>
</h2>
</tr>
<tr rowspan="4">
<td colspan="4">
<span t-field="e.description"/>
</td>
</tr>
</table>
</div>
</form>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<report id="report_project_detail"
model="project.project"
report_type="qweb-pdf"
name="project_base.project_detail_qweb_report"
file="project_base.project_detail_qweb_report"
string="Project Charter"/>
<record id="paperformat_project_invoice" model="report.paperformat">
<field name="name">A4 for E-Invoice</field>
<field name="default" eval="False"/>
<field name="format">A4</field>
<field name="orientation">Portrait</field>
<field name="margin_top">0</field>
<field name="margin_bottom">5</field>
<field name="margin_left">7</field>
<field name="margin_right">7</field>
<field name="header_line" eval="False"/>
<field name="header_spacing">10</field>
</record>
<record id="action_project_invoice_report" model="ir.actions.report">
<field name="name">Print invoice</field>
<field name="model">project.project</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">project_base.print_project_invoice</field>
<field name="report_file">project_base.print_project_invoice</field>
<field name="paperformat_id" ref="project_base.paperformat_project_invoice"/>
<field name="binding_type">report</field>
</record>
</odoo>

View File

@ -0,0 +1,23 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_project_category,project.category,model_project_category,project.group_project_user,1,1,1,1
access_project_phase_manager,project.phase.manager,model_project_phase,project.group_project_user,1,1,1,1
access_project_phase_user,project.phase.internal.user,model_project_phase,project.group_project_user,1,1,1,1
access_project_phase_int_user,project.phase.user,model_project_phase,base.group_user,1,0,0,0
access_project_invoice,project.invoice,model_project_invoice,project.group_project_user,1,1,1,1
access_project_invoice_line,project.invoice.line,model_project_invoice_line,project.group_project_user,1,1,1,1
access_project_hold_reason,project.hold.reason,model_project_hold_reason,,1,1,1,1
access_project_hold_wizard,project.hold.wizard,model_project_hold_wizard,,1,1,1,1
access_edit_project_phase,edit.project.phase,model_edit_project_phase,project.group_project_user,1,1,1,1
access_project_tasks_department_manager,project.tasks.department.manager,model_project_task,project_base.group_project_department_manager,1,1,1,1
sale_advance_payment_inv_account,sale.advance.payment.inv,sale.model_sale_advance_payment_inv,account.group_account_manager,1,1,1,1
project.access_project_project_manager,project.project,model_project_project,project.group_project_manager,1,1,0,0
access_project_project_super_user,project.project,model_project_project,project_base.group_project_department_manager,1,1,1,1
access_project_team_user,project.team,model_project_team,project.group_project_user,1,1,1,1
access_project_phase_type_manager,project.phase.type.manager,model_project_phase_type,project.group_project_manager,1,1,1,1
access_project_phase_type_user,project.phase.internal.type.user,model_project_phase_type,project.group_project_user,1,1,1,0
access_project_phase_type_int_user,project.phase.type.user,model_project_phase_type,base.group_user,1,0,0,0
access_account_move_manager,project.account.move.manager,account.model_account_move,project.group_project_manager,1,1,1,0
access_account_move_line_manager,project.account.move.line.manager,account.model_account_move_line,project.group_project_manager,1,1,1,0
access_account_partial_reconcile_manager,project.account.partial.reconcile.manager,account.model_account_partial_reconcile,project.group_project_manager,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_project_category project.category model_project_category project.group_project_user 1 1 1 1
3 access_project_phase_manager project.phase.manager model_project_phase project.group_project_user 1 1 1 1
4 access_project_phase_user project.phase.internal.user model_project_phase project.group_project_user 1 1 1 1
5 access_project_phase_int_user project.phase.user model_project_phase base.group_user 1 0 0 0
6 access_project_invoice project.invoice model_project_invoice project.group_project_user 1 1 1 1
7 access_project_invoice_line project.invoice.line model_project_invoice_line project.group_project_user 1 1 1 1
8 access_project_hold_reason project.hold.reason model_project_hold_reason 1 1 1 1
9 access_project_hold_wizard project.hold.wizard model_project_hold_wizard 1 1 1 1
10 access_edit_project_phase edit.project.phase model_edit_project_phase project.group_project_user 1 1 1 1
11 access_project_tasks_department_manager project.tasks.department.manager model_project_task project_base.group_project_department_manager 1 1 1 1
12 sale_advance_payment_inv_account sale.advance.payment.inv sale.model_sale_advance_payment_inv account.group_account_manager 1 1 1 1
13 project.access_project_project_manager project.project model_project_project project.group_project_manager 1 1 0 0
14 access_project_project_super_user project.project model_project_project project_base.group_project_department_manager 1 1 1 1
15 access_project_team_user project.team model_project_team project.group_project_user 1 1 1 1
16 access_project_phase_type_manager project.phase.type.manager model_project_phase_type project.group_project_manager 1 1 1 1
17 access_project_phase_type_user project.phase.internal.type.user model_project_phase_type project.group_project_user 1 1 1 0
18 access_project_phase_type_int_user project.phase.type.user model_project_phase_type base.group_user 1 0 0 0
19 access_account_move_manager project.account.move.manager account.model_account_move project.group_project_manager 1 1 1 0
20 access_account_move_line_manager project.account.move.line.manager account.model_account_move_line project.group_project_manager 1 1 1 0
21 access_account_partial_reconcile_manager project.account.partial.reconcile.manager account.model_account_partial_reconcile project.group_project_manager 1 1 1 0

View File

@ -0,0 +1,63 @@
<odoo>
<data noupdate="0">
<!-- (project.project) -->
<record id="project_project_rule_internal_users" model="ir.rule">
<field name="name">Project: Internal Users Access</field>
<field name="model_id" ref="project.model_project_project"/>
<field name="domain_force">[('allowed_internal_user_ids', 'in', user.ids)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="project.project_project_rule_portal" model="ir.rule">
<field name="name">Project: Portal Users Access</field>
<field name="model_id" ref="project.model_project_project"/>
<field name="domain_force">[('allowed_portal_user_ids', 'in', user.ids)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<!-- (project.phase) -->
<record id="project_phase_rule_internal_users" model="ir.rule">
<field name="name">Phase: Internal Users Access</field>
<field name="model_id" ref="project_base.model_project_phase"/>
<field name="domain_force">[('allowed_internal_user_ids', 'in', user.ids)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="project_phase_rule_portal_users" model="ir.rule">
<field name="name">Phase: Portal Users Access</field>
<field name="model_id" ref="project_base.model_project_phase"/>
<field name="domain_force">[('allowed_portal_user_ids', 'in', user.ids)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<!-- (project.invoice) -->
<record id="project_invoice_rule_internal_users" model="ir.rule">
<field name="name">Invoice: Internal Users Access</field>
<field name="model_id" ref="project_base.model_project_invoice"/>
<field name="domain_force">[('allowed_internal_user_ids', 'in', user.ids)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="project_invoice_rule_portal_users" model="ir.rule">
<field name="name">Invoice: Portal Users Access</field>
<field name="model_id" ref="project_base.model_project_invoice"/>
<field name="domain_force">[('allowed_portal_user_ids', 'in', user.ids)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<!-- (project.task) -->
<record id="project_task_rule_internal_users" model="ir.rule">
<field name="name">Task: Internal Users Access</field>
<field name="model_id" ref="project.model_project_task"/>
<field name="domain_force">[('allowed_internal_user_ids', 'in', user.ids)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="project.project_task_rule_portal" model="ir.rule">
<field name="name">Task: Portal Users Access</field>
<field name="model_id" ref="project.model_project_task"/>
<field name="domain_force">[('allowed_portal_user_ids', 'in', user.ids)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
</data>
</odoo>

View File

@ -0,0 +1,157 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Project Group -->
<record id="group_project_department_manager" model="res.groups">
<field name="name">PMO</field>
<field name="implied_ids" eval="[(4, ref('project.group_project_manager'))]"/>
<field name="category_id" ref="base.module_category_services_project"/>
</record>
<!-- configration multi company rule -->
<record id="project_task_type_multi_company_rule" model="ir.rule">
<field name="name">Project Task Type multi company rule</field>
<field name="model_id" ref="model_project_task_type"/>
<field name="domain_force">['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
<record id="project_category_multi_company_rule" model="ir.rule">
<field name="name">project category multi company rule</field>
<field name="model_id" ref="model_project_category"/>
<field name="domain_force">['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
<record id="project_hold_reason_multi_company_rule" model="ir.rule">
<field name="name">project hold reason multi company rule</field>
<field name="model_id" ref="model_project_hold_reason"/>
<field name="domain_force">['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
<!-- TODO:Review Delete Unneeded rules -->
<delete model="ir.rule" id="project.project_project_manager_rule"/>
<delete model="ir.rule" id="project.project_public_members_rule"/>
<delete model="ir.rule" id="project.project_manager_all_project_tasks_rule"/>
<!-- project rules -->
<record id="project_manager_rule" model="ir.rule">
<field name="name">Project Manager rule</field>
<field name="model_id" ref="model_project_project"/>
<field name="groups" eval="[(4, ref('project.group_project_manager'))]"/>
<field name="domain_force">[('user_id','=', user.id)]</field>
</record>
<record id="project_department_manager_rule" model="ir.rule">
<field name="name">Project PMO rule</field>
<field name="model_id" ref="model_project_project"/>
<field name="groups" eval="[(4, ref('project_base.group_project_department_manager'))]"/>
<field name="domain_force">[(1, '=', 1)]</field>
</record>
<!-- TODO:Review edit base on new team model
<record id="rule_team_member_project" model="ir.rule">
<field name="name">Project Team Member</field>
<field name="model_id" ref="model_project_project"/>
<field name="domain_force">['|',('employee_ids.user_id','in',
[user.id]),'|',('resource_allocation_line_ids.employee_id.user_id','in',[user.id]),('dummy_project','=',True)]
</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_unlink" eval="False"/>
<field name="perm_create" eval="False"/>
</record>-->
<!--Stage rules -->
<record id="project_stage_multi_company_rule" model="ir.rule">
<field name="name">Project stage Multi Company rule</field>
<field name="model_id" ref="model_project_phase"/>
<field eval="True" name="global"/>
<field name="domain_force">['|',('company_id', '=',False),('company_id', 'in', company_ids)]</field>
</record>
<!-- TODO:Review sub-project stages -->
<record id="project_phase_manager_rule" model="ir.rule">
<field name="name">Project Phase Manager rule</field>
<field name="model_id" ref="model_project_phase"/>
<field name="groups" eval="[(4, ref('project.group_project_manager'))]"/>
<field name="domain_force">[('project_id.user_id','=', user.id)]</field>
</record>
<record id="project_phase_department_rule" model="ir.rule">
<field name="name">Phase PMO rule</field>
<field name="model_id" ref="model_project_phase"/>
<field name="groups" eval="[(4, ref('project_base.group_project_department_manager'))]"/>
<field name="domain_force">[(1, '=', 1)]</field>
</record>
<!-- TODO:Review team member phase rule
<record id="rule_team_member_project_phase" model="ir.rule">
<field name="name">Project Stage Team Member</field>
<field name="model_id" ref="model_project_phase"/>
<field name="domain_force">[('department_ids','in', [user.department_id.id])]
</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_unlink" eval="False"/>
<field name="perm_create" eval="False"/>
</record>-->
<!-- Project Tasks Rule -->
<!-- TODO:Review get also the flower of the tasks-->
<record model="ir.rule" id="project.task_visibility_rule">
<field name="domain_force">[('user_id','=',user.id)]</field>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_unlink" eval="True"/>
<field name="perm_create" eval="True"/>
</record>
<!-- TODO:Review sub-project-->
<record model="ir.rule" id="project_manager_project_tasks_rule">
<field name="name">Project/Task: project manager: see his project tasks</field>
<field name="model_id" ref="model_project_task"/>
<field name="domain_force">[('project_id.user_id','in',[user.id])]</field>
<field name="groups" eval="[(4,ref('project.group_project_manager'))]"/>
</record>
<record id="task_project_department_manager" model="ir.rule">
<field name="name">Task PMO rule</field>
<field name="model_id" ref="model_project_task"/>
<field name="groups" eval="[(4, ref('project_base.group_project_department_manager'))]"/>
<field name="domain_force">[(1, '=', 1)]</field>
</record>
<!-- Project invoice rules -->
<record id="project_invoice_multi_company_rule" model="ir.rule">
<field name="name">Project Invoice Multi Company rule</field>
<field name="model_id" ref="model_project_invoice"/>
<field eval="True" name="global"/>
<field name="domain_force">['|',('company_id', '=',False),('company_id', 'in', company_ids)]</field>
</record>
<record model="ir.rule" id="project_manager_invoice_request_rule">
<field name="name">Invoice Request Project Manager</field>
<field name="model_id" ref="model_project_invoice"/>
<field name="domain_force">[('project_id.user_id','in',[user.id])]</field>
<field name="groups" eval="[(4, ref('project.group_project_manager'))]"/>
</record>
<record model="ir.rule" id="department_manager_invoice_request_rule">
<field name="name">Project Invoice PMO rule</field>
<field name="model_id" ref="model_project_invoice"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('project_base.group_project_department_manager'))]"/>
</record>
<record model="ir.rule" id="project_invoice_group_account_manager_rule">
<field name="name">Invoice Request account manager: see all</field>
<field name="model_id" ref="model_project_invoice"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('account.group_account_manager'))]"/>
</record>
</data>
</odoo>

View File

@ -0,0 +1,16 @@
.o_progressbar .o_progress .o_progressbar_complete.o_progress_red {
background-color: #FF0000;
height: 100%;
}
.o_progressbar .o_progress .o_progressbar_complete.o_progress_yellow {
background-color: #FFFF00;
height: 100%;
}
.o_progressbar .o_progress .o_progressbar_complete.o_progress_light_green {
background-color: #00FF00;
height: 100%;
}
.o_progressbar .o_progress .o_progressbar_complete.o_progress_green {
background-color: #008000;
height: 100%;
}

View File

@ -0,0 +1,48 @@
odoo.define('module_name.progress_bar_color', function (require) {
"use strict";
var core = require('web.core');
var utils = require('web.utils');
var Widget = require('web.Widget');
var FieldRegistry = require('web.field_registry');
var FieldProgressBar = require('web.basic_fields').FieldProgressBar;
FieldProgressBar.include({
_render_value: function (v) {
var value = this.value;
var max_value = this.max_value;
if (!isNaN(v)) {
if (this.edit_max_value) {
max_value = v;
} else {
value = v;
}
}
value = value || 0;
max_value = max_value || 0;
var widthComplete;
if (value <= max_value) {
widthComplete = value / max_value * 100;
} else {
widthComplete = 100;
}
this.$('.o_progress').toggleClass('o_progress_overflow', value > max_value)
.attr('aria-valuemin', '0')
.attr('aria-valuemax', max_value)
.attr('aria-valuenow', value);
// this.$('.o_progressbar_complete').css('width', widthComplete + '%');
this.$('.o_progressbar_complete').toggleClass('o_progress_red', widthComplete > 0 && widthComplete <= 40).css('width', widthComplete + '%');
this.$('.o_progressbar_complete').toggleClass('o_progress_yellow', widthComplete > 40 && widthComplete <= 70).css('width', widthComplete + '%');
this.$('.o_progressbar_complete').toggleClass('o_progress_light_green', widthComplete > 70 && widthComplete <= 90).css('width', widthComplete + '%');
this.$('.o_progressbar_complete').toggleClass('o_progress_green', widthComplete > 90 && widthComplete <= 100).css('width', widthComplete + '%');
if (!this.write_mode) {
if (max_value !== 100) {
this.$('.o_progressbar_value').text(utils.human_number(value) + " / " + utils.human_number(max_value));
} else {
this.$('.o_progressbar_value').text(utils.human_number(value) + "%");
}
} else if (isNaN(v)) {
this.$('.o_progressbar_value').val(this.edit_max_value ? max_value : value);
this.$('.o_progressbar_value').focus().select();
}
},
});
});

View File

@ -0,0 +1,11 @@
<odoo>
<template id="assets_backend" inherit_id="web.assets_backend"
name="Progress bar">
<xpath expr="." position="inside">
<link rel="stylesheet" type="text/scss"
href="/project_base/static/src/css/progress_bar_color.css"/>
<script type="text/javascript"
src="/project_base/static/src/js/progress_bar_color.js"/>
</xpath>
</template>
</odoo>

View File

@ -0,0 +1,191 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="project_invoice_form_view" model="ir.ui.view">
<field name="name">project.invoice.form</field>
<field name="model">project.invoice</field>
<field name="arch" type="xml">
<form string="Invoice Request" create="false">
<header>
<button name="action_confirm" type="object" string="Confirm" class="btn btn-primary" states="draft" groups="project.group_project_manager"/>
<button name="action_request" type="object" string="Request" class="btn btn-primary" states="confirm" groups="project.group_project_manager"/>
<button name="create_invoice" type="object" string="Create Invoice" class="btn btn-primary" states="request" groups="project_base.group_project_department_manager"/>
<button name="action_cancel" type="object" string="Cancel" class="btn btn-primary" states="confirm,request" groups="project.group_project_manager"/>
<button name="action_set_to_draft" type="object" string="Set to Draft" class="btn btn-primary" states="cancel" groups="project.group_project_manager"/>
<field name="state" widget="statusbar" statusbar_visible="draft,confirm"/>
</header>
<sheet>
<field name="payment_state" invisible="1" force_save="1"/>
<widget name="web_ribbon" title="Paid" attrs="{'invisible': [('payment_state', '!=', 'paid'),]}"/>
<widget name="web_ribbon" title="In Payment" attrs="{'invisible': [('payment_state', '!=', 'in_payment')]}"/>
<widget name="web_ribbon" title="Partial" attrs="{'invisible': [('payment_state', '!=', 'partial'),]}"/>
<widget name="web_ribbon" title="Reversed" bg_color="bg-danger" attrs="{'invisible': [('payment_state', '!=', 'reversed')]}"/>
<widget name="web_ribbon" text="Invoicing App Legacy" bg_color="bg-info" attrs="{'invisible': [('payment_state', '!=', 'invoicing_legacy')]}" tooltip="This entry has been generated through the Invoicing app, before installing Accounting. It has been disabled by the 'Invoicing Switch Threshold Date' setting so that it does not impact your accounting."/>
<group>
<field name="invoice_type" widget="radio" options="{'horizontal': true}"/>
</group>
<group>
<group>
<!-- TODO depend on invoice method on setting -->
<field name="name"/>
<field name="phase_id" domain="[('project_id', '=', project_id)]" attrs="{'readonly':[('state', '!=', 'draft')]}"/>
<field name="project_id" readonly="1" force_save="1"/>
<!-- <field name="invoice_type" invisible="1"/> -->
<field name="has_downpayment" invisible="1"/>
<field name="sale_order_id" invisible="1"/>
<field name="currency_id" invisible="1"/>
<field name="project_type" invisible="1"/>
</group>
<group>
<field name="plan_date" attrs="{'readonly':[('state', '!=', 'draft')]}" required="1"/>
<field name="plan_payment_date" required="1"/>
<field name="actual_date" readonly="1"/>
<field name="actual_payment_date" readonly="1"/>
<field name="invoice_id" readonly="1" options="{'no_open': True}" attrs="{'invisible': [('invoice_id', '=', False)]}"/>
</group>
</group>
<notebook>
<page string="Invoice Lines" name="invoice_lines">
<field name="project_invline_ids" attrs="{'readonly': [('state', '=', 'done')]}">
<tree editable="bottom">
<field name="order_line_id" string="Contract Item" domain="[('order_id','=',parent.sale_order_id),('is_downpayment','=',False)]" attrs="{'column_invisible':[('parent.project_type', '!=', 'revenue')]}" invisible="1"/>
<field name="product_id" attrs="{'readonly': ['|', ('parent.state', '!=', 'draft'), ('parent.invoice_type', '=', 'project')],'column_invisible':[('parent.project_type', '=', 'internal')]}" force_save="1"/>
<field name="name"/>
<field name="account_id"/>
<field name="product_uom_qty" force_save="1"/>
<field name="qty_invoiced" decoration-info="(qty_invoiced)" decoration-bf="(qty_invoiced)" force_save="1" optional="hide"/>
<field name="product_uom" attrs="{'readonly': ['|', ('parent.state', '!=', 'draft'), ('parent.invoice_type', '=', 'project')]}" optional="hide" force_save="1"/>
<field name="price_unit" attrs="{'readonly': ['|', ('parent.state', '!=', 'draft'), ('parent.invoice_type', '=', 'project')]}" force_save="1"/>
<field name="price_subtotal"/>
<field name="tax_id" widget="many2many_tags" options="{'no_create': True}" attrs="{'readonly': [ ('parent.state', '!=', 'draft')]}" force_save="1"/>
<field name="price_tax"/>
<field name="price_total"/>
</tree>
</field>
<separator string="DowmPayment" attrs="{'invisible':[('has_downpayment', '!=', True)]}"/>
<field name="project_downinv_ids" string="DowmPayment" attrs="{'readonly': [('state', '=', 'done')],'invisible':[('has_downpayment', '!=', True)]}">
<tree editable="bottom">
<field name="order_line_id" string="Contract Item" domain="[('order_id','=',parent.sale_order_id),('is_downpayment','=',True)]" attrs="{'column_invisible':[('parent.invoice_type', '=', 'variation_order')]}"/>
<field name="product_id" required="1" attrs="{'readonly': ['|', ('parent.state', '!=', 'draft'), ('parent.invoice_type', '=', 'project')],'column_invisible':[('parent.invoice_type', '=', 'project')]}" force_save="1"/>
<field name="amount" attrs="{'column_invisible':[('parent.invoice_type', '=', 'variation_order')]}" force_save="1"/>
<field name="product_uom_qty" force_save="1"/>
<field name="qty_invoiced" decoration-info="(qty_invoiced)" decoration-bf="(qty_invoiced)" force_save="1" optional="show"/>
<field name="product_uom" attrs="{'readonly': ['|', ('parent.state', '!=', 'draft'), ('parent.invoice_type', '=', 'project')]}" optional="hide" force_save="1"/>
<field name="price_unit" attrs="{'readonly': ['|', ('parent.state', '!=', 'draft'), ('parent.invoice_type', '=', 'project')]}" force_save="1"/>
<!-- <field name="tax_id" widget="many2many_tags" options="{'no_create': True}" attrs="{'readonly': ['|', ('parent.state', '!=', 'draft'), ('parent.invoice_type', '=', 'project')]}" force_save="1"/>
<field name="price_tax"/>
-->
<field name="price_subtotal"/>
<!-- <field name="price_total"/> -->
</tree>
</field>
<group name="note_group" col="6" class="mt-2 mt-md-0">
<group colspan="4">
</group>
<group class="oe_subtotal_footer oe_right" colspan="2" name="sale_total">
<field name="amount" force_save="1"/>
<!-- <field name="amount_tax" widget="monetary" options="{'currency_field': 'currency_id'}"/>-->
<div class="oe_subtotal_footer_separator oe_inline o_td_label">
<label for="payment_amount"/>
</div>
<field name="payment_amount" nolabel="1" class="oe_subtotal_footer_separator" widget="monetary" options="{'currency_field': 'currency_id'}"/>
<div class="oe_subtotal_footer_separator oe_inline o_td_label">
<label for="residual_amount"/>
</div>
<field name="residual_amount" nolabel="1" class="oe_subtotal_footer_separator" widget="monetary" options="{'currency_field': 'currency_id'}"/>
</group>
<div class="oe_clear"/>
</group>
</page>
<page string="Note">
<field name="name" nolabel="1" attrs="{'required':[('phase_id', '=', False)]}" placeholder="Enter Description For invoice"/>
</page>
<page string="Settings Allowed Users" groups="project.group_project_manager">
<group>
<field name="allowed_internal_user_ids" widget="many2many_tags" groups="project.group_project_manager"/>
<field name="allowed_portal_user_ids" widget="many2many_tags" options="{'no_create': True}" groups="project.group_project_manager" />
</group>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/>
<field name="message_ids" widget="mail_thread"/>
<field name="name" widget="mail_followers"/>
</div>
</form>
</field>
</record>
<record id="project_invoice_tree_view" model="ir.ui.view">
<field name="name">project.invoice.tree</field>
<field name="model">project.invoice</field>
<field name="arch" type="xml">
<tree string="Invoice Request" expand="1" decoration-danger="state != 'done' and plan_date &lt; current_date">
<field name="name" optional="show"/>
<field name="project_id" optional="show"/>
<field name="plan_date" width="100px" optional="show"/>
<field name="amount" sum="Total" width="100px"/>
<field name="payment_amount" sum="payment_amount" width="100px"/>
<field name="residual_amount" sum="residual_amount" width="100px"/>
<field name="actual_date" width="100px" optional="show"/>
<field name="plan_payment_date" width="100px" optional="show"/>
<field name="actual_payment_date" width="100px" optional="show"/>
<field name="payment_state" widget="badge" decoration-danger="payment_state == 'not_paid'" decoration-warning="payment_state in ('partial', 'in_payment')" decoration-success="payment_state in ('paid', 'reversed')" attrs="{'invisible': [('payment_state', 'in', ('invoicing_legacy'))]}"/>
<field name="state" widget="badge" optional="show"/>
</tree>
</field>
</record>
<record id="view_project_invoice_filter" model="ir.ui.view">
<field name="name">project.invoice.search</field>
<field name="model">project.invoice</field>
<field name="arch" type="xml">
<search string="Search Project Invoice">
<field name="plan_date"/>
<filter string="Date" name="plan_date" date="plan_date"/>
<separator/>
<group expand="0" string="Group By">
<filter string="Project" name="project" domain="[]" context="{'group_by': 'project_id'}"/>
</group>
</search>
</field>
</record>
<record id="project_view_order_form" model="ir.ui.view">
<field name="name">sale.order.form</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='%(sale.action_view_sale_advance_payment_inv)d'][1]" position="attributes">
<attribute name="attrs">{'invisible': ['|',('project_ids', '!=', []),('invoice_status', '!=', 'to invoice')]}
</attribute>
</xpath>
<xpath expr="//button[@name='%(sale.action_view_sale_advance_payment_inv)d'][2]" position="attributes">
<attribute name="attrs">{'invisible': ['|','|',('project_ids', '!=', []),('invoice_status', '!=',
'no'),('state', '!=', 'sale')]}
</attribute>
</xpath>
<xpath expr="//button[@name='action_view_invoice']" position="before">
<button type="object" name="action_view_project_invoice" class="oe_stat_button" icon="fa-tasks" attrs="{'invisible': [('project_invoice_count', '=', 0)]}">
<field name="project_invoice_count" widget="statinfo" string="Project Invoices"/>
</button>
</xpath>
</field>
</record>
<record id="action_project_invoice" model="ir.actions.act_window">
<field name="name">Invoice Request</field>
<field name="res_model">project.invoice</field>
<field name="view_mode">tree,form</field>
<field name="context">{'search_default_project': 1}}</field>
</record>
<menuitem action="action_project_invoice" id="menu_project_invoice" name="Invoice Request" parent="project.menu_main_pm" sequence="9" groups="account.group_account_manager,project.group_project_manager,project_base.group_project_department_manager"/>
</odoo>

View File

@ -0,0 +1,169 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Phase Search -->
<record id="view_phase_search_form" model="ir.ui.view">
<field name="name">project.phase.search.form</field>
<field name="model">project.phase</field>
<field name="arch" type="xml">
<search string="Phase">
<field name="name" string="Stage"/>
<field name="project_id"/>
<separator/>
<filter string="Draft" name="draft" domain="[('state', 'in', ['draft'])]"/>
<filter string="Open" name="open" domain="[('state', 'in', ['open'])]"/>
<filter string="Closed" name="close" domain="[('state', '=', 'close')]"/>
<separator/>
<filter string="Start Date" name="group_date_start" context="{'group_by': 'start_date'}"/>
<filter string="End Date" name="group_date_end" context="{'group_by': 'end_date'}"/>
<group expand="0" string="Group By">
<filter string="Project" name="project" context="{'group_by': 'project_id'}"/>
</group>
</search>
</field>
</record>
<!-- Project Phase -->
<record id="project_phase_form_view" model="ir.ui.view">
<field name="name">project.phase.form</field>
<field name="model">project.phase</field>
<field name="arch" type="xml">
<form string="Project Phases" create="0" edit="1">
<header>
<button name="action_confirm" string="Confirm" type="object" attrs="{'invisible':[('state','not in',['draft'])]}" class="oe_highlight" groups="project.group_project_manager"/>
<button name="action_close" string="Close" type="object" attrs="{'invisible':[('state','not in',['open'])]}" class="oe_highlight" groups="project.group_project_manager"/>
<button name="action_reopen" string="Reopen" type="object" attrs="{'invisible':[('state','not in',['close'])]}" groups="project.group_project_manager"/>
<field name="state" widget="statusbar" statusbar_visible="draft,open,close"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box" groups="base.group_user">
<button class="oe_stat_button" name="action_project_phase_task" string='Tasks' type="object" icon="fa-tasks">
</button>
<button class="oe_stat_button" name="action_view_project_invoice" string='Invoices' type="object" icon="fa-edit"/>
</div>
<group>
<group>
<field name="name" attrs="{'readonly':[('state','not in',['draft'])]}" invisible="1"/>
<field name="phase_id" attrs="{'readonly':[('state','not in',['draft'])]}" required="1"/>
<field name="project_id" attrs="{'readonly':[('state','not in',['draft'])]}" required="1"/>
<field name="weight" attrs="{'readonly':[('state','not in',['draft'])]}" />
<field name="estimated_hours" attrs="{'readonly':[('state','not in',['draft'])]}" required="1" />
</group>
<group>
<field name="start_date" attrs="{'readonly':[('state','not in',['draft'])]}" required="1"/>
<field name="end_date" attrs="{'readonly':[('state','not in',['draft'])]}" required="1"/>
<field name="allowed_internal_user_ids" widget="many2many_tags" groups="project.group_project_manager" />
<field name="allowed_portal_user_ids" widget="many2many_tags" groups="project.group_project_manager" options="{'no_create': True}" />
</group>
</group>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" groups="base.group_user"/>
<field name="activity_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
<record id="project_phase_tree" model="ir.ui.view">
<field name="name">project.phase.tree</field>
<field name="model">project.phase</field>
<field name="arch" type="xml">
<tree string="Project Phases" create="0" edit="0" expand="1" decoration-danger="state != 'close' and end_date &lt; current_date">
<field name="name"/>
<field name="project_id"/>
<field name="start_date"/>
<field name="end_date"/>
<field name="estimated_hours" widget="float_time" invisible="1"/>
<field name="weight" />
<field name="progress" widget="progressbar"/>
<field name="state" />
<field name="task_count" />
<button class="d-none d-md-inline oe_stat_button" type="object" name="action_project_phase_task" icon="fa-tasks">
<field name="task_ids" attrs="{'invisible': True}"/>
</button>
</tree>
</field>
</record>
<record id="view_project_phase_kanban" model="ir.ui.view">
<field name="name">project.phase.kanban</field>
<field name="model">project.phase</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile" sample="1">
<field name="name"/>
<field name="notes"/>
<field name="start_date"/>
<field name="end_date"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_content">
<div t-attf-class="oe_kanban_card oe_kanban_global_click">
<div class="o_kanban_record_top mb8">
<div class="o_kanban_record_headings ml-1">
<strong class="o_kanban_record_title">
<t t-esc="record.name.value"/>
</strong>
</div>
</div>
</div>
<div class="o_kanban_record_bottom" t-if="!selection_mode">
<div class="oe_kanban_bottom_left">
<t t-esc="record.end_date.value and record.end_date.value.split(' ')[0] or False"/>
</div>
<div class="oe_kanban_bottom_right">
<t t-esc="record.end_date.value and record.end_date.value.split(' ')[0] or False"/>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="open_project_phase_form" model="ir.actions.act_window">
<field name="name">Project Phases</field>
<field name="res_model">project.phase</field>
<field name="view_mode">tree,form,kanban</field>
<field name="view_id" ref="project_phase_tree"/>
<field name="context">{'search_default_project': 1}</field>
</record>
<menuitem action="open_project_phase_form" id="menu_project_phase" groups="project.group_project_manager,project_base.group_project_department_manager" name="Phases" parent="project.menu_main_pm" sequence="3"/>
<!-- Project Hold Reason -->
<record id="project_phase_type_form_view" model="ir.ui.view">
<field name="name">project.phase.type.form</field>
<field name="model">project.phase.type</field>
<field name="arch" type="xml">
<form string="Project Phase Type">
<sheet>
<group>
<group>
<field name="name" required="1"/>
<field name="company_id" groups="base.group_multi_company" readonly="1"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="project_phase_type_tree_view" model="ir.ui.view">
<field name="name">project.phase.type.tree</field>
<field name="model">project.phase.type</field>
<field name="arch" type="xml">
<tree string="Project Phase Type">
<field name="name"/>
</tree>
</field>
</record>
<record id="open_project_phase_type_form" model="ir.actions.act_window">
<field name="name">Project Phase Type</field>
<field name="res_model">project.phase.type</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="project_phase_type_tree_view"/>
</record>
</odoo>

View File

@ -0,0 +1,169 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Project Tasks from inherite-->
<record id="view_task_tree2_inherited" model="ir.ui.view">
<field name="name">project.task.tree</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="hr_timesheet.view_task_tree2_inherited" />
<field name="arch" type="xml">
<field name="progress" position="attributes">
<attribute name="invisible">1</attribute>
</field>
<field name="total_hours_spent" position="after">
<field name="weight" widget="" class="mr-3" required="1" />
<field name="task_progress" widget="progressbar" class="mr-3" required="1" />
</field>
</field>
</record>
<!-- Project Tasks from inherite-->
<record id="view_task_form2" model="ir.ui.view">
<field name="name">project.task.form</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_form2" />
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='partner_id']" position="after">
<field name="weight" widget="" class="mr-3" required="1" />
<field name="task_progress" widget="progressbar" options="{'max_value': 'maximum_rate', 'editable': True}" readonly="0" class="mr-3"/>
</xpath>
<xpath expr="//field[@name='parent_id']" position="attributes">
<attribute name="domain">[('project_id','=',project_id),('phase_id','=',phase_id)]</attribute>
<attribute name="attrs">{'invisible' : [('allow_subtasks', '=', False)]}</attribute>
<attribute name="force_save">1</attribute>
</xpath>
<xpath expr="//h1[hasclass('justify-content-between')]" position="replace">
<h1 class="d-flex flex-row">
<field name="priority" widget="priority" class="mr-3"/>
<field name="name" class="o_task_name text-truncate" placeholder="Task Title..." default_focus="1"/>
</h1>
</xpath>
<xpath expr="//field[@name='project_id']" position="after">
<field name="phase_id" required="1" string="Stage" domain="[('project_id','=',project_id)]"/>
</xpath>
<xpath expr="//field[@name='user_id']" position="after">
<field name="user_ids" widget="many2many_tags"/>
</xpath>
<xpath expr="//notebook" position="inside">
<page string="Settings Allowed Users" groups="project.group_project_manager">
<group>
<field name="allowed_internal_user_ids" widget="many2many_tags" groups="project.group_project_manager"/>
<field name="allowed_portal_user_ids" widget="many2many_tags" groups="project.group_project_manager" options="{'no_create': True}" />
</group>
</page>
</xpath>
</field>
</record>
<!-- Project Task tree Inherit-->
<record id="view_task_tree" model="ir.ui.view">
<field name="name">project.task.tree</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_tree2" />
<field name="arch" type="xml">
<xpath expr="//tree" position="attributes">
<attribute name="expand">1</attribute>
</xpath>
<field name="project_id" position="after">
<field name="phase_id" string="Stage"/>
</field>
<field name="partner_id" position="replace"/>
<field name="kanban_state" position="replace"/>
<field name="user_id" position="attributes">
<attribute name="string">Responsible</attribute>
</field>
<field name="stage_id" position="attributes">
<attribute name="string">State</attribute>
<attribute name="optional">hide</attribute>
</field>
<field name="company_id" position="attributes">
<attribute name="optional">hide</attribute>
</field>
</field>
</record>
<!-- Project Task calendar inherit-->
<record id="view_task_calendar_inherited" model="ir.ui.view">
<field name="name">calendar2.inherited</field>
<field name="model">project.task</field>
<field eval="2" name="priority"/>
<field name="inherit_id" ref="project.view_task_calendar" />
<field name="arch" type="xml">
<field name="user_id" position="attributes">
<attribute name="string">Responsible</attribute>
</field>
<field name="stage_id" position="attributes">
<attribute name="string">State</attribute>
</field>
</field>
</record>
<!-- Project Task search view Inherit-->
<record id="view_task_search_form" model="ir.ui.view">
<field name="name">project.task.filter</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_search_form" />
<field name="arch" type="xml">
<field name="project_id" position="after">
<field name="phase_id" />
</field>
<filter name="project" position="after">
<filter string="Stage" name="group_phase_id" icon="terp-folder-violet" domain="[]" context="{'group_by':'phase_id'}" />
<filter string="State" name="state" context="{'group_by': 'stage_id'}" />
<separator/>
</filter>
<filter name="message_needaction" position="attributes">
<attribute name="invisible">1</attribute>
</filter>
<filter name="my_followed_tasks" position="attributes">
<attribute name="invisible">1</attribute>
</filter>
<filter name="late" position="replace">
<filter string="Late Tasks" name="late" domain="[('date_end', '&lt;', context_today().strftime('%Y-%m-%d')) , ('is_closed','=',False)]" />
</filter>
<filter name="group_create_date" position="attributes">
<attribute name="invisible">1</attribute>
</filter>
<filter name="stage" position="replace">
</filter>
</field>
</record>
<!-- Task Gragh-->
<record id="view_project_task_graph" model="ir.ui.view">
<field name="name">project.task.pivot</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_project_task_graph" />
<field name="arch" type="xml">
<field name="stage_id" position="replace">
<field name="phase_id"/>
</field>
</field>
</record>
<!-- inherit action to remove default filter-->
<record id="project.action_view_all_task" model="ir.actions.act_window">
<field name="context">{'all_task':0,'search_default_project':1}</field>
</record>
<!-- Inherit Form View to Modify it -->
<record id="project_task_type_inherit" model="ir.ui.view">
<field name="name">project.task.type.inherit</field>
<field name="model">project.task.type</field>
<field name="inherit_id" ref="project.task_type_edit"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='sequence']" position="after">
<field name="is_default"/>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,728 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Project Project -->
<record id="view_edit_project_inherit_form" model="ir.ui.view">
<field name="name">project.project.view.inherit</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.edit_project" />
<field name="arch" type="xml">
<xpath expr="//header" position="inside">
<button class="btn btn-primary" type="object" name="create_invoice" string="Create Invoice" attrs="{'invisible':['|', '|', ('sale_order_id', '=', False), ('state', '!=', 'done'),('to_invoice', '=', False)]}" />
</xpath>
<xpath expr="//sheet" position="before">
<div class="alert alert-danger" role="alert" style="margin-bottom:0px;" attrs="{'invisible':['|',('status', 'not in', ('close')),('close_reason', '=', False)]}">
<field name="close_reason" attrs="{'readonly': True}" />
</div>
</xpath>
<div class="oe_button_box" position="inside">
<!-- TODO : open if use sub-project
<button class="d-none d-md-inline oe_stat_button" type="object" name="action_view_subproject" icon="fa-code-fork" attrs="{'invisible': [('sub_project_id', '=', [])]}" string="Sub-Projects">
<field name="sub_project_id" attrs="{'invisible': True}"/>
</button>-->
<button type="object" name="action_project_invoice" icon="fa-edit" context="{'default_project_id':active_id}">
<div class="o_form_field o_stat_info">
<span class="o_stat_value">
<field name="total_invoiced_amount" />
</span>
<span class="o_stat_text">Invoices</span>
</div>
</button>
<button class="d-none d-md-inline oe_stat_button" type="object" name="action_view_expense" icon="fa-dollar" string="Expenses">
</button>
<button class="d-none d-md-inline oe_stat_button" type="object" name="action_view_purchase" icon="fa-credit-card" string="Purchase">
</button>
</div>
<!-- Project status -->
<xpath expr="//widget[@name='web_ribbon']" position="before">
<widget name="web_ribbon" title="Running" bg_color="bg-info" attrs="{'invisible': [('status', '!=', 'open')]}" />
<widget name="web_ribbon" title="Hold" bg_color="bg-danger" attrs="{'invisible': [('status', '!=', 'hold')]}" />
<widget name="web_ribbon" title="Closed" attrs="{'invisible': [('status', '!=', 'close')]}" />
</xpath>
<xpath expr="//button[@name='%(portal.portal_share_action)d']" position="attributes">
<attribute name="attrs">{'invisible':True}</attribute>
</xpath>
<xpath expr="//header" position="inside">
<button name="action_confirm" string="Confirm" type="object" attrs="{'invisible':[('state','not in',['draft'])]}" class="oe_highlight" groups="project_base.group_project_department_manager" />
<button name="action_done" string="Done" type="object" attrs="{'invisible':[('state','not in',['confirm'])]}" class="oe_highlight" groups="project.group_project_manager" />
<button name="action_draft" string="Set to Draft" type="object" icon="fa-rotate-right" attrs="{'invisible':[('state','in',['draft'])]}" groups="project.group_project_manager" />
<button name="action_open" string="Open" type="object" attrs="{'invisible':['|',('state','not in',['done']),('status','in',['open','hold','pending','close'])]}" groups="project.group_project_manager" class="btn-info" icon="fa-caret-right" />
<button name="action_reopen" string="Re Open" type="object" attrs="{'invisible':['|',('state','not in',['done']),('status','not in',['hold','pending','close'])]}" groups="project.group_project_manager" class="btn-info" icon="fa-rotate-right" />
<button name="%(project_base.action_project_hold)d" string="Hold" type="action" attrs="{'invisible':[('status','not in',['open'])]}" groups="project.group_project_manager" class="btn-danger" icon="fa-pause" />
<button name="action_close" string="Close" type="object" attrs="{'invisible':[('status','not in',['open'])]}" groups="project.group_project_manager" class="btn-success" icon="fa-power-off" />
<field name="state" widget="statusbar" statusbar_visible="draft,confirm,approve,review,done" />
</xpath>
<xpath expr="//div[hasclass('oe_title')]" position="replace">
<div class="oe_title">
<h1>
<field name="name" placeholder="Project Name" attrs="{'readonly':[('state','not in',['draft','confirm'])],'required':True}" />
</h1>
<div name="options_active">
<div>
<label for="project_no" class="oe_inline" string="Project #:" />
<field name="project_no" attrs="{'readonly':True}" />
</div>
</div>
</div>
<group>
<group>
<field name="to_invoice" invisible="1" />
<field name="status" attrs="{'invisible':[('active','=',True)],'readonly':True}" />
<field name="category_id" options="{'no_create': True}" attrs="{'readonly':[('state','not in',['draft','confirm'])],'required':[('state','not in',['draft'])]}" />
<field name="partner_id" string="Partner" attrs="{'readonly':[('state','not in',['draft','confirm'])],'required':True}" />
<field name="consultant_id" attrs="{'readonly':[('state','not in',['draft','confirm'])]}" />
<field name="beneficiary_id" attrs="{'readonly':[('state','not in',['draft','confirm'])]}" />
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" />
</group>
<group>
<field name="department_id" attrs="{'readonly':[('state','not in',['draft','confirm'])]}" />
<field name="user_id" widget="many2one_avatar_user" attrs="{'readonly':[('state','not in',['draft'])],'required':True}" />
<label for="start" string="Period" />
<div name="dates" class="o_row">
<field name="start" attrs="{'readonly':[('state','not in',['draft','confirm'])],'required':[('state','not in',['draft'])]}" />
<i class="fa fa-long-arrow-right mx-2 oe_edit_only" aria-label="Arrow icon" title="Arrow" />
<i class="fa fa-long-arrow-right mx-2 oe_read_only" aria-label="Arrow icon" title="Arrow" attrs="{'invisible': [('start', '=', False), ('date', '=', False)]}" />
<field name="date" attrs="{'readonly':[('state','not in',['draft','confirm'])],'required':[('state','not in',['draft'])]}" />
</div>
<field name="launch_date" invisible="1"/>
<field name="progress" widget="progressbar" />
</group>
</group>
</xpath>
<xpath expr="//page[@name='settings']" position="attributes">
<attribute name="groups">project_base.group_project_department_manager</attribute>
</xpath>
<xpath expr="//page[@name='settings']/group/group/field[@name='user_id']" position="replace" />
<xpath expr="//page[@name='settings']/group/group/field[@name='partner_id']" position="replace" />
<xpath expr="//page[@name='settings']/group/group/field[@name='partner_phone']" position="attributes">
<attribute name="attrs">{'invisible':True}</attribute>
</xpath>
<xpath expr="//page[@name='settings']/group/group/field[@name='partner_email']" position="attributes">
<attribute name="attrs">{'invisible':True}</attribute>
</xpath>
<xpath expr="//page[@name='settings']/group/group/div[@name='alias_def']" position="attributes">
<attribute name="attrs">{'invisible':True}</attribute>
</xpath>
<xpath expr="//page[@name='settings']/group/group/field[@name='alias_contact']" position="attributes">
<attribute name="attrs">{'invisible':True}</attribute>
</xpath>
<xpath expr="//div[@id='rating_settings']" position="attributes">
<attribute name="attrs">{'invisible':True}</attribute>
</xpath>
<xpath expr="//field[@name='active']" position="after">
<field name="resource_calendar_id" attrs="{'readonly':[('state','not in','draft')]}" />
<field name="type" attrs="{'readonly':[('state','not in','draft')]}" />
<field name="invoice_method" attrs="{'required':True,'readonly':[('state','not in',['draft'])]}" widget="radio" />
<label for="invoice_period" attrs="{'invisible':[('invoice_method','!=','per_period')]}" />
<div class="oe_inline" style="display: inline;" attrs="{'invisible':[('invoice_method','!=','per_period'),],'required': [('invoice_method','=','per_period')],'readonly':[('state','not in',['draft'])]}">
<field name="invoice_period" class="oe_inline" />
<span class="oe_inline">Days</span>
</div>
<field name="man_hours" />
<field name="type_ids" widget="many2many_tags" />
</xpath>
<xpath expr="//field[@name='description']" position="attributes">
<attribute name="attrs">{'readonly':[('state','not in',['draft','confirm'])]}</attribute>
</xpath>
<xpath expr="//field[@name='company_id']" position="attributes">
<attribute name="attrs">{'readonly':True}</attribute>
</xpath>
<xpath expr="//field[@name='analytic_account_id']" position="attributes">
<attribute name="attrs">{'readonly':[('state','not in',['draft','confirm'])]}</attribute>
</xpath>
<xpath expr="//page[@name='description_page']" position="before">
<page name="project_phase" string="Project Phases">
<field name="project_phase_ids" nolabel="1" attrs="{'readonly':[('state','not in',['confirm'])]}">
<tree>
<field name="phase_id" attrs="{'required':True,'readonly':[('state','not in',['draft'])]}" />
<field name="start_date" attrs="{'required':True,'readonly':[('state','not in',['draft'])]}" />
<field name="end_date" attrs="{'required':True,'readonly':[('state','not in',['draft'])]}" />
<field name="weight" />
<field name="progress" widget="progressbar" />
<field name="estimated_hours" attrs="{'required':True,'readonly':[('state','not in',['draft'])],'column_invisible': [('parent.man_hours', '=', False)]}" widget="float_time" sum="estimated_hours" />
<field name="state" />
<button name="action_confirm" string="Open" type="object" class="btn-info" icon="fa-caret-right" attrs="{'invisible':[('state','not in',['draft'])]}" />
<button name="action_close" string="Close" type="object" class="btn-success" icon="fa-power-off" attrs="{'invisible':[('state','not in',['open'])]}" />
<button name="action_reopen" string="Re Open" type="object" class="btn-info" icon="fa-rotate-right" attrs="{'invisible':[('state','not in',['close'])]}" />
<field name="task_count" />
<button class="d-none d-md-inline oe_stat_button" type="object" name="action_project_phase_task" icon="fa-tasks">
<field name="task_ids" attrs="{'invisible': True}" />
</button>
</tree>
</field>
</page>
</xpath>
<xpath expr="//page[@name='description_page']" position="after">
<page name="team" string="Project Team">
<field name="project_team_ids" nolabel="1">
<tree editable="top">
<field name="employee_id" required="1" />
<field name="department_id" optional="hide" />
<field name="job_id" optional="hide" />
<field name="project_job" force_save="1" required="1" />
<field name="project_id" invisible="1" />
</tree>
</field>
</page>
<page name="invoice" string="Invoicing" attrs="{'invisible':[('type','=','internal')]}">
<group>
<group>
<field name="contract_number" attrs="{'readonly':[('state','not in',['draft','confirm'])],'required':False}" />
<field name="signature_date" attrs="{'readonly':[('state','not in',['draft','confirm'])]}" />
</group>
<group>
<field name="contract_value_untaxed" attrs="{'readonly':[('state','not in',['draft','confirm'])],'required':[('state','not in',['draft','done'])]}" />
<field name="tax_amount" attrs="{'readonly':[('state','not in',['draft','confirm'])],'required':[('state','not in',['draft','done'])]}" />
<field name="contract_value" attrs="{'readonly':[('state','not in',['draft','confirm'])],'required':[('state','not in',['draft','done'])]}" />
</group>
</group>
<field name="invoice_ids" nolabel="1">
<tree decoration-info="invoice_type=='variation_order'">
<field name="name" string="Description" />
<field name="phase_id" />
<field name="plan_date" required="1" />
<field name="project_id" invisible="1" />
<field name="invoice_type" invisible="1" />
<field name="amount" sum="Amount" />
<field name="actual_date" />
<field name="plan_payment_date" />
<field name="actual_payment_date" />
<field name="to_invoice" invisible="1" />
<field name="invoice_type" />
<field name="state" />
<!-- <button name="action_confirm" type="object" icon="fa-check" attrs="{'invisible':[('state','not in',['draft'])]}" />
<button name="action_get_invoice" type="object" string="Details" icon="fa-list" /> -->
</tree>
</field>
<group name="note_group" col="6" class="mt-2 mt-md-0">
<group colspan="4">
</group>
<group class="oe_subtotal_footer oe_right" colspan="2" name="project_total">
<field name="total_invoiced_amount" widget="monetary" force_save="1" />
<div class="oe_subtotal_footer_separator oe_inline o_td_label">
<label for="total_invoiced_payment" />
</div>
<field name="total_invoiced_payment" nolabel="1" class="oe_subtotal_footer_separator" widget="monetary" options="{'currency_field': 'currency_id'}" />
<div class="oe_subtotal_footer_separator oe_inline o_td_label">
<label for="residual_amount" />
</div>
<field name="residual_amount" nolabel="1" class="oe_subtotal_footer_separator" widget="monetary" options="{'currency_field': 'currency_id'}" />
</group>
<div class="oe_clear" />
</group>
</page>
<page name="location" string="Location">
<group>
<group>
<span class="o_form_label o_td_label" name="address_name">
<b>Address</b>
</span>
<div class="o_address_format">
<field name="street" placeholder="Street..." class="o_address_street" attrs="{'readonly':[('state','not in',['draft','confirm'])]}" />
<field name="street2" placeholder="Street 2..." class="o_address_street" attrs="{'readonly':[('state','not in',['draft','confirm'])]}" />
<field name="city" placeholder="City" class="o_address_city" attrs="{'readonly':[('state','not in',['draft','confirm'])]}" />
<field name="state_id" class="o_address_state" placeholder="Region" options="{'no_open': True, 'no_quick_create': True}" attrs="{'readonly':[('state','not in',['draft','confirm'])]}" context="{'country_id': country_id, 'default_country_id': country_id, 'zip': zip}" />
<field name="zip" placeholder="ZIP" class="o_address_zip" attrs="{'readonly':[('state','not in',['draft','confirm'])]}" />
<field name="country_id" placeholder="Country" class="o_address_country" options="{&quot;no_open&quot;: True, &quot;no_create&quot;: True}" attrs="{'readonly':[('state','not in',['draft','confirm'])]}" />
</div>
</group>
</group>
</page>
<page name="lesson_learned" string="Lesson Learned">
<field name="lessons_learned" />
</page>
</xpath>
<xpath expr="//field[@name='message_follower_ids']" position="after">
<field name="message_ids" />
</xpath>
<xpath expr="//field[@name='privacy_visibility']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='allowed_internal_user_ids']" position="attributes">
<attribute name="attrs">{}</attribute>
</xpath>
<xpath expr="//field[@name='allowed_portal_user_ids']" position="attributes">
<attribute name="attrs">{}</attribute>
</xpath>
</field>
</record>
<!-- Inherit Project Search -->
<record id="view_project_project_filter_inherit" model="ir.ui.view">
<field name="name">project.project.select</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.view_project_project_filter" />
<field name="arch" type="xml">
<xpath expr="//filter[@name='Manager']" position="before">
<filter string="Project Category" name="Category" context="{'group_by': 'category_id'}" />
<filter string="Status" name="Status" context="{'group_by': 'status'}" />
</xpath>
<xpath expr="//field[@name='partner_id']" position="replace">
<field name="partner_id" string="Partner" filter_domain="[('partner_id', 'child_of', self)]" />
</xpath>
<xpath expr="//filter[@name='Partner']" position="replace">
<filter string="Partner" name="Partner" context="{'group_by': 'partner_id'}" />
</xpath>
<xpath expr="//field[@name='name']" position="after">
<field name="project_no" string="Project Number" />
</xpath>
<xpath expr="//filter[@name='followed_by_me']" position="replace">
<filter string="Open" name="open" domain="[('status','=','open')]" />
<filter string="Hold" name="hold" domain="[('status','=','hold')]" />
<filter string="Closed" name="closed" domain="[('status','=','closed')]" />
<searchpanel>
<field name="category_id" string="Categories" enable_counters="1" />
</searchpanel>
</xpath>
</field>
</record>
<!-- Inherit Form Kanban -->
<record id="quick_create_task_inherit_form" model="ir.ui.view">
<field name="name">project.project.view.inherit</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.quick_create_task_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="after">
<field name="phase_id" options="{'no_open': True,'no_create': True}"/>
<field name="weight"/>
</xpath>
</field>
</record>
<!-- Project New kanban -->
<record model="ir.ui.view" id="view_new_project_kanban">
<field name="name">project.project.kanban</field>
<field name="model">project.project</field>
<field name="arch" type="xml">
<kanban string="Project Overview" class="o_emphasize_colors o_kanban_dashboard o_slide_kanban o_slide_channel_kanban" sample="1">
<field name="name" />
<field name="project_no" />
<field name="partner_id" />
<field name="color" />
<field name="is_favorite" />
<field name="analytic_account_id" />
<field name="start" />
<field name="date" />
<field name="doc_count" />
<field name="status" />
<field name="state" />
<field name="progress" />
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click">
<div class="o_kanban_card_header">
<div class="o_kanban_card_header_title mb16">
<div class="o_primary">
<a type="edit" class="me-auto">
<field name="is_favorite" widget="boolean_favorite" nolabel="1" force_save="1" />
<span> . </span>
<span>
<field name="name" class="o_primary" />
</span>
<br />
<span>
<field name="project_no" class="o_primary" />
</span>
</a>
<br />
<span class="o_text_overflow text-muted" t-if="record.partner_id.value">
<span class="fa fa-user mr-2" aria-label="Partner" title="Partner"></span>
<t t-esc="record.partner_id.value" />
</span>
</div>
<div t-if="record.date.raw_value or record.start.raw_value" class="text-muted o_row">
<span class="fa fa-clock-o mr-2" title="Dates"></span>
<field name="start" />
<i t-if="record.date.raw_value and record.start.raw_value" class="fa fa-long-arrow-right mx-2 oe_read_only" aria-label="Arrow icon" title="Arrow" />
<field name="date" />
</div>
<div t-if="record.tag_ids" class="w-75">
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" />
</div>
<field name="status" widget="badge" decoration-success="status == 'close'" decoration-info="status == 'open'" decoration-danger="status in ['hold','pending']" />
</div>
<div class="container o_kanban_card_manage_pane dropdown-menu" role="menu" groups="base.group_user">
<div class="row">
<div class="col-6 o_kanban_card_manage_section o_kanban_manage_view">
<div role="menuitem" class="o_kanban_card_manage_title">
<span>View</span>
</div>
<div role="menuitem">
<a name="action_view_tasks" type="object">Tasks</a>
</div>
<div role="menuitem" t-if="record.doc_count.raw_value">
<a name="attachment_tree_view" type="object">
Documents</a>
</div>
</div>
<div class="col-6 o_kanban_card_manage_section o_kanban_manage_reporting" groups="project.group_project_manager">
<div role="menuitem" class="o_kanban_card_manage_title">
<span>Reporting</span>
</div>
<div role="menuitem">
<a name="%(project_base.report_project_detail)d" type="action">Project
Charter
</a>
</div>
<div role="menuitem">
<a name="action_view_tasks_analysis" type="object">Tasks
Analysis
</a>
</div>
</div>
</div>
<div class="o_kanban_card_manage_settings row" groups="project.group_project_manager">
<div role="menuitem" aria-haspopup="true" class="col-8">
<ul class="oe_kanban_colorpicker" data-field="color" role="popup" />
</div>
<div role="menuitem" class="col-4">
<a class="dropdown-item" role="menuitem" type="edit" name="action_view_kanban_project">Edit
</a>
</div>
</div>
</div>
<a class="o_kanban_manage_toggle_button o_dropdown_kanban" href="#" groups="base.group_user">
<i class="fa fa-ellipsis-v" role="img" aria-label="Manage" title="Manage" />
</a>
</div>
<div class="container o_kanban_card_content mt1">
<div class="row mb16">
<div class="col-4 o_kanban_primary_left">
<button class="btn btn-primary" name="action_creat_tasks" type="object">Task</button>
</div>
<div class="col-4 ">
<div class="d-flex" name="info_avg_rating" t-if="record.no_of_invoices.raw_value">
<a name="action_project_invoice" type="object" class="me-auto"> Invoices<field name="no_of_invoices" />
</a>
</div>
<div class="d-flex" name="info_paid" t-if="record.no_of_invoices.raw_value">
<a name="action_paid_invoice" type="object" class="me-auto"> Paid<field name="no_of_paid_invoices" />
</a>
</div>
</div>
<div class="col-4 o_kanban_primary_right">
<div class="d-flex" name="info_avg_invoiced" t-if="record.no_of_invoices.raw_value">
<span>
<field name="total_invoiced_amount" />
</span>
</div>
<div class="d-flex" name="info_total_paid" t-if="record.no_of_invoices.raw_value">
<span>
<field name="total_invoiced_payment" />
</span>
</div>
</div>
</div>
<div class="row mt3 o_kanban_card_attendees_buttons">
<div class="col-3 border-end">
<a name="action_view_new_tasks" type="object" class="d-flex flex-column align-items-center">
<field name="task_count_new" class="fw-bold" />
<span class="text-muted">New</span>
</a>
</div>
<div class="col-3 border-end">
<a name="action_view_inprogress_tasks" type="object" class="d-flex flex-column align-items-center">
<field name="task_count_inprogress" class="fw-bold" />
<span class="text-muted">In progress</span>
</a>
</div>
<div class="col-3 border-end">
<a name="action_view_finished_tasks" type="object" class="d-flex flex-column align-items-center">
<field name="task_count_finished" class="fw-bold" />
<span class="text-muted">Finished</span>
</a>
</div>
<div class="col-3">
<a name="action_view_tasks" type="object" class="d-flex flex-column align-items-center">
<field name="task_count_all" class="fw-bold" />
<span class="text-muted">All</span>
</a>
</div>
</div>
<div class="o_kanban_record_bottom mt-3">
<div class="oe_kanban_bottom_left">
<field name="progress" widget="progressbar" />
</div>
<div class="oe_kanban_bottom_right">
<field name="user_id" widget="many2one_avatar_user" />
</div>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="act_project_project_2_project_task_finished" model="ir.actions.act_window">
<field name="name">Tasks</field>
<field name="res_model">project.task</field>
<field name="view_mode">kanban,tree,form,calendar,pivot,graph,activity</field>
<field name="domain">[('project_id', '=', active_id),('stage_id.is_closed', '=', True)]</field>
<field name="context">{
'pivot_row_groupby': ['user_id'],
'default_project_id': active_id,
}</field>
<field name="search_view_id" ref="project.view_task_search_form" />
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No tasks found. Let's create one!
</p>
<p> To get things done, use activities and status on tasks.<br />
Chat in real time or
by email to collaborate efficiently. </p>
</field>
</record>
<record id="act_project_project_2_project_task_inprogress" model="ir.actions.act_window">
<field name="name">Tasks</field>
<field name="res_model">project.task</field>
<field name="view_mode">kanban,tree,form,calendar,pivot,graph,activity</field>
<field name="domain">[('project_id', '=', active_id),('stage_id.is_closed', '=', False),
('stage_id.fold', '=', False), ('stage_id.sequence', '!=', 0)]</field>
<field name="context">{
'pivot_row_groupby': ['user_id'],
'default_project_id': active_id,
}</field>
<field name="search_view_id" ref="project.view_task_search_form" />
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No tasks found. Let's create one!
</p>
<p> To get things done, use activities and status on tasks.<br />
Chat in real time or
by email to collaborate efficiently. </p>
</field>
</record>
<record id="act_project_project_2_project_task_new" model="ir.actions.act_window">
<field name="name">Tasks</field>
<field name="res_model">project.task</field>
<field name="view_mode">kanban,tree,form,calendar,pivot,graph,activity</field>
<field name="domain">[('project_id', '=', active_id),('stage_id.sequence', '=', 0)]</field>
<field name="context">{
'pivot_row_groupby': ['user_id'],
'default_project_id': active_id,
}</field>
<field name="search_view_id" ref="project.view_task_search_form" />
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No tasks found. Let's create one!
</p>
<p> To get things done, use activities and status on tasks.<br />
Chat in real time or
by email to collaborate efficiently. </p>
</field>
</record>
<record id="create_tast_proj" model="ir.actions.act_window">
<field name="res_model">project.task</field>
<field name="name">Project's tasks</field>
<field name="view_mode">form,tree,calendar,graph,kanban</field>
<field name="domain">[('project_id', '=', active_id)]</field>
<field name="context">{'project_id':active_id,'default_project_id': active_id}</field>
</record>
<record id="project.open_view_project_all" model="ir.actions.act_window">
<field name="view_mode">kanban,tree,form,pivot,graph</field>
<field name="view_id" ref="project_base.view_new_project_kanban" />
</record>
<record id="project.open_view_project_all_config" model="ir.actions.act_window">
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('project.view_project')}),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('project_base.view_new_project_kanban')})]" />
</record>
<record id="view_project_inherit" model="ir.ui.view">
<field name="name">project.project.tree</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.view_project" />
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="before">
<field name="project_no" />
</xpath>
<xpath expr="//field[@name='company_id']" position="after">
<field name="progress" widget="progressbar" />
<field name="state" widget="badge" decoration-success="state == 'done'" decoration-info="state == 'draft'" optional="show" />
<field name="status" widget="badge" decoration-success="status == 'close'" decoration-info="status == 'open'" decoration-danger="status == 'hold'" optional="show" />
</xpath>
<xpath expr="//field[@name='user_id']" position="before">
<field name="start" />
<field name="date" />
<field name="launch_date" optional="hide"/>
</xpath>
<xpath expr="//field[@name='partner_id']" position="replace">
<field name="partner_id" optional="show" string="Partner" />
</xpath>
</field>
</record>
<record id="view_edit_project_sale_timesheet" model="ir.ui.view">
<field name="name">project.project.view.inherit</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="sale_timesheet.project_project_view_form" />
<field name="arch" type="xml">
<xpath expr="//div[@id='allow_billable_container']" position="attributes">
<attribute name="attrs">{'invisible':True}</attribute>
</xpath>
<xpath expr="//page[@name='billing_employee_rate']" position="attributes">
<attribute name="attrs">{'invisible':True}</attribute>
</xpath>
<xpath expr="//field[@name='sale_order_id']" position="attributes">
<attribute name="context">{'no_create': True, 'no_edit': True, 'delete': False}</attribute>
</xpath>
</field>
</record>
<record model="ir.ui.view" id="view_project_kanban_inherit">
<field name="name">project.project.kanban</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.view_project_kanban" />
<field name="arch" type="xml">
<xpath expr="//div[hasclass('o_primary')]" position="inside">
<div class="row">
<div class="col-12">
<span class="float-right">
<field name="status" widget="label_selection" options="{'classes': {'open': 'default', 'pending': 'danger','hold': 'danger','close':'success'}}" />
</span>
</div>
</div>
</xpath>
<xpath expr="//div[hasclass('o_project_kanban_boxes')]/a[@name='action_view_tasks']" position="replace">
</xpath>
</field>
</record>
<!-- Project Category -->
<record id="project_category_form_view" model="ir.ui.view">
<field name="name">project.category.form</field>
<field name="model">project.category</field>
<field name="arch" type="xml">
<form string="Project Category">
<sheet>
<group>
<group>
<field name="name" required="1" />
<field name="company_id" groups="base.group_multi_company" readonly="1" />
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="project_category_tree" model="ir.ui.view">
<field name="name">project.category.tree</field>
<field name="model">project.category</field>
<field name="arch" type="xml">
<tree string="Project Category">
<field name="name" />
</tree>
</field>
</record>
<record id="open_project_category_form" model="ir.actions.act_window">
<field name="name">Project Category</field>
<field name="res_model">project.category</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="project_category_tree" />
</record>
<menuitem action="open_project_category_form" id="menu_project_category" name="Category" parent="project.menu_project_config" sequence="4" groups="project_base.group_project_department_manager" />
<!-- Project Hold Reason -->
<record id="project_hold_reason_form_view" model="ir.ui.view">
<field name="name">project.hold.reason.form</field>
<field name="model">project.hold.reason</field>
<field name="arch" type="xml">
<form string="Project Hold Reason">
<sheet>
<group>
<group>
<field name="name" required="1" />
<field name="company_id" groups="base.group_multi_company" readonly="1" />
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="project_hold_reason_tree_view" model="ir.ui.view">
<field name="name">project.hold.reason.tree</field>
<field name="model">project.hold.reason</field>
<field name="arch" type="xml">
<tree string="Project Hold Reason">
<field name="name" />
</tree>
</field>
</record>
<record id="open_project_hold_reason_form" model="ir.actions.act_window">
<field name="name">Project Hold Reason</field>
<field name="res_model">project.hold.reason</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="project_hold_reason_tree_view" />
</record>
<menuitem action="open_project_hold_reason_form" id="menu_hold_reason" name="Hold Reason" parent="project.menu_project_config" sequence="5" groups="project_base.group_project_department_manager"/>
<!-- TODO: Create Sub-project Action
<record id="sub-project_project_action" model="ir.actions.server">
<field name="name">Create Sub-Project</field>
<field name="model_id" ref="project.model_project_project"/>
<field name="binding_model_id" ref="project.model_project_project"/>
<field name="binding_view_types">form</field>
<field name="state">code</field>
<field name="code">action = records.create_sub_project()</field>
</record> -->
<delete model="ir.ui.menu" id="project.menu_project_report_task_analysis" />
<!-- TODO Move to timesheet module -->
<!-- <delete model="ir.ui.menu" id="sale_timesheet.menu_project_profitability_analysis"/> -->
<delete model="ir.ui.menu" id="project.project_menu_config_activity_type" />
</odoo>

View File

@ -0,0 +1,122 @@
<odoo>
<record id="view_config_settings_form" model="ir.ui.view">
<field name="name">view.config.settings.form</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="project.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='project_time']" position="after">
<h2>Odex25 Projects Management</h2>
<div class="row mt16 o_settings_container" name="main_currency_setting_container">
<div class="col-12 col-lg-6 o_setting_box" id="project_invoice" title="Project invoicing method.">
<div class="o_setting_left_pane"/>
<div class="o_setting_right_pane">
<span class="o_form_label">Project Invoicing Method</span>
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." aria-label="Values set here are company-specific." groups="base.group_multi_company" role="img"/>
<div class="text-muted">
Create Invoice for project per stage or every specific period
</div>
<div class="content-group">
<field name="invoice_method" required="1" class="o_light_label mt16" widget="radio"/>
</div>
<div class="row" attrs="{'invisible': [('invoice_method','!=','per_period')],'required': [('invoice_method','=','per_period')]}">
<label string="Invoice Every" for="invoice_period" class="col-lg-3 o_light_label"/>
<div class="content-group">
<field name="invoice_period" class="text-center oe_inline"/> days
</div>
</div>
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box" id="project_budget">
<div class="o_setting_left_pane">
<field name="module_project_budget"/>
</div>
<div class="o_setting_right_pane" name="intrastat_right_pane">
<label for="module_project_budget"/>
<div class="text-muted">
Project Budget
</div>
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box" id="risk_register">
<div class="o_setting_left_pane">
<field name="module_project_risk_register"/>
</div>
<div class="o_setting_right_pane" name="intrastat_right_pane">
<label for="module_project_risk_register"/>
<div class="text-muted">
Add Risk Register Log in your Projects
</div>
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box" id="customer_team">
<div class="o_setting_left_pane">
<field name="module_project_customer_team"/>
</div>
<div class="o_setting_right_pane" name="intrastat_right_pane">
<label for="module_project_customer_team"/>
<div class="text-muted">
Add Customer Team Details in your Projects
</div>
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box" id="scrum_agile">
<div class="o_setting_left_pane">
<field name="module_project_scrum_agile"/>
</div>
<div class="o_setting_right_pane" name="intrastat_right_pane">
<label for="module_project_scrum_agile"/>
<div class="text-muted">
Use Scrum Agile Methodology in your Projects
</div>
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box" id="helpdisk_task">
<div class="o_setting_left_pane">
<field name="module_project_helpdisk_task"/>
</div>
<div class="o_setting_right_pane" name="intrastat_right_pane">
<label for="module_project_helpdisk_task"/>
<div class="text-muted">
Link Task with its associated ticket
</div>
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box" id="variation_order">
<div class="o_setting_left_pane">
<field name="module_project_variation_order"/>
</div>
<div class="o_setting_right_pane" name="intrastat_right_pane">
<label for="module_project_variation_order"/>
<div class="text-muted">
Add Variation Order to your Projects
</div>
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box" id="project_metrics">
<div class="o_setting_left_pane">
<field name="module_project_metrics"/>
</div>
<div class="o_setting_right_pane" name="intrastat_right_pane">
<label for="module_project_metrics"/>
<div class="text-muted">
Project Metrics
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" id="project_type">
<div class="o_setting_left_pane"/>
<div class="o_setting_right_pane">
<span class="o_form_label">Project Type</span>
<div class="text-muted">
Project Type
</div>
<div class="content-group">
<field name="type" class="o_light_label mt16" widget="radio"/>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
from . import project_hold_reason
from . import edit_project_phase
from . import down_payment_invoice_advance

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
class SaleAdvancePaymentInv(models.TransientModel):
_inherit = "sale.advance.payment.inv"
advance_down_payment_method = fields.Selection([
('percentage', 'Down payment (percentage)'),
('fixed', 'Down payment (fixed amount)')
], string='Create Down Payment', default='percentage', required=True,
help="A standard invoice is issued with all the order lines ready for invoicing, \
according to their invoicing policy (based on ordered or delivered quantity).")
@api.onchange('advance_down_payment_method')
def onchange_advance_down_payment_method(self):
if self.advance_down_payment_method:
self.advance_payment_method = self.advance_down_payment_method
def create_invoices(self):
res = super(SaleAdvancePaymentInv, self).create_invoices()
project_id = self.env['project.project'].browse(self._context.get('project_id'))
project_id.is_down_payment = True
project_id.update_invoices(is_down_payment=True)
return res

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_sale_advance_payment_inv_inherit" model="ir.ui.view">
<field name="name">Invoice Orders</field>
<field name="model">sale.advance.payment.inv</field>
<field name="inherit_id" ref="sale.view_sale_advance_payment_inv"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='advance_payment_method']" position="before">
<field name="advance_down_payment_method" class="oe_inline" widget="radio" />
</xpath>
<xpath expr="//field[@name='advance_payment_method']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,43 @@
from odoo import fields, models,api,_
from odoo.exceptions import UserError, ValidationError
class EditProjectPhase(models.TransientModel):
_name = "edit.project.phase"
phase_id = fields.Many2one("project.phase", string="Phase", readonly=False)
start_date = fields.Date(string='Start Date')
end_date = fields.Date(string='End Date')
estimated_hours = fields.Float("Additional Estimated Hours")
edit_reasone = fields.Text("Reason")
#TODO open in vo module
# edit_type = fields.Selection(selection=[('vo','Variation Order'),('other','Other')],string="Edit type")
# from_vo = fields.Boolean("From VO")
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
res['phase_id'] = self._context.get('active_id', False)
phase = self.env['project.phase'].browse(res['phase_id'])
res['start_date'] = phase.start_date
res['end_date'] = phase.end_date
return res
@api.constrains('start_date', 'end_date')
def _check_dates(self):
if self.filtered(lambda c: c.end_date and c.start_date > c.end_date):
raise ValidationError(_('Phase start date must be earlier than Phase end date.'))
def edit_phase(self):
self.ensure_one()
to_write = {}
act_close = {"type": "ir.actions.act_window_close"}
active_id = self._context.get("active_id")
phase = self.env['project.phase'].browse(active_id)
if self.estimated_hours:
estimated_hours= phase.estimated_hours + self.estimated_hours
to_write.update({'estimated_hours':estimated_hours})
to_write.update({'start_date':self.start_date,'end_date':self.end_date})
phase.write(to_write)
return act_close

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_edit_project_phase_wizard" model="ir.ui.view">
<field name="name">Edit Project Phase</field>
<field name="model">edit.project.phase</field>
<field name="arch" type="xml">
<form string="Edit Project Phase">
<p style="color: #FF0000;">
<b>
Kindly make sure to Save current project stages before edit.
</b>
</p>
<group>
<group>
<field name="phase_id" readonly="1" force_save="1"/>
<field name="start_date" required="1"/>
</group>
<group>
<!-- <field name="from_vo" invisible="1"/> -->
<!-- <field name="edit_type" required="1" attrs="{'invisible':[('variation_order_id','!=',False)],'readonly':True}" force_save="1"/> -->
<field name="end_date" required="1"/>
<field name="edit_reasone"/>
<field name="estimated_hours"/>
</group>
</group>
<footer>
<button
name="edit_phase"
string="Edit"
type="object"
class="oe_highlight"
/>
or
<button string="Cancel" class="oe_link" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="action_edit_project_phase" model="ir.actions.act_window">
<field name="name">Edit Project Phase</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">edit.project.phase</field>
<field name="view_mode">form</field>
<field name="view_id" ref="view_edit_project_phase_wizard" />
<field name="target">new</field>
</record>
</odoo>

View File

@ -0,0 +1,27 @@
from odoo import fields, models,_
class ProjectHoldWizard(models.TransientModel):
_name = "project.hold.wizard"
reason_id = fields.Many2one("project.hold.reason", string="Reason", required=True)
reason = fields.Text(string="Reason")
def hold_project(self):
self.ensure_one()
act_close = {"type": "ir.actions.act_window_close"}
project_ids = self._context.get("active_ids")
if project_ids is None:
return act_close
project = self.env["project.project"].browse(project_ids)
msg1 = _('Project Hold for %s',self.reason_id.name)
msg2 = " "
if self.reason:
msg2 = _(' with Reason: %s',self.reason)
msg = msg1 + msg2
project.hold_reason = self.reason
project.action_hold()
project.message_post(body=msg)
return act_close

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_project_hold_wizard" model="ir.ui.view">
<field name="name">Reason for the Project Hold</field>
<field name="model">project.hold.wizard</field>
<field name="arch" type="xml">
<form string="Reason for the Project Hold">
<p class="oe_grey">
Choose the reason for the project hold.
</p>
<group>
<field name="reason_id"/>
</group>
<group>
<field name="reason" nolabel="1"/>
</group>
<footer>
<button
name="hold_project"
string="Hold"
type="object"
class="oe_highlight"
/>
or
<button string="Cancel" class="oe_link" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="action_project_hold" model="ir.actions.act_window">
<field name="name">Reason for Project Hold</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">project.hold.wizard</field>
<field name="view_mode">form</field>
<field name="view_id" ref="view_project_hold_wizard" />
<field name="target">new</field>
</record>
</odoo>

View File

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

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
{
'name': 'Odex Project Budget Custom For Projects',
'version': '1.0',
'sequence': 4,
'website': 'http://exp-sa.com',
'license': 'GPL-3',
'author': 'Expert Ltd',
'summary': 'Customize Budget for Project',
'category': 'Odex25-Project/Odex25-Project',
'description': """
Add Project Budget
- Add Is Timesheet Hours in Budget Post
- Hours line that read the counsume hours from timesheet
""",
'depends': ['project_base', 'account_budget_custom'],
'data': [
'security/ir.model.access.csv',
'security/account_budget_security.xml',
'views/view.xml',
],
}

View File

@ -0,0 +1,438 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * project_budget
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-05-12 12:18+0000\n"
"PO-Revision-Date: 2024-05-12 12:18+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_budget
#: model_terms:ir.ui.view,arch_db:project_budget.project_project_financial_form
msgid ""
"<span attrs=\"{'invisible': ['|','|',('ready_budget', '=', True),('project_phase_ids','!=',[]),('id','=',False)]}\" class=\"badge badge-warning\">\n"
" The Project Budget Must be Approved First in order to Enter Stages\n"
" </span>"
msgstr ""
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.crossovered_budget_line_project_pivot
#: model_terms:ir.ui.view,arch_db:project_budget.project_budget_custom_form
msgid "Actual Amount"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,help:project_budget.field_crossovered_budget_lines__planned_amount
msgid ""
"Amount you plan to earn/spend. Record a positive amount if it is a revenue "
"and a negative amount if it is a cost."
msgstr ""
"المبلغ الذي تخطط لربحه/إنفاقه. سجله كقيمة موجبة إذا كان ربحًا وسالبة إذا "
"كانت تكلفة."
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_crossovered_budget__analytic_account_id
msgid "Analytic Account"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,help:project_budget.field_crossovered_budget__analytic_account_id
msgid ""
"Analytic account to which this project is linked for financial management. "
"Use an analytic account to record cost and revenue on your project."
msgstr ""
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.project_budget_custom_form
msgid "Are you sure you want to edit the Budget?"
msgstr ""
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.project_project_financial_form
msgid "Bill Info"
msgstr ""
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.project_project_financial_form
msgid "Billed Amount"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,help:project_budget.field_project_project__gross_amount
msgid "Billed amount - Total Cost"
msgstr ""
#. module: project_budget
#: model:ir.model,name:project_budget.model_crossovered_budget
#: model_terms:ir.ui.view,arch_db:project_budget.project_project_financial_form
msgid "Budget"
msgstr "الموازنة"
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_crossovered_budget_lines__project_planned_amount
#: model_terms:ir.ui.view,arch_db:project_budget.crossovered_budget_line_log_tree
#: model_terms:ir.ui.view,arch_db:project_budget.crossovered_budget_line_project_pivot
#: model_terms:ir.ui.view,arch_db:project_budget.project_budget_custom_form
msgid "Budget Amount"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_project_project__estimated_hours
#: model_terms:ir.ui.view,arch_db:project_budget.project_budget_custom_form
msgid "Budget Hours"
msgstr ""
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.crossovered_budget_line_log_form
#: model_terms:ir.ui.view,arch_db:project_budget.crossovered_budget_line_project_pivot
#: model_terms:ir.ui.view,arch_db:project_budget.project_budget_custom_form
msgid "Budget Item"
msgstr ""
#. module: project_budget
#: code:addons/project_budget/models/account_budget.py:0
#: model:ir.model,name:project_budget.model_crossovered_budget_lines
#, python-format
msgid "Budget Line"
msgstr "بند الموازنة"
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.crossovered_budget_line_log_form
#: model_terms:ir.ui.view,arch_db:project_budget.crossovered_budget_line_log_tree
#: model_terms:ir.ui.view,arch_db:project_budget.crossovered_budget_line_project_pivot
#: model_terms:ir.ui.view,arch_db:project_budget.project_budget_custom_form
msgid "Budget Lines"
msgstr ""
#. module: project_budget
#: code:addons/project_budget/models/account_budget.py:0
#, python-format
msgid "Budget amount can't be less than actual amount!"
msgstr "لا يمكن أن يكون الصرف اكبر من مبلغ الميزانية"
#. module: project_budget
#: model:ir.model,name:project_budget.model_account_budget_post
msgid "Budgetary Position"
msgstr "بند الموازنة"
#. module: project_budget
#: model:ir.actions.act_window,name:project_budget.project_budget_custom_action
#: model:ir.ui.menu,name:project_budget.project_budget_custom_main_menu
msgid "Budgets"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_crossovered_budget_lines__timesheet_hours
msgid "Consumed Hours"
msgstr ""
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.project_project_financial_form
msgid "Contract Amount"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,help:project_budget.field_crossovered_budget__projected_profit
msgid "Contract Value(With VAT & other taxes) - Total Cost"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_project_project__project_cost
msgid "Cost"
msgstr ""
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.project_project_financial_form
msgid "Cost Info"
msgstr ""
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.project_budget_custom_form
msgid "Edit"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_crossovered_budget_lines__date_to
msgid "End Date"
msgstr "تاريخ النهاية"
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.project_project_financial_form
msgid "Financials"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_crossovered_budget__fiscalyear_id
msgid "Fiscal Year"
msgstr "السنة"
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_project_project__gross_amount
msgid "Gross Margin"
msgstr ""
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.project_budget_custom_form
msgid "Hour Cost"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_crossovered_budget__expensess_line_ids
#: model:ir.model.fields,field_description:project_budget.field_crossovered_budget__hour_line_ids
msgid "Hour lines"
msgstr ""
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.project_budget_custom_form
msgid "Hours"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_account_budget_post__is_timesheet_hours
#: model:ir.model.fields,field_description:project_budget.field_crossovered_budget_lines__is_timesheet_hours
msgid "Is Timesheet Hours"
msgstr ""
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.project_budget_custom_form
msgid "Log"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_crossovered_budget__man_hours
#: model:ir.model.fields,field_description:project_budget.field_crossovered_budget_lines__man_hours
msgid "Man hours"
msgstr ""
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.project_budget_custom_form
msgid "Man-Hours"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_project_project__project_hours_cost
msgid "Man-Hours Cost"
msgstr ""
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.project_budget_custom_form
msgid "Others Expensess"
msgstr ""
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.project_budget_custom_form
msgid "Percentage"
msgstr ""
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.project_budget_custom_form
msgid "Period"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_crossovered_budget_lines__planned_amount
#: model_terms:ir.ui.view,arch_db:project_budget.project_budget_custom_form
msgid "Planned Amount"
msgstr "المبلغ المخطط"
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.crossovered_budget_line_log_tree
msgid "Practical Amount"
msgstr ""
#. module: project_budget
#: model:ir.model,name:project_budget.model_project_project
#: model:ir.model.fields,field_description:project_budget.field_crossovered_budget__project_id
#: model:ir.model.fields,field_description:project_budget.field_crossovered_budget_lines__project_id
msgid "Project"
msgstr "المشروع"
#. module: project_budget
#: code:addons/project_budget/models/project.py:0
#, python-format
msgid "Project Allocated hours must not be more than Man-Hours in budget!"
msgstr ""
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.project_budget_custom_form
msgid "Project Amount"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_project_project__budget_id
msgid "Project Budget"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,help:project_budget.field_project_project__project_total_cost
msgid "Project Cost + Project Man Hours Cost"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,help:project_budget.field_project_project__project_cost
msgid "Project Cost to date"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,help:project_budget.field_project_project__project_hours_cost
msgid "Project Man Hours Cost to date"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_crossovered_budget__project_user_id
msgid "Project Manager"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_crossovered_budget__project_no
msgid "Project Number"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_crossovered_budget__projected_profit
msgid "Projected Profit"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_crossovered_budget__sale_order_id
msgid "Proposal"
msgstr ""
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.crossovered_budget_line_log_tree
msgid "Qty"
msgstr ""
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.project_project_financial_form
msgid "Received Amount"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_crossovered_budget_lines__project_remaining_amount
msgid "Remaining Amount"
msgstr ""
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.project_budget_custom_form
msgid "Sale Order"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,help:project_budget.field_crossovered_budget__sale_order_id
msgid "Sales order to which the project is linked."
msgstr ""
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_crossovered_budget_lines__date_from
msgid "Start Date"
msgstr "تاريخ البداية"
#. module: project_budget
#: code:addons/project_budget/models/account_budget.py:0
#, python-format
msgid "The Budget Total Amount Must Not Exceed Project Sale Value"
msgstr ""
#. module: project_budget
#: code:addons/project_budget/models/project.py:0
#, python-format
msgid "The Budget must approved from accounting first!"
msgstr "يجب الموافقة على الميزانية من المالية أولا!"
#. module: project_budget
#: model:ir.model.fields,help:project_budget.field_crossovered_budget__fiscalyear_id
msgid ""
"The fiscalyear\n"
" used for this budget."
msgstr ""
#. module: project_budget
#: code:addons/project_budget/models/project.py:0
#, python-format
msgid ""
"There is No enough Man-Hours in project budget %s , please contact your "
"manager"
msgstr ""
#. module: project_budget
#: code:addons/project_budget/models/project.py:0
#, python-format
msgid "There is no budget created yet for this project."
msgstr "لم يتم إنشاء ميزانية لهذا المشروع بعد"
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.project_budget_custom_form
msgid "Timesheet hours"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_crossovered_budget__sale_order_amount
msgid "Total Amount"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_crossovered_budget__total_budget_cost
#: model:ir.model.fields,field_description:project_budget.field_project_project__project_total_cost
#: model:ir.model.fields,help:project_budget.field_crossovered_budget__total_budget_cost
msgid "Total Cost"
msgstr ""
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.crossovered_budget_line_log_tree
msgid "Unit Cost"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_crossovered_budget_lines__amount
msgid "Unit cost"
msgstr ""
#. module: project_budget
#: code:addons/project_budget/models/account_budget.py:0
#, python-format
msgid "You cannot remove the Budget in %s state."
msgstr ""
#. module: project_budget
#: code:addons/project_budget/models/account_budget.py:0
#, python-format
msgid ""
"You have to enter at least a budgetary position or analytic account on a "
"budget line."
msgstr ""
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_project_project__ready_budget
msgid "project budget is ready"
msgstr ""
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.project_budget_view_tree
msgid "project_budget_tree"
msgstr ""
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.crossovered_budget_line_log_tree
#: model_terms:ir.ui.view,arch_db:project_budget.project_budget_custom_form
msgid "project_remain"
msgstr ""
#. module: project_budget
#: model:ir.model.fields,field_description:project_budget.field_crossovered_budget_lines__qty
msgid "qty/hours"
msgstr ""
#. module: project_budget
#: model_terms:ir.ui.view,arch_db:project_budget.crossovered_budget_line_log_tree
#: model_terms:ir.ui.view,arch_db:project_budget.project_budget_custom_form
msgid "remain"
msgstr ""

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import account_budget
from . import project

View File

@ -0,0 +1,248 @@
# -*- coding: utf-8 -*-
from odoo import api, models, fields, tools, _
from odoo.exceptions import UserError, ValidationError
class ProjectBudget(models.Model):
_inherit = "crossovered.budget"
fiscalyear_id = fields.Many2one(required=False)
project_id = fields.Many2one('project.project', 'Project', required=False,tracking=True)
project_no = fields.Char("Project Number", related="project_id.project_no", tracking=True)
sale_order_id = fields.Many2one('sale.order', related="project_id.sale_order_id", string="Proposal")
sale_order_amount = fields.Monetary("Total Amount", related="project_id.sale_order_amount",tracking=True)
analytic_account_id = fields.Many2one('account.analytic.account', string="Analytic Account",
related="project_id.analytic_account_id")
hour_line_ids = fields.One2many('crossovered.budget.lines', 'crossovered_budget_id', string='Hour lines',
copy=False,domain=[('is_timesheet_hours', '=', True)])
expensess_line_ids = fields.One2many('crossovered.budget.lines', 'crossovered_budget_id', string='Hour lines',
copy=False,domain=[('is_timesheet_hours', '=', False)])
project_user_id = fields.Many2one("res.users", related="project_id.user_id", string="Project Manager", store=True)
total_budget_cost = fields.Monetary('Total Cost', compute='_compute_budget_amounts',
help="Total Cost", tracking=True)
projected_profit = fields.Monetary('Projected Profit', compute='_compute_budget_amounts',
help="Contract Value(With VAT & other taxes) - Total Cost",
tracking=True)
man_hours = fields.Boolean('Man hours' , related="project_id.man_hours",default=False)
@api.depends("crossovered_budget_line", "crossovered_budget_line.planned_amount",
"crossovered_budget_line.project_planned_amount")
def _compute_budget_amounts(self):
"""
Get Total Budget amounts
"""
for rec in self:
total_budget_cost = 0.0
projected_profit = 0.0
if rec.crossovered_budget_line:
total_budget_cost = sum(rec.crossovered_budget_line.mapped("project_planned_amount"))
projected_profit = rec.sale_order_amount - total_budget_cost
rec.total_budget_cost = total_budget_cost
rec.projected_profit = projected_profit
@api.onchange('project_id')
def _onchange_project(self):
"""
set budget name by project name
"""
if self.project_id:
self.name = self.project_id.display_name or "/"
self.date_from = self.project_id.start
self.date_to = self.project_id.date
else:
pass
@api.constrains('date_from', 'date_to', 'fiscalyear_id')
def _check_dates_fiscalyear(self):
"""
pass fiscalyear constrain in case of project budget
"""
if not self.project_id:
return super(ProjectBudget, self)._check_dates_fiscalyear()
else:
pass
@api.constrains('project_id')
def _check_project_id(self):
"""
make sure that the project have just one budget
"""
if self.project_id:
projects = self.env['crossovered.budget'].search(
[('project_id', '=', self.project_id.id), ('id', '!=', self.id)])
if len(projects) > 0:
raise ValidationError("The Project Must have just only one budget")
@api.constrains('project_id','crossovered_budget_line','crossovered_budget_line.qty')
def _check_project_hours(self):
"""
Make sure that budget hours not more than project Allocated hours
"""
if self.project_id:
project_hours = sum(self.project_id.project_phase_ids.mapped("estimated_hours"))
budget_hours = sum(self.hour_line_ids.mapped("qty"))
if project_hours > budget_hours:
raise ValidationError("Project Allocated hours must not be more than Man-Hours in budget!")
def action_budget_confirm(self):
self._check_project_hours()
self.write({'state': 'confirm'})
def action_budget_approve(self):
self._check_project_hours()
self.write({'state': 'approve'})
def action_budget_edit(self):
self.write({'state': 'draft'})
def action_budget_validate(self):
self.write({'state': 'validate'})
@api.constrains('sale_order_amount', 'crossovered_budget_line','crossovered_budget_line.project_planned_amount')
def budget_total(self):
for item in self:
if item.project_id and item.project_id.sale_order_id and sum(
item.crossovered_budget_line.mapped('project_planned_amount')) > item.sale_order_amount:
raise ValidationError(_('The Budget Total Amount Must Not Exceed Project Sale Value'))
def open_sale_order(self):
return {
'name': 'Sale Order',
'view_type': 'form',
'view_mode': 'tree,form',
'res_model': 'sale.order',
'type': 'ir.actions.act_window',
'domain': [('id', '=', self.project_id.sale_order_id.id)],
"res_id": self.project_id.sale_order_id.id,
}
def unlink(self):
for record in self:
if record.state != 'draft':
raise UserError(_('You cannot remove the Budget in %s state.' % (record.state)))
return super(ProjectBudget, self).unlink()
class ProjectBudgetPost(models.Model):
_inherit = "account.budget.post"
is_timesheet_hours = fields.Boolean(string='Is Timesheet Hours')
def _check_account_ids(self, vals):
# Inherit to pass the check in case of timesheet.
if ('is_timesheet_hours' in vals and not vals['is_timesheet_hours']):
return super(ProjectBudgetPost, self)._check_account_ids(vals)
else:
pass
class ProjectBudgetLine(models.Model):
_name = "crossovered.budget.lines"
_inherit = ["crossovered.budget.lines","mail.thread"]
project_id = fields.Many2one("project.project", related='crossovered_budget_id.project_id',store=True,string='Project')
is_timesheet_hours = fields.Boolean(related='general_budget_id.is_timesheet_hours',string='Is Timesheet Hours')
qty = fields.Float(string="qty/hours", tracking=True)
amount = fields.Float(string="Unit cost", tracking=True)
timesheet_hours = fields.Float(string="Consumed Hours", tracking=True)
planned_amount = fields.Monetary('Planned Amount', required=True, tracking=False)
project_planned_amount = fields.Monetary('Budget Amount', tracking=True)
project_remaining_amount = fields.Monetary('Remaining Amount', compute="_compute_project_remaining_amount",tracking=False)
date_from = fields.Date('Start Date', tracking=True)
date_to = fields.Date('End Date', tracking=True)
man_hours = fields.Boolean('Man hours' , related="project_id.man_hours",default=False)
@api.onchange('qty', 'amount','practical_amount')
def _onchange_budget_amount(self):
for rec in self:
rec.planned_amount = (rec.qty * rec.amount) * -1
rec.project_planned_amount = abs(rec.planned_amount)
def _compute_project_remaining_amount(self):
for rec in self:
rec.project_remaining_amount = 0.0
rec.planned_amount = (rec.qty * rec.amount) * -1
rec.project_planned_amount = abs(rec.planned_amount)
rec.project_remaining_amount = rec.project_planned_amount + rec.practical_amount
# todo review this funtion
def _compute_practical_amount(self):
for line in self:
if line.general_budget_id.is_timesheet_hours:
hours_domain = [('project_id', '=', line.crossovered_budget_id.project_id.id),
# ('date', '>=', line.date_from),
# ('date', '<=', line.date_to),
# ('sheet_id.state', '=', 'done')
]
total_canceled_hours = sum(
self.env['account.analytic.line'].search(hours_domain).mapped('unit_amount'))
line.timesheet_hours = total_canceled_hours
line.practical_amount = - (line.timesheet_hours * line.amount)
else:
# return super(ProjectBudgetLine, self)._compute_practical_amount()
acc_ids = line.general_budget_id.account_ids.ids
date_to = line.date_to
date_from = line.date_from
if line.analytic_account_id.id:
analytic_line_obj = self.env['account.analytic.line']
domain = [('account_id', '=', line.analytic_account_id.id),
('date', '>=', date_from),
('date', '<=', date_to),
]
if acc_ids:
domain += [('general_account_id', 'in', acc_ids)]
where_query = analytic_line_obj._where_calc(domain)
analytic_line_obj._apply_ir_rules(where_query, 'read')
from_clause, where_clause, where_clause_params = where_query.get_sql()
select = "SELECT SUM(amount) from " + from_clause + " where " + where_clause
else:
aml_obj = self.env['account.move.line']
domain = [('account_id', 'in',
line.general_budget_id.account_ids.ids),
('date', '>=', date_from),
('date', '<=', date_to),
('move_id.state', '=', 'posted')
]
where_query = aml_obj._where_calc(domain)
aml_obj._apply_ir_rules(where_query, 'read')
from_clause, where_clause, where_clause_params = where_query.get_sql()
select = "SELECT sum(credit)-sum(debit) from " + from_clause + " where " + where_clause
self.env.cr.execute(select, where_clause_params)
line.practical_amount = self.env.cr.fetchone()[0] or 0.0
@api.constrains('project_id','planned_amount','practical_amount')
def _check_project_amounts(self):
for rec in self:
if rec.project_id:
if abs(rec.planned_amount) < abs(rec.practical_amount):
raise ValidationError(_("Budget amount can't be less than actual amount!"))
@api.constrains('general_budget_id', 'analytic_account_id')
def _must_have_analytical_or_budgetary_or_both(self):
for record in self:
if not record.analytic_account_id and not record.general_budget_id:
raise ValidationError(
_("You have to enter at least a budgetary position or analytic account on a budget line."))
def open_line(self):
self.ensure_one()
return {
'name': _('Budget Line'),
'view_type': 'form',
'view_mode': 'form',
'res_model': 'crossovered.budget.lines',
'res_id': self.id,
'views': [(self.env.ref('project_budget.crossovered_budget_line_log_form').id, 'form'), ],
'target': 'current',
'type': 'ir.actions.act_window',
}

View File

@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
from odoo import api, models, fields, tools, _
from odoo.exceptions import UserError, ValidationError
class ProjectProject(models.Model):
_inherit = "project.project"
budget_id = fields.One2many('crossovered.budget', 'project_id', string="Project Budget")
project_cost = fields.Float('Cost',compute="_get_project_costs",help="Project Cost to date")
project_hours_cost = fields.Float('Man-Hours Cost',compute="_get_project_costs",help="Project Man Hours Cost to date")
project_total_cost = fields.Float('Total Cost',compute="_get_project_costs",help="Project Cost + Project Man Hours Cost")
gross_amount = fields.Float('Gross Margin',compute="_get_project_costs",help="Billed amount - Total Cost")
ready_budget = fields.Boolean('project budget is ready',compute="_get_budget_status")
estimated_hours = fields.Float("Budget Hours",compute="_get_project_hours",copy=False)
def _get_project_hours(self):
for rec in self:
estimated_hours = 0
for budget in rec.budget_id:
estimated_hours = sum(budget.hour_line_ids.mapped("qty"))
rec.estimated_hours = estimated_hours
# @api.depends('budget_id','budget_id.crossovered_budget_line','budget_id.crossovered_budget_line.practical_amount','total_invoiced_amount')
def _get_project_costs(self):
for rec in self:
project_cost = 0.0
project_hours_cost = 0.0
project_total_cost = 0.0
gross_amount = 0.0
for budget in rec.budget_id:
project_hours_cost = abs(sum(budget.mapped('crossovered_budget_line').filtered(lambda l:l.general_budget_id.is_timesheet_hours).mapped('practical_amount')))
project_cost = abs(sum(budget.mapped('crossovered_budget_line').filtered(lambda l:not l.general_budget_id.is_timesheet_hours).mapped('practical_amount')))
project_total_cost = project_hours_cost + project_cost
rec.project_cost = project_cost
rec.project_hours_cost = project_hours_cost
rec.project_total_cost = project_total_cost
rec.gross_amount = rec.total_invoiced_amount - rec.project_total_cost
def _get_budget_status(self):
for rec in self:
ready_budget = False
for budget in rec.budget_id:
if budget.state == 'done':
ready_budget = True
rec.ready_budget = ready_budget
def _check_hour_budget(self, to_add_hours):
for rec in self:
for budget in rec.budget_id:
for line in budget.hour_line_ids:
add_amount = to_add_hours * line.amount
if add_amount > line.project_remaining_amount:
raise ValidationError(
_("There is No enough Man-Hours in project budget %s , please contact your manager") % (
rec.display_name))
def _check_project_budget(self):
for rec in self:
if not rec.budget_id:
raise ValidationError(_("There is no budget created yet for this project."))
for budget in rec.budget_id:
if budget.state != 'done':
raise ValidationError(_("The Budget must approved from accounting first!"))
budget_hours = sum(budget.hour_line_ids.mapped("qty"))
project_hours = sum(rec.project_phase_ids.mapped("estimated_hours"))
if project_hours > budget_hours:
raise ValidationError(_("Project Allocated hours must not be more than Man-Hours in budget!"))
def action_done(self):
for record in self:
record._check_project_budget()
return super().action_done()
def action_view_budget(self):
self.ensure_one()
action_window = {
"type": "ir.actions.act_window",
"res_model": "crossovered.budget",
"name": "Budget",
'view_type': 'form',
'view_mode': 'tree,form',
"context": {"default_project_id": self.id,"create": False,"edit": False,
'form_view_ref': 'project_budget.project_budget_custom_form',
'tree_view_ref': 'project_budget.project_budget_view_tree'},
"domain": [('project_id', '=', self.id)]
}
return action_window

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="0">
<!-- crossovered.budget rules -->
<record id="budget_project_manager_rule" model="ir.rule">
<field name="name">Budget Project Manager rule</field>
<field name="model_id" ref="model_crossovered_budget"/>
<field name="groups" eval="[(4, ref('project.group_project_manager'))]"/>
<field name="domain_force">
['|',('project_id.parent_id.user_id','child_of',[user.id]),('project_user_id',
'=', user.id)]
</field>
</record>
<record id="budget_department_rule" model="ir.rule">
<field name="name">Budget department manager rule</field>
<field name="model_id" ref="model_crossovered_budget"/>
<field name="groups" eval="[(4, ref('project_base.group_project_department_manager'))]"/>
<field name="domain_force">['|',('department_id.manager_id.user_id','child_of', [user.id]),
('department_id.parent_id.manager_id.user_id','child_of', [user.id])]
</field>
</record>
<record model="ir.rule" id="budget_group_account_manager_rule">
<field name="name">Budget: account manager: see all</field>
<field name="model_id" ref="model_crossovered_budget"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4,ref('account.group_account_manager')),(4, ref('base.group_erp_manager'))]"/>
</record>
</odoo>

View File

@ -0,0 +1,6 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_crossovered_budget_project,crossovered.budget.project,model_crossovered_budget,project.group_project_user,1,1,1,0
access_budget_line_project,crossovered.budget.lines.project,model_crossovered_budget_lines,project.group_project_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_crossovered_budget_project crossovered.budget.project model_crossovered_budget project.group_project_user 1 1 1 0
3 access_budget_line_project crossovered.budget.lines.project model_crossovered_budget_lines project.group_project_user 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -0,0 +1,343 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- project form view inherit -->
<record id="project_project_financial_form" model="ir.ui.view">
<field name="name">project.project.form</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project_base.view_edit_project_inherit_form"/>
<field name="arch" type="xml">
<div class="oe_button_box" position="inside">
<button class="oe_stat_button" type="object"
name="action_view_budget" string="Budget" icon="fa-balance-scale">
</button>
</div>
<xpath expr="//page[@name='invoice']" position="after">
<page name="financial" string="Financials">
<group>
<group string="Bill Info">
<field name="sale_order_amount" string="Contract Amount" readonly="0"/>
<field name="total_invoiced_amount" string="Billed Amount"/>
<field name="total_invoiced_payment" string="Received Amount"/>
<!-- <field name="back_log_amount" string="Remaining of Contract"/> -->
</group>
<group string="Cost Info" attrs="{'invisible': [('man_hours','=',True)]}">
<field name="project_cost"/>
<field name="project_hours_cost"/>
<field name="project_total_cost"/>
<field name="gross_amount" style="font-size:20pt;" decoration-danger="gross_amount &lt; 0.0" decoration-success="gross_amount &gt; 0.0"/>
</group>
</group>
</page>
</xpath>
<xpath expr="//field[@name='active']" position="after">
<!-- <field name="resource_calendar_id" attrs="{'readonly':[('state','not in','draft')]}"/>-->
<!-- <field name="type" attrs="{'readonly':[('state','not in','draft')]}"/>-->
</xpath>
<xpath expr="//group[1]/group[1]" position="before">
<field name="ready_budget" invisible="1"/>
<div attrs="{'invisible': [('ready_budget', '=', True)]}">
<span attrs="{'invisible': ['|','|',('ready_budget', '=', True),('project_phase_ids','!=',[]),('id','=',False)]}" class="badge badge-warning">
The Project Budget Must be Approved First in order to Enter Stages
</span>
</div>
</xpath>
<xpath expr="//field[@name='project_phase_ids']" position="attributes">
<attribute name="attrs">{'readonly':['|',('state','not in',['draft','confirm']),('ready_budget','!=',True)]}</attribute>
</xpath>
</field>
</record>
<!--Tree and form view for Project Budget Custom-->
<record id="project_budget_view_tree" model="ir.ui.view">
<field name="name">project_budget_view_tree</field>
<field name="model">crossovered.budget</field>
<field name="mode">primary</field>
<field name="arch" type="xml">
<tree string="project_budget_tree">
<field name="name"/>
<field name="date_from"/>
<field name="date_to"/>
<field name="user_id"/>
<field name="project_id"/>
<field name="sale_order_id"/>
<field name="state"/>
</tree>
</field>
</record>
<record id="crossovered_budget_line_log_tree" model="ir.ui.view">
<field name="name">crossovered.budget.line.log</field>
<field name="model">crossovered.budget.lines</field>
<field name="arch" type="xml">
<tree string="Budget Lines">
<field name="general_budget_id"/>
<field name="analytic_account_id" groups="analytic.group_analytic_accounting"/>
<field name="date_from"/>
<field name="date_to"/>
<field name="qty" string="Qty"/>
<field name="amount" string="Unit Cost"/>
<field name="project_planned_amount" sum="Budget Amount"/>
<field name="practical_amount" sum="Practical Amount"/>
<field name="remain" sum="remain" invisible="1"/>
<field name="project_remaining_amount" sum="project_remain"/>
</tree>
</field>
</record>
<record id="crossovered_budget_line_log_form" model="ir.ui.view">
<field name="name">crossovered.budget.line.log</field>
<field name="model">crossovered.budget.lines</field>
<field name="arch" type="xml">
<form string="Budget Lines" create="0" edit="0">
<sheet>
<group>
<group>
<field name="currency_id" invisible="1"/>
<field name="general_budget_id" string="Budget Item"/>
<field name="project_planned_amount" force_save="1"/>
<field name="analytic_account_id"
domain="['|', ('company_id', '=', parent.company_id), ('company_id', '=', False)]"
groups="analytic.group_analytic_accounting"/>
<field name="project_id"/>
</group>
<group>
<field name="date_from" />
<field name="date_to" />
</group>
</group>
</sheet>
<!-- Chatter -->
<div class="oe_chatter">
<field name="message_follower_ids" groups="base.group_user"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
<record id="project_budget_custom_form" model="ir.ui.view">
<field name="name">project_budget_custom.form</field>
<field name="model">crossovered.budget</field>
<field name="inherit_id" ref="odex25_account_budget.crossovered_budget_view_form"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//button[@name='action_budget_confirm'][1]" position="attributes">
<attribute name="groups">project.group_project_manager</attribute>
</xpath>
<xpath expr="//button[@name='action_budget_validate']" position="attributes">
<attribute name="groups">project_base.group_project_department_manager</attribute>
</xpath>
<xpath expr="//button[@name='action_budget_done']" position="after">
<button string="Edit" name="action_budget_edit" states="done" type="object"
groups="project.group_project_manager" confirm="Are you sure you want to edit the Budget?"/>
</xpath>
<xpath expr="//button[@name='action_budget_done']" position="attributes">
<attribute name="groups">account.group_account_manager</attribute>
</xpath>
<xpath expr="//field[@name='state']" position="replace">
<field name="state" widget="statusbar" statusbar_visible="draft,confirm,validate,done"/>
</xpath>
<xpath expr="//label[@for='date_from']" position="replace">
</xpath>
<xpath expr="//form/sheet/group[1]/group[2]/div" position="replace">
<field name="date_from" class="oe_inline" attrs="{'readonly':[('state','!=','draft')]}"/>
<field name="date_to" class="oe_inline" attrs="{'readonly':[('state','!=','draft')]}"/>
</xpath>
<xpath expr="//field[@name='company_id']" position="after">
<field name="analytic_account_id"/>
</xpath>
<xpath expr="//form/sheet/div[1]" position="before">
<div class="oe_button_box" name="button_box">
<button name="open_sale_order" type="object" class="oe_stat_button" icon="fa-money">
<field name="sale_order_id" widget="statinfo" string="Sale Order"/>
</button>
</div>
</xpath>
<!-- <xpath expr="//field[@name='fiscalyear_id']" position="attributes">
<attribute name="required">0</attribute>
<attribute name="invisible">1</attribute>
</xpath> -->
<xpath expr="//label[@for='name']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='name']" position="attributes">
<attribute name="required">0</attribute>
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='user_id']" position="after">
<field name="project_id" required='1' attrs="{'readonly':[('state','!=','draft')]}"/>
<field name="project_no"/>
<field name="project_user_id"/>
<field name="man_hours" invisible="1"/>
<!-- <field name="fiscalyear_id" required="0" invisible="1"/> -->
<field name="sale_order_amount"/>
</xpath>
<xpath expr="//field[@name='crossovered_budget_line']" position="replace">
<!-- type Man hours -->
<!-- <field name="crossovered_budget_line" attrs="{'invisible':[('man_hours','=', False)]}"></field>-->
<separator string="Man-Hours" attrs="{'invisible':[('man_hours','=',False)]}"/>
<field name="hour_line_ids"
context="{'default_date_from': date_from,'default_date_to': date_to,'default_analytic_account_id':analytic_account_id,'default_crossovered_budget_id':id}"
widget="section_and_note_one2many" attrs="{'readonly':[('state','!=','draft')],'invisible':[('man_hours','=',False)]}">
<tree string="Budget Lines" editable="bottom">
<field name="general_budget_id" string="Budget Item"
required="1"
domain="[('is_timesheet_hours', '=', True)]"/>
<field name="analytic_account_id"
invisible="1"
domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"
groups="analytic.group_analytic_accounting"/>
<field name="date_from" />
<field name="date_to" />
<field name="currency_id" invisible="1"/>
<field name="qty" string="Hours"/>
<field name="amount" string="Hour Cost"/>
<field name="planned_amount" sum="Planned Amount" readonly="1" force_save="1" invisible="1"/>
<field name="project_planned_amount" sum="Budget Hours" string="Budget Hours" readonly="1" force_save="1"/>
<field name="timesheet_hours" sum="Timesheet hours" readonly="1" force_save="1"/>
<field name="practical_amount" string="Actual Amount" sum="Actual Amount" force_save="1"/>
<field name="remain" sum="remain" invisible="1"/>
<field name="project_remaining_amount" sum="project_remain"/>
<button type="object" name="open_line" string="Log"
icon="fa-list"/>
</tree>
<form string="Budget Lines">
<group>
<group>
<field name="currency_id" invisible="1"/>
<field name="general_budget_id" string="Budget Item"/>
<field name="planned_amount" force_save="1" invisible="1"/>
<field name="project_planned_amount" readonly="1" force_save="1"/>
<field name="analytic_account_id"
domain="['|', ('company_id', '=', parent.company_id), ('company_id', '=', False)]"
groups="analytic.group_analytic_accounting"/>
<field name="project_id"/>
</group>
<group>
<label for="date_from" string="Period"/>
<div>
<field name="date_from" class="oe_inline"/>
-
<field name="date_to" class="oe_inline"/>
</div>
<field name="company_id" options="{'no_create': True}"
groups="base.group_multi_company"/>
</group>
</group>
</form>
</field>
<!-- type Other Expensess -->
<separator string="Others Expensess" />
<field name="expensess_line_ids"
context="{'default_date_from': date_from,'default_date_to': date_to,'default_analytic_account_id':analytic_account_id,'default_crossovered_budget_id':id}"
widget="section_and_note_one2many" attrs="{'readonly':[('state','!=','draft')],}">
<tree string="Budget Lines" editable="bottom">
<field name="general_budget_id" string="Budget Item"
required="1"
domain="[('is_timesheet_hours', '=', False)]"/>
<field name="analytic_account_id"
invisible="1"
domain="['|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]"
groups="analytic.group_analytic_accounting"/>
<field name="date_from" />
<field name="date_to"/>
<field name="currency_id" invisible="1"/>
<field name="qty" string="Percentage"/>
<field name="amount" string="Project Amount"/>
<field name="planned_amount" sum="Planned Amount" readonly="1" force_save="1" invisible="1"/>
<field name="project_planned_amount" sum="Budget Amount" readonly="1" force_save="1"/>
<field name="practical_amount" string="Actual Amount" sum="Actual Amount"/>
<field name="remain" sum="remain" invisible="1"/>
<field name="project_remaining_amount" sum="project_remain"/>
<button type="object" name="open_line" string="Log"
icon="fa-list"/>
</tree>
<form string="Budget Lines">
<group>
<group>
<field name="currency_id" invisible="1"/>
<field name="general_budget_id" string="Budget Item"/>
<field name="planned_amount" force_save="1" invisible="1"/>
<field name="project_planned_amount" sum="Budget Amount" readonly="1" force_save="1"/>
<field name="analytic_account_id"
domain="['|', ('company_id', '=', parent.company_id), ('company_id', '=', False)]"
groups="analytic.group_analytic_accounting"/>
<field name="project_id"/>
</group>
<group>
<label for="date_from" string="Period"/>
<div>
<field name="date_from" class="oe_inline"/>
-
<field name="date_to" class="oe_inline"/>
</div>
<field name="company_id" options="{'no_create': True}"
groups="base.group_multi_company"/>
</group>
</group>
</form>
</field>
<group>
<label for="projected_profit" class="oe_inline"/>
<div>
<field name="projected_profit" class="oe_inline"/>
</div>
</group>
<group class="oe_subtotal_footer oe_right" name="budget_total">
<field name="total_budget_cost" class="oe_subtotal_footer_separator"/>
</group>
</xpath>
</field>
</record>
<!--Tree and form view for Account Budget Post Custom-->
<record id="project_budget_post_form" model="ir.ui.view">
<field name="name">budgetary.positions.form</field>
<field name="model">account.budget.post</field>
<field name="inherit_id" ref="odex25_account_budget.view_budget_post_form"/>
<field name="arch" type="xml">
<field name="company_id"/>
<xpath expr="//field[@name='company_id']" position="after">
<field name="is_timesheet_hours" widget="boolean_toggle"/>
</xpath>
</field>
</record>
<!--Project Budget Custom Action-->
<record model="ir.actions.act_window" id="project_budget_custom_action">
<field name="name">Budgets</field>
<field name="res_model">crossovered.budget</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('project_id','!=',False)]</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree','view_id':ref('project_budget.project_budget_view_tree')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('project_budget.project_budget_custom_form')})]"/>
</record>
<record id="crossovered_budget_line_project_pivot" model="ir.ui.view">
<field name="name">project_budget_view_pivot</field>
<field name="model">crossovered.budget.lines</field>
<field name="arch" type="xml">
<pivot string="Budget Lines" sample="1">
<field name="general_budget_id" string="Budget Item" type="row"/>
<field name="project_planned_amount" type="measure" string="Budget Amount"/>
<field name="practical_amount" type="measure" string="Actual Amount"/>
</pivot>
</field>
</record>
<!--Project Budget Menu-->
<menuitem id="project_budget_custom_main_menu" name="Budgets"
parent="project.menu_main_pm" groups="account.group_account_manager,project.group_project_manager,project_base.group_project_department_manager" action="project_budget_custom_action" sequence="2"/>
<!-- <menuitem id="project_budget_custom_menu" name="Budgets"
parent="project_budget_custom_main_menu"
action="project_budget_custom_action" sequence="1"/>
-->
</odoo>

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import models

View File

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
{
'name': "Odex25 Project Customer Team",
'summary': """Odex25 Project Customer Team""",
'description': """
Add Customer team details in project
""",
'category': 'Odex25-Project/Odex25-Project',
'version': '1.0',
'depends': ['project_base'],
'data': [
'security/ir.model.access.csv',
'views/project_views.xml',
],
}

View File

@ -0,0 +1,92 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * project_customer_team
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-05-12 12:13+0000\n"
"PO-Revision-Date: 2024-05-12 12:13+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_customer_team
#: model:ir.model.fields,field_description:project_customer_team.field_project_customer_team__create_uid
msgid "Created by"
msgstr ""
#. module: project_customer_team
#: model:ir.model.fields,field_description:project_customer_team.field_project_customer_team__create_date
msgid "Created on"
msgstr ""
#. module: project_customer_team
#: model_terms:ir.ui.view,arch_db:project_customer_team.view_edit_project_inherit2_form
msgid "Customer Team Info"
msgstr "فريق الجهة المنفذة"
#. module: project_customer_team
#: model:ir.model.fields,field_description:project_customer_team.field_project_customer_team__display_name
msgid "Display Name"
msgstr ""
#. module: project_customer_team
#: model:ir.model.fields,field_description:project_customer_team.field_project_customer_team__email
msgid "E-mail"
msgstr "الايميل"
#. module: project_customer_team
#: model:ir.model.fields,field_description:project_customer_team.field_project_customer_team__name
msgid "Employee"
msgstr "الموظف"
#. module: project_customer_team
#: model:ir.model.fields,field_description:project_customer_team.field_project_customer_team__id
msgid "ID"
msgstr ""
#. module: project_customer_team
#: model:ir.model.fields,field_description:project_customer_team.field_project_customer_team__job
msgid "Job In Project"
msgstr "الوظيفة"
#. module: project_customer_team
#: model:ir.model.fields,field_description:project_customer_team.field_project_customer_team____last_update
msgid "Last Modified on"
msgstr ""
#. module: project_customer_team
#: model:ir.model.fields,field_description:project_customer_team.field_project_customer_team__write_uid
msgid "Last Updated by"
msgstr ""
#. module: project_customer_team
#: model:ir.model.fields,field_description:project_customer_team.field_project_customer_team__write_date
msgid "Last Updated on"
msgstr ""
#. module: project_customer_team
#: model:ir.model.fields,field_description:project_customer_team.field_project_customer_team__phone
msgid "Phone"
msgstr "الهاتف"
#. module: project_customer_team
#: model:ir.model,name:project_customer_team.model_project_project
#: model:ir.model.fields,field_description:project_customer_team.field_project_customer_team__project_id
msgid "Project"
msgstr "المشروع"
#. module: project_customer_team
#: model:ir.model.fields,field_description:project_customer_team.field_project_project__project_customer_team_ids
msgid "Project Customer Team"
msgstr ""
#. module: project_customer_team
#: model:ir.model,name:project_customer_team.model_project_customer_team
msgid "project.customer.team"
msgstr ""

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import project

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
class Project(models.Model):
_inherit = "project.project"
project_customer_team_ids = fields.One2many('project.customer.team','project_id',string="Project Customer Team")
class ProjectTeam(models.Model):
_name = "project.customer.team"
name = fields.Char(string="Employee")
project_id = fields.Many2one('project.project',string="Project")
phone = fields.Char(string="Phone")
email = fields.Char(string="E-mail")
job = fields.Char(string="Job In Project")

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_project_customer_team,project.customer.team,model_project_customer_team,project.group_project_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_project_customer_team project.customer.team model_project_customer_team project.group_project_user 1 1 1 1

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Project Project -->
<record id="view_edit_project_inherit2_form" model="ir.ui.view">
<field name="name">project.project.view.inherit</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project_base.view_edit_project_inherit_form"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='team']" position="after">
<page name="customer_team" string="Customer Team Info">
<field name="project_customer_team_ids" nolabel="1">
<tree editable="top">
<field name="name" required="1"/>
<field name="phone" widget="phone" required="1" optional="show"/>
<field name="email" widget="email" context="{'gravatar_image': True}" required="1" optional="show"/>
<field name="job" required="1" optional="show"/>
<field name="project_id" invisible="1"/>
</tree>
</field>
</page>
</xpath>
</field>
</record>
</odoo>

Some files were not shown because too many files have changed in this diff Show More