project modifications

This commit is contained in:
karuna 2026-05-19 12:08:41 +05:30
parent f6bfd46f2c
commit c2e33753bb
17 changed files with 380 additions and 345 deletions

View File

@ -6,7 +6,7 @@
'version': '1.0',
'category': 'Accounting/Localizations/Point of Sale',
'description': """
This add-on brings the technical requirements of the French regulation CGI art. 286, I. 3° bis that stipulates certain criteria concerning the inalterability, security, storage and archiving of data related to sales to private individuals (B2C).
This add-on brings the technical requirements of the French regulation CGI art. 286, I. 3° bis that stipulates certain criteria concerning the inalterability,security, storage and archiving of data related to sales to private individuals (B2C).
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Install it if you use the Point of Sale app to sell to individuals.

View File

@ -24,7 +24,7 @@
},
'installable': True,
'data': [
# security.xml first, data.xml need the group to exist (checking it)
#security.xml first, data.xml need the group to exist (checking it)
'security/website_security.xml',
'security/ir.model.access.csv',
'data/image_library.xml',

View File

@ -49,9 +49,9 @@ Key Features:
'view/maintenance_support.xml',
'view/project_closer.xml',
'view/project_actual_costings.xml',
'view/project_task.xml',
'view/project.xml',
'view/project_portfolio.xml',
'view/project_task.xml',
'view/timesheets.xml',
'view/pro_task_gantt.xml',
'view/user_availability.xml',
@ -61,9 +61,10 @@ Key Features:
'view/stage_approval_wizard.xml',
],
'assets': {
'web.assets_backend':{
'project_task_timesheet_extended/static/src/css/delopyment.css'
}
'web.assets_backend': [
'project_task_timesheet_extended/static/src/css/delopyment.css',
'project_task_timesheet_extended/static/src/js/involved_assignee_avatar_user_field.js',
]
},
'installable': True,
'application': False,

View File

@ -263,4 +263,5 @@
</record>
</data>
<function model="project.task" name="_sync_all_involved_assignees_from_timelines"/>
</odoo>

View File

@ -120,14 +120,18 @@ def post_init_hook(env):
'|',
'&',
('task_id.is_generic', '=', False),
'|',
('user_id', 'in', 'task_id.user_ids'),
('user_id', 'in', 'task_id.involved_user_ids'),
'&',
('task_id.is_generic', '=', True),
('user_id.partner_id', 'in', 'project_id.message_partner_ids'),
'&', '&',
('project_id.privacy_visibility', '!=', 'followers'),
('task_id.is_generic', '=', False),
('user_id', 'in', 'task_id.user_ids')
'|',
('user_id', 'in', 'task_id.user_ids'),
('user_id', 'in', 'task_id.involved_user_ids')
]
"""
})

View File

@ -333,8 +333,6 @@ class ProjectProject(models.Model):
users_list = list()
if project.assign_approval_flow:
users_list.extend(project.project_stages.involved_users.ids)
else:
users_list.extend(project.showable_stage_ids.user_ids.ids)
if project.project_sponsor:
users_list.append(project.project_sponsor.id)

View File

@ -167,12 +167,12 @@ class projectTask(models.Model):
task.assignee_domain_ids = all_internal_users
continue
# # GENERIC all internal
# # GENERIC: all internal
# if getattr(task, 'is_generic', False):
# task.assignee_domain_ids = all_internal_users
# continue
# PRIVATE invited users only
# PRIVATE: invited users only
if task.project_id.privacy_visibility == 'followers':
task.assignee_domain_ids = (
task.project_id.message_partner_ids
@ -196,18 +196,18 @@ class projectTask(models.Model):
for task in self:
employees = Employee.browse()
# 1GENERIC TASK
# GENERIC TASK
if task.is_generic and task.project_id:
project = task.project_id
# 🔐 Private → followers only
# Private: followers only
if project.privacy_visibility == 'followers':
users = (
project.message_partner_ids
.mapped('user_ids')
.filtered(lambda u: u and not u.share)
)
# 🌍 Internal / Public → all internal users
# Internal / Public: all internal users
else:
users = self.env['res.users'].search([
('share', '=', False),
@ -216,7 +216,7 @@ class projectTask(models.Model):
employees = users.mapped('employee_id').filtered(lambda e: e)
# 2⃣ NORMAL TASK → task assignees only
# NORMAL TASK: assignees and involved collaborators
else:
employees = (
task.user_ids
@ -393,7 +393,7 @@ class projectTask(models.Model):
if start_dt.tzinfo is None:
start_dt = pytz.UTC.localize(start_dt)
# Convert UTC calendar timezone
# Convert UTC to calendar timezone
start_dt_tz = start_dt.astimezone(tz)
# Call plan_hours
@ -460,7 +460,7 @@ class projectTask(models.Model):
task.suggested_deadline.strftime('%Y-%m-%d %H:%M') if task.suggested_deadline else _('Not available')
))
@api.depends("project_id", "stage_id")
@api.depends("project_id")
def _compute_has_supervisor_access(self):
administrative_users = self.env['project.role'].search([
('role_level', '=', 'administrative')
@ -478,26 +478,13 @@ class projectTask(models.Model):
stages = project.type_ids.sorted("sequence")
if not stages:
continue
if first_stage:
create_access_users = first_stage.team_id.team_lead + first_stage.involved_user_ids + administrative_users.user_ids
else:
create_access_users = administrative_users.user_ids
first_stage = stages[0]
create_access_users = (
first_stage.team_id.team_lead
+ first_stage.involved_user_ids
+ administrative_users
)
if (
current_user.has_group("project.group_project_manager")
or current_user == project.user_id
or current_user == project.project_lead
or (
current_user in create_access_users
and task.stage_id == first_stage
)
):
if current_user.has_group("project.group_project_manager") or current_user == task.project_id.user_id or current_user == task.project_id.project_lead or (current_user.id in list(set(create_access_users.ids)) and task.stage_id.id == first_stage.id):
task.has_supervisor_access = True
@api.depends('assignees_timelines.estimated_time', 'show_approval_flow')
@ -637,11 +624,11 @@ class projectTask(models.Model):
task.show_approval_button = True
task.show_refuse_button = True # both approve & refuse in review state
# b) No assigned user directly approvable
# b) No assigned user: directly approvable
elif not assigned_to and (responsible_lead == user or project_manager == user):
task.show_approval_button = True
# c) Assigned_to == responsible_lead no submission needed, direct approve
# c) Assigned_to == responsible_lead: no submission needed, direct approve
elif (
assigned_to
and assigned_to == responsible_lead
@ -797,7 +784,7 @@ class projectTask(models.Model):
task.stage_id = n_stage
task.approval_status = "approved"
activity_log = "%s: approved by %s and moved to %s" % (
activity_log = "%s: approved by %s and moved to %s" % (
current_stage.name,
self.env.user.employee_id.name,
n_stage.name)
@ -838,9 +825,9 @@ class projectTask(models.Model):
)
else:
task.approval_status = "approved"
notes = "%s: Task approved and completed by %s" % (task.sequence_name, self.env.user.employee_id.name)
notes = "%s: Task approved and completed by %s" % (task.sequence_name, self.env.user.employee_id.name)
activity_log = "%s: approved by %s" % (
activity_log = "%s: approved by %s" % (
current_stage.name,
self.env.user.employee_id.name)
@ -876,9 +863,9 @@ class projectTask(models.Model):
# Optional: find previous stage if you want to send back
stage = task.assignees_timelines.filtered(lambda s: s.stage_id == task.stage_id)
notes = "%s: %s rejected by %s" % (task.sequence_name, current_stage.name, self.env.user.employee_id.name)
notes = "%s: %s rejected by %s" % (task.sequence_name, current_stage.name, self.env.user.employee_id.name)
activity_log = "%s: rejected by %s: %s" % (
activity_log = "%s: rejected by %s: %s" % (
current_stage.name,
self.env.user.employee_id.name,
reason)
@ -1017,19 +1004,27 @@ class projectTask(models.Model):
"You are not allowed to change the stage of this task because stage editing is restricted."
))
return super(projectTask, self).write(vals)
result = super(projectTask, self).write(vals)
if any(field in vals for field in ['allocation_start_date', 'allocation_end_date']):
self._sync_allocated_hours_from_allocation_dates()
if any(field in vals for field in ['user_ids', 'is_generic']):
self._sync_involved_assignees_from_timelines()
return result
def button_update_assignees(self):
def _sync_involved_assignees_from_timelines(self):
for task in self:
if task.assignees_timelines:
users_list = list(
set(task.assignees_timelines.responsible_lead.ids + task.assignees_timelines.assigned_to.ids + task.assignees_timelines.team_id.team_lead.ids))
task.user_ids = [(6, 0, users_list)]
if task.is_generic:
continue
# Post to project channel about assignee update
channel_message = _("Assignees updated for task %s") % (task.sequence_name or task.name)
task._post_to_project_channel(channel_message)
timeline_user_ids = set(
task.assignees_timelines.responsible_lead.ids
+ task.assignees_timelines.assigned_to.ids
+ task.assignees_timelines.team_id.team_lead.ids
)
existing_user_ids = set(task.involved_user_ids.ids)
involved_users = list((existing_user_ids | timeline_user_ids) - set(task.user_ids.ids))
task.involved_user_ids = [(6, 0, involved_users)]
def _fetch_planning_overlap(self, additional_domain=None):
use_timeline_logic = any(

View File

@ -0,0 +1,34 @@
/** @odoo-module **/
import {
Many2ManyTagsAvatarUserField,
many2ManyTagsAvatarUserField,
} from "@mail/views/web/fields/many2many_avatar_user_field/many2many_avatar_user_field";
import { registry } from "@web/core/registry";
export class InvolvedAssigneeAvatarUserField extends Many2ManyTagsAvatarUserField {
getDomain() {
const involved = this.props.record.data.involved_user_ids;
const involvedIds = involved?.records?.map((record) => record.resId).filter(Boolean) || [];
if (involvedIds.length) {
return [["id", "in", involvedIds]];
}
return [["id", "=", false]];
}
}
export const involvedAssigneeAvatarUserField = {
...many2ManyTagsAvatarUserField,
component: InvolvedAssigneeAvatarUserField,
extractProps(fieldInfo, dynamicInfo) {
const props = many2ManyTagsAvatarUserField.extractProps(fieldInfo, dynamicInfo);
return {
...props,
canCreate: false,
canQuickCreate: false,
canCreateEdit: false,
};
},
};
registry.category("fields").add("involved_assignee_avatar_user", involvedAssigneeAvatarUserField);

View File

@ -41,29 +41,31 @@
[('id', 'in', parent.allowed_employee_ids)]
</attribute>
</xpath>
<!-- <xpath expr="//field[@name='timesheet_ids']//field[@name='stage_id']"-->
<!-- position="attributes">-->
<!-- <attribute name="domain">-->
<!-- [('assigned_user_ids.employee_id', '=', employee_id)]-->
<!-- </attribute>-->
<!-- </xpath>-->
<!-- <xpath expr="//field[@name='timesheet_ids']//field[@name='stage_id']"-->
<!-- position="attributes">-->
<!-- <attribute name="domain">-->
<!-- [('assigned_user_ids.employee_id', '=', employee_id)]-->
<!-- </attribute>-->
<!-- </xpath>-->
<xpath expr="//div[hasclass('oe_title','pe-0')]" position="after">
<group>
<h1><field name="sequence_name" readonly="1"/></h1>
</group>
</xpath>
<!-- <xpath expr="//field[@name='user_ids']" position="before">-->
<!-- <field name="assigned_team"/>-->
<!-- </xpath>-->
<!-- <xpath expr="//field[@name='user_ids']" position="before">-->
<!-- <field name="assigned_team"/>-->
<!-- </xpath>-->
<xpath expr="//field[@name='user_ids']" position="after">
<field name="is_generic" readonly="not has_supervisor_access"/>
<field name="record_paused" invisible="1"/>
<field name="model_id" readonly="not has_supervisor_access" options="{'no_open': True}"/>
</xpath>
<!-- <xpath expr="//field[@name='allocated_hours']" position="after">-->
<!-- <field name="estimated_hours"/>-->
<!-- <field name="actual_hours"/>-->
<!-- </xpath>-->
<!-- <xpath expr="//field[@name='allocated_hours']" position="after">-->
<!-- <field name="estimated_hours"/>-->
<!-- <field name="actual_hours"/>-->
<!-- </xpath>-->
<xpath expr="//sheet/notebook" position="inside">
<page string="Assignees Timelines" invisible="not show_approval_flow">
@ -143,16 +145,17 @@
</div>
<!-- <div>-->
<!-- <field name="suggested_deadline" invisible="not suggested_deadline or not show_approval_flow or not timelines_requested"/>-->
<!-- <field name="is_suggested_deadline_warning" invisible="1"/>-->
<!-- <label for="suggested_deadline"-->
<!-- class="text-warning"-->
<!-- invisible="not is_suggested_deadline_warning">-->
<!-- ⚠ Based on the timelines, the deadline can't be met-->
<!-- </label>-->
<!-- </div>-->
<field name="estimated_hours" widget="timesheet_uom_no_toggle" readonly="show_approval_flow and timelines_requested"/>
<!-- <div>-->
<!-- <field name="suggested_deadline" invisible="not suggested_deadline or not show_approval_flow or not timelines_requested"/>-->
<!-- <field name="is_suggested_deadline_warning" invisible="1"/>-->
<!-- <label for="suggested_deadline"-->
<!-- class="text-warning"-->
<!-- invisible="not is_suggested_deadline_warning">-->
<!-- Based on the timelines, the deadline can't be met -->
<!-- </label>-->
<!-- </div>-->
<field name="estimated_hours" widget="timesheet_uom_no_toggle"
readonly="show_approval_flow and timelines_requested"/>
<field name="actual_hours" widget="timesheet_uom_no_toggle"/>
<field name="is_suggested_deadline_warning" />
</xpath>

View File

@ -34,7 +34,7 @@
<!-- </div>-->
<!-- <div name="project_id">-->
<!-- <strong>Project </strong>-->
<!-- <strong>Project —</strong>-->
<!-- <t t-if="project_id" t-esc="project_id[1]"/>-->
<!-- <t t-else="">-->
<!-- <span class="fst-italic text-muted">-->
@ -45,7 +45,7 @@
<!-- </div>-->
<!-- <div t-if="show_approval_flow and current_stage_performance">-->
<!-- <strong>Performance </strong>-->
<!-- <strong>Performance —</strong>-->
<!-- <t t-if="current_stage_performance == 'good'">-->
<!-- <span class="text-success">-->
<!-- <i class="fa fa-smile-o"></i>-->
@ -67,21 +67,21 @@
<!-- </div>-->
<!-- <div t-if="allow_milestones and milestone_id" groups="project.group_project_milestone">-->
<!-- <strong>Milestone </strong>-->
<!-- <strong>Milestone —</strong>-->
<!-- <t t-esc="milestone_id[1]"/>-->
<!-- </div>-->
<!-- <div t-if="user_names">-->
<!-- <strong>Assignees </strong>-->
<!-- <strong>Assignees —</strong>-->
<!-- <t t-esc="user_names"/>-->
<!-- </div>-->
<!-- <div t-if="partner_id">-->
<!-- <strong>Customer </strong>-->
<!-- <strong>Customer —</strong>-->
<!-- <t t-esc="partner_id[1]"/>-->
<!-- </div>-->
<!-- <div t-if="show_approval_flow">-->
<!-- <strong>Timeline </strong>-->
<!-- <strong>Timeline —</strong>-->
<!-- <t t-esc="estimated_hours"/>-->
<!-- hours estimated /-->
<!-- <t t-esc="actual_hours"/>-->
@ -89,7 +89,7 @@
<!-- </div>-->
<!-- <div t-if="project_id" name="allocated_hours">-->
<!-- <strong>Allocated Time </strong>-->
<!-- <strong>Allocated Time —</strong>-->
<!-- <t t-esc="allocated_hours"/>-->
<!-- </div>-->
@ -201,7 +201,7 @@
<div class="mt-2">
<div name="project_id">
<strong>Project </strong>
<strong>Project —</strong>
<t t-if="project_id" t-esc="project_id[1]"/>
<t t-else="">
<span class="fst-italic text-muted">
@ -211,7 +211,7 @@
</div>
<div t-if="show_approval_flow and current_stage_performance and not is_completed" class="mt-1">
<strong>Performance </strong>
<strong>Performance —</strong>
<t t-if="current_stage_performance == 'good'">
<span class="text-success">
<i class="fa fa-smile-o"></i> Good (Actual &lt; Estimated)
@ -230,24 +230,24 @@
</div>
<div t-if="allow_milestones and milestone_id" groups="project.group_project_milestone" class="mt-1">
<strong>Milestone </strong> <t t-esc="milestone_id[1]"/>
<strong>Milestone —</strong> <t t-esc="milestone_id[1]"/>
</div>
<div t-if="user_names" class="mt-1">
<strong>Assignees </strong> <t t-esc="user_names"/>
<strong>Assignees —</strong> <t t-esc="user_names"/>
</div>
<div t-if="partner_id" class="mt-1">
<strong>Customer </strong> <t t-esc="partner_id[1]"/>
<strong>Customer —</strong> <t t-esc="partner_id[1]"/>
</div>
<div t-if="show_approval_flow and not is_completed" class="mt-1">
<strong>Timeline </strong>
<strong>Timeline —</strong>
<t t-esc="estimated_hours"/> hours estimated /
<t t-esc="actual_hours"/> hours actual
</div>
<div t-if="project_id" name="allocated_hours" class="mt-1">
<strong>Allocated Time </strong> <t t-esc="allocated_hours"/>
<strong>Allocated Time —</strong> <t t-esc="allocated_hours"/>
</div>
<div class="mt-2">
@ -302,7 +302,6 @@
<footer class="mt-3">
<button name="action_unschedule_task" type="object" string="Unschedule" class="btn btn-sm btn-secondary"/>
<button name="button_update_assignees" type="object" string="Update Assignees" class="btn btn-sm btn-primary"/>
</footer>
</div>

View File

@ -412,7 +412,7 @@ token.QWEB = token.NT_OFFSET - 1
token.tok_name[token.QWEB] = 'QWEB'
# security safe eval opcodes for generated expression validation, used in `_compile_expr`
#security safe eval opcodes for generated expression validation, used in `_compile_expr`
_SAFE_QWEB_OPCODES = _EXPR_OPCODES.union(to_opcodes([
'MAKE_FUNCTION', 'CALL_FUNCTION', 'CALL_FUNCTION_KW', 'CALL_FUNCTION_EX',
'CALL_METHOD', 'LOAD_METHOD',