Add odex25_project
This commit is contained in:
parent
3a3f834c54
commit
432ae2c303
|
|
@ -1,2 +1,2 @@
|
|||
# odex25-standard-moduless
|
||||
# odex25-standard-modules
|
||||
This Repo contains general standard modules for all projects.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
from . import reports
|
||||
|
||||
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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')
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
@ -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 < 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
|
|
@ -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'],
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 ""
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import odex25_helpdesk
|
||||
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
@ -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})
|
||||
|
|
@ -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'
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 "تحذير"
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import analytic
|
||||
from . import odex25_helpdesk
|
||||
from . import project
|
||||
|
|
@ -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')
|
||||
]
|
||||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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 |
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_project
|
||||
|
||||
|
|
@ -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")
|
||||
|
|
@ -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 > 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 > 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 > 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 && 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 && record.timer_start.raw_value">
|
||||
<i class="fa fa-play text-success" title="Timer is Running"></i>
|
||||
</t>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import odex25_helpdesk_ticket_create_timesheet
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
from . import wizard
|
||||
|
||||
|
|
@ -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',
|
||||
|
||||
],
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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 ''
|
||||
|
|
@ -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)]
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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')
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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="{"widget": "contact", "fields": ["address", "name"], "no_marker": 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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%;
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 < 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>
|
||||
|
|
@ -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 < 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>
|
||||
|
|
@ -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', '<', 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>
|
||||
|
|
@ -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="{"no_open": True, "no_create": 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import project_hold_reason
|
||||
from . import edit_project_phase
|
||||
from . import down_payment_invoice_advance
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
|
|
@ -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',
|
||||
],
|
||||
}
|
||||
|
|
@ -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 ""
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import account_budget
|
||||
from . import project
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
|
|
@ -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 < 0.0" decoration-success="gross_amount > 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>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
|
||||
|
||||
|
|
@ -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',
|
||||
|
||||
],
|
||||
}
|
||||
|
|
@ -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 ""
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import project
|
||||
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue