diff --git a/bemade_fsm/models/__init__.py b/bemade_fsm/models/__init__.py
index 997de6e..3cb9595 100644
--- a/bemade_fsm/models/__init__.py
+++ b/bemade_fsm/models/__init__.py
@@ -4,3 +4,4 @@ from . import sale_order
from . import equipment
from . import task
from . import res_partner
+from . import fsm_visit
diff --git a/bemade_fsm/models/fsm_visit.py b/bemade_fsm/models/fsm_visit.py
new file mode 100644
index 0000000..4426ef5
--- /dev/null
+++ b/bemade_fsm/models/fsm_visit.py
@@ -0,0 +1,37 @@
+from odoo import models, fields, api, _
+
+
+class FSMVisit(models.Model):
+ _name = "bemade_fsm.visit"
+ _description = 'Represents a single visit by assigned service personnel.'
+
+ label = fields.Text(string="Label",
+ required=True,
+ related='so_section_id.name',
+ readonly=False)
+ approx_date = fields.Date(string='Approximate Date')
+ so_section_id = fields.Many2one(comodel_name="sale.order.line",
+ string="Sale Order Section",
+ help="The section on the sale order that represents the labour and parts for "
+ "this visit")
+ sale_order_id = fields.Many2one(comodel_name="sale.order",
+ string="Sales Order",
+ required=True)
+ is_completed = fields.Boolean(string="Completed",
+ related="so_section_id.is_fully_delivered")
+ is_invoiced = fields.Boolean(string="Invoiced",
+ compute="_compute_is_invoiced")
+
+ def _compute_is_invoiced(self):
+ self.is_invoiced = False
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ recs = super().create(vals_list)
+ for i, rec in enumerate(recs):
+ rec.so_section_id = rec.env['sale.order.line'].create({
+ 'order_id': rec.sale_order_id.id,
+ 'display_type': 'line_section',
+ 'name': vals_list[i]['label'],
+ })
+ return recs
diff --git a/bemade_fsm/models/sale_order.py b/bemade_fsm/models/sale_order.py
index ed547d6..cb2f88f 100644
--- a/bemade_fsm/models/sale_order.py
+++ b/bemade_fsm/models/sale_order.py
@@ -1,4 +1,5 @@
from odoo import fields, models, api, _, Command
+from odoo.exceptions import ValidationError
class SaleOrder(models.Model):
@@ -24,6 +25,9 @@ class SaleOrder(models.Model):
inverse='_inverse_default_contacts',
string='Work Order Recipients',
store=True)
+ visit_ids = fields.One2many(comodel_name='bemade_fsm.visit',
+ inverse_name="sale_order_id",
+ readonly=False)
@api.onchange('partner_shipping_id')
def _onchange_partner_shipping_id(self):
@@ -53,6 +57,12 @@ class SaleOrder(models.Model):
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
+ visit_id = fields.One2many(comodel_name="bemade_fsm.visit",
+ inverse_name="so_section_id",
+ string="Visit")
+ is_fully_delivered = fields.Boolean(string="Fully Delivered",
+ compute="_compute_is_fully_delivered")
+
def _timesheet_create_task(self, project):
""" Generate task for the given so line, and link it.
:param project: record of project.project in which the task should be created
@@ -78,7 +88,7 @@ class SaleOrderLine(models.Model):
task.child_ids.write({'sale_order_id': None, 'sale_line_id': None, })
return task
- def _generate_task_name(template = None):
+ def _generate_task_name(template=None):
template_name = template and template.name
return f"{self.order_id.name}: {self.order_id.partner_shipping_id.name} - {self.name} ({template_name})"
@@ -116,3 +126,27 @@ class SaleOrderLine(models.Model):
task.equipment_id = self.order_id.equipment_id
task.name = _generate_task_name(tmpl)
return task
+
+ @api.depends('order_id.order_line', 'display_type', 'qty_to_deliver', 'order_id.order_line.qty_to_deliver',
+ 'order_id.order_line.display_type')
+ def _compute_is_fully_delivered(self):
+ if not self.display_type:
+ self.is_fully_delivered = self.qty_to_deliver == 0
+ elif self.display_type == 'line_note':
+ self.is_fully_delivered = True
+ else:
+ for line in self.order_id.order_line:
+ # Iterate through the lines on the SO in order until we find this one in the list
+ found = False
+ if line == self:
+ found = True
+ if not found:
+ continue
+ # If we've hit the next section without returning False, all lines in this section were delivered
+ if found and line.display_type == 'line_section':
+ self.is_fully_delivered = True
+ return
+ if line.qty_to_deliver > 0:
+ self.is_fully_delivered = False
+ return
+ self.is_fully_delivered = True
diff --git a/bemade_fsm/security/ir.model.access.csv b/bemade_fsm/security/ir.model.access.csv
index 82eb4f7..46a72bd 100644
--- a/bemade_fsm/security/ir.model.access.csv
+++ b/bemade_fsm/security/ir.model.access.csv
@@ -3,4 +3,5 @@ access_bemade_fsm_task_template,bemade_fsm_task_template,model_project_task_temp
access_bemade_fsm_task_template,bemade_fsm_task_template,model_project_task_template,project.group_project_user,1,1,1,1
access_bemade_fsm_equipment,bemade_fsm_equipment,model_bemade_fsm_equipment,base.group_user,1,1,1,1
access_bemade_fsm_equipment_tag,bemade_fsm_equipment_tag,model_bemade_fsm_equipment_tag,base.group_user,1,1,1,1
-access_bemade_fsm_equipment_type,access_bemade_fsm_equipment_type,model_bemade_fsm_equipment_type,base.group_user,1,0,0,0
\ No newline at end of file
+access_bemade_fsm_equipment_type,access_bemade_fsm_equipment_type,model_bemade_fsm_equipment_type,base.group_user,1,0,0,0
+access_bemade_fsm_visit,access_bemade_fsm_visit,model_bemade_fsm_visit,base.group_user,1,1,1,1
diff --git a/bemade_fsm/tests/__init__.py b/bemade_fsm/tests/__init__.py
index 5e3c145..e838478 100644
--- a/bemade_fsm/tests/__init__.py
+++ b/bemade_fsm/tests/__init__.py
@@ -4,3 +4,4 @@ from . import test_sale_order
from . import test_equipment
from . import test_fsm_contact_setting
from . import test_so_task_contacts
+from . import test_fsm_visit
diff --git a/bemade_fsm/tests/test_fsm_contact_setting.py b/bemade_fsm/tests/test_fsm_contact_setting.py
index 81ceb1a..ce8a60e 100644
--- a/bemade_fsm/tests/test_fsm_contact_setting.py
+++ b/bemade_fsm/tests/test_fsm_contact_setting.py
@@ -1,29 +1,33 @@
-from odoo.tests import TransactionCase, HttpCase, tagged
+from odoo.tests import TransactionCase, HttpCase, tagged, Form
from odoo import Command
from .test_bemade_fsm_common import FSMManagerUserTransactionCase
+def create_base_contacts(cls):
+ Partner = cls.env['res.partner']
+ cls.parent_co = Partner.create({
+ 'name': 'Parent Co',
+ 'company_type': 'company',
+ })
+ cls.contact_1 = Partner.create({
+ 'name': 'Contact 1',
+ 'company_type': 'person',
+ 'parent_id': cls.parent_co.id,
+ })
+ cls.contact_2 = Partner.create({
+ 'name': 'Contact 2',
+ 'company_type': 'person',
+ 'parent_id': cls.parent_co.id,
+ })
+
+
@tagged("-at_install", "post_install")
class SaleOrderFSMContactsCase(FSMManagerUserTransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
- Partner = cls.env['res.partner']
- cls.parent_co = Partner.create({
- 'name': 'Parent Co',
- 'company_type': 'company',
- })
- cls.contact_1 = Partner.create({
- 'name': 'Contact 1',
- 'company_type': 'person',
- 'parent_id': cls.parent_co.id,
- })
- cls.contact_2 = Partner.create({
- 'name': 'Contact 2',
- 'company_type': 'person',
- 'parent_id': cls.parent_co.id,
- })
+ create_base_contacts(cls)
def test_site_contacts(self):
# Make sure the SO pulls the defaults from the partner correctly
@@ -56,10 +60,11 @@ class SaleOrderFSMContactsCase(FSMManagerUserTransactionCase):
so.work_order_contacts != so.partner_id.work_order_contacts and len(so.partner_id.work_order_contacts) == 2)
-class SaleOrderMultiLocationContactsTest(SaleOrderFSMContactsCase):
+class SaleOrderMultiLocationContactsTest(FSMManagerUserTransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
+ create_base_contacts(cls)
cls.shipping_location = cls.env['res.partner'].create({
'name': 'Shipping location',
'company_type': 'company',
@@ -90,3 +95,18 @@ class SaleOrderMultiLocationContactsTest(SaleOrderFSMContactsCase):
self.assertEqual(so.partner_shipping_id, self.shipping_location)
self.assertEqual(so.site_contacts, self.shipping_location.site_contacts)
self.assertEqual(so.work_order_contacts, self.shipping_location.work_order_contacts)
+
+ def test_onchange_shipping_address(self):
+ so = self.env['sale.order'].create({
+ 'partner_id': self.parent_co.id,
+ 'partner_shipping_id': self.parent_co.id,
+ })
+ self.assertFalse(so.work_order_contacts)
+ self.assertFalse(so.site_contacts)
+
+ form = Form(so)
+ form.partner_shipping_id = self.shipping_location
+ form.save()
+
+ self.assertEqual(so.work_order_contacts, self.shipping_location.work_order_contacts)
+ self.assertEqual(so.site_contacts, self.shipping_location.site_contacts)
diff --git a/bemade_fsm/tests/test_fsm_visit.py b/bemade_fsm/tests/test_fsm_visit.py
new file mode 100644
index 0000000..9928f4e
--- /dev/null
+++ b/bemade_fsm/tests/test_fsm_visit.py
@@ -0,0 +1,91 @@
+from odoo.tests import TransactionCase, tagged, Form
+
+
+@tagged('-at_install', 'post_install')
+class FSMVisitTest(TransactionCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ def test_create_visit_sets_name_on_section(self):
+ so = self.generate_sale_order()
+ self.add_service_so_line(so)
+
+ visit = self.generate_visit(so)
+
+ self.assertTrue(visit.so_section_id)
+ self.assertEqual(visit.so_section_id.name, visit.label)
+
+ def test_change_visit_section_name(self):
+ so = self.generate_sale_order()
+ visit = self.generate_visit(so, label="First Label")
+ line = visit.so_section_id
+
+ line.name = "Second Label"
+
+ self.assertEqual(visit.label, "Second Label")
+
+ def test_change_visit_label_changes_section_name(self):
+ so = self.generate_sale_order()
+ visit = self.generate_visit(so, label="First Label")
+ line = visit.so_section_id
+
+ visit.label = "Second Label"
+
+ self.assertEqual(line.name, "Second Label")
+
+ def test_visit_completes_when_task_completes(self):
+ so = self.generate_sale_order()
+ visit = self.generate_visit(so)
+ sol = self.add_service_so_line(so, task=True)
+ so.action_confirm()
+ task = so.order_line.filtered(lambda l: l.task_id).task_id
+
+ task.action_fsm_validate()
+
+ self.assertTrue(visit.is_completed)
+
+ def generate_visit(self, sale_order, label="Test Label"):
+ return self.env['bemade_fsm.visit'].create([{
+ 'sale_order_id': sale_order.id,
+ 'label': label,
+ }])
+
+ def generate_sale_order(self, partner_id=None):
+ so = self.env['sale.order'].create({
+ 'partner_id': partner_id or self.generate_partner().id,
+ 'client_order_ref': 'Test',
+ })
+ return so
+
+ def add_service_so_line(self, sale_order, task: bool = False):
+ """ Generates a sales order line for a service product.
+
+ :param sale_order: The sales order to which the new line is to be added
+ :param task: If true, the created line will be for a product with service_tracking=task_global_project
+ """
+ if not task:
+ service_tracking = 'no'
+ project = False
+ else:
+ project = self.env.ref("industry_fsm.fsm_project")
+ service_tracking = 'task_global_project'
+ product = self.env['product.product'].create({
+ 'name': 'Test Product',
+ 'type': 'service',
+ 'service_tracking': service_tracking,
+ 'project_id': project and project.id,
+ 'price': 100.0,
+ 'sale_ok': True,
+ })
+ return self.env['sale.order.line'].create({
+ 'product_id': product.id,
+ 'order_id': sale_order.id,
+ })
+
+ def generate_partner(self):
+ partner = self.env['res.partner'].create({
+ 'name': 'Test Partner',
+ 'company_type': 'company',
+ })
+ return partner
diff --git a/bemade_fsm/views/sale_order_views.xml b/bemade_fsm/views/sale_order_views.xml
index d0b01fb..0421d41 100644
--- a/bemade_fsm/views/sale_order_views.xml
+++ b/bemade_fsm/views/sale_order_views.xml
@@ -8,7 +8,11 @@
-
+
+
+
+
@@ -21,5 +25,17 @@
+
+ bemade_fsm.visit.tree
+ bemade_fsm.visit
+
+
+
+
+
+
+
+
+
\ No newline at end of file