Merge branch '17.0-dev' into '17.0' for partner_scrapper fixes

This commit is contained in:
Marc Durepos 2024-01-09 15:25:04 -05:00
commit 89056849e2
51 changed files with 773 additions and 286 deletions

View file

@ -0,0 +1 @@
from . import models

View file

@ -0,0 +1,21 @@
{
'name': 'Account Credit Hold',
'version': '16.0.1.0.0',
'summary': 'Allows setting clients on credit hold, blocking the ability confirm a new sales order.',
'description': 'Allows setting clients on hold, blocking the ability confirm a new sales order.',
'category': 'Accounting/Accounting',
'author': 'Bemade Inc.',
'maintainer': 'Marc Durepos <marc@bemade.org>',
'website': 'http://www.bemade.org',
'license': 'LGPL-3',
'depends': ['sale', 'account_followup', 'stock'],
'data': [
'views/account_followup_views.xml',
'views/sale_order_views.xml',
'views/res_partner_views.xml',
'views/stock_picking_views.xml',
],
'demo': [],
'installable': True,
'auto_install': False
}

View file

@ -0,0 +1,5 @@
from . import account_followup
from . import res_partner
from . import sale_order
from . import stock_picking
from . import account_followup_report

View file

@ -0,0 +1,8 @@
from odoo import fields, models, api
class FollowupLine(models.Model):
_inherit = 'account_followup.followup.line'
account_hold = fields.Boolean(string="Place on Credit Hold",
help="Place clients on account hold, restricting confirmation of new orders.")

View file

@ -0,0 +1,12 @@
from odoo import models, fields, api, _
class FollowUpReport(models.AbstractModel):
_inherit = 'account.followup.report'
def _get_line_info(self, followup_line):
res = super()._get_line_info(followup_line)
res.update({
'credit_hold': followup_line.account_hold
})
return res

View file

@ -0,0 +1,63 @@
from odoo import fields, models, api, _
from datetime import date
class Partner(models.Model):
_inherit = 'res.partner'
postpone_hold_until = fields.Date(string="Postpone Hold",
help="Grace period specific to this partner despite unpaid invoices.", )
hold_bg = fields.Boolean(string="Hold (technical)",
compute="_compute_hold_bg",
store=True,
default=False)
on_hold = fields.Boolean(string="Account on Hold",
help="Client account is on hold for unpaid overdue invoices.",
compute="_compute_on_hold")
@api.depends('postpone_hold_until', 'hold_bg')
def _compute_on_hold(self):
# manually re-compute hold_bg since followup_status doesn't get updated in Python but gets recalculated
# by an SQL query every time
self._compute_hold_bg()
for rec in self:
if rec.hold_bg and not (rec.postpone_hold_until and rec.postpone_hold_until > date.today()):
rec.on_hold = True
else:
if rec.on_hold:
rec.message_post(_("Credit hold lifted."))
rec.on_hold = False
@api.autovacuum
def _cleanup_expired_hold_postponements(self):
expired_holds = self.search([('postpone_hold_until', '<=', date.today())])
expired_holds.write({'postpone_hold_until': False})
def action_credit_hold(self):
for rec in self:
rec.hold_bg = True
rec.message_post(body=_('Placed on credit hold.'))
def action_lift_credit_hold(self):
for rec in self:
rec.hold_bg = False
rec.message_post(body=_('Credit hold lifted.'))
def _execute_followup_partner(self):
res = super()._execute_followup_partner()
if self.followup_status == 'in_need_of_action':
if self.followup_line_id.account_hold:
self.action_credit_hold()
return res
@api.depends('followup_status', 'followup_line_id')
def _compute_hold_bg(self):
first_followup_level = self._get_first_followup_level()
for rec in self:
prev_hold_bg = rec.hold_bg
level = rec.followup_line_id
if rec.followup_status == 'no_action_needed' and level == first_followup_level:
rec.hold_bg = False
else:
rec.hold_bg = prev_hold_bg

View file

@ -0,0 +1,17 @@
from odoo import fields, models, api, _
from odoo.exceptions import UserError
class SaleOrder(models.Model):
_inherit = "sale.order"
client_on_hold = fields.Boolean(string='Client on Hold',
help="Whether or not a client has been put on hold due to unpaid invoices.",
related="partner_id.on_hold")
@api.depends('client_on_hold')
def action_confirm(self):
if any(self.mapped('client_on_hold')):
raise UserError(_("This client is on credit hold. No new orders can be confirmed until past-due invoices "
"are paid or the accounting team postpones the hold."))
super().action_confirm()

View file

@ -0,0 +1,9 @@
from odoo import fields, models, api
class ModelName(models.Model):
_inherit = "stock.picking"
client_on_hold = fields.Boolean(string='Client on Hold',
help="Whether or not a client has been put on hold due to unpaid invoices.",
related="partner_id.on_hold")

View file

@ -0,0 +1,35 @@
# Overview
This module adds the notion of placing clients on credit hold to the followup levels from the Odoo Enterprise
account_followup module. It adds an option to followup levels to mark clients matching the followup criteria as on
credit hold. This hold restricts the confirmation of new sales orders for these clients.
Accountant and admin users can set a date until which the account hold will be
postponed on a specific partner's form view. This effectively gives clients an extra
grace period, allowing orders to be confirmed until the period ends.
# Change Log
## 15.0.2.0.0 (2023-05-04)
Complete remake of the module, making the "Credit Hold" an action that is either manually or
automatically triggered from the Accounting > Followup Reports section or by setting the automatic application field
on followup levels.
## 15.0.1.1.0 (2023-05-03)
Adds a ribbon to stock pickings for clients on hold, and therefore a dependency on stock.
## 15.0.1.0.2 (2023-05-03)
Fix to sale order view and sale order confirmation for clients not on hold.
## 15.0.1.0.1 (2023-05-03)
Fix clients on hold when status is "outstanding_invoices".
## 15.0.1.0.0 (2023-05-02) Initial Release
Initial release of the module, including a setting on follow-up levels to toggle placing on credit hold. Blocks
the confirmation of sales orders for clients on credit hold. Red "Credit Hold" banner appears on sales orders and
partner form view when a client is on credit hold. Credit hold can be postponed by setting the "Postpone Hold" field
on the partner form view.

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="account_followup_followup_line_form_inherit" model="ir.ui.view">
<field name="name">account_credit_hold.account_followup_line.form</field>
<field name="model">account_followup.followup.line</field>
<field name="inherit_id" ref="account_followup.view_account_followup_followup_line_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='send_email']" position="before">
<field name="account_hold"/>
</xpath>
</field>
</record>
<record id="customer_statements_form_view_inherit" model="ir.ui.view">
<field name="name">customer.statements.form.view.inherit</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="account_followup.customer_statements_form_view"/>
<field name="arch" type="xml">
<xpath expr="//button[last()]" position="after">
<field name="hold_bg" invisible="1"/>
<button
string="Credit Hold"
type="object"
name="action_credit_hold"
class="button btn-secondary"
attrs="{'invisible': [('hold_bg', '=', True)]}"
/>
<button
string="Lift Credit Hold"
type="object"
name="action_lift_credit_hold"
class="button btn-secondary"
attrs="{'invisible': [('hold_bg', '=', False)]}"
/>
</xpath>
</field>
</record>
<record id="action_credit_hold" model="ir.actions.server">
<field name="name">action_credit_hold</field>
<field name="model_id" ref="base.model_res_partner"/>
<field name="binding_model_id" ref="base.model_res_partner"/>
<field name="binding_view_types">list,form</field>
<field name="state">code</field>
<field name="code">
records.action_credit_hold()
</field>
</record>
</data>
</odoo>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="res_partner_form_inherit" model="ir.ui.view">
<field name="name">account_credit_hold.res_partner.form</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="before">
<field name="hold_bg" invisible="True" />
<field name="on_hold" invisible="True" />
<widget name="web_ribbon" title="Credit Hold"
bg_color="bg-danger" attrs="{'invisible': [('on_hold','=',False)]}"/>
</xpath>
</field>
</record>
<record id="view_partner_property_form_inherit" model="ir.ui.view">
<field name="name">account_credit_hold.view_partner_property_form</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="account.view_partner_property_form"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='banks']" position="before">
<group string="Credit Hold" >
<field name="postpone_hold_until"
groups="account.group_account_manager,account.group_account_user"
attrs="{'readonly': [('hold_bg','=',False), ('postpone_hold_until','=',False)]}"/>
</group>
</xpath>
</field>
</record>
</data>
</odoo>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="sale_order_form_inherit" model="ir.ui.view">
<field name="name">account_credit_hold.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="//div[@name='button_box']" position="before">
<field name="client_on_hold" invisible="True" />
<widget name="web_ribbon" title="Credit Hold"
bg_color="bg-danger" attrs="{'invisible': [('client_on_hold','=',False)] }"/>
</xpath>
</field>
</record>
</data>
</odoo>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="stock_picking_form_inherit" model="ir.ui.view">
<field name="name">account_credit_hold.stock_picking.form</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_form"/>
<field name="priority" eval="8"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="before">
<field name="client_on_hold" invisible="True" />
<widget name="web_ribbon" title="Credit Hold"
bg_color="bg-danger" attrs="{'invisible': [('client_on_hold','=',False)] }"/>
</xpath>
</field>
</record>
</data>
</odoo>

View file

@ -1 +1,2 @@
from . import models
from . import wizard

View file

@ -20,7 +20,7 @@
########################################################################################
{
'name': 'Improved Field Service Management',
'version': '15.0.1.0.0',
'version': '17.0.0.1.0',
'summary': 'Adds functionality necessary for managing field service operations at Durpro.',
'description': 'Adds functionality necessary for managing field service operations at Durpro.',
'category': 'Services/Field Service',
@ -43,7 +43,6 @@
'mail',
],
'data': [
# BV: FOR MIGRATION
'data/fsm_data.xml',
'views/task_template_views.xml',
'views/equipment.xml',
@ -53,8 +52,14 @@
'views/menus.xml',
'views/task_views.xml',
'views/sale_order_views.xml',
<<<<<<< HEAD
'reports/worksheet_custom_report_templates.xml',
'reports/worksheet_custom_reports.xml',
=======
# 'reports/worksheet_custom_report_templates.xml',
# 'reports/worksheet_custom_reports.xml',
>>>>>>> 5a006ec3
'wizard/new_task_from_template.xml',
],
'assets': {
'web.assets_tests': [
@ -64,6 +69,13 @@
],
'web.report_assets_common': [
'bemade_fsm/static/src/scss/bemade_fsm.scss'
],
'web.assets_backend': [
'bemade_fsm/static/src/js/kanban_view.js',
'bemade_fsm/static/src/js/list_view.js',
],
'web.assets_qweb': [
'bemade_fsm/static/src/xml/project_view_buttons.xml',
]
},
'installable': True,

View file

@ -76,6 +76,12 @@ class Task(models.Model):
work_order_number = fields.Char(readonly=True)
propagate_assignment = fields.Boolean(
string='Propagate Assignment',
help='Propagate assignment of this task to all subtasks.',
default=False,
)
@api.model_create_multi
def create(self, vals):
res = super().create(vals)
@ -92,6 +98,16 @@ class Task(models.Model):
+ f"-{seq}"
return res
def write(self, vals):
super().write(vals)
if not self: # End recursion on empty RecordSet
return
if 'user_ids' in vals:
to_propagate = self.filtered(lambda task: task.propagate_assignment)
# Here we use child_ids instead of _get_all_subtasks() so as to allow for setting propagate_assignment
# to false on a child task.
to_propagate.child_ids.write({'user_ids': vals['user_ids']})
@api.depends('sale_order_id')
def _compute_relevant_order_lines(self):
for rec in self:
@ -234,6 +250,11 @@ class Task(models.Model):
task.write(values)
def _get_full_hierarchy(self):
if self.child_ids:
return self | self.child_ids._get_full_hierarchy()
return self
def synchronize_name_fsm(self):
""" Applies naming to the entire task tree for tasks that are part of this
recordset. Root tasks are named:

View file

@ -81,3 +81,38 @@ class TaskTemplate(models.Model):
for rec in self:
new_equipment_ids = [eq.id for eq in rec.equipment_ids if eq.partner_location_id == rec.customer]
rec.write({'equipment_ids': [Command.set(new_equipment_ids)]})
def _prepare_new_task_values_from_self(self, project, name=False, parent_id=False):
vals = {
'project_id': project.id,
'name': name or self.name,
'description': self.description,
'parent_id': parent_id,
'user_ids': self.assignees.ids,
'tag_ids': self.tags.ids,
'planned_hours': self.planned_hours,
'sequence': self.sequence,
'equipment_ids': [Command.set(self.equipment_ids.ids)] if self.equipment_ids else False,
'partner_id': project.partner_id and project.partner_id.id,
'company_id': self.company_id.id,
}
return vals
def create_task_from_self(self, project, name=False, parent_id=False):
""" Create a project.task from this template and return it. Can be called on a RecordSet of multiple templates.
:param project: project.project record the task should be added to
:param name: name for the new task (defaults to template name)
:param parent_id: parent task for the new task (none by default)
:return: project.task record created from this template
"""
tasks = self.env['project.task']
for rec in self:
vals = rec._prepare_new_task_values_from_self(project, name, parent_id)
task = rec.env['project.task'].create(vals)
rec.subtasks.create_task_from_self(project, name=False, parent_id=task.id)
tasks |= task
return tasks

View file

@ -5,3 +5,4 @@ access_bemade_fsm_equipment,bemade_fsm_equipment,model_bemade_fsm_equipment,base
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
access_bemade_fsm_visit,access_bemade_fsm_visit,model_bemade_fsm_visit,base.group_user,1,1,1,1
access_bemade_fsm_task_wizard,access_bemade_fsm_task_wizard,model_project_task_from_template_wizard,project.group_project_user,1,1,1,1

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
5 access_bemade_fsm_equipment_tag bemade_fsm_equipment_tag model_bemade_fsm_equipment_tag base.group_user 1 1 1 1
6 access_bemade_fsm_equipment_type access_bemade_fsm_equipment_type model_bemade_fsm_equipment_type base.group_user 1 0 0 0
7 access_bemade_fsm_visit access_bemade_fsm_visit model_bemade_fsm_visit base.group_user 1 1 1 1
8 access_bemade_fsm_task_wizard access_bemade_fsm_task_wizard model_project_task_from_template_wizard project.group_project_user 1 1 1 1

View file

@ -0,0 +1,37 @@
/** @odoo-module **/
import KanbanController from 'web.KanbanController';
import KanbanView from 'web.KanbanView';
const viewRegistry = require('web.view_registry')
const ProjectKanbanController = KanbanController.extend({
buttons_template: 'project.KanbanView.buttons',
custom_events: _.extend({}, KanbanController.prototype.custom_events, {
create_from_template: '_onCreateFromTemplate',
}),
_onCreateFromTemplate(ev) {
ev.stopPropagation();
const record = this.model.get(this.handle, {raw: true});
let context = record.context;
context.active_id = record.project_id;
this.do_action({
type: 'ir.actions.act_window',
res_model: 'project.task.from.template.wizard',
views: [[false, 'form']],
target: 'new',
context: {...record.context, res_model: 'project.task'},
});
},
renderButtons ($node) {
this._super.apply(this, arguments);
this.$buttons.on('click', 'button.o-kanban-button-new-from-template', this._onCreateFromTemplate.bind(this))
},
})
const ProjectKanbanView = KanbanView.extend({
config: _.extend({}, KanbanView.prototype.config, {
Controller: ProjectKanbanController,
})
});
viewRegistry.add('project_kanban', ProjectKanbanView)

View file

@ -0,0 +1,36 @@
/** @odoo-module **/
import ListController from 'web.ListController';
import ListView from 'web.ListView';
const viewRegistry = require('web.view_registry')
const ProjectListController = ListController.extend({
buttons_template: 'project.ListView.buttons',
custom_events: _.extend({}, ListController.prototype.custom_events, {
create_from_template: '_onCreateFromTemplate',
}),
_onCreateFromTemplate(ev) {
ev.stopPropagation();
const record = this.model.get(this.handle, {raw: true});
this.do_action({
type: 'ir.actions.act_window',
res_model: 'project.task.from.template.wizard',
views: [[false, 'form']],
target: 'new',
context: {...record.context, res_model: 'project.task'},
});
},
renderButtons ($node) {
self = this;
this._super.apply(this, arguments);
this.$buttons.on('click', 'button.o_list_button_add_from_template', this._onCreateFromTemplate.bind(this))
},
})
const ProjectListView = ListView.extend({
config: _.extend({}, ListView.prototype.config, {
Controller: ProjectListController,
})
});
viewRegistry.add('project_list', ProjectListView)

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<!-- Add a button to create a task from a template to task (project) list and kanban views -->
<t t-name='project.KanbanView.buttons' t-inherit="web.KanbanView.buttons" t-inherit-mode="primary">
<xpath expr="//button[contains(@t-attf-class, 'o-kanban-button-new')]" position="after">
<button title="Create Task from Template" t-if="!noCreate" type="button"
t-attf-class="btn {{ btnClass }} o-kanban-button-new-from-template">
<t t-esc="create_text &amp;&amp; (create_text + ' from Template') || _t('Create from Template')"/>
</button>
</xpath>
</t>
<t t-name='project.ListView.buttons' t-inherit="web.ListView.buttons" t-inherit-mode="primary">
<xpath expr="//button[hasclass('o_list_button_add')]" position="after">
<!-- Create is enabled in the parent template at this point; check is done prior -->
<button type="button" class="btn ml-1 btn-primary o_list_button_add_from_template"
title="Create Task from Template">
Create from Template
</button>
</xpath>
</t>
</templates>

View file

@ -1,104 +0,0 @@
/** @odoo-module **/
import tour from 'web_tour.tour';
const TEST_COMPANY = "Test Partner";
const TEST_EQPT1 = "Test Equipment 1";
const TEST_EQPT2 = "Test Equipment 2";
tour.register('equipment_base_tour', {
test: true,
url: '/web',
}, /* Create a new "Test Equipment" and link it to "Test Partner Company" */
[tour.stepUtils.showAppsMenuItem(), {
content: 'Navigate to the Service menu',
trigger: '.o_app[data-menu-xmlid="industry_fsm.fsm_menu_root"]',
}, {
content: 'Navigate to the Clients submenu',
trigger: 'button.dropdown-toggle[data-menu-xmlid="bemade_fsm.menu_service_client"]',
}, {
content: 'Navigate to the Equipment menu',
trigger: '.dropdown-item[data-menu-xmlid="bemade_fsm.menu_service_client_equipment"]',
}, {
content: 'Click the create button',
trigger: '.o_list_button_add',
}, {
content: 'Add a tag',
trigger: 'input[name="pid_tag"]',
run: 'text TestPIDTag',
}, {
content: 'Set the name',
trigger: 'input[name="name"]',
run: `text ${TEST_EQPT2}`,
}, {
content: 'Set the partner',
trigger: 'div[name="partner_location_id"] div div input',
run: `text ${TEST_COMPANY}`,
}, {
content: 'Click the partner in the dropdown',
trigger: `li a.dropdown-item:contains(${TEST_COMPANY})`,
}, {
content: 'Save equipment',
trigger: 'button.o_list_button_save',
}, {
/* Navigate to the client and make sure that there are two equipments saved (one from the Python test case) */
content: 'Navigate to the Clients submenu',
trigger: 'button.dropdown-toggle[data-menu-xmlid="bemade_fsm.menu_service_client"]',
}, {
content: 'Click on the clients submenu',
trigger: '.dropdown-item[data-menu-xmlid="bemade_fsm.menu_service_client_clients"]',
}, {
content: 'Search for the Test Partner Company',
trigger: 'input.o_searchview_input',
extra_trigger: 'li.breadcrumb-item.active:has(span:contains(Customers))',
run: `text ${TEST_COMPANY}`
}, {
content: 'Validate Search',
trigger: '.o_menu_item.o_selection_focus',
run: 'click',
}, {
content: 'Open the test client.',
trigger: `div.o_kanban_record:has(span:contains(${TEST_COMPANY}))`,
}, {
content: 'Click the Field Service tab.',
trigger: 'a.nav-link:contains(Field Service)',
extra_trigger: `h1 span.o_field_partner_autocomplete[name="name"]:contains(${TEST_COMPANY})`,
}, {
content: 'Make sure we have a first test equipment',
trigger: `td:contains(${TEST_EQPT1})`,
run: function () {
},
}, {
content: 'Make sure we have a second test equipment',
trigger: `div[name="equipment_ids"]:has(td:contains(${TEST_EQPT2}))`,
run: function () {
},
}
]);
tour.register('equipment_sale_order_tour', {
test: true,
url: '/web',
}, [tour.stepUtils.showAppsMenuItem(), {
content: 'Navigate to the Sales menu',
trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]',
}, {
content: 'Create an order',
trigger: 'button.o_list_button_add',
}, {
content: 'Select the test partner',
trigger: 'div[name="partner_id"] input',
run: `text ${TEST_COMPANY}`,
}, {
content: 'Click the partner in the dropdown',
trigger: `li a.dropdown-item:contains(${TEST_COMPANY})`,
}, {
content: 'Save',
trigger: 'button.o_form_button_save',
}, {
content: 'Navigate to the Field Service tab.',
trigger: 'a.nav-link[role="tab"]:contains(Field Service)'
}, {
content: 'Check that the equipment is listed in the tab.',
trigger: `li.ui-menu-item > a:contains(${TEST_EQPT1})`,
}
]);

View file

@ -1,40 +0,0 @@
/** @odoo-module **/
import tour from 'web_tour.tour';
const SO_NAME = "TEST ORDER 2"
const PRODUCT_NAME = "Test Product 3"
tour.register('sale_order_tour', {
test: true,
url: '/web',
},
[tour.stepUtils.showAppsMenuItem(), {
content: 'Navigate to the Service menu',
trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]',
}, {
content: 'Search for the sales order',
trigger: 'input.o_searchview_input',
run: `text ${SO_NAME}`,
}, {
content: 'Validate Search',
trigger: '.o_menu_item.o_selection_focus',
run: 'click',
}, {
content: 'Open the test order',
trigger: `.o_data_cell[name="client_order_ref"]:contains(${SO_NAME})`,
}, {
content: 'Click the view tasks button',
trigger: 'button[name="action_view_task"]',
}, /*{
content: 'Click the first task',
trigger: `div.o_kanban_record:has(span:contains(${PRODUCT_NAME}))`,
},*/ {
content: 'Click on the ready to invoice button',
trigger: 'button[name="action_fsm_validate"]',
extra_trigger: `li.breadcrumb-item.active:has(span:contains(${PRODUCT_NAME}))`
}, {
content: 'View the SO',
trigger: 'button[name="action_view_so"]',
// extra_trigger: 'button[title="Current state"]:contains(Done)',
}
]);

View file

@ -1,75 +0,0 @@
/** @odoo-module **/
import tour from 'web_tour.tour';
const TEST_COMPANY = "Test Partner Company";
const TEST_EQPT1 = "Test Equipment 1";
const TEST_EQPT2 = "Test Equipment 2";
tour.register('task_equipment_tour', {
test: true,
url: '/web',
}, /* Create a new "Test Equipment" and link it to "Test Partner Company" */
[tour.stepUtils.showAppsMenuItem(), {
content: 'Navigate to the Service menu',
trigger: '.o_app[data-menu-xmlid="industry_fsm.fsm_menu_root"]',
}, {
content: 'Navigate to the Clients submenu',
trigger: 'button.dropdown-toggle[data-menu-xmlid="bemade_fsm.menu_service_client"]',
}, {
content: 'Navigate to the Equipment menu',
trigger: '.dropdown-item[data-menu-xmlid="bemade_fsm.menu_service_client_equipment"]',
}, {
content: 'Click the create button',
trigger: '.o_list_button_add',
extra_trigger: 'li.breadcrumb-item.active:has(span:contains(Equipment))',
}, {
content: 'Add a tag',
trigger: 'input[name="pid_tag"]',
run: 'text TestPIDTag',
}, {
content: 'Set the name',
trigger: 'input[name="name"]',
run: `text ${TEST_EQPT2}`,
}, {
content: 'Set the partner',
trigger: 'div[name="partner_location_id"] div div input',
run: 'text Test Partner Company',
}, {
content: 'Click the partner in the dropdown',
trigger: `li a.dropdown-item:contains(${TEST_COMPANY})`,
}, {
content: 'Save equipment',
trigger: 'button.o_list_button_save',
}, {
/* Navigate to the client and make sure that there are two equipments saved (one from the Python test case) */
content: 'Navigate to the Clients submenu',
trigger: 'button.dropdown-toggle[data-menu-xmlid="bemade_fsm.menu_service_client"]',
}, {
content: 'Click on the clients submenu',
trigger: '.dropdown-item[data-menu-xmlid="bemade_fsm.menu_service_client_clients"]',
}, {
content: 'Search for the Test Partner Company',
trigger: 'input.o_searchview_input',
extra_trigger: 'li.breadcrumb-item.active:has(span:contains(Customers))',
run: `text ${TEST_COMPANY}`
}, {
content: 'Validate Search',
trigger: '.o_menu_item.o_selection_focus',
run: 'click',
}, {
content: 'Open the test client.',
trigger: `div.o_kanban_record:has(span:contains(${TEST_COMPANY}))`,
}, {
content: 'Click the Field Service tab.',
trigger: 'a.nav-link:contains(Field Service)',
extra_trigger: `h1 span.o_field_partner_autocomplete[name="name"]:contains(${TEST_COMPANY})`,
}, {
content: 'Make sure we have a first test equipment',
/*trigger: `div[name="equipment_ids"]:has(td:contains(${TEST_EQPT1}))`,*/
trigger: `td:contains(${TEST_EQPT1})`,
run: function() {},
}, {
content: 'Make sure we have a second test equipment',
trigger: `div[name="equipment_ids"]:has(td:contains(${TEST_EQPT2}))`,
run: function() {},
}
])

View file

@ -1,47 +0,0 @@
/** @odoo-module */
import tour from 'web_tour.tour';
tour.register('task_template_tour', {
test: true,
url: '/web',
}, /* Make sure the test task is visible via the Project > Task Templates path */
[tour.stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="project.menu_main_pm"]',
}, {
content: 'Open the task templates list view.',
trigger: '.o_nav_entry[data-menu-xmlid="bemade_fsm.project_task_template_menu"]',
}, {
content: 'Open the existing task template.',
trigger: '.o_data_cell:contains("Template 1"):parent()',
}, {
content: 'Confirm that the header contains the task title.',
trigger: 'span[name="name"]:contains("Template 1")',
}, /* Make sure we can see the task template from the product */
tour.stepUtils.toggleHomeMenu(),
{
content: 'Open the sales menu.',
trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]',
}, {
content: 'Open the product dropdown menu.',
trigger: 'button[data-menu-xmlid="sale.product_menu_catalog"]',
}, {
content: 'Click the products menu item.',
trigger: 'a.dropdown-item[data-menu-xmlid="sale.menu_product_template_action"]',
}, {
content: 'Search for the test product.',
trigger: 'input.o_searchview_input',
extra_trigger: 'li.breadcrumb-item.active:has(span:contains(Products))',
run: 'text Test Product 1',
}, {
trigger: '.o_menu_item.o_selection_focus',
content: 'Validate search',
run: 'click',
}, {
content: 'Open the test product.',
trigger: 'div.o_kanban_record:has(span:contains(Test Product 1))',
}, {
content: 'Ensure the product_template_id field is displayed with the "Template 1" mention.',
trigger: 'a.o_quick_editable[name="task_template_id"]:first-child:contains("Template 1")',
},
])

View file

@ -4,3 +4,4 @@ from . import test_sale_order
from . import test_equipment
from . import test_fsm_contact_setting
from . import test_fsm_visit
from . import test_task

View file

@ -251,7 +251,7 @@ class TestSalesOrder(BemadeFSMBaseTest):
self.assertFalse(visit_task.user_ids)
self.assertFalse(subtask1.user_ids)
self.assertFalse(subtask2.user_ids)
def test_long_line_name_overflows_to_task_description(self):
so = self._generate_sale_order()
product = self._generate_product()

View file

@ -0,0 +1,42 @@
from .test_bemade_fsm_common import BemadeFSMBaseTest
from odoo.tests.common import tagged
from odoo import Command
@tagged('post_install', '-at_install')
class TaskTest(BemadeFSMBaseTest):
def test_reassigning_assignment_propagating_task_changes_subtasks(self):
# This creation step is a bit lazy - we use defaults to make tasks with a hierachy and settings we want
so = self._generate_sale_order()
template = self._generate_task_template(names=['Parent', 'Child'], structure=[2])
product = self._generate_product(task_template=template)
sol = self._generate_sale_order_line(sale_order=so, product=product)
user = self._generate_project_manager_user('Bob', 'Bob')
so.action_confirm()
task = sol.task_id
task.write({
'user_ids': [Command.set([user.id])],
'propagate_assignment': True,
})
self.assertTrue(all([t.user_ids == user for t in task | task._get_all_subtasks()]))
def test_reassigning_assignment_non_propagating_task_doesnt_change_subtasks(self):
so = self._generate_sale_order()
template = self._generate_task_template(names=['Parent', 'Child', 'Grandchild'], structure=[2, 1])
product = self._generate_product(task_template=template)
sol = self._generate_sale_order_line(sale_order=so, product=product)
user = self._generate_project_manager_user('Bob', 'Bob')
so.action_confirm()
task = sol.task_id
task.child_ids.write({'propagate_assignment': False}) # Stop propagation after the first level
task.write({
'user_ids': [Command.set([user.id])],
'propagate_assignment': True,
})
self.assertTrue(all([t.user_ids == user for t in task | task.child_ids]))
self.assertFalse(any([t.user_ids for t in task.child_ids.child_ids]))

View file

@ -78,3 +78,17 @@ class TestTaskTemplate(BemadeFSMBaseTest):
self.assertEqual(sol1.task_id.name, "Short Name 1")
self.assertEqual(sol2.task_id.name, "Short Name 2")
self.assertEqual(sol3.task_id.name, "Task")
def test_task_creation_directly_from_template(self):
project = self.env.ref("industry_fsm.fsm_project")
template = self._generate_task_template(names=['Task', 'Child', 'Grandchild'], structure=[2, 1])
task = template.create_task_from_self(project, "My new task")
self.assertEqual(len(task.child_ids), len(template.subtasks))
self.assertEqual(len(task.child_ids[0].child_ids), len(template.subtasks[0].subtasks))
self.assertEqual(len(task.child_ids[1].child_ids), len(template.subtasks[1].subtasks))
self.assertEqual(task.name, "My new task")
self.assertEqual(task.child_ids[0].name, template.subtasks[0].name)
self.assertTrue(all([t.project_id == project for t in task | task._get_all_subtasks()]))

View file

@ -46,6 +46,9 @@
<xpath expr="//field[@name='child_ids']/tree//field[@name='name']" position="after">
<field name="description" string="Description/Comments"/>
</xpath>
<field name="user_ids" position="after">
<field name="propagate_assignment"/>
</field>
</field>
</record>
<!-- Add parent_id = false to domain for My Tasks, All Tasks: To Schedule, All Tasks and To Invoice-->
@ -168,4 +171,4 @@
</field>
</record>
</data>
</odoo>
</odoo>

View file

@ -0,0 +1 @@
from . import new_task_from_template

View file

@ -0,0 +1,51 @@
from odoo import models, fields, api, _
from odoo.exceptions import UserError
class NewTaskFromTemplateWizard(models.TransientModel):
_name = "project.task.from.template.wizard"
_description = "Create Task from Template Wizard"
project_id = fields.Many2one(
comodel_name='project.project',
string='Project',
help='The project the new task should be created in.',
required=True,
)
task_template_id = fields.Many2one(
comodel_name='project.task.template',
string='Task Template',
help='The template to use when creating the new task.',
required=True,
)
new_task_title = fields.Char(
help='The title (name) for the newly created task. If left blank, the name of the template will be used.',
)
def default_get(self, fields_list):
res = super().default_get(fields_list)
active_id = self.env.context.get('active_id', False)
active_model = self.env.context.get('active_model', False)
if not active_model:
params = self.env.context.get('params', False)
active_model = params and params.get('model', False)
if active_model == 'project.task.template' and active_id and 'task_template_id' in fields_list:
res.update({'task_template_id': active_id})
if active_model == 'project.task' and 'project_id' in fields_list:
res.update({'project_id': self.env.ref('industry_fsm.fsm_project').id})
return res
def action_create_task_from_template(self):
self.ensure_one()
task = self.task_template_id.create_task_from_self(self.project_id, self.new_task_title)
return {
'type': 'ir.actions.act_window',
'res_model': 'project.task',
'res_id': task.id,
'view_mode': 'form',
'target': 'current',
}

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="project_task_from_template_wizard_view_form" model="ir.ui.view">
<field name="name">project.task.from.template.wizard.view.form</field>
<field name="model">project.task.from.template.wizard</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="task_template_id"/>
<field name="new_task_title"/>
<field name="project_id"/>
</group>
</sheet>
<footer>
<button class='btn btn-primary' name="action_create_task_from_template" type="object" string="Create Task"/>
<button class='btn btn-secondary' special="cancel" string="Cancel"/>
</footer>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="action_new_task_from_template">
<field name="name">New Task from Template</field>
<field name="res_model">project.task.from.template.wizard</field>
<field name="binding_model_id" ref="bemade_fsm.model_project_task_template"/>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>

View file

@ -0,0 +1,37 @@
#
# Bemade Inc.
#
# Copyright (C) 2023-June Bemade Inc. (<https://www.bemade.org>).
# Author: Marc Durepos (Contact : mdurepos@durpro.com)
#
# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1)
# It is forbidden to publish, distribute, sublicense, or sell copies of the Software
# or modified copies of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
#
{
'name': 'Full Form from Dialog',
'version': '16.0.1.0.0',
'summary': 'Allows opening the full form view from the dialog (modal) view.',
'description': 'Adds a button to open the full form view when viewing the form view for a record in a dialog.',
'category': 'Technical',
'author': 'Bemade Inc.',
'website': 'http://www.bemade.org',
'license': 'OPL-1',
'depends': [],
'data': [],
'assets': {
'web.assets_backend': [
'bemade_full_formview_from_modal/static/src/**/*',
],
},
'installable': True,
'auto_install': False
}

View file

@ -0,0 +1,22 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { FormController } from "@web/views/form/form_controller";
import { useService } from "@web/core/utils/hooks";
patch(FormController.prototype, "bemade_full_formview_from_modal.FormController", {
setup () {
this._super();
this.action = useService("action")
},
onOpenButtonClicked: function () {
this.action.doAction({
type: "ir.actions.act_window",
res_model: this.props.resModel,
res_id: this.props.resId,
views: [[false, "form"]],
target: "current",
context: this.props.context,
})
}
});

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-inherit="web.FormViewDialog.ToMany.buttons" t-inherit-mode="extension" owl="1">
<xpath expr="//button[hasclass('o_form_button_save_new')]" position="after">
<button class="btn btn-secondary o_form_button_open" t-on-click="_onOpen">Open</button>
</xpath>
</t>
<t t-inherit="web.FormViewDialog.ToOne.buttons" t-inherit-mode="extension" owl="1">
<xpath expr="//button[hasclass('o_form_button_save')]" position="after">
<button class="btn btn-secondary o_form_button_open" t-on-click="onOpenButtonClicked">Open</button>
</xpath>
</t>
</templates>

View file

@ -20,12 +20,13 @@
'data': [
# No security
'views/res_partner_views.xml',
# Commented for migration to 17, raises error on update
'data/res_partner_relation_type.xml'
],
"assets": {
"web.assets_backend": [
# "bemade_odoo_partner_scrapper/static/src/js/odoo_scrapper.js",
# "bemade_odoo_partner_scrapper/static/src/xml/odoo_scrapper_templates.xml",
"bemade_odoo_partner_scrapper/static/src/js/odoo_scrapper.js",
"bemade_odoo_partner_scrapper/static/src/xml/odoo_scrapper_templates.xml",
],
},
'demo': [],

View file

@ -9,8 +9,9 @@
<record id="rel_type_odoo_partner" model="res.partner.relation.type">
<field name="name">Is Odoo Partner Of</field>
<field name="name_inverse">Is Odoo Client Of</field>
<field name="contact_type_left">c</field>
<field name="contact_type_right">c</field>
<!-- Commented two lines below due to data integrity issue when migrating to 17.0. Are they really needed? -->
<!-- <field name="contact_type_left">c</field>-->
<!-- <field name="contact_type_right">c</field>-->
</record>
<!-- Categories -->

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
{
'name': 'Bemade Odoo Partner Scrapper JS Only',
'version': '1.0.0',
'version': '17.0.1.0.0',
'category': 'Administration',
'summary': 'Module for scraping partners from odoo.com.',
'description': """

View file

@ -1,12 +1,12 @@
{
'name': 'Packing wizard',
'version': '15.0.1.0.0',
'version': '16.0.1.0.0',
'category': 'Extra Tools',
'summary': 'Allow automated packing type creation',
'description': """
This module allows users simply enter all the package dimensions on every packing and it will create the packing
type automatically if it not exists, use it the create the package with the weight and related package type.
You need to activate the auto create package feature on the delivery carrier to use this feature.
""",
'depends': [

View file

@ -1,6 +1,6 @@
{
'name': 'Automated Partner Association by Email Domain',
'version': '15.0.0.0.0',
'version': '16.0.0.0.0',
'category': 'Extra Tools',
'summary': 'Automatically associates partners with companies using matching email domains',
'description': """

View file

@ -0,0 +1,31 @@
#
# Bemade Inc.
#
# Copyright (C) 2023-June Bemade Inc. (<https://www.bemade.org>).
# Author: Marc Durepos (Contact : marc@bemade.org)
#
# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1)
# It is forbidden to publish, distribute, sublicense, or sell copies of the Software
# or modified copies of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
#
{
'name': 'Payslip Done as To Pay',
'version': '17.0.1.0.0',
'summary': 'Payslips in "Done" state appear as "To Pay"',
'category': 'Human Resources/Payroll',
'author': 'Bemade Inc.',
'website': 'http://www.bemade.org',
'license': 'OPL-1',
'depends': ['hr_payroll'],
'data': ['views/hr_payslip_views.xml'],
'installable': True,
'auto_install': True,
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="hr_payroll.hr_payslip_action_view_to_pay" model="ir.actions.act_window">
<field name="domain">[('state', 'in', ['draft', 'verify', 'done'])]</field>
</record>
</odoo>

View file

@ -18,7 +18,7 @@
#
{
'name': 'Sports Clinic Management',
'version': '16.0.1.5.0',
'version': '16.0.1.5.2',
'summary': 'Manage the patients of a sports medicine clinic.',
'description': """
Adds the notion of sports teams, players (patients), coaches and treatment

View file

@ -61,7 +61,7 @@ class Patient(models.Model):
"return to match play.")
is_injured = fields.Boolean(compute="_compute_is_injured")
stage = fields.Selection(
selection=[('no_play', 'Injured'), ('practice_ok', 'Cleared for Practice'), ('healthy', 'Cleared to Play')],
selection=[('no_play', 'Injured'), ('practice_ok', 'Practice OK'), ('healthy', 'Play OK')],
compute='_compute_stage')
last_consultation_date = fields.Date()
active_injury_count = fields.Integer(compute='_compute_active_injury_count')

View file

@ -165,18 +165,18 @@
<t t-call="portal.portal_table">
<thead>
<tr>
<th width="20%">Active Injury</th>
<th width="80%">Notes</th>
<th width="30%">Active Injury</th>
<th width="70%">Notes</th>
</tr>
</thead>
<tbody>
<t t-foreach="injuries" t-as="injury">
<tr>
<td>
<span t-field="injury.diagnosis"/>
<span t-field="injury.diagnosis" class="text-wrap"/>
</td>
<td>
<span t-field="injury.external_notes"/>
<span t-raw="injury.external_notes" class="text-wrap"/>
</td>
</tr>
</t>

View file

@ -62,6 +62,49 @@
</tree>
</field>
</record>
<record id="sports_patient_view_kanban" model="ir.ui.view">
<field name="name">sports.patient.view.kanban</field>
<field name="model">sports.patient</field>
<field name="arch" type="xml">
<kanban>
<field name="first_name"/>
<field name="last_name"/>
<field name="date_of_birth"/>
<field name="age"/>
<field name="team_ids" widget="many2many_tags"/>
<field name="match_status" optional="hide"/>
<field name="practice_status" optional="hide"/>
<field name="stage" optional="show"/>
<field name="predicted_return_date" widget="date"/>
<field name="return_date" widget="date"/>
<field name="activity_ids" widget="list_activity"/>
<templates>
<t t-name="kanban-box">
<t t-set="stage" t-value="record.stage.raw_value"/>
<div t-attf-class="oe_kanban_global_click
{{stage === 'no_play' ? 'text-danger' : stage === 'practice_ok' ? 'text-warning' : ''}}">
<div>
<div class="o_kanban_record_top mb16">
<div class="o_kanban_record_headings mt-4">
<field name="name"/>
</div>
<div class="oe_kanban_card_header_stage">
Status: <field name="stage"/>
</div>
</div>
<div class="o_kanban_record_body">
DOB: <field name="date_of_birth" widget="date"/>
<field name="team_ids" widget="many2many_tags"/>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="sports_patient_view_form" model="ir.ui.view">
<field name="name">sports.patient.view.form</field>
<field name="model">sports.patient</field>

View file

@ -18,7 +18,7 @@
#
{
'name': 'Stock Quant Valuation',
'version': '15.0.1.0.0',
'version': '16.0.1.0.0',
'summary': 'Adds valuation to stock quant for better inventory adjustment management',
'description': '',
'category': 'Inventory',

View file

@ -1,6 +1,6 @@
{
'name': 'Temporary Change for Image Full Size',
'version': '15.0.0.0.0',
'version': '16.0.0.0.0',
'category': 'Extra Tools',
'summary': 'Temporary Change for Image Full Size',
'description': """