summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjem-odoo <jem@openerp.com>2018-09-10 15:06:53 +0200
committerjem-odoo <jem@openerp.com>2018-09-13 14:44:35 +0200
commit64adff43d8fcea9e44a7a42008cd6d8f470839ac (patch)
tree38de98cd0aef3a2e00e80cbfc0ec673f11b324c4
parentecc8ff826083d3c157b268ff03221bbee5e390e4 (diff)
[REF] sale_timesheet: make project billable wizard
When introducing the 'employee rate' mode to timesheet on project, we break the wizard: its goal was to link the existing timesheets to the SO line in order to invoice them. (Employee rate commit: b4af54f9c23b65abe50dde3dfb397510ec2fcee4) This commit makes the wizard compatible with the employee rate feature, without breaking invoicing at task rate flow. Two possible flows, starting with a non billable project: * project rate: the wizard will create a SO with one SOL, and will link the task without SOL set to the new one. The project will also be linked to that SOL. * employee rate: the user has to fill a map to associate product and employee. If the same product is set 2 times with the same price (and so, 2 different employees), only one SOL will be created on the SO. When submitting, the project map will be filled and the project will be linked to the SO. No fallback SOL will be set on project or on the tasks. Both use cases will be stopped if a timesheet of the project's tasks is already linked to a sale line. If not, the timesheets will be associated to the correct SOL (dependning of the billable type), and the delivered quantity will be incremented, making the SO invoicable. The tasks'customer will also be set. Future work: setting a product per task (which will be a real 'task rate' feature). Task #1883640
-rw-r--r--addons/sale_timesheet/tests/test_project_billing.py97
-rw-r--r--addons/sale_timesheet/wizard/project_create_sale_order.py192
-rw-r--r--addons/sale_timesheet/wizard/project_create_sale_order_views.xml28
3 files changed, 242 insertions, 75 deletions
diff --git a/addons/sale_timesheet/tests/test_project_billing.py b/addons/sale_timesheet/tests/test_project_billing.py
index a3467054d25..da6b5ee6eb6 100644
--- a/addons/sale_timesheet/tests/test_project_billing.py
+++ b/addons/sale_timesheet/tests/test_project_billing.py
@@ -116,16 +116,38 @@ class TestProjectBilling(TestCommonSaleTimesheetNoChart):
def test_make_billable_at_task_rate(self):
""" Starting from a non billable project, make it billable at task rate, using the wizard """
+ Timesheet = self.env['account.analytic.line']
+ Task = self.env['project.task']
# set a customer on the project
self.project_non_billable.write({
'partner_id': self.partner_2.id
})
+ # create a task and 2 timesheets
+ task = Task.with_context(default_project_id=self.project_non_billable.id).create({
+ 'name': 'first task',
+ 'partner_id': self.project_non_billable.partner_id.id,
+ 'planned_hours': 10,
+ })
+ timesheet1 = Timesheet.create({
+ 'name': 'Test Line',
+ 'project_id': task.project_id.id,
+ 'task_id': task.id,
+ 'unit_amount': 3,
+ 'employee_id': self.employee_manager.id,
+ })
+ timesheet2 = Timesheet.create({
+ 'name': 'Test Line tde',
+ 'project_id': task.project_id.id,
+ 'task_id': task.id,
+ 'unit_amount': 2,
+ 'employee_id': self.employee_tde.id,
+ })
# create wizard
wizard = self.env['project.create.sale.order'].with_context(active_id=self.project_non_billable.id, active_model='project.project').create({
- 'line_ids': [
- (0, 0, {'product_id': self.product_delivery_timesheet3.id, 'quantity': 10, 'price_unit': self.product_delivery_timesheet3.list_price}) # product creates new T in new P
- ]
+ 'product_id': self.product_delivery_timesheet3.id, # product creates new T in new P
+ 'price_unit': self.product_delivery_timesheet3.list_price,
+ 'billable_type': 'project_rate',
})
self.assertEqual(self.project_non_billable.billable_type, 'no', "The project should still be non billable")
@@ -138,24 +160,49 @@ class TestProjectBilling(TestCommonSaleTimesheetNoChart):
self.assertEqual(self.project_non_billable.billable_type, 'task_rate', "The project should be 'task rate' billable")
self.assertEqual(sale_order.partner_id, self.project_non_billable.partner_id, "The customer of the SO should be the same as the project")
self.assertEqual(len(sale_order.order_line), 1, "The SO should have 1 line")
- self.assertEqual(sale_order.order_line.product_id, wizard.line_ids.product_id, "The product of the only SOL should be the selected on the wizard")
+ self.assertEqual(sale_order.order_line.product_id, wizard.product_id, "The product of the only SOL should be the selected on the wizard")
self.assertEqual(sale_order.order_line.project_id, self.project_non_billable, "SOL should be linked to the project")
self.assertTrue(sale_order.order_line.task_id, "The SOL creates a task as they were no task already present in the project (system limitation)")
self.assertEqual(sale_order.order_line.task_id.project_id, self.project_non_billable, "The created task should be in the project")
+ self.assertEqual(sale_order.order_line.qty_delivered, timesheet1.unit_amount + timesheet2.unit_amount, "The create SOL should have an delivered quantity equals to the sum of tasks'timesheets")
def test_make_billable_at_employee_rate(self):
""" Starting from a non billable project, make it billable at employee rate, using the wizard """
+ Timesheet = self.env['account.analytic.line']
+ Task = self.env['project.task']
# set a customer on the project
self.project_non_billable.write({
'partner_id': self.partner_2.id
})
+ # create a task and 2 timesheets
+ task = Task.with_context(default_project_id=self.project_non_billable.id).create({
+ 'name': 'first task',
+ 'partner_id': self.project_non_billable.partner_id.id,
+ 'planned_hours': 10,
+ })
+ timesheet1 = Timesheet.create({
+ 'name': 'Test Line',
+ 'project_id': task.project_id.id,
+ 'task_id': task.id,
+ 'unit_amount': 3,
+ 'employee_id': self.employee_manager.id,
+ })
+ timesheet2 = Timesheet.create({
+ 'name': 'Test Line tde',
+ 'project_id': task.project_id.id,
+ 'task_id': task.id,
+ 'unit_amount': 2,
+ 'employee_id': self.employee_user.id,
+ })
# create wizard
wizard = self.env['project.create.sale.order'].with_context(active_id=self.project_non_billable.id, active_model='project.project').create({
+ 'billable_type': 'employee_rate',
+ 'partner_id': self.partner_2.id,
'line_ids': [
- (0, 0, {'product_id': self.product_delivery_timesheet1.id, 'quantity': 4, 'price_unit': self.product_delivery_timesheet1.list_price}), # product creates no T
- (0, 0, {'product_id': self.product_delivery_timesheet2.id, 'quantity': 5, 'price_unit': self.product_delivery_timesheet2.list_price}), # product creates T in global project
- (0, 0, {'product_id': self.product_delivery_timesheet3.id, 'quantity': 10, 'price_unit': self.product_delivery_timesheet3.list_price}), # product creates new T in new P
+ (0, 0, {'product_id': self.product_delivery_timesheet1.id, 'price_unit': 15, 'employee_id': self.employee_tde.id}), # product creates no T
+ (0, 0, {'product_id': self.product_delivery_timesheet1.id, 'price_unit': 15, 'employee_id': self.employee_manager.id}), # product creates no T (same product than previous one)
+ (0, 0, {'product_id': self.product_delivery_timesheet3.id, 'price_unit': self.product_delivery_timesheet3.list_price, 'employee_id': self.employee_user.id}), # product creates new T in new P
]
})
@@ -167,29 +214,31 @@ class TestProjectBilling(TestCommonSaleTimesheetNoChart):
action = wizard.action_create_sale_order()
sale_order = self.env['sale.order'].browse(action['res_id'])
- self.assertEqual(self.project_non_billable.billable_type, 'task_rate', "The project should be 'employee rate' billable")
+ self.assertEqual(self.project_non_billable.billable_type, 'employee_rate', "The project should be 'employee rate' billable")
self.assertEqual(sale_order.partner_id, self.project_non_billable.partner_id, "The customer of the SO should be the same as the project")
- self.assertEqual(len(sale_order.order_line), 3, "The SO should have 3 lines")
+ self.assertEqual(len(sale_order.order_line), 2, "The SO should have 2 lines, as in wizard map there were 2 time the same product with the same price (for 2 different employees)")
+ self.assertEqual(len(self.project_non_billable.sale_line_employee_ids), 3, "The project have 3 lines in its map")
+ self.assertEqual(self.project_non_billable.sale_line_id, sale_order.order_line[0], "The wizard sets sale line fallbakc on project as the first of the list")
+ self.assertEqual(task.sale_line_id, sale_order.order_line[0], "The wizard sets sale line fallback on tasks")
+ self.assertEqual(task.partner_id, wizard.partner_id, "The wizard sets the customer on tasks to make SOL line field visible")
line1 = sale_order.order_line.filtered(lambda sol: sol.product_id == self.product_delivery_timesheet1)
- line2 = sale_order.order_line.filtered(lambda sol: sol.product_id == self.product_delivery_timesheet2)
- line3 = sale_order.order_line.filtered(lambda sol: sol.product_id == self.product_delivery_timesheet3)
+ line2 = sale_order.order_line.filtered(lambda sol: sol.product_id == self.product_delivery_timesheet3)
- self.assertTrue(line1, "Sale line with product 1 should exists")
- self.assertTrue(line2, "Sale line with product 2 should exists")
- self.assertTrue(line3, "Sale line with product 3 should exists")
+ self.assertTrue(line1, "Sale line 1 with product 1 should exists")
+ self.assertTrue(line2, "Sale line 2 with product 3 should exists")
self.assertFalse(line1.project_id, "Sale line 1 should be linked to the 'non billable' project")
- self.assertFalse(line2.project_id, "Sale line 2 should not be linked to a project, as it creates a task in a global one")
- self.assertEqual(line3.project_id, self.project_non_billable, "Sale line 3 should be linked to the 'non billable' project")
-
- # filling the map will set billable type to emlpoyee rate
- self.env['project.sale.line.employee.map'].create({
- 'project_id': self.project_non_billable.id,
- 'sale_line_id': line1.id,
- 'employee_id': self.employee_manager.id,
- })
- self.assertEqual(self.project_non_billable.billable_type, 'employee_rate', "The project should be 'employee rate' billable")
+ self.assertEqual(line2.project_id, self.project_non_billable, "Sale line 3 should be linked to the 'non billable' project")
+ self.assertEqual(line1.price_unit, 15, "The unit price of SOL 1 should be 15")
+ self.assertEqual(line1.product_uom_qty, 0, "The ordered qty of SOL 1 should be one")
+ self.assertEqual(line2.product_uom_qty, 0, "The ordered qty of SOL 1 should be one")
+
+ self.assertEqual(self.project_non_billable.sale_line_employee_ids.mapped('sale_line_id'), sale_order.order_line, "The SO lines of the map should be the same of the sales order")
+ self.assertEqual(timesheet1.so_line, line1, "Timesheet1 should be linked to sale line 1, as employee manager create the timesheet")
+ self.assertEqual(timesheet2.so_line, line2, "Timesheet2 should be linked to sale line 2, as employee tde create the timesheet")
+ self.assertEqual(timesheet1.unit_amount, line1.qty_delivered, "Sale line 1 should have a delivered qty equals to the sum of its linked timesheets")
+ self.assertEqual(timesheet2.unit_amount, line2.qty_delivered, "Sale line 2 should have a delivered qty equals to the sum of its linked timesheets")
def test_billing_employee_rate(self):
""" Check task and subtask creation, and timesheeting in a project billed at 'employee rate'. Then move the task into a 'task rate' project. """
diff --git a/addons/sale_timesheet/wizard/project_create_sale_order.py b/addons/sale_timesheet/wizard/project_create_sale_order.py
index fd0a0d8622c..c67499b0ee2 100644
--- a/addons/sale_timesheet/wizard/project_create_sale_order.py
+++ b/addons/sale_timesheet/wizard/project_create_sale_order.py
@@ -27,18 +27,49 @@ class ProjectCreateSalesOrder(models.TransientModel):
result['partner_id'] = project.partner_id.id
return result
- project_id = fields.Many2one('project.project', "Project", domain=[('sale_line_id', '=', False)], help="Project to make billable", required=True)
- partner_id = fields.Many2one('res.partner', string="Customer", domain=[('customer', '=', True)], required=True)
+ project_id = fields.Many2one('project.project', "Project", domain=[('sale_line_id', '=', False)], help="Project for which we are creating a sales order", required=True)
+ partner_id = fields.Many2one('res.partner', string="Customer", domain=[('customer', '=', True)], required=True, help="Customer of the sales order")
+ product_id = fields.Many2one('product.product', domain=[('type', '=', 'service'), ('invoice_policy', '=', 'delivery'), ('service_type', '=', 'timesheet')], string="Service", help="Product of the sales order item. Must be a service invoiced based on timesheets on tasks.")
+ price_unit = fields.Float("Unit Price", help="Unit price of the sales order item.")
+ currency_id = fields.Many2one('res.currency', string="Currency", related='product_id.currency_id')
+
+ billable_type = fields.Selection([
+ ('project_rate', 'At Project Rate'),
+ ('employee_rate', 'At Employee Rate'),
+ ], string="Billing Type", default='project_rate', required=True, help="* At Project Rate: All timesheets on the project will be billed at the same rate\n* At Employee Rate: Timesheets will be billed at a rate defined at employee level")
+
line_ids = fields.One2many('project.create.sale.order.line', 'wizard_id', string='Lines')
+ @api.onchange('billable_type', 'product_id')
+ def _onchange_product_id(self):
+ if self.billable_type == 'project_rate':
+ if self.product_id:
+ self.price_unit = self.product_id.lst_price
+ else:
+ self.price_unit = 0.0
+
@api.multi
def action_create_sale_order(self):
# if project linked to SO line or at least on tasks with SO line, then we consider project as billable.
if self.project_id.sale_line_id:
raise UserError(_("The project is already linked to a sales order item."))
- if not self.line_ids:
- raise UserError(_("At least one Sale Order Items should be filled."))
+ if self.billable_type == 'employee_rate':
+ # at least one line
+ if not self.line_ids:
+ raise UserError(_("At least one line should be filled."))
+
+ # all employee having timesheet should be in the wizard map
+ timesheet_employees = self.env['account.analytic.line'].search([('task_id', 'in', self.project_id.tasks.ids)]).mapped('employee_id')
+ map_employees = self.line_ids.mapped('employee_id')
+ missing_meployees = timesheet_employees - map_employees
+ if missing_meployees:
+ raise UserError(_('The Sales Order cannot be created because you did not enter some employees that entered timesheets on this project. Please list all the relevant employees before creating the Sales Order.\nMissing employee(s): %s') % (', '.join(missing_meployees.mapped('name'))))
+
+ # check here if timesheet already linked to SO line
+ timesheet_with_so_line = self.env['account.analytic.line'].search_count([('task_id', 'in', self.project_id.tasks.ids), ('so_line', '!=', False)])
+ if timesheet_with_so_line:
+ raise UserError(_('The sales order cannot be created because some timesheets of this project are already linked to another sales order.'))
# create SO
sale_order = self.env['sale.order'].create({
@@ -49,47 +80,15 @@ class ProjectCreateSalesOrder(models.TransientModel):
sale_order.onchange_partner_id()
sale_order.onchange_partner_shipping_id()
- # trying to simulate the SO line created a task, according to the product configuration
- # To avoid, generating a task when confirming the SO
- task_id = self.env['project.task'].search([('project_id', '=', self.project_id.id)], order='create_date DESC', limit=1).id
- project_id = self.project_id.id
-
- # create SO lines
- for wizard_line in self.line_ids:
- values = {
- 'order_id': sale_order.id,
- 'product_id': wizard_line.product_id.id,
- 'price_unit': wizard_line.price_unit,
- 'product_uom_qty': wizard_line.quantity,
- }
- if wizard_line.product_id.service_tracking in ['task_new_project', 'task_global_project']:
- values['task_id'] = task_id
- if wizard_line.product_id.service_tracking in ['task_new_project', 'project_only']:
- values['project_id'] = project_id
-
- sale_order_line = self.env['sale.order.line'].create(values)
+ # create the sale lines, the map (optional), and assign existing timesheet to sale lines
+ if self.billable_type == 'project_rate':
+ self._make_billable_at_project_rate(sale_order)
+ else:
+ self._make_billable_at_employee_rate(sale_order)
# confirm SO
sale_order.action_confirm()
- # link project and tasks to SO line according to billable type
- if len(self.line_ids) == 1:
- self.project_id.write({
- 'sale_order_id': sale_order.id,
- 'sale_line_id': sale_order_line.id,
- 'partner_id': self.partner_id.id,
- })
- self.project_id.tasks.filtered(lambda task: task.billable_type == 'no').write({
- 'sale_line_id': sale_order_line.id,
- 'partner_id': sale_order.partner_id.id,
- 'email_from': sale_order.partner_id.email,
- })
- else:
- self.project_id.write({
- 'sale_order_id': sale_order.id,
- 'partner_id': self.partner_id.id,
- })
-
view_form_id = self.env.ref('sale.view_order_form').id
action = self.env.ref('sale.action_orders').read()[0]
action.update({
@@ -100,16 +99,121 @@ class ProjectCreateSalesOrder(models.TransientModel):
})
return action
+ def _make_billable_at_project_rate(self, sale_order):
+ # trying to simulate the SO line created a task, according to the product configuration
+ # To avoid, generating a task when confirming the SO
+ task_id = False
+ if self.product_id.service_tracking in ['task_new_project', 'task_global_project']:
+ task_id = self.env['project.task'].search([('project_id', '=', self.project_id.id)], order='create_date DESC', limit=1).id
+
+ # create SO line
+ sale_order_line = self.env['sale.order.line'].create({
+ 'order_id': sale_order.id,
+ 'product_id': self.product_id.id,
+ 'price_unit': self.price_unit,
+ 'project_id': self.project_id.id, # prevent to re-create a project on confirmation
+ 'task_id': task_id,
+ 'product_uom_qty': 0.0,
+ })
+
+ # link the project and the tasks 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,
+ })
+ self.project_id.tasks.filtered(lambda task: task.billable_type == 'no').write({
+ 'sale_line_id': sale_order_line.id,
+ 'partner_id': sale_order.partner_id.id,
+ 'email_from': sale_order.partner_id.email,
+ })
+
+ # assign SOL to timesheets
+ self.env['account.analytic.line'].search([('task_id', 'in', self.project_id.tasks.ids), ('so_line', '=', False)]).write({
+ 'so_line': sale_order_line.id
+ })
+
+ return sale_order_line
+
+ def _make_billable_at_employee_rate(self, sale_order):
+ # trying to simulate the SO line created a task, according to the product configuration
+ # To avoid, generating a task when confirming the SO
+ task_id = self.env['project.task'].search([('project_id', '=', self.project_id.id)], order='create_date DESC', limit=1).id
+ project_id = self.project_id.id
+
+ non_billable_tasks = self.project_id.tasks.filtered(lambda task: task.billable_type == 'no')
+
+ map_entries = self.env['project.sale.line.employee.map']
+ EmployeeMap = self.env['project.sale.line.employee.map'].sudo()
+
+ # create SO lines: create on SOL per product/price. So many employee can be linked to the same SOL
+ map_product_price_sol = {} # (product_id, price) --> SOL
+ for wizard_line in self.line_ids:
+ map_key = (wizard_line.product_id.id, wizard_line.price_unit)
+ if map_key not in map_product_price_sol:
+ values = {
+ 'order_id': sale_order.id,
+ 'product_id': wizard_line.product_id.id,
+ 'price_unit': wizard_line.price_unit,
+ 'product_uom_qty': 0.0,
+ }
+ if wizard_line.product_id.service_tracking in ['task_new_project', 'task_global_project']:
+ values['task_id'] = task_id
+ if wizard_line.product_id.service_tracking in ['task_new_project', 'project_only']:
+ values['project_id'] = project_id
+
+ sale_order_line = self.env['sale.order.line'].create(values)
+ map_product_price_sol[map_key] = sale_order_line
+
+ map_entries |= EmployeeMap.create({
+ 'project_id': self.project_id.id,
+ 'sale_line_id': map_product_price_sol[map_key].id,
+ 'employee_id': wizard_line.employee_id.id,
+ })
+
+ # link the project to the SO
+ self.project_id.write({
+ 'sale_order_id': sale_order.id,
+ 'sale_line_id': sale_order.order_line[0].id,
+ 'partner_id': self.partner_id.id,
+ })
+ non_billable_tasks.write({
+ 'sale_line_id': sale_order.order_line[0].id,
+ 'partner_id': sale_order.partner_id.id,
+ 'email_from': sale_order.partner_id.email,
+ })
+
+ # assign SOL to timesheets
+ for map_entry in map_entries:
+ self.env['account.analytic.line'].search([('task_id', 'in', self.project_id.tasks.ids), ('employee_id', '=', map_entry.employee_id.id), ('so_line', '=', False)]).write({
+ 'so_line': map_entry.sale_line_id.id
+ })
+
+ return map_entries
+
class ProjectCreateSalesOrderLine(models.TransientModel):
_name = 'project.create.sale.order.line'
_order = 'id,create_date'
+ def _default_domain_employee_id(self):
+ active_id = self._context.get('active_id')
+ if active_id:
+ project = self.env['project.project'].browse(active_id)
+ employee_ids = [item['employee_id'][0] for item in self.env['account.analytic.line'].search_read([('task_id', 'in', project.tasks.ids)], ['employee_id'])]
+ return [('id', 'in', employee_ids)]
+ return []
+
wizard_id = fields.Many2one('project.create.sale.order', required=True)
- product_id = fields.Many2one('product.product', domain=[('type', '=', 'service')], string="Service", required=True)
- price_unit = fields.Float("Price", default=1.0)
- quantity = fields.Float("Quantity", digits=dp.get_precision('Product Unit of Measure'), required=True, default=1.0)
+ product_id = fields.Many2one('product.product', domain=[('type', '=', 'service'), ('invoice_policy', '=', 'delivery'), ('service_type', '=', 'timesheet')], string="Service", required=True,
+ help="Product of the sales order item. Must be a service invoiced based on timesheets on tasks.")
+ price_unit = fields.Float("Unit Price", default=1.0, help="Unit price of the sales order item.")
currency_id = fields.Many2one('res.currency', string="Currency", related='product_id.currency_id')
+ employee_id = fields.Many2one('hr.employee', string="Employee", domain=lambda self: self._default_domain_employee_id(), required=True, help="Employee that has timesheets on the project.")
+
+ _sql_constraints = [
+ ('unique_employee_per_wizard', 'UNIQUE(wizard_id, employee_id)', "An employee cannot be selected more than once in the mapping. Please remove duplicate(s) and try again."),
+ ]
@api.onchange('product_id')
def _onchange_product_id(self):
diff --git a/addons/sale_timesheet/wizard/project_create_sale_order_views.xml b/addons/sale_timesheet/wizard/project_create_sale_order_views.xml
index b7bda0947a6..e847efaddaa 100644
--- a/addons/sale_timesheet/wizard/project_create_sale_order_views.xml
+++ b/addons/sale_timesheet/wizard/project_create_sale_order_views.xml
@@ -5,23 +5,37 @@
<field name="name">project.create.sale.order.wizard.form</field>
<field name="model">project.create.sale.order</field>
<field name="arch" type="xml">
- <form string="Make Project Billable">
+ <form string="Create a Sales Order">
<group>
- <field name="project_id" readonly="1"/>
- <field name="partner_id"/>
+ <group>
+ <field name="project_id" readonly="1"/>
+ <field name="partner_id"/>
+ </group>
+ <group>
+ <field name="billable_type" widget="radio"/>
+ </group>
</group>
- <group>
- <field name="line_ids" nolabel="1">
+ <group attrs="{'invisible': [('billable_type', '=', 'employee_rate')]}">
+ <group>
+ <field name="product_id" options="{'no_create_edit': True, 'no_create': True}" attrs="{'required': [('billable_type', '=', 'project_rate')]}"/>
+ </group>
+ <group>
+ <field name="price_unit" widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}" attrs="{'required': [('billable_type', '=', 'project_rate')]}"/>
+ <field name="currency_id" invisible="1"/>
+ </group>
+ </group>
+ <group attrs="{'invisible': [('billable_type', '=', 'project_rate')]}">
+ <field name="line_ids" nolabel="1" attrs="{'required': [('billable_type', '=', 'employee_rate')]}">
<tree editable="bottom">
+ <field name="employee_id" options="{'no_create_edit': True, 'no_create': True}"/>
<field name="product_id" options="{'no_create_edit': True, 'no_create': True}"/>
- <field name="quantity"/>
<field name="price_unit" widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}"/>
<field name="currency_id" invisible="1"/>
</tree>
</field>
</group>
<footer>
- <button string="Create Order" type="object" name="action_create_sale_order" class="oe_highlight"/>
+ <button string="Create Sales Order" type="object" name="action_create_sale_order" class="oe_highlight"/>
<button string="Cancel" special="cancel" type="object" class="btn btn-secondary oe_inline"/>
</footer>
</form>