[FIX] project,sale_*: changes various issues in behaviour
This commit is mainly a revert to 13 functionalities in project_* modules as functional changes introduced in 14 do not seem to be well accepted by users. Details: - sale_timesheet: rename 'Ordered quantities' service_policy into 'Prepaid' project, sale_project, sale_timesheet: refactor task form view - sale_timesheet: modify sale_line_employee_ids many2one widget on project form - sale_timesheet: add link between ticket and sale order - sale_timesheet: remove the option 'link to an existing SOL' - sale_timesheet: remove the warning messages in project.task - sale_timesheet: reword labels in customer_type and pricing - sale_timesheet: adapt timesheet section on invoices in portal - sale_timesheet: add timesheet section on orders in portal - sale_timesheet: remove employee rate related warning on projects in Kanban view - sale_timesheet: update sale_line_id of all the not invoiced timesheets accordingly to the employee mappings - sale_timesheet: timesheet entries for employees that have no mapped SOL in project get the sol of the task or project task-2388500 closes odoo/odoo#62568 Related: odoo/enterprise#15040 Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
@@ -663,29 +663,28 @@
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="partner_id" class="o_task_customer_field"/>
|
||||
<field name="partner_email" widget="email" attrs="{'invisible': [('partner_id', '=', False)]}"/>
|
||||
<field name="partner_phone" widget="phone" attrs="{'invisible': [('partner_id', '=', False)]}"/>
|
||||
<field name="legend_blocked" invisible="1"/>
|
||||
<field name="legend_normal" invisible="1"/>
|
||||
<field name="legend_done" invisible="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="project_id" required="1" domain="[('active', '=', True), ('company_id', '=', company_id)]"/>
|
||||
<field name="user_id"
|
||||
class="o_task_user_field"
|
||||
domain="[('share', '=', False)]"/>
|
||||
<field
|
||||
name="parent_id"
|
||||
domain="[('parent_id', '=', False)]"
|
||||
attrs="{'invisible' : [('allow_subtasks', '=', False)]}"
|
||||
/>
|
||||
<field name="user_id"
|
||||
class="o_task_user_field"
|
||||
domain="[('share', '=', False)]"/>
|
||||
<field name="date_deadline" attrs="{'invisible': [('is_closed', '=', True)]}"/>
|
||||
<field name="recurring_task" attrs="{'invisible': [('allow_recurring_tasks', '=', False)]}" />
|
||||
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="partner_id" class="o_task_customer_field"/>
|
||||
<field name="partner_phone" widget="phone" attrs="{'invisible': [('partner_id', '=', False)]}"/>
|
||||
<field name="legend_blocked" invisible="1"/>
|
||||
<field name="legend_normal" invisible="1"/>
|
||||
<field name="legend_done" invisible="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page name="description_page" string="Description">
|
||||
|
||||
@@ -15,7 +15,7 @@ class Project(models.Model):
|
||||
domain="[('is_expense', '=', False), ('order_id', '=', sale_order_id), ('state', 'in', ['sale', 'done']), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
||||
help="Sales order item to which the project is linked. Link the timesheet entry to the sales order item defined on the project. "
|
||||
"Only applies on tasks without sale order item defined, and if the employee is not in the 'Employee/Sales Order Item Mapping' of the project.")
|
||||
sale_order_id = fields.Many2one('sale.order', 'Sales Order', domain="[('partner_id', '=', partner_id)]", readonly=True, copy=False, help="Sales order to which the project is linked.")
|
||||
sale_order_id = fields.Many2one('sale.order', 'Sales Order', domain="[('partner_id', '=', partner_id)]", copy=False, help="Sales order to which the project is linked.")
|
||||
|
||||
_sql_constraints = [
|
||||
('sale_order_required_if_sale_line', "CHECK((sale_line_id IS NOT NULL AND sale_order_id IS NOT NULL) OR (sale_line_id IS NULL))", 'The project should be linked to a sale order to select a sale order item.'),
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
groups="sales_team.group_sale_salesman"/>
|
||||
</div>
|
||||
<xpath expr="//field[@name='partner_phone']" position="after">
|
||||
<field name="sale_line_id" string="Sales Order Item" attrs="{'invisible': ['|', ('partner_id', '=', False), '&', ('sale_order_id', '!=', False), ('sale_line_id', '=', False)]}" options='{"no_open": True}' readonly="1" context="{'create': False, 'edit': False, 'delete': False}" groups="sales_team.group_sale_salesman"/>
|
||||
<field name="sale_line_id" string="Sales Order Item" attrs="{'invisible': [('partner_id', '=', False)]}" options='{"no_open": True}' readonly="1" context="{'create': False, 'edit': False, 'delete': False}" groups="sales_team.group_sale_salesman"/>
|
||||
<field name="sale_order_id" attrs="{'invisible': [('project_id', '!=', False)]}" groups="sales_team.group_sale_salesman"/>
|
||||
<field name="commercial_partner_id" invisible="1" />
|
||||
<field name="project_sale_order_id" invisible="1"/>
|
||||
|
||||
@@ -431,7 +431,7 @@ msgstr ""
|
||||
#. module: sale_timesheet
|
||||
#: model:ir.model.fields,field_description:sale_timesheet.field_project_project__bill_type
|
||||
#: model:ir.model.fields,field_description:sale_timesheet.field_project_task__bill_type
|
||||
msgid "Customer Type"
|
||||
msgid "Invoice Tasks to"
|
||||
msgstr ""
|
||||
|
||||
#. module: sale_timesheet
|
||||
@@ -557,7 +557,7 @@ msgstr ""
|
||||
|
||||
#. module: sale_timesheet
|
||||
#: model:ir.model.fields.selection,name:sale_timesheet.selection__project_project__pricing_type__fixed_rate
|
||||
msgid "Fixed rate"
|
||||
msgid "Project rate"
|
||||
msgstr ""
|
||||
|
||||
#. module: sale_timesheet
|
||||
@@ -663,7 +663,7 @@ msgstr ""
|
||||
|
||||
#. module: sale_timesheet
|
||||
#: model:ir.model.fields.selection,name:sale_timesheet.selection__project_project__bill_type__customer_project
|
||||
msgid "Invoice all tasks to a single customer"
|
||||
msgid "A unique customer"
|
||||
msgstr ""
|
||||
|
||||
#. module: sale_timesheet
|
||||
@@ -673,7 +673,7 @@ msgstr ""
|
||||
|
||||
#. module: sale_timesheet
|
||||
#: model:ir.model.fields.selection,name:sale_timesheet.selection__project_project__bill_type__customer_task
|
||||
msgid "Invoice tasks separately to different customers"
|
||||
msgid "Different customers"
|
||||
msgstr ""
|
||||
|
||||
#. module: sale_timesheet
|
||||
@@ -920,7 +920,7 @@ msgstr ""
|
||||
|
||||
#. module: sale_timesheet
|
||||
#: model:ir.model.fields.selection,name:sale_timesheet.selection__product_template__service_policy__ordered_timesheet
|
||||
msgid "Ordered quantities"
|
||||
msgid "Prepaid"
|
||||
msgstr ""
|
||||
|
||||
#. module: sale_timesheet
|
||||
|
||||
@@ -123,6 +123,8 @@ class AccountAnalyticLine(models.Model):
|
||||
map_entry = self.env['project.sale.line.employee.map'].search([('project_id', '=', task.project_id.id), ('employee_id', '=', employee.id)])
|
||||
if map_entry:
|
||||
return map_entry.sale_line_id
|
||||
if task.sale_line_id or project.sale_line_id:
|
||||
return task.sale_line_id or project.sale_line_id
|
||||
return self.env['sale.order.line']
|
||||
|
||||
def _timesheet_get_portal_domain(self):
|
||||
@@ -139,7 +141,7 @@ class AccountAnalyticLine(models.Model):
|
||||
'|',
|
||||
'&',
|
||||
('timesheet_invoice_id', 'in', invoice_ids.ids),
|
||||
#TODO Question to Reviewer: non_billable was part of domain in _timesheet_get_portal_domain so I kept it here, does this make sense?
|
||||
# TODO : Master: Check if non_billable should be removed ?
|
||||
('timesheet_invoice_type', 'in', ['billable_time', 'non_billable']),
|
||||
'&',
|
||||
('timesheet_invoice_type', '=', 'billable_fixed'),
|
||||
|
||||
@@ -11,7 +11,7 @@ class ProductTemplate(models.Model):
|
||||
_inherit = 'product.template'
|
||||
|
||||
service_policy = fields.Selection([
|
||||
('ordered_timesheet', 'Ordered quantities'),
|
||||
('ordered_timesheet', 'Prepaid'),
|
||||
('delivered_timesheet', 'Timesheets on tasks'),
|
||||
('delivered_manual', 'Milestones (manually set quantities on order)')
|
||||
], string="Service Invoicing Policy", compute='_compute_service_policy', inverse='_inverse_service_policy')
|
||||
|
||||
@@ -25,12 +25,12 @@ class Project(models.Model):
|
||||
return self.env.ref('sale_timesheet.time_product', False)
|
||||
|
||||
bill_type = fields.Selection([
|
||||
('customer_task', 'Invoice tasks separately to different customers'),
|
||||
('customer_project', 'Invoice all tasks to a single customer')
|
||||
], string="Customer Type", default="customer_task",
|
||||
('customer_task', 'Different customers'),
|
||||
('customer_project', 'A unique customer')
|
||||
], string="Invoice Tasks to", default="customer_task",
|
||||
help='When billing tasks individually, a Sales Order will be created from each task. It is perfect if you would like to bill different services to different customers at different rates. \n When billing the whole project, a Sales Order will be created from the project instead. This option is better if you would like to bill all the tasks of a given project to a specific customer either at a fixed rate, or at an employee rate.')
|
||||
pricing_type = fields.Selection([
|
||||
('fixed_rate', 'Fixed rate'),
|
||||
('fixed_rate', 'Project rate'),
|
||||
('employee_rate', 'Employee rate')
|
||||
], string="Pricing", default="fixed_rate",
|
||||
help='The fixed rate is perfect if you bill a service at a fixed rate per hour or day worked regardless of the employee who performed it. The employee rate is preferable if your employees deliver the same service at a different rate. For instance, junior and senior consultants would deliver the same service (= consultancy), but at a different rate because of their level of seniority.')
|
||||
@@ -40,7 +40,7 @@ class Project(models.Model):
|
||||
allow_billable = fields.Boolean("Billable", help="Invoice your time and material from tasks.")
|
||||
display_create_order = fields.Boolean(compute='_compute_display_create_order')
|
||||
timesheet_product_id = fields.Many2one(
|
||||
'product.product', string='Timesheet Product',
|
||||
'product.product', string='Timesheet Product',
|
||||
domain="""[
|
||||
('type', '=', 'service'),
|
||||
('invoice_policy', '=', 'delivery'),
|
||||
@@ -83,7 +83,7 @@ class Project(models.Model):
|
||||
def _compute_warning_employee_rate(self):
|
||||
projects = self.filtered(lambda p: p.allow_billable and p.allow_timesheets and p.bill_type == 'customer_project' and p.pricing_type == 'employee_rate')
|
||||
tasks = projects.task_ids.filtered(lambda t: not t.non_allow_billable)
|
||||
employees = self.env['account.analytic.line'].read_group([('task_id', 'in', tasks.ids), ('non_allow_billable', '=', False)], ['employee_id', 'project_id'], ['employee_id', 'project_id'], lazy=False)
|
||||
employees = self.env['account.analytic.line'].read_group([('task_id', 'in', tasks.ids), ('non_allow_billable', '=', False)], ['employee_id', 'project_id'], ['employee_id', 'project_id'], ['employee_id', 'project_id'], lazy=False)
|
||||
dict_project_employee = defaultdict(list)
|
||||
for line in employees:
|
||||
dict_project_employee[line['project_id'][0]] += [line['employee_id'][0]]
|
||||
@@ -92,7 +92,6 @@ class Project(models.Model):
|
||||
|
||||
(self - projects).warning_employee_rate = False
|
||||
|
||||
|
||||
@api.constrains('sale_line_id', 'pricing_type')
|
||||
def _check_sale_line_type(self):
|
||||
for project in self:
|
||||
@@ -119,6 +118,19 @@ class Project(models.Model):
|
||||
})
|
||||
return res
|
||||
|
||||
def _get_not_billed_timesheets(self):
|
||||
return self.mapped('timesheet_ids').filtered(
|
||||
lambda t: not t.timesheet_invoice_id or t.timesheet_invoice_id.state == 'cancel')
|
||||
|
||||
def _update_timesheets_sale_line_id(self):
|
||||
for project in self.filtered(lambda p: p.allow_billable and p.allow_timesheets):
|
||||
timesheet_ids = project._get_not_billed_timesheets()
|
||||
if not timesheet_ids:
|
||||
continue
|
||||
for employee_id in project.sale_line_employee_ids.filtered(lambda l: l.project_id == project).employee_id:
|
||||
sale_line_id = project.sale_line_employee_ids.filtered(lambda l: l.project_id == project and l.employee_id == employee_id).sale_line_id
|
||||
timesheet_ids.filtered(lambda t: t.employee_id == employee_id).so_line = sale_line_id
|
||||
|
||||
def action_view_timesheet(self):
|
||||
self.ensure_one()
|
||||
if self.allow_timesheets:
|
||||
@@ -217,6 +229,7 @@ class ProjectTask(models.Model):
|
||||
'|', ('company_id', '=', False), ('company_id', '=', company_id)]""",
|
||||
help='Select a Service product with which you would like to bill your time spent on this task.')
|
||||
|
||||
# TODO: [XBO] remove me in master
|
||||
non_allow_billable = fields.Boolean("Non-Billable", help="Your timesheets linked to this task will not be billed.")
|
||||
|
||||
@api.depends(
|
||||
@@ -232,29 +245,13 @@ class ProjectTask(models.Model):
|
||||
|
||||
@api.onchange('sale_line_id')
|
||||
def _onchange_sale_line_id(self):
|
||||
if self._get_timesheet() and self.allow_timesheets:
|
||||
if self.sale_line_id:
|
||||
if self.sale_line_id.product_id.service_policy == 'delivered_timesheet' and self._origin.sale_line_id.product_id.service_policy == 'delivered_timesheet':
|
||||
message = _("All timesheet hours that are not yet invoiced will be assigned to the selected Sales Order Item on save. Discard to avoid the change.")
|
||||
else:
|
||||
message = _("All timesheet hours will be assigned to the selected Sales Order Item on save. Discard to avoid the change.")
|
||||
else:
|
||||
message = _("All timesheet hours that are not yet invoiced will be removed from the selected Sales Order Item on save. Discard to avoid the change.")
|
||||
|
||||
return {'warning': {
|
||||
'title': _("Warning"),
|
||||
'message': message
|
||||
}}
|
||||
# TODO: remove me in master
|
||||
return
|
||||
|
||||
@api.onchange('project_id')
|
||||
def _onchange_project_id(self):
|
||||
if self._origin.allow_timesheets and self._get_timesheet():
|
||||
message = _("All timesheet hours that are not yet invoiced will be assigned to the selected Project on save. Discard to avoid the change.")
|
||||
|
||||
return {'warning': {
|
||||
'title': _("Warning"),
|
||||
'message': message
|
||||
}}
|
||||
# TODO: remove me in master
|
||||
return
|
||||
|
||||
@api.depends('analytic_account_id.active')
|
||||
def _compute_analytic_account_active(self):
|
||||
|
||||
@@ -19,7 +19,7 @@ class ProjectProductEmployeeMap(models.Model):
|
||||
('invoice_policy', '=', 'delivery'),
|
||||
('service_type', '=', 'timesheet'),
|
||||
'|', ('company_id', '=', False), ('company_id', '=', company_id)]""")
|
||||
price_unit = fields.Float("Unit Price", compute='_compute_price_unit', store=True, readonly=False)
|
||||
price_unit = fields.Float("Unit Price", compute='_compute_price_unit', store=True, readonly=True)
|
||||
currency_id = fields.Many2one('res.currency', string="Currency", compute='_compute_price_unit', store=True, readonly=False)
|
||||
|
||||
_sql_constraints = [
|
||||
@@ -49,10 +49,13 @@ class ProjectProductEmployeeMap(models.Model):
|
||||
@api.model
|
||||
def create(self, values):
|
||||
res = super(ProjectProductEmployeeMap, self).create(values)
|
||||
for project_id in res.filtered(lambda l: l.sale_line_id).project_id:
|
||||
if project_id.allow_timesheets and project_id.allow_billable and project_id.task_ids._get_timesheet():
|
||||
timesheet_ids = project_id.task_ids._get_timesheet()
|
||||
for employee_id in res.filtered(lambda l: l.project_id == project_id).employee_id:
|
||||
sale_line_id = res.filtered(lambda l: l.project_id == project_id and l.employee_id == employee_id).sale_line_id
|
||||
timesheet_ids.filtered(lambda t: t.employee_id == employee_id).so_line = sale_line_id
|
||||
res._update_project_timesheet()
|
||||
return res
|
||||
|
||||
def write(self, values):
|
||||
res = super(ProjectProductEmployeeMap, self).write(values)
|
||||
self._update_project_timesheet()
|
||||
return res
|
||||
|
||||
def _update_project_timesheet(self):
|
||||
self.filtered(lambda l: l.sale_line_id).project_id._update_timesheets_sale_line_id()
|
||||
|
||||
@@ -7,19 +7,9 @@ var session = require('web.session');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
|
||||
var SaleProjectKanbanController = ProjectKanbanController.include({
|
||||
events: _.extend({}, ProjectKanbanController.prototype.events, {
|
||||
'click .o_create_sale_order': '_onCreateSaleOrder'
|
||||
}),
|
||||
// YTI TODO : Master remove file
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
willStart: async function () {
|
||||
const _super = this._super.bind(this);
|
||||
await this._showCreateSOButton();
|
||||
return _super(...arguments);
|
||||
},
|
||||
var SaleProjectKanbanController = ProjectKanbanController.include({
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
@@ -53,39 +43,6 @@ var SaleProjectKanbanController = ProjectKanbanController.include({
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {jQuery} [$node]
|
||||
*/
|
||||
renderButtons: function ($node) {
|
||||
this._super.apply(this, arguments);
|
||||
this.$saleButtons = $(QWeb.render('SaleProjectKanbanView.buttons'));
|
||||
var state = this.model.get(this.handle, {raw: true});
|
||||
var createHidden = this.is_action_enabled('group_create') && state.isGroupedByM2ONoColumn || !this.showCreateSaleOrder;
|
||||
this.$saleButtons.toggleClass('o_hidden', createHidden);
|
||||
this.$saleButtons.appendTo(this.$buttons);
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
updateButtons: async function () {
|
||||
if (!this.$buttons) {
|
||||
return;
|
||||
}
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
await this._showCreateSOButton();
|
||||
var state = this.model.get(this.handle, {raw: true});
|
||||
var createHidden = this.is_action_enabled('group_create') && state.isGroupedByM2ONoColumn || !this.showCreateSaleOrder;
|
||||
this.$buttons.find('.o_create_sale_order').toggleClass('o_hidden', createHidden);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
@@ -20,25 +20,23 @@
|
||||
<xpath expr="//page[@name='settings']" position="after">
|
||||
<page name="billing_employee_rate" string="Invoicing" attrs="{'invisible': [('allow_billable', '=', False)]}">
|
||||
<group>
|
||||
<field name="display_create_order" invisible="1"/>
|
||||
<field name="bill_type" widget="radio"/>
|
||||
<field name="pricing_type" attrs="{'invisible': ['|', ('allow_billable', '=', False), ('bill_type', '!=', 'customer_project')], 'required': ['&', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}" widget="radio"/>
|
||||
<div class="o_td_label" attrs="{'invisible': ['|', '|', ('allow_timesheets', '=', False), ('sale_order_id', '!=', False), '&', ('pricing_type', '!=', 'fixed_rate'), ('bill_type', '!=', 'customer_task')]}">
|
||||
<label for="timesheet_product_id" string="Default Service" attrs="{'invisible': [('bill_type', '!=', 'customer_task')]}"/>
|
||||
<label for="timesheet_product_id" string="Service" attrs="{'invisible': [('bill_type', '=', 'customer_task')]}"/>
|
||||
</div>
|
||||
<field name="timesheet_product_id" nolabel="1" attrs="{'invisible': ['|', '|', ('allow_timesheets', '=', False), ('sale_order_id', '!=', False), '&', ('pricing_type', '!=', 'fixed_rate'), ('bill_type', '!=', 'customer_task')], 'required': ['&', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}" context="{'default_type': 'service', 'default_service_policy': 'delivered_timesheet', 'default_service_type': 'timesheet'}"/>
|
||||
<field name="sale_order_id" invisible="1"/>
|
||||
<field name="sale_line_id" string="Default Sales Order Item" attrs="{'invisible': ['|', '|', ('sale_order_id', '=', False), ('bill_type', '!=', 'customer_project'), ('pricing_type', '!=', 'fixed_rate')], 'readonly': [('sale_order_id', '=', False)]}" options="{'no_create': True, 'no_edit': True, 'delete': False}"/>
|
||||
<group>
|
||||
<field name="display_create_order" invisible="1"/>
|
||||
<field name="bill_type" widget="radio"/>
|
||||
<field name="pricing_type" attrs="{'invisible': ['|', ('allow_billable', '=', False), ('bill_type', '!=', 'customer_project')], 'required': ['&', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}" widget="radio"/>
|
||||
<field name="timesheet_product_id" string="Service" attrs="{'invisible': ['|', '|', ('allow_timesheets', '=', False), ('sale_order_id', '!=', False), ('bill_type', '!=', 'customer_task')], 'required': ['&', ('allow_billable', '=', True), ('allow_timesheets', '=', True)]}" context="{'default_type': 'service', 'default_service_policy': 'delivered_timesheet', 'default_service_type': 'timesheet'}"/>
|
||||
<field name="sale_order_id" attrs="{'invisible': [('bill_type', '!=', 'customer_project')], 'readonly': [('sale_order_id', '!=', False)]}" force_save="1" options="{'no_create': True, 'no_edit': True, 'delete': False}"/>
|
||||
<field name="sale_line_id" string="Default Sales Order Item" attrs="{'invisible': [('bill_type', '!=', 'customer_project')]}" options="{'no_create': True, 'no_edit': True, 'delete': False}"/>
|
||||
</group>
|
||||
</group>
|
||||
<field name="sale_line_employee_ids" attrs="{'invisible': ['|', ('bill_type', '!=', 'customer_project'), ('pricing_type', '!=', 'employee_rate')]}">
|
||||
<tree editable="top">
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="project_id" invisible="1"/>
|
||||
<field name="employee_id" options="{'no_create': True}"/>
|
||||
<field name="timesheet_product_id" attrs="{'column_invisible': [('parent.sale_order_id', '!=', False)]}"/>
|
||||
<field name="sale_line_id" options="{'no_create': True}" attrs="{'column_invisible': [('parent.sale_order_id', '=', False)]}" domain="[('order_id','=',parent.sale_order_id), ('is_service', '=', True)]"/>
|
||||
<field name="price_unit" widget="monetary" options="{'currency_field': 'currency_id'}" attrs="{'readonly': [('parent.sale_order_id', '!=', False)]}"/>
|
||||
<field name="timesheet_product_id" attrs="{'column_invisible': [('parent.sale_order_id', '!=', False)]}" invisible="1"/>
|
||||
<field name="sale_line_id" attrs="{'required': True}" options="{'no_create': True}" domain="[('order_id','=',parent.sale_order_id), ('is_service', '=', True)]"/>
|
||||
<field name="price_unit" widget="monetary" force_save="1" options="{'currency_field': 'currency_id'}"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
@@ -81,11 +79,11 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='allow_timesheets']" position="after">
|
||||
<field name="allow_billable"/>
|
||||
<field name="warning_employee_rate"/>
|
||||
<field name="warning_employee_rate" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('o_primary')]//span" position="inside">
|
||||
<t t-if="record.warning_employee_rate.raw_value">
|
||||
<i class="ml-2 fa fa-exclamation-triangle text-danger small" role="img" title="Some of the employees who are recording time on this project are not linked to any Sales Order Item. This means that their time will be considered as non-billable." aria-label="Some of the employees who are recording time on this project are not linked to any Sales Order Item. This means that their time will be considered as non-billable."/>
|
||||
<t t-if="1 == 0">
|
||||
<i class="ml-2 fa fa-exclamation-triangle text-danger small" role="img" title="Some of the employees who are recording time on this project are not linked to any Sales Order Item. This means that their time will be considered as non-billable." aria-label="Some of the employees who are recording time on this project are not linked to any ales Order Item. This means that their time will be considered as non-billable."/>
|
||||
</t>
|
||||
</xpath>
|
||||
<xpath expr="//a[@name='action_view_account_analytic_line']" position="attributes">
|
||||
@@ -110,7 +108,7 @@
|
||||
<xpath expr="//header" position='inside'>
|
||||
<field name="allow_billable" invisible="1"/>
|
||||
<field name="display_create_order" invisible="1"/>
|
||||
<button name="action_make_billable" string="Create Sales Order" type="object" attrs="{'invisible': [('display_create_order', '=', False)]}" groups="sales_team.group_sale_salesman"/>
|
||||
<button name="action_make_billable" string="Create Sales Order" type="object" attrs="{'invisible': [('display_create_order', '=', False)]}" groups="sales_team.group_sale_salesman" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='email_cc']" position="after">
|
||||
<field name="analytic_account_id" groups="base.group_no_one"/>
|
||||
@@ -133,13 +131,13 @@
|
||||
<xpath expr="//field[@name='partner_phone']" position="after">
|
||||
<field name="bill_type" invisible="1"/>
|
||||
<field name="pricing_type" invisible="1"/>
|
||||
<field name="timesheet_product_id" attrs="{'invisible': ['|', '|', '|', '|', ('allow_billable', '=', False), ('allow_timesheets', '=', False), '&', ('partner_id', '=', False), ('bill_type', '=', 'customer_project'), ('sale_order_id', '!=', False), '&', ('pricing_type', '!=', 'fixed_rate'), ('bill_type', '!=', 'customer_task')]}" context="{'default_type': 'service', 'default_service_policy': 'delivered_timesheet', 'default_service_type': 'timesheet'}" placeholder="Leave empty if non-billable"/>
|
||||
<field name="non_allow_billable" attrs="{'invisible': ['|', '|', '|', ('allow_billable', '=', False), ('allow_timesheets', '=', False), ('pricing_type', '!=', 'employee_rate'), ('bill_type', '=', 'customer_task')]}"/>
|
||||
<field name="timesheet_product_id" invisible="1"/>
|
||||
<field name="non_allow_billable" attrs="{'invisible': ['|', '|', '|', ('allow_billable', '=', False), ('allow_timesheets', '=', False), ('pricing_type', '!=', 'employee_rate'), ('bill_type', '=', 'customer_task')]}" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='timesheet_ids']/tree" position="inside">
|
||||
<field name="timesheet_invoice_id" invisible="1"/>
|
||||
<field name="so_line" readonly="1" attrs="{'column_invisible': [('parent.has_multi_sol', '=', False), '|', ('parent.is_project_map_empty', '=', True), ('parent.pricing_type', '!=', 'employee_rate')]}" optional="hide"/>
|
||||
<field name="non_allow_billable" attrs="{'column_invisible': ['|', '|', ('parent.allow_billable', '=', False), '&', '&', '|', ('parent.bill_type', '!=', 'customer_project'), ('parent.pricing_type', '!=', 'employee_rate'), ('parent.timesheet_product_id', '=', False), ('parent.sale_line_id', '=', False), '&', '&', ('parent.bill_type', '=', 'customer_project'), ('parent.pricing_type', '=', 'employee_rate'), ('parent.non_allow_billable', '=', True)]}" optional="hide"/>
|
||||
<field name="non_allow_billable" attrs="{'column_invisible': ['|', '&', '&', '|', ('parent.bill_type', '!=', 'customer_project'), ('parent.pricing_type', '!=', 'employee_rate'), ('parent.timesheet_product_id', '=', False), ('parent.sale_line_id', '=', False), '&', ('parent.bill_type', '=', 'customer_project'), ('parent.pricing_type', '=', 'employee_rate')]}" optional="hide"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
@@ -153,7 +151,6 @@
|
||||
<attribute name="attrs">
|
||||
{'invisible': ['|', '|', '|', ('allow_billable', '=', False), ('sale_order_id', '=', False), '&', ('bill_type', '=', 'customer_project'), ('pricing_type', '=', 'employee_rate'), ('partner_id', '=', False)]}
|
||||
</attribute>
|
||||
<attribute name="placeholder">Leave empty if non-billable</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -191,7 +191,9 @@ class ProjectCreateSalesOrder(models.TransientModel):
|
||||
self._make_billable_at_employee_rate(sale_order)
|
||||
|
||||
def _make_billable_at_project_rate(self, sale_order):
|
||||
self.ensure_one()
|
||||
task_left = self.project_id.tasks.filtered(lambda task: not task.sale_line_id)
|
||||
ticket_timesheet_ids = self.env.context.get('ticket_timesheet_ids', [])
|
||||
for wizard_line in self.line_ids:
|
||||
task_ids = self.project_id.tasks.filtered(lambda task: not task.sale_line_id and task.timesheet_product_id == wizard_line.product_id)
|
||||
task_left -= task_ids
|
||||
@@ -211,6 +213,17 @@ class ProjectCreateSalesOrder(models.TransientModel):
|
||||
'product_uom_qty': 0.0,
|
||||
})
|
||||
|
||||
if ticket_timesheet_ids and not self.project_id.sale_line_id and not task_ids:
|
||||
# With pricing = "project rate" in project. When the user wants to create a sale order from a ticket in helpdesk
|
||||
# The project cannot contain any tasks. Thus, we need to give the first sale_order_line created to link
|
||||
# the timesheet to this first sale order line.
|
||||
# link the project to the SO line
|
||||
self.project_id.write({
|
||||
'sale_order_id': sale_order.id,
|
||||
'sale_line_id': sale_order_line.id,
|
||||
'partner_id': self.partner_id.id,
|
||||
})
|
||||
|
||||
# link the tasks to the SO line
|
||||
task_ids.write({
|
||||
'sale_line_id': sale_order_line.id,
|
||||
@@ -219,19 +232,29 @@ class ProjectCreateSalesOrder(models.TransientModel):
|
||||
})
|
||||
|
||||
# assign SOL to timesheets
|
||||
self.env['account.analytic.line'].search([('task_id', 'in', task_ids.ids), ('so_line', '=', False)]).write({
|
||||
search_domain = [('task_id', 'in', task_ids.ids), ('so_line', '=', False)]
|
||||
if ticket_timesheet_ids:
|
||||
search_domain = [('id', 'in', ticket_timesheet_ids), ('so_line', '=', False)]
|
||||
|
||||
self.env['account.analytic.line'].search(search_domain).write({
|
||||
'so_line': sale_order_line.id
|
||||
})
|
||||
sale_order_line.with_context({'no_update_planned_hours': True}).write({
|
||||
'product_uom_qty': sale_order_line.qty_delivered
|
||||
})
|
||||
|
||||
# link the project to the SO line
|
||||
self.project_id.write({
|
||||
'sale_order_id': sale_order.id,
|
||||
'sale_line_id': sale_order_line.id, # we take the last sale_order_line created
|
||||
'partner_id': self.partner_id.id,
|
||||
})
|
||||
if ticket_timesheet_ids and self.project_id.sale_line_id and not self.project_id.tasks and len(self.line_ids) > 1:
|
||||
# Then, we need to give to the project the last sale order line created
|
||||
self.project_id.write({
|
||||
'sale_line_id': sale_order_line.id
|
||||
})
|
||||
else: # Otherwise, we are in the normal behaviour
|
||||
# link the project to the SO line
|
||||
self.project_id.write({
|
||||
'sale_order_id': sale_order.id,
|
||||
'sale_line_id': sale_order_line.id, # we take the last sale_order_line created
|
||||
'partner_id': self.partner_id.id,
|
||||
})
|
||||
|
||||
if task_left:
|
||||
task_left.sale_line_id = False
|
||||
@@ -296,7 +319,13 @@ class ProjectCreateSalesOrder(models.TransientModel):
|
||||
tasks = self.project_id.tasks.filtered(lambda t: not t.non_allow_billable)
|
||||
# assign SOL to timesheets
|
||||
for map_entry in map_entries:
|
||||
self.env['account.analytic.line'].search([('task_id', 'in', tasks.ids), ('employee_id', '=', map_entry.employee_id.id), ('so_line', '=', False)]).write({
|
||||
search_domain = [('employee_id', '=', map_entry.employee_id.id), ('so_line', '=', False)]
|
||||
ticket_timesheet_ids = self.env.context.get('ticket_timesheet_ids', [])
|
||||
if ticket_timesheet_ids:
|
||||
search_domain.append(('id', 'in', ticket_timesheet_ids))
|
||||
else:
|
||||
search_domain.append(('task_id', 'in', tasks.ids))
|
||||
self.env['account.analytic.line'].search(search_domain).write({
|
||||
'so_line': map_entry.sale_line_id.id
|
||||
})
|
||||
map_entry.sale_line_id.with_context({'no_update_planned_hours': True}).write({
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
<field name="model">project.create.sale.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Create a Sales Order">
|
||||
<group>
|
||||
<field name="link_selection" nolabel="1" widget="radio" options="{'horizontal': true}"/>
|
||||
</group>
|
||||
<field name="link_selection" nolabel="1" widget="radio" options="{'horizontal': true}" invisible="1"/>
|
||||
<group>
|
||||
<group>
|
||||
<field name="project_id" readonly="1"/>
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
<field name="model">project.task.create.sale.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Create a Sales Order">
|
||||
<group>
|
||||
<field name="link_selection" nolabel="1" widget="radio" options="{'horizontal': true}"/>
|
||||
</group>
|
||||
<field name="link_selection" nolabel="1" widget="radio" options="{'horizontal': true}" invisible="1"/>
|
||||
<group>
|
||||
<group>
|
||||
<field name="task_id" readonly="1"/>
|
||||
|
||||
Reference in New Issue
Block a user